Kubernetes External-Secrets und Vault, eine leistungsstarke Kombination für den sicheren Umgang mit Secrets

03.07.2023Stefan Welsch
Cloud Kubernetes Security b-nova techup Stay Tuned

banner

In der modernen Anwendungsentwicklung sind Secrets wie Zugangsdaten, API-Schlüssel und Zertifikate unverzichtbar. Da wir bei b-nova sehr viele Dinge ausprobieren, haben sich in den letzten Jahren recht viele Secrets an verschiedenen Orten verteilt. Wir haben beispielsweise Secrets im AWS Secrets Manager, welche wir per API in unterschiedlichen internen Applikationen auslesen. Wir haben Secrets in Github gespeichert, welche wir zur Ausführung unserer Pipelines brauchen, oder für das Deployment auf die verschiedenen Umgebungen. Weiterhin haben wir mehrere Kubernetes Cluster, in denen ebenfalls verschiedenste Secrets hinterlegt sind. Nun sind wir aber an dem Punkt angekommen, an dem wir langsam aber sicher wieder etwas Ordnung in die Verwaltung unserer Secrets bringen müssen. Die Gründe dafür liegen auf der Hand.

  • Secrets liegen aktuell an mehreren Orten und es gibt keine SSOT (Single Source of Truth)
  • Secrets sind redundant gespeichert. Das bedeutet, wenn man ein Secret anpasst, man es an mehreren Orten anpassen muss
  • Die Übersicht, wo welches Secret liegt, ist nicht mehr gewährleistet
  • Die Verwaltung, wer auf welches Secret Zugriff haben darf, ist nur noch schwer handhabbar
  • Lifecycle-Management: Automatisierung und Verwaltung der Secrets-Rotation kann komplex werden
  • Teilweise liegen die Secrets unverschlüsselt (K8s Secrets)

So kam also die Frage auf, wie wir das Secret Handling wieder in den Griff bekommen können. Die Wahl fiel dabei recht schnell auf Hashicorp Vault, da dieses Tool alle Anforderungen, die wir an einen Secret Manager haben, mit Bravour erfüllt.

  • Verschlüsselung: Vault speichert Secrets verschlüsselt und bietet zusätzliche Sicherheit
  • Zugriffskontrolle: Vault ermöglicht eine feingranulare Zugriffskontrolle durch ACLs und Rollen
  • Lifecycle-Management: Vault unterstützt die Automatisierung der Secret-Rotation und -Verwaltung

Der Vault Server war sehr schnell aufgesetzt und die Secrets schnell übertragen. Aber wie funktioniert nun der Secret-Zugriff aus den einzelnen Applikationen, aus GitHub und aus dem K8s Cluster? Es gibt verschiedene Möglichkeiten, die wir in Betracht gezogen haben. Dabei ist wichtig zu betonen, dass der Grossteil unserer Applikationen derzeit auf K8s läuft. Die Secrets werden per Secret-Referenz zu den K8s Secrets als Umgebungsvariablen beim Deployment in die Applikationen injiziert.

Eine mögliche Lösung wäre nun beispielsweise, die Secrets per API aus Vault auszulesen. Der Vorteil dabei ist, dass das Secret nur noch dort gelesen wird, wo es auch wirklich gebraucht wird. Niemand könnte mehr über die Secrets irgendwelche Passwörter auslesen. Der Nachteil ist jedoch, dass wir in jeder Applikation den API Zugriff erst implementieren müssen. Eine andere Möglichkeit, Secrets sicher und effizient in Kubernetes zu verwalten, ist die Nutzung von Kubernetes External-Secrets in Kombination mit HashiCorp Vault als externen Secret-Speicher. Heute wollen wir daher untersuchen, wie diese beiden Technologien zusammenarbeiten und wie sie den Umgang mit Secrets in Kubernetes-Clustern optimieren können.

Schauen wir uns doch gleich mal an, was der Kubernetes External-Secrets Operator überhaupt ist.

Kubernetes External-Secrets Operator

Der Kubernetes External-Secrets Operator ist ein Kubernetes-Controller, der das Synchronisieren von Secrets aus externen Quellen, wie z. B. HashiCorp Vault, AWS Secrets Manager oder Google Cloud Secret Manager, in Kubernetes-Cluster ermöglicht. Er überwacht den Cluster auf das Vorhandensein von Custom-Resource-Definitions (CRDs) namens ExternalSecrets. Diese CRDs enthalten Metadaten, die beschreiben, wie die Secrets aus der externen Quelle abgerufen und in Kubernetes-Secrets gespeichert werden sollen. Der Operator liest die Konfiguration, holt die entsprechenden Secrets aus der externen Quelle ab und erstellt Kubernetes-Secrets, die diese Geheimnisse enthalten. Durch die Verwendung des External-Secrets Operators können Entwickler und Administratoren Secrets zentral in externen Secrets-Stores verwalten, während Anwendungen in Kubernetes weiterhin die nativen Kubernetes-Secrets verwenden. Dies erhöht die Sicherheit und ermöglicht die zentrale Verwaltung von Secrets, vereinfachte Zugriffskontrollen und Lifecycle-Management.

Der External-Secrets Operator kann über Helm oder kubectl installiert werden. Mit Helm sieht das folgendermassen aus:

1
2
3
$ helm repo add external-secrets https://godaddy.github.io/kubernetes-external-secrets/
$ helm repo update
$ helm install external-secrets external-secrets/kubernetes-external-secrets

Secret Store erstellen

Wenn der Operator installiert ist, müssen wir uns als erstes einen SecretStore erstellen. Dazu erstellen wir uns eine Datei mit dem Namen secret-store.yml mit dem folgenden Inhalt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
apiVersion: external-secrets.io/v1beta1
kind: SecretStore # You could also choose ClusterSecretStore 
metadata:
  name: secret-store-vault
  namespace: external-secret-test
spec:
  provider:
    vault:
      server: "https://your-vault-server:8200"
      # Version is the Vault KV secret engine version.
      # This can be either "v1" or "v2", defaults to "v2"
      version: "v2"
      auth:
        # points to a secret that contains a vault token
        # https://www.vaultproject.io/docs/auth/token
        tokenSecretRef:
          name: "vault-token"
          key: "token"

Wir könnten hier auch einen globalen ClusterSecretStore erstellen. Dieser müsste nicht pro Namespace definiert werden, sondern wäre über alle Namespaces verfügbar.

Wir definieren nun noch die Adresse unseres Vault-Servers, sowie die Version der KV (Key Value) Secret Engine. In der KV Secret Engine v2 wird nun auch die Versionierung von Secrets unterstützt. Nun erstellen wir einen Namespace und erstellen darin den SecretStore:

1
2
3
4
$ kubectl create namespace external-secret-test
$ kubectl apply -f secret-store.yml

$ secretstore.external-secrets.io/secret-store-vault created

Wie wir sehen können, konnte der Store erfolgreich erstellt werden. Schauen wir, ob er auch ordnungsgemäss funktioniert:

1
2
3
4
$ k get secretstore -n external-secret-test

NAME                 AGE   STATUS                  CAPABILITIES   READY
secret-store-vault   45s   InvalidProviderConfig                  False

Wir kriegen den Status InvalidProviderConfig, es scheint also noch irgendwas falsch zu laufen. Schauen wir uns kurz an, was genau schief läuft:

1
2
3
4
5
6
7
8
9
$ kubectl describe secretstor secret-store-vault -n external-secret-test

Name:         secret-store-vault
Namespace:    external-secret-test
....
Events:
  Type     Reason                 Age                   From          Message
  ----     ------                 ----                  ----          -------
  Warning  InvalidProviderConfig  7m29s (x19 over 29m)  secret-store  cannot get Kubernetes secret "vault-token": secrets "vault-token" not found

Wir sehen, dass der Token, den wir in unserer Konfiguration angegeben haben, nicht existiert. Diesen Token müssen wir natürlich zuerst in unserem Vault generieren.

Einen Token generieren

Ich gehe in diesem Techup davon aus, dass der Vault Server bereits vollständig aufgesetzt ist. Wenn dies noch nicht der Fall ist, so müsste man diesen erst aufsetzen. Eine detaillierte Anleitung dazu findest du direkt auf der Hashicorp Vault Seite. Für unsere Applikation brauchen wir nun einen entsprechenden Vault Benutzer. Hier könnte man zum Beispiel unter Access -> AuthMethods eine neue userpass Methode hinzufügen. Wie der Name schon sagt, kann man damit einen Benutzer anlegen, welcher sich dann mit einem Passwort authentifizieren kann.

image-20230322063519808

In der userpass Auth Methode können wir uns diesen neuen Benutzer erstellen:

image-20230322072435202

Wenn der Benutzer erstellt wurde, müssen wir uns bei Vault mit diesem einloggen, um den erforderlichen Token zu bekommen. Wir wählen in der Login Maske als Methode Username und geben die zuvor vergebenen Zugangsdaten ein. Anschliessend können wir über das Benutzermenu den Token des Benutzers kopieren.

image-20230322072617638

image-20230322072637073

Nun müssen wir das Secret in unserem Namespace entsprechend hinterlegen. Dies können wir am schnellsten mit kubectl erledigen:

1
2
$ kubectl create secret -n external-secret-test generic vault-token \
    --from-literal=token='your-copied-token'

Nun wollen wir schauen, ob der Secret Store nach Anlegen des Secrets korrekt funktioniert:

1
2
3
4
$ kubectl get secretstores -n external-secret-test

NAME                                                 AGE    STATUS   CAPABILITIES   READY
secret-store-vault            											 2m     Valid    ReadWrite      True

Wir sehen nun, dass der Status “Valid” ist und der Secret Store funktioniert! 🎉

Sobald der Secret Store erfolgreich eingerichtet wurde, lassen sich die externen Secrets verwalten. Auch hierzu bietet uns der External-Secrets Operator eine CRD. Bevor wir das External-Secret jedoch einrichten, müssen wir dieses erstmal im Vault anlegen. Unter kv2 erstellen wir ein Secret mit dem Pfad application/secrets. Hier können wir nun ein Key/Value-Paar anlegen mit dem Inhalt MY_TOKEN=1234567890:

image-20230428140842237

Wenn wir das Secret erstellt haben, können wir weitere Datei namens external-secret.yml mit dem nachfolgenden Inhalt erstellen. Danach legen wir das Secret mit kubectl apply -f external-secret.yml an:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: external-secret-github
  namespace: external-secret-test
spec:
  refreshInterval: "15s" # The secrets will be refreshed every 15 seconds
  secretStoreRef:
    name: secret-store-vault # Reference to the secret store we have created above
    kind: SecretStore 
  target:
    name: my-secret # K8S secret name
  data:
    - secretKey: MY_TOKEN # K8S secret key in the secret
      remoteRef:
        key: kv2/application/secrets # Path to the vault secret
        property: MY_TOKEN # Property name of the secret 

Schauen wir uns wieder an, ob alles ordnungsgemäss funktioniert:

1
2
3
4
$ kubectl get externalsecret -n external-secret-test

NAME                        STORE                         REFRESH INTERVAL   STATUS         READY
external-secret-github      secret-store-vault            15s                SecretSynced   True

Um weitere Informationen zum Beispiel im Falle eine Fehlers zu erhalten, können wir diesen Befehl verwenden:

1
$ kubectl describe externalsecret external-secret-github -n external-secret-test

Das Secret sollte nun synchronisiert worden sein und wir können uns dieses nun anschauen:

1
2
3
4
5
6
$ kubectl get secret -n external-secret-test                                                                                                                            ⎈ do-fra1-dev-cluster
NAME                  TYPE                                  DATA   AGE
b-nova-dev            kubernetes.io/dockerconfigjson        1      37d
default-token-qrltt   kubernetes.io/service-account-token   3      37d
my-secret             Opaque                                1      15s
vault-token           Opaque                                1      37d

Wie wir sehen können, wurde das Secret mit dem Namen my-secret erfolgreich erstellt. Wenn wir uns den Inhalt anschauen sehen wir auch, dass der Inhalt dem entspricht, welcher auch im Vault hinterlegt wurde.

1
2
3
4
5
$ kubectl get secret my-secret -n external-secret-test -o jsonpath='{.data}' 
{"MY_TOKEN":"MTIzNDU2Nzg5MA=="}

echo "MTIzNDU2Nzg5MA==" | base64 -d
1234567890

Nun steht der erwartete Wert im Secret. Testen wir nun einmal, ob das Update eines Secrets auch funktioniert. Dazu erstellen wir im Vault eine neue Version des Secrets und ändern den Wert ab:

image-20230428141709340

Wir müssen jetzt noch maximal 15 Sekunden warten und sollten dann das neue Secret auch in Kubernetes sehen.

1
2
3
4
5
kubectl get secret my-secret -n external-secret-test -o jsonpath='{.data}' 
{"MY_TOKEN":"MDk4NzY1NDMyMQo="}

echo "MDk4NzY1NDMyMQo=" | base64 -d
0987654321

Perfekt, wir haben das neue Secret nun auch in unserem neuen Namespace verfügbar und können dieses über eine Secret-Referenz direkt in unserer Applikation als Umgebungsvariable oder als Volume nutzen. Eine detaillierte Anleitung dazu findest du direkt auf der Kubernetes Seite. 🤓

Zusammenfassung

Heute haben wir uns angeschaut, wie man seine Secrets zentral verwalten kann und diese ganz einfach mit dem Kubernetes Secrets Operator in einem Namespace synchronisieren kann. Es sind dafür keine Anpassungen am Code in der Applikation notwendig. Dies ist ein sehr eleganter Weg, da die Secrets erst dann ausgeliefert werden, wenn diese von der Applikation wirklich gebraucht werden. Bleib dran! 🚀

Stefan Welsch

Stefan Welsch – Manitu, Pionier, Stuntman, Mentor. Als Gründer von b-nova ist Stefan immer auf der Suche nach neuen und vielversprechenden Entwicklungsfeldern. Er ist durch und durch Pragmatiker und schreibt daher auch am liebsten Beiträge die sich möglichst nahe an 'real-world' Szenarien anlehnen.