Authentifizierung von Applikationen mit Vault und AppRole

HashiCorp Vault ist ein Open Source Tool zur Verwaltung von Geheimnissen. Im Folgenden wird beschrieben, wie sich Applikationen an Vault anmelden und auf Geheimnisse zugreifen können.

Warum AppRole?

Authentifizierungsmethoden in Vault sind diejenigen Komponenten, die – wie der Name bereits sagt – eine Identitätsprüfung eines Benutzers ermöglicht und definierte Policies an zum Beispiel Benutzer, aber auch Applikationen oder ganze Systeme zuweisen kann.

Vault unterstützt dabei mehrere Authentifizierungsmethoden zur Absicherung diverser Use-Cases. Organisationen die Active Directory einsetzen, können zum Beispiel LDAP verwenden, damit Benutzer Zugriff auf Geheimnisse in Vault bekommen. Ähnlich verhält es sich beispielsweise mit Github. EC2-Instanzen in AWS dagegen können die AWS-Methode benutzen.

Während Methoden wie LDAP oder Github menschlichen Benutzern erlauben sich in Vault anzumelden, besteht die gleiche Notwendigkeit natürlich auch für Applikationen. Genau da kommt AppRole in Spiel. AppRole erlaubt es Applikationen (od. ganzen Systemen) sich gegen in Vault definierte Rollen zu authentifizieren. Diese Methode eignet sich besonders gut für die Automatisierung von speziellen Workflows (Pipelines zum Beispiel).

Eine AppRole stellt dabei ein Set von Vault-Policies und Login-Beschränkungen dar, die alle erfüllt sein müssen um einen gültigen Token mit diesen Policies zu erhalten.

Vorbedingungen

Selbstverständlich wird eine produktive Vault-Instanz bzw. ein Vault-Cluster benötigt. Wie das geht findet sich zum Beispiel hier. Vault lässt sich jedoch auch sehr einfach im Dev-Modus starten.

CLI oder API

Im Grunde spielt es keine Rolle ob per CLI oder API mit Vault kommuniziert wird. Im folgenden Beispiel wird von beiden Varianten Gebrauch gemacht.

Beispiel-Use-Case: Deployment von SSL-Zertifikaten

Im Folgenden soll eine Jenkins-Pipeline entstehen, durch diese regelmäßig SSL-Zertifikate auf bekannte Nodes deployed werden. Die Pipeline soll also Geheimnisse aus Vault lesen und etwas damit tun.

In einer ersten Ausbaustufe werden einfach nur die Zertifikate in Vault benutzt, die dort bereits liegen. Zum jetzigen Zeitpunkt spielt es dabei keine Rolle, wie diese dort landen. Dieses Beispiel dient vielmehr zur Veranschaulichung des sicheren Zugriffs auf Geheimnisse mit AppRole.

In diesem skizzierten Szenario partizipieren 3 grundlegende Rollen:

  • Administrator
  • Trusted Entity (Jenkins)
  • Application (Pipeline)

Mit dem Konzept der 3 Personen löst sich das Problem, wie die ROLE_ID und SECRET_ID (Erklärung erfolgt im weiteren Verlauf) der Application zugänglich gemacht werden.

Überblick AppRole Workflow

1. Vault Login

Rolle: Administrator

$ docker exec -ti <vault container> /bin/sh

$ export VAULT_ADDR=http://127.0.0.1:8200

$ vault login

<Eingabe des Tokens eines Operators (zum Beispiel der Root-Token)>

2. AppRole auth method aktivieren

Rolle: Administrator

$ vault auth enable approle

3. Policy „sslcert“ erstellen

Rolle: Administrator

Folgende Policy soll gesetzt werden:

$ cat << 'EOF' > sslcert.hcl
  path "secret/ssl/*" {
    capabilities = [
      "read",
      "list"]
  }
EOF

Übersetzt bedeutet das, dass die Rolle „sslcert“ alle Geheimnisse aus secret/ssl/* lesen und auflisten können soll. Mittels

$ vault policy write sslcert sslcert.hcl

im Vault Container wird diese Policy gesetzt.

4. approle „sslcert“ erstellen

Rolle: Administrator

$ vault write auth/approle/role/sslcert \
    secret_id_num_uses=1 \
    secret_id_ttl=1m \
    token_ttl=10m \
    token_max_tll=10m \
    policies="sslcert"

In diesem Beispiel werden folgende Parameter gesetzt und benutzt:

  • secret_id_num_uses: Number of times any particular SecretID can be used to fetch a token from this AppRole, after which the SecretID will expire.
  • secret_id_ttl: Duration* after which any SecretID expires.
  • token_ttl: Duration* to set as the TTL** for issued tokens and at renewal time.
  • token_max_tll: Duration* after which the issued token can no longer be renewed.

* an integer number of seconds (3600) or an integer time unit (60m)
** time to live

Weitere Informationen zu den Parametern gibt es hier.

5. ROLE_ID auslesen und abspeichern

Rolle: Administrator

Eine Application, in unserem Fall die Jenkins-Pipeline, benötigt einen Benutzernamen sowie ein Passwort um sich an Vault anzumelden und Geheimnisse auszulesen. In Vault-Sprache reden wir dabei von der ROLE_ID sowie der SECRET_ID. Die ROLE_ID ist dabei nicht besonders schützenswert, da sie ohne gültigen Token sowie SECRET_ID keinen Wert besitzt.

Mittels

$ vault read -field=role_id auth/approle/role/sslcert/role-id

lesen wir die ROLE_ID aus. Um sie später in unserer Applikation – der Pipeline – benutzen zu können, speichern wir diese (trotzdem relativ sicher) im Jenkins Credential Store.

Bei mehreren Projekten (Pipelines) die unterschiedliche ROLE_ID’s benutzen könnte es Sinn ergeben, diese direkt im Jenkinsfile abzuspeichern.

6. Trusted Entity Policy erstellen

Rolle: Administrator

Im weiteren Verlauf wollen wir nicht mit einem administrativen Benutzer weiterarbeiten um die noch notwendige SECRET_ID zu generieren. Diese Aufgabe soll von einer Trusted Entity – nämlich Jenkins – übernommen werden.

Dabei beschränken wir trotzdem die Rechte dieser Instanz und ermöglichen ihr ausschließlich create, read und update Operationen auf der SECRET_ID. Dafür gibt es diese Policy

$ cat << 'EOF' > jenkins.hcl
path "auth/approle/role/sslcert/secret-id" {
  capabilities = [
    "create",
    "read",
    "update"]
  }
EOF

die wir mittels

$ vault policy write jenkins jenkins.hcl

erstellen.

7. Token für die Trusted Entity erstellen

Rolle: Administrator

An dieser Stelle brechen wir kurz aus dem Vault Container aus. Alle Vorbereitungen sind größtenteils getroffen. Jetzt müssen wir uns darum kümmern, wie Jenkins einen Token zur verfügung gestellt bekommt mit dem eine SECRET_ID generiert werden kann.

Nehmen wir an, wir arbeiten mit einem Automatisierungstool wie beispielsweise Ansible. Wir wollen einen Token für Jenkins mit der Jenkins Policy generieren und diesen als Datei in das Home-Verzeichnis von Jenkins legen. Dabei wählen wir den Namen „.vault_jenkins_token“. Existiert diese Datei noch nicht, erstellen wir diese mittels

$ curl -H "X-Vault-Token: $VAULT_ROOT_TOKEN"
     -XPOST \
     -d '{"policies":["jenkins"],"ttl":"168h","renewable":true}' \
       http://vault:8200/v1/auth/token/create" \
         | jq '.auth.client_token' \
         | tr -d '"' \
> /var/jenkins_docker_data/.vault_jenkins_token

Ist diese Datei und damit der Token bereits vorhanden, erneuern wir den Token mit jedem Provisionierungslauf mittels

$ curl -H "X-Vault-Token: $(cat /var/jenkins_docker_data/.vault_jenkins_token)"
     -XPOST \
     -d '{"increment":"168h"}' \
       http://vault:8200/v1/auth/token/renew-self

Damit sind nun endgültig alle Aufgaben des Administrators erledigt.

8. SECRET_ID für den Zugriff der Pipeline erstellen

Rolle: Trusted Entity (Jenkins)

Wie bereits etwas weiter oben beschrieben, benötigt eine Applikation (hier unsere Pipeline) eine ROLE_ID sowie eine SECRET_ID um auf Geheimnisse zugreifen zu können. Erstere ist der Trusted Entity (dem Jenkins) bereits bekannt und im Jenkins Credential Store hinterlegt. Letztere wird in folgendem Schritt direkt durch die Trusted Entity (der Jenkins) erstellt.

$ VAULT_TOKEN=$(cat /var/jenkins_docker_data/.vault_jenkins_token) vault write -field=secret_id -f auth/approle/role/sslcert/secret-id

An diesem Punkt ist es jetzt Zeit, dass wir uns etwas näher an den echten Use-Case begeben und endgültig aus dem Vault Container ausbrechen.

Innerhalb einer Jenkins Pipeline können wir die SECRET_ID durch die Trusted Entity (Jenkins) wie folgt ermitteln:

#!groovy

node('master') {
  currentBuild.result = "SUCCESS"
    try {
      stage('generate SECRET_ID') {
        sh '''
          set +x

          export SECRET_ID=$(curl -s -H "X-Vault-Token: $(cat ~/.vault_jenkins_token)" -XPOST http://vault:8200/v1/auth/approle/role/sslcert/secret-id | jq '.data.secret_id' | tr -d '"')
        ''' }
    }
    
    catch (err) {
      currentBuild.result = "FAILURE"
      // DO THINGS IN CASE OF FAILURES
      throw err
    }
}

9. Lesen der Geheimnisse / Deployment der Zertifikate

Rolle: Application (Pipeline)

An dieser Stelle haben wir alles damit sich unsere Application (die Pipeline) an Vault anmelden und die Zertifikate auslesen kann. Mit Hilfe der ROLE_ID und der SECRET_ID ist unsere Application (die Pipeline) in der Lage sich einen Token zu generieren, der alles im Pfad „secret/ssl/*“ lesen darf. Mit der CLI würde die Erstellung eines Application-Tokens wie folgt aussehen (im Vault Container):

$ vault write -field=token auth/approle/login role_id=${ROLE_ID} secret_id=${SECRET_ID}

Das Auslesen eines echten Zertifikates passiert mittels

$ curl -X GET -H "X-Vault-Token: <Ausgabe des vorangegangenen Befehls>" http://vault:8200/v1/secret/ssl/dev/de-de/intern

In unserer Pipeline sieht ein realer Schritt dabei wie folgt aus (als Ausschnitt aus der obigen Pipeline):

...

stage('read ssl cert') {
  withCredentials([string(credentialsId: 'ROLE_ID', variable: 'ROLE_ID')]) {
  sh '''
    set +x

    export SECRET_ID=$(curl -s -H "X-Vault-Token: $(cat ~/.vault_jenkins_token)" -XPOST http://vault:8200/v1/auth/approle/role/sslcert/secret-id | jq '.data.secret_id' | tr -d '"')

    export APP_TOKEN=$(curl -s -XPOST -d '{"role_id":"'"$ROLE_ID"'","secret_id":"'"$SECRET_ID"'"}' http://vault:8200/v1/auth/approle/login | jq '.auth.client_token' | tr -d '"')

    # das ist das Zertifikat
    curl -s -H "X-Vault-Token: $APP_TOKEN" -XGET http://vault:8200/v1/secret/ssl/dev/de-de/intern
  '''
  }
}

...

10. Revoke des Application-Token

Ein Application-Token hat eine insgesamte Lebensdauer von 10 Minuten (inkl. ein eventuelle Renew-Event). Am Ende der Pipeline ist es denkbar und durchaus sinnvoll, den Application-Token zu revoken.

Das geschieht mittels:

$ curl -s -H "X-Vault-Token: $APP_TOKEN" -XPOST http://vault:8200/v1/auth/token/revoke-self

Nachbetrachtungen

Aufgrund der Trennung der Rollen erreichen wir ein sicheres Login für unsere Application (die Pipeline).
Jede generierte SECRET_ID darf durch die Trusted Entity (Jenkins) genau einmal benutzt werden um einen Application Token für die Pipeline zu generieren. Dazu kommt eine TTL von 1 Minute.

Jeder Application-Token hat eine insgesamte Lebensdauer von 10 Minuten.