Secrets für Jenkins Pipelines aus Vault beziehen

von

Im Folgenden geht es darum ein funktionierendes Proof-of-Concept zu basteln wie man Secrets in HashiCorp Vault speichern und dann mit einer Jenkins-Pipeline auslesen kann.

Hintergrund

Dieses Vorgehen funktioniert, ist aber nicht optimal wenn man eine Jenkins-Instanz für verschiedene Projekte hat. 
Das Hauptproblem ist das Jenkins-Credentials keine granulare Steuerung ermöglichen. 
Einerseits in der User-Verwaltung (nur Administratoren können Secrets anlegen).
Andererseits fehlt Jenkins eine genaue Steuerung für das Auslesen der Credentials.
Konkret: Wer hat von Wo Wie lange Zugriff auf Was.
Ebenso fehlen genaue Access-Logs für die Credentials. 

Wenn wir diese Probleme angehen wollen benötigen wir also ein anderes Tool, welches hoffentlich etwas mehr unseren Anforderungen entspricht. Wenn wir uns für ein passendes System entscheiden, hätten wir gleichzeitig noch die Möglichkeit eine Single Source of Truth zu etablieren.

Ablauf

Bevor es jetzt losgeht noch ein paar Hinweise:
Im folgenden haben wir einige Abläufe vereinfacht, entsprechend gibt es ein paar Stellen die man so nicht produktiv setzen sollte. Wir haben uns bemüht, an den wichtigen Stellen dann auch eine bessere Alternative zu erklären.
Diese Hinweise sind jeweils immer Hellblau markiert. Bei den relevanten Vault-Befehlen haben wir den zu erwartenden Output in Grün markiert und wichtige Hinweise sind in Orange markiert, hier gleich der Erste:

Wir starten mit dem anlegen einer Vault-Config damit wir im nächsten Schritt Vault und Jenkins lokal per Docker hochfahren und einrichten können.
Danach werden wir ein Geheimnis in Vault hinterlegen und den Zugriff darauf erstellen.
Anschließend fügen wir diesen Zugriff in Jenkins ein und lesen das Geheimnis per Pipeline aus.

Vorbereitung (Jenkins + Vault aufsetzen)

Wie bereits eben erwähnt legen wir zuerst eine Vault-Config an.
Dafür erstellen wir ein Verzeichnis „/vault-config/“ und packen in dieses Verzeichnis eine Datei mit dem Namen „initial-config.json“.

{
  "storage": {
    "file": {"path": "/vault/file"}
  },
  "listener": [{
    "tcp": {
      "address": "0.0.0.0:8200",
      "tls_disable": true
    }
  }],
  "default_lease_ttl": "168h",
  "max_lease_ttl": "720h",
  "ui": true
}

Jetzt können wir Vault und Jenkins per docker-compose hochfahren.
Dazu diese docker-compose.yml neben dem Verzeichnis „/vault-config/“ anlegen und mit docker compose up ausführen.

version: '3.8'
services:
  vault:
    image: hashicorp/vault
    environment:
      VAULT_LISTEN_ADDRESS: "0.0.0.0:8200"
      VAULT_ADDR: "http://0.0.0.0:8200"
    ports:
      - "8200:8200"
    cap_add:
      - IPC_LOCK
    command: server
    volumes:
      - vault-file:/vault/file:rw
      - type: bind
        source: ./vault-config/
        target: /vault/config
  jenkins:
    image: jenkins/jenkins:lts
    ports:
      - "8080:8080"
    volumes:
      - jenkins_home:/var/jenkins_home
      - /var/run/docker.sock:/var/run/docker.sock
volumes:
  vault-file:
  jenkins_home:

Dadurch wurden die beiden Container hochgefahren (Im Verzeichnis aus Zeile 16 muss unsere Vault-Config liegen, sonst fährt der Vault-Container nicht hoch). 

vault operator init -key-shares=1 -key-threshold=1

Unseal Key 1: nHACa77F193D5J8RhxqwsOmllJsJ3jff/WJdWNoH2Fo=

Initial Root Token: hvs.ognCtf2fQrN1yaG1P6vUQXTZ

Vault initialized with 1 key shares and a key threshold of 1. Please ...

Unbedingt den Unseal-Key und das Root-Token aufschreiben! Diese werden noch benötigt.

Danach befindet sich der Vault im sogenannten „Sealed-State“. Das bedeutet, dass er keine Anfragen entgegennimmt und viel wichtiger keine Geheimnisse rausgibt. In diesem Zustand befindet sich Vault nach jedem Neustart, bzw. nach jedem „seal“-Befehl (Es gibt diese Art Kill-Switch auch direkt im UI).

Um Vault ansprechbar zu machen, müssen wir ihn mit folgendem Befehl öffnen:

vault operator unseal

Nun muss der zuvor notierte Unseal-Key eingegeben werden.

Key           Value
---           -----
Seal Type     shamir
Initialized   true
Sealed        false

An dem Feld „Sealed“ -> „false“ erkennen wir, dass der Vault geöffnet wurde.

Wir haben der Einfachheit halber sowohl key-shares als auch den key-threshold auf 1 gesetzt. Diese sind allerdings standardmäßig auf 5 und 3 gesetzt (und das auch aus gutem Grund).
Würden wir diese Standardeinstellungen verwenden, müssten wir den Unseal-Befehl dreimal mit jeweils unterschiedlichen Unseal-Keys wiederholen, damit sich Vault der Welt öffnet. 
In der Praxis könnte man nun mehrere Unseal-Keys strategisch auf verschiedene Personen verteilen, um das unerwünschte Öffnen des Vaults zu erschweren.

vault login

oder das Token im Environment (im weiteren als env-var bezeichnet) hinterlegen. In diesem Fall nutzt Vault das dort hinterlegte Token für die Anmeldung.

export VAULT_TOKEN=<root-token zb. hvs.46SFyUuEdha2f4nrl4X9ud40>

Bei solchen direkten Exports besteht immer das Problem, dass der Befehl mitsamt Secrets im Klartext in der Shell-History landet.
Um dies zu umgehen empfiehlt sich das folgende Vorgehen:
Den Inhalt der env-var (in diesem Fall das Root-Token) in einer Datei speichern (z.B. per „vim root_token”).
Anschließend wird die env-var mit dem Inhalt der Datei erstellt. In diesem Fall wäre das Folgendes: „VAULT_TOKEN=$(cat root_token)„.
Zum Schluss sollte man die Datei natürlich wieder löschen: „rm root_token“.

Jetzt haben wir einen benutzbaren Vault zur Verfügung.

Geheimnisse verstecken

Nun wollen wir zum spaßigen Teil kommen und lernen, wie wir Secrets in Vault ablegen können. Das Ganze funktioniert kurz und schmerzlos mit nur zwei Befehlen.
Erstmal müssen wir die KV-v2-Engine aktivieren.

vault secrets enable -version=2 -path=secret kv

Danach können wir mit dem Verstecken beginnen.

vault kv put secret/versteck geheimnis=53cr37

Damit haben wir unser Secret als Key-Value-Pair (mit dem Key „geheimnis“) unter dem Pfad „secret/data/versteck“ in Vault abgelegt (Vault fügt zu dem Pfad noch ein „/data/“ hinzu).

Mithilfe der Kombination aus Pfad und Key können wir dann unser Secret abfragen.

vault kv get secret/versteck

Zu erwarten wäre hier folgender Output:

====== Data ======
Key          Value
---          -----
geheimnis    53cr37

Jenkins und Vault miteinander bekannt machen

AppRole in Vault einrichten

Eine AppRole kann man sich wie einen Benutzer vorstellen. Es gibt eine Art Benutzernamen „RoleID“ und eine Art Passwort „SecretID“ (zusätzlich hat die AppRole noch einen Vault-internen Namen, der hier aber erstmal nicht relevant ist). Mit dieser Benutzername/Passwort-Kombination kann man sich bei Vault „anmelden“ und allerlei Sachen abfragen.
Nun kann man zum Glück nicht alles abfragen, sondern nur die Dinge für die man auch entsprechende Zugriffsrechte hat.
Mit genau dieser Erlaubnis beginnen wir nun.
Wir definieren eine sogenannte „policy“ und geben ihr die Rechte auf das oben angelegte Secret zuzugreifen.
Dazu legen wir im Docker-Container unter „/vault/config/“ eine Datei mit dem Namen „jenkins_policy.hcl“ an, in der wir die Policy niederschreiben.
In diese Datei schreiben wir nun Folgendes hinein:

path "secret/data/versteck" {
  capabilities = ["read", "list"]
}

Anschließend fügen wir diese Policy in Vault ein:

vault policy write jenkins-policy vault/config/jenkins_policy.hcl

Nun gibt es eine Policy mit Zugriff auf den Secret-Pfad „versteck“. Das heißt, die policy hat Lesezugriff auf alle key-value Secrets die unter „versteck“ liegen.
Mit nur einer Policy allein können wir erstmal wenig anfangen. Daher erstellen wir jetzt eine AppRole und weisen ihr diese Policy zu.

Zunächst aktivieren wir die AppRole-Authentification.

vault auth enable approle

Danach erstellen wir die AppRole und weisen ihr die Policy zu.

vault write auth/approle/role/jenkins-role token_type=batch secret_id_ttl=0 token_num_uses=0 token_ttl=0 token_max_ttl=0 secret_id_num_uses=0 policies="jenkins-policy"

Wir haben nun eine AppRole mit Zugriffsrechten. Wie können wir diese nutzen?
Dazu müssen wir die RoleID auslesen (diese bleibt immer gleich) und eine SecretID erstellen. SecretIDs haben eine einstellbare Anzahl an Nutzungen und eine Time-to-Live-Frist, siehe Command oben.
Wir beginnen mit dem Auslesen der RoleID.

vault read auth/approle/role/jenkins-role/role-id

Das müsste in etwa so aussehen:

Key        Value
---        -----
role_id    0fc9a138-db85-2f83-7653-766c0e2cef8e

Nun müssen wir uns noch eine SecretID erstellen.

vault write -force auth/approle/role/jenkins-role/secret-id

Hier ist das Feld „secret_id“ relevant.

Key                   Value
---                   -----
secret_id             fb834ff9-93aa-e2c4-e645-3ecc0b95eee9
secret_id_accessor    073a46d5-54b3-6836-0c69-22383f75bab0
secret_id_num_uses    0
secret_id_ttl         0s

AppRole testen

Danach erstellen wir ein Token (vergleichbar mit dem root-Token) für unsere AppRole.

vault write auth/approle/login role_id="$ROLE_ID" secret_id="$SECRET_ID"

Mit diesem Token können wir uns auch bei Vault Authentifizieren, dafür speichern wir es in einer env-var.

export APP_TOKEN=<240 Zeichen langes Token, z.B. hvb.AAAmDL-h4mOM27wgL...4MybZeKw59vcpf5zOaqHTxA2>

Und dann fragen wir unser angelegtes Secret noch einmal ab, wobei wir diesmal statt des Root-Tokens unser neues AppRole-Token nutzen.

VAULT_TOKEN=$APP_TOKEN vault kv get secret/versteck

AppRole in Jenkins einbauen

Wir erstellen ein neues Credential vom Typ „Vault App Role Credential“, tragen dort die RoleID und die SecretID ein und geben dem Secret die ID „vault-role“.

node {
     def configuration = [vaultUrl: 'http://host.docker.internal:8200',
                         vaultCredentialId: 'vault-role',
                         engineVersion: 2] 

    def secrets = [
        [path: 'secret/versteck', engineVersion: 2, secretValues: [
            [envVar: 'geheimnis', vaultKey: 'geheimnis']]]
    ]

    withVault([configuration: configuration, vaultSecrets: secrets]) {
        sh "echo $geheimnis > confidential.txt"
    }
}


Wenn wir dann die Pipeline speichern und einmal ausführen bleibt uns als letzte Amtshandlung nur noch übrig zu schauen ob sich unser Geheimnis in der Datei „confidential.txt“ befindet.
Die Datei ist wie folgt zu finden:

Und fertig.

Addendum

– Korbinian Hurt, Sebastian Jenke

Entdecke mehr von Spree-Blog

Jetzt abonnieren, um weiterzulesen und auf das gesamte Archiv zuzugreifen.

Weiterlesen

WordPress Cookie Hinweis von Real Cookie Banner