x

Menü

HashiCorp Vault SSH Authentifizierung mit Signed Public Keys

veröffentlicht in: HashiCorp Datum: 08.03.2021
Martin Buchleitner, Senior IT-Consultant

Über den Autor

Martin Buchleitner ist ein Senior IT-Berater für Infralovers und für Commandemy. Twitter github LinkedIn

Alle Artikel von diesem Autor sehen

HashiCorp Vault SSH Authentifierung

Im letzten HashiCorp Vault Artikel habne wir bereits einen Weg gezeigt, wie man in seiner Infrastruktur One Time Passwörter von Vault nutzen kann. Dieses Mal verwenden wir nun signierte Public Keys, um auf die Infrastruktur zuzugreifen.

Diese Methode ist für automatisierte Anwendungen nützlicher, aber auch für Benutzer ist es angenehmer - zudem müssen von Administrationsseite keinerlei SSH Benutzer Keys auf der Infrastruktur gewartet werden.

Als Administratoren können wir nun mit dieser Methode die Server so konfigurieren, dass diese einzelne SSH Keys gegen eine Certificate Authority ( CA ) validieren. Wenn ein Benutzer sich mit einem SSH Key, der von dieser CA signiert wurde, verbindet, wird sein Login zugelassen. Zusätzlich ist es möglich diesen signierten Keys auch eine Gültigkeitsdauer ( TTL = time to live ) mitzugeben. So können Benutzer sich zum Beispiel nur für 30 Minuten mittels diesem signierten Key verbinden und müssen dannach wieder einen neuen anfordern.

Weiters können wir auch mehrere Rollen dafür anlegen, um diverse Subnetzbereiche abzudecken und so den Zugriff auf diverse Subnetze auch steuern.

Konfiguation von Hashicorp Vault mit Terraform

Wir beginnen mit der Konfiguration von HashiCorp Vault mittels terraform, um hier das Authentifizierungs Backend anzulegen. Der erste Block definiert unsere Vault Installation und erstellt in dieser ein Backend mit dem Typ ssh. Dieses wird auch unter dem Pfad ssh eingebunden. Dieser Pfad kann leicht adaptiert werden, da er im Terraform Code weitervererbt wird, aber innerhalb des Ansible Codes muss er dann auch angepasst werden.

provider "vault" {
  address = "https://vault.local:8200/"
}
resource "vault_mount" "ssh" {
  type = "ssh"
  path = "ssh_signed_keys"

  default_lease_ttl_seconds = "14400"  # 4h
  max_lease_ttl_seconds     = "604800" # 1 week

}

Nun konfigurieren wir die Rolle, welche es uns erlaubt die Keys zu signieren. Hier setzen wir den key_type entsprechend auf ca. Es ist notwendig, dass man es nicht erlaubt, dass hier Host Zertifikate von dieser Rolle erstellt werden. Wie das gemacht werden kann wird in einem nachfolgenden Artikel behandelt.

Innerhalb dieser Rolle definieren wir einen kurzen Gültiskeitsbereich dieser Keys, da wir erreichen wollen, dass Benutzer immer wieder neue Keys anfordern, um neue Verbindungen zu erstellen.

Dieser Code Block verfügt auch schon über eine Policy, die den Benutzern innerhalb von Vault zugeordnet werden muss, damit diese Keys erzeugen können.

resource "vault_ssh_secret_backend_role" "client_keys" {
  name     = "client_keys"
  backend  = vault_mount.ssh.path
  key_type = "ca"

  allow_host_certificates = false
  allow_subdomains        = false
  allow_user_key_ids      = false
  allow_user_certificates = true
  default_extensions = {
    "permit-pty" = ""
  }
  allowed_extensions = "permit-pty,permit-port-forwarding"
  default_user       = "martin"
  allowed_users      = "martin,ubuntu"
  max_ttl            = "30m"
  ttl                = "10m"
  cidr_list          = "172.16.0.0/16"
}

resource "vault_policy" "user_signing" {
  name   = "user_signing"
  policy = <<EOT
path "${vault_mount.ssh.path}/sign/${vault_ssh_secret_backend_role.client_keys.name}" {
    capabilities = ["create", "read", "update"]
}
EOT
}

Anschließend können wir diesen Code mittels Terraform anwenden - es bedarf hier eines gültigen Tokens, der diese Änderungen in Vault einbringen kann.

export VAULT_TOKEN="my-vault-token"
terraform apply

HashiCorp Vault Approle Authentifizierung

Da wir diesen Provisionierung ja automatisiert durchführen wollen, sollte hier auch nicht unser persönliches Vault token verwendet werden. Aus diesem Grund fügen wir hier Vault noch eine Approle Authentifizierung hinzu um die erfolderlichen Operationen in Vault durchführen zu können.

resource "vault_auth_backend" "approle" {
  type = "approle"
}
resource "vault_approle_auth_backend_role" "automated_access" {
  backend        = data.vault_auth_backend.approle.path
  role_name      = "automated_access"
  token_policies = [ vault_policy.ssh_ca_read.name ]
}
resource "vault_approle_auth_backend_role_secret_id" "automated_access" {
  backend   = vault_auth_backend.approle.path
  role_name = vault_approle_auth_backend_role.automated_access.role_name
}
resource "vault_policy" "ssh_ca_read" {
  name   = "ssh_ca_read"
  policy = <<EOT
path "${vault_mount.ssh.path}/config/ca" {
  capabilities = [ "read" ]
}
EOT
}
output "approle_id" {
  value = vault_approle_auth_backend_role.automated_access.role_id
}
output "secret_id" {
  value = vault_approle_auth_backend_role_secret_id.automated_access.secret_id
}

Und abermals wenden wir diese Änderung gegenüber Vault an, um diese Konfiguration anzuwenden.

export VAULT_TOKEN="my-vault-token"
terraform apply

Zu diesem Zeitpunkt müssen wir nun unser Vault Token nicht mehr verwenden, um unsere Infrastruktur zu provisionieren.

Ansible um die Infrastruktur zu provisionieren

Ansible Rolle

Leider gab es für diesen Zweck noch keine Ansible Rolle, so haben wir diese selbst geschrieben, um hier eine Provisionierung durchführen zu können. Mit dem folgenden ansible-galaxy Kommando kann man die Rolle benutzen:

ansible-galaxy install https://github.com/infralovers/ansible-vault-ssh-signed-keys

Die Rolle interagiert rein mit der HTTP API von HashiCorp Vault. Es bedarf also keiner weiteren Toolinstallation.

Ansible Rollout

Um die Rolle nun auch in einem Playbook zu verwenden dient der folgende Code Block. Zustätzlich zu diesem müssen auch die Variablen vault_host_role_id und vault_host_secret_id zur Verfügung gestellt werden. Wir empfehlen hierfür Ansible Vault zu verwenden. Diese beiden Variablen wurden oben in der Approle Authentifizierung erzeugt. Aber es ist natürlich auch möglich diese Variablen über die Kommandzeile anzugeben. Für die Validierung von Benutzer Keys wird die Option TrustedUserCAKeys von SSH Dämon konfiguriert.

- hosts: all
  become: yes
  vars:
  - vault_addr: http://vault.local:8200/
  - user_ssh_path: "ssh_signed_keys"
  roles:
  - role: vault-ssh-signed-keys

Wenn wir nun Ansible Vault verwenden, schaut der Aufruf folgendermaßen aus:

ansible-playbook vault-ssh.yml

Wenn wir sie per Kommandozeile angeben so ( wobei diese nicht der empfohlene Weg ist ):

ansible-playbook vault-ssh.yml -e vault_host_role_id="<your-approle-id>" -e vault_host_secret_id="<your-secret-id>"

Nach diesem Schritt sind nun alle Hosts so konfiguriert, dass sich Benutzer mit einem von HashiCorp Vault signierten SSH Key authentifizieren können.

Benutzen der HashiCorp Vault signierten Public Keys

Nun wollen wir diese Konfiguration auch als Benutzer verwenden, um uns mit der Infrastruktur zu verbinden. Dies ist nun ein 2 stufiger Prozess:

  • Signierung unseres eigenen SSH Keys
  • Eine SSH Verbindung mit diesem signierten Key erstellen

Die Signierung des Keys kann durch die unten folgende Fuktionen gemacht werden. Sämtliche Parameter der Funktion können über Umgebungsvariablen gesteuert werden. Hierzu lohnt ein Blick auf “Verzeichnis basierte Profile”, um diese Variablen auf Grund des aktuellen Zeichnisses zu verändern.

Zu diesem Punkt erwarten die Funktionen eine Umgebungsvariable VAULT_TOKEN. Diese kann zum Beispiel über eine weitere Funktion erstellt werden.

vault_sign_key () {
  VAULT_ADDR="${VAULT_ADDR:-http://vault.local:8200}"
  VAULT_MOUNT=${VAULT_MOUNT:-signed_keys}
  VAULT_ROLE=${VAULT_ROLE:-client_keys}

  VAULT_PUBLIC_SSH_KEY=${VAULT_PUBLIC_SSH_KEY:-"$HOME/.ssh/id_rsa.pub"}
  VAULT_SIGNED_KEY=${VAULT_SIGNED_KEY:-"$HOME/.ssh/vault_signed_key.pub"}
  SSH_USER=${SSH_USER:-ubuntu}


  if [[ ! -n "${VAULT_TOKEN}" ]]; then
    echo "[ERR] No vault access token found at ${VAULT_TOKEN}"
    return
  fi

  export TMP_DIR=$(mktemp -d)
  cat > "$(echo ${TMP_DIR}/ssh-ca.json)" << EOF
{
    "public_key": "$(cat ${VAULT_PUBLIC_SSH_KEY})",
    "valid_principals": "${SSH_USER}"
}
EOF
  if ! curl -s --fail -H "X-Vault-Token: ${VAULT_TOKEN}" -X POST -d @${TMP_DIR}/ssh-ca.json \
      ${VAULT_ADDR}/v1/${VAULT_MOUNT}/sign/${VAULT_ROLE} | jq -r .data.signed_key > "${VAULT_SIGNED_KEY}" ; then
    echo "[ERR] Failed to sign public key."
  fi
  chmod 0600 $VAULT_SIGNED_KEY
  rm -rf $TMP_DIR
}

Wir können nun einen gültig signierten SSH Key erzeugen, der nun innerhalb der Infrastruktur benutzt werden kann. So nutzen wir diese Funktion anschließend innerhalb einer weiteren, welche uns genau diese SSH Verbindung aufbaut.

vault_ssh () {

  if [[ -z "${1}" ]]; then
    echo "[INFO] Usage: vault_ssh user@host [-p 2222]"
    return
  fi

  if [[ "${1}" =~ ^-+ ]]; then
    echo "[ERR] Additional SSH flags must be passed after the hostname. e.g. 'vssh user@host -p 2222'"
    return
  elif [[ "${1}" =~ ^[a-zA-Z]+@[a-zA-Z]+ ]]; then
    SSH_USER=$(echo $1 | cut -d'@' -f1)
    SSH_HOST=$(echo $1 | cut -d'@' -f2)
  else
    SSH_USER=$(whoami)
    SSH_HOST=${1}
  fi

  SSH_CONFIG_USER=$(ssh -G "$SSH_HOST" | awk '$1 == "user" p{ print $2 }')
  if [ -n "$SSH_CONFIG_USER" ]; then
    SSH_USER=$SSH_CONFIG_USER
  fi
  VAULT_PRIVATE_SSH_KEY=${VAULT_PRIVATE_SSH_KEY:$HOME/.ssh/id_ed25519_private}
  VAULT_SIGNED_KEY=$(echo "$HOME/.ssh/vault_signed_key.pub")

  # sign the public key
  vault_sign_key

  # shift arguments one to the left to remove target address
  shift 1

  # construct an SSH command with the credentials, and append any extra args
  ssh -i ${VAULT_SIGNED_KEY} -i ${VAULT_PRIVATE_SSH_KEY} ${SSH_USER}@${SSH_HOST} $@
}

Mit diesen Funktionen können wir nun eine SSH Verbindung zu jenen Server erzeugen, wo vorhin die Option TrustedUserCAKeys in ssh gesetzt wurde und welche auch im gültigen Subnetzbereich sind. Für Administratoren bedeutet dies auch, dass sie auf den jeweiligen Servern keine Anpassen machen müssen, diese Berechtigungen müssen rein per HashiCorp Vault getätigt werden

$ vault_ssh my-server
Welcome to Ubuntu 20.04.1 LTS (GNU/Linux 5.4.0-1025-raspi aarch64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Wed Mar  3 10:16:00 UTC 2021

  System load:  0.08               Temperature:           42.3 C
  Usage of /:   10.6% of 29.05GB   Processes:             147
  Memory usage: 45%                Users logged in:       0
  Swap usage:   0%                 IPv4 address for eth0: 1.2.3.4

 * Introducing self-healing high availability clusters in MicroK8s.
   Simple, hardened, Kubernetes for production, from RaspberryPi to DC.

     https://microk8s.io/high-availability

0 updates can be installed immediately.
0 of these updates are security updates.

Last login: Wed Mar  8 00:00:00 2021 from 127.0.0.1
ubuntu at my-server in ~