5Secrets sicher verwalten

Was sind Secrets und warum spielen sie bei GitOps eine so große Rolle? Als Secrets betrachten wir alle Daten, die wir standardmäßig geheim halten wollen. Unter dieser Definition von schützenswerten Informationen sehen wir hauptsächlich folgende Kategorien:

  1. Zugangsdaten wie Usernamen mit Passwörtern, API-Keys, SSH Private Keys und Kubeconfig-Dateien
  2. Zahlungsmittel wie Kreditkartennummern
  3. Geschäftsgeheimnisse
  4. persönlich identifizierbare Informationen (PII) wie E-Mail-Adressen

Unverschlüsselt committete Secrets sind ein Risiko.

Im Kontext von GitOps spielen Secrets eine große Rolle, weil wir laut Prinzip 1 unser System deklarativ verwalten und laut Prinzip 2 die Deklarationen in etwas wie Git speichern wollen – Secrets wollen wir aber aus Sicherheitsgründen nicht unmittelbar in Git lagern. Unter den OWASP Top 10 von 20211, einer ausführlich analysierten Zusammenstellung häufig auftretender Sicherheitsschwachstellen in Software, nimmt die Kategorie »Verschlüsselungsfehler« (Cryptographic Failures) Platz 2 von 10 ein. Diese Kategorie hieß vorher »Preisgabe sensibler Daten« (Sensitive Data Exposure) und umfasst die Nutzung riskanter Verschlüsselungsalgorithmen, aber eben auch das Hartkodieren von Passwörtern. GitHub hat aus solchen Erfahrungen heraus bereits proaktiv Secret-Scanning auf öffentlichen Repositories aktiviert2, und auch andere SCMs wie GitLab bieten solche Funktionalitäten an.

Wir betrachten in diesem Kapitel die Möglichkeiten, die uns zur Verfügung stehen, um GitOps umzusetzen und trotzdem unsere Secrets sicher zu behandeln. Inhaltlich orientieren wir uns stark an dem sehr umfassenden Vortrag »100,000 Different Ways to Manage Secrets in GitOps« von Andrew Block (Red Hat) auf der GitOpsCon 20223. Mit folgenden Fragen beschäftigen wir uns:

  1. Wo können wir sinnvollerweise Secrets lagern und verwalten? (Diesen Ort bezeichnen wir im Folgenden als Secret-Store.) Wir betrachten das verschlüsselte Speichern im Repo und die externe Verwaltung.

    Die externe Verwaltung kann im Zielsystem, im CI-Server oder in einem dedizierten Tool erfolgen.

  2. Wie können Workloads kontinuierlich Secrets konsumieren? Wir betrachten native Kubernetes-Secrets, geteilte Volumes aus injizierten Sidecar-Containern und CSI-Mounts.

Nachdem wir diese Fragestellungen betrachtet haben, erweitern wir unsere Implementierung aus Abschnitt 3.3 auf Seite 51 um eine dieser Varianten, indem wir HashiCorp Vault und den External Secrets Operator4 (ESO) ins Spiel bringen.

5.1Secrets lagern und verwalten

5.1.1Secrets verschlüsselt im Repo speichern

Der einfachste Weg unsere Secrets zu verwalten, ohne sie im Klartext in Git abzulegen, ist sie verschlüsselt in Git abzulegen. Es gibt einige beliebte Werkzeuge, die in dieser Kategorie helfen:

Gut integriert: das Git-Repo als Secret-Store mit externem Master-Key

Das Grundprinzip ist bei allen diesen Tools ähnlich: Mithilfe eines Master-Keys werden ganze Dateien oder nur Werte verschlüsselt und in Git gespeichert. Damit wird das Git-Repo zum Secret-Store. Der Master-Key selbst muss außerhalb des Repos gelagert werden, damit Werte nicht direkt wieder entschlüsselt werden können.

Zur Illustration des Grundprinzips spielen wir ein kleines Beispiel mit SOPS und einem GPG-Key durch. Sagen wir, wir haben folgende Datei, in der wir alle Werte verschlüsseln wollen, aber nicht die Namen:

Listing 5–1
Beispiel-Datei example.yaml mit vertraulichen Werten

1

database:

2

username: technical-user

3

password: qfjJN4gGK77PeCSx

Zuerst erzeugen wir einen GPG-Key für unseren aktuellen User und verschlüsseln dann die Datei mit SOPS:

Listing 5–2
Erzeugen eines GPG-Keys und Verschlüsseln der Datei mit SOPS

1

gpg --quick-generate-key $(whoami)

2

SOPS_PGP_FP=$(gpg --with-colons --list-keys $(whoami) \

3

| grep 'fpr:' | tail -n1 | sed -E 's/fpr:*(.*):/\1/')

4

sops --pgp $SOPS_PGP_FP --encrypt example.yaml \

5

> example.enc.yaml

Die resultierende verschlüsselte Datei sieht dann ungefähr so aus (einige Werte haben wir der Darstellung halber mit »… « abgekürzt):

Listing 5–3
Verschlüsselte Variante der Beispiel-Datei

1

database:

2

username:

3

ENC[AES256_GCM,data:9GBzem8zRpg12AxHCaQ=,iv:X62b...,

4

tag:gbtA...,type:str]

5

password:

6

ENC[AES256_GCM,data:1UalOZYDlCtiglpBuAM2mw==,iv:Wt2r...,

7

tag:BfVC...,type:str]

8

sops:

9

# ...

10

mac:

11

image ENC[AES256_GCM,data:mAds...,iv:XOHV...,tag:FLgL...,type:str]

12

pgp:

13

- created_at: "2023-02-24T11:06:45Z"

14

enc: |

15

-----BEGIN PGP MESSAGE-----

16

# ...

17

-----END PGP MESSAGE-----

18

fp: 6390DE41093F5CC2FF6D885513A027C4225739EE

Die verschlüsselte Datei kann mit dem Befehl sops example.enc.yaml entschüsselt, editiert und wieder verschlüsselt werden, wenn der editierende Prozess Zugriff auf den Schlüssel hat, mit dem die Datei verschlüsselt wurde.

In Tabelle 5–1 stellen wir einen Vergleich an zwischen den drei genannten Tools.

Tab. 5–1
Vergleich von Tools für Secret-Verschlüsselung

image

SOPS als Empfehlung bei Secrets im Repo

SOPS bietet aus unserer Sicht in der aktuellen Tool-Landschaft den größten Nutzen: Sowohl Menschen als auch technische User können Dateien auf die gleiche Art und Weise entschlüsseln; man muss nur vor einem Apply dafür sorgen, dass die Manifeste entschlüsselt werden. Die Integration mit externen KMS erhöht die Resilienz, weil der Master-Key extern gelagert ist und nicht (wie bei Sealed Secrets) von der Verfügbarkeit eines Kubernetes-Clusters abhängt. Sehr positiv ist auch, dass die Unterstützung im umgebenden Ökosystem bei Helm, Flux und Argo CD groß ist, wie wir in Abschnitt 4.10 auf Seite 85 genauer beschreiben.

Sealed Secrets verbreitet, aber begrenzt auf ein Cluster

Unserer Erfahrung nach ist auch Sealed Secrets weit verbreitet. Es besticht durch einfache Konfiguration, solange man es in wenigen Clustern einsetzt. Andererseits macht man sich sehr abhängig vom Cluster, in dem der Sealed Secrets Operator läuft. Ist dieser Cluster nicht erreichbar, dann können keine damit verschlüsselten Secrets entschlüsselt werden. Sollte der Cluster sogar vollständig verschwinden, dann wären die Secrets nie wieder entschlüsselbar, außer man hat vorher den Private Key des Operators gesichert, wozu wir eindringlich raten.

Repo als Secret-Store hat bessere Recovery im Disaster-Fall.

Ein gewichtiges Argument für verschlüsselte Secrets im Git-Repo führt Schlomo Schapiro ins Feld in seinem Vortrag »Betriebliche Geheimnisse in der Cloud und offline verwalten«12: Wenn im Disaster-Fall das KMS des Cloud-Providers oder dessen Keys verschwinden, dann hätte man das gleiche Problem wie bei Sealed Secrets, wenn der verschlüsselnde Operator oder dessen Cluster verschwindet. Bei SOPS kann man über das Konfigurieren mehrerer Trust-Anchors festlegen, dass das Ver- und Entschlüsseln sowohl mit bestimmten Cloud-KMS als auch dateibasierten Keys (zum Beispiel age-Keys) möglich sein soll. Im Disaster-Fall können verschlüsselte Dateien mit der offline gelagerten Key-Datei entschlüsselt und mit neu provisionierten Cloud-KMS-Keys wieder neu verschlüsselt werden.

Repo als Secret-Store erfüllt Prinzip 2 besser.

In Abschnitt 5.1.2 auf Seite 105 werden wir sehen, dass kein Setup das GitOps-Prinzip der Unveränderlichkeit so gut erfüllt wie das Speichern von verschlüsselten Secrets in Git. Im gleichen Abschnitt werden wir auch genauer darauf eingehen, warum bei Git-basiertem Secrets Management die Developer Experience besser ist.

Verschlüsselte Secrets zu committen kann womöglich zu mehr Plaintext-Commits führen.

All die genannten Vorteile machen SOPS besonders dann attraktiv, wenn man stark erhöhte Ausfallsicherheit haben will oder keine Cloud-KMS nutzen kann oder will. Wir Autoren mahnen dennoch zur Vorsicht: Die weite Verbreitung von SOPS ist definitiv eine Verbesserung gegenüber Klartext-Secrets in Git.

Aber mit dem Repo als Secret-Store gewöhnen wir Entwickelnde daran, dass es unter gewissen Umständen akzeptabel ist, Secrets zu committen. Aus unserer Sicht kann daraus schnell eine »slippery slope« werden, sodass versehentliche Commits mit Plaintext-Secrets in solchen Setups wahrscheinlicher sind. Zwar lässt sich mit Pre-Push-Hooks in SCMs das Hochladen von Plaintext-Secrets blockieren, aber damit geht ein Wartungsaufwand einher, und keine solche Filterung wird immer lückenlos sein. Stattdessen empfehlen wir als Startwert die Nutzung eines externen Secret-Stores, um dieses Risiko zu verringern.

5.1.2Secrets extern verwalten

Secrets im Zielsystem verwalten

Wir können unser Zielsystem (meist ein Kubernetes-Cluster) zum Secret-Store machen, indem wir unsere Secrets manuell als native Kubernetes-Secrets erstellen (beispielsweise mit kubectl create secret). Solche Wege können sich (zumindest anfangs) lohnen, wenn der manuelle Aufwand zu verkraften ist und erprobte Runbooks dafür im Team vorhanden sind.

Kubernetes-Secrets sind nur flüchtig.

Allerdings haben wir keine Garantien, dass die Secrets, die wir heute erstellt haben, morgen noch vorhanden sind. Wenn wir das Löschen von Secrets nicht sehr streng mit RBAC begrenzen, können wir unsere Secrets jederzeit durch Missgeschick oder Böswilligkeit verlieren. Nur regelmäßige Backups der Secret-Manifeste (beispielsweise mit kubectl get secret -o=yaml) oder der Inhalte von etcd (beispielsweise mit Velero13) an einen mit Verschlüsselung konfigurierten Ort außerhalb des Clusters (beispielsweise ein verschlüsselter AWS-S3-Bucket) können hier etwas Abhilfe schaffen. Als primärer Mechanismus für Secrets-Verwaltung lohnt sich dieser Weg auf Dauer nicht.

Secrets im CI-Server verwalten

Zentralisierung ist mit CI vereinfacht.

Eine Verbesserung gegenüber Git und Kubernetes als Verwaltungsebene für Secrets sind CI-Server. GitHub Actions beispielsweise ermöglicht das Festlegen von Umgebungsvariablen auf Repo- und Organisationsebene14, GitLab ermöglicht zusätzlich das Verwalten von Secrets-Dateien15 und Jenkins bietet über das Credentials-Plugin16 verschiedenste Secrets-Typen an. Meist werden diese Secrets automatisch in Logs maskiert, sodass man aus CI-Logs keine Secrets auslesen kann.

Die manuellen Aufwände im vorherigen Ansatz (»Secrets im Zielsystem verwalten«) können in dieser Herangehensweise großteils automatisiert werden: Wir speichern Templates unserer Secrets als Manifeste in Git, lassen sie von unserem GitOps-Operator ignorieren (beispielsweise indem wir sie in eine Kustomization eintragen, die nicht von Argo CD überwacht wird) und tragen als Werte Umgebungsvariablen ein. Eine CI-Pipeline nimmt diese Manifeste, interpoliert die im CI-Server hinterlegten Werte hinein (beispielsweise mit envsubst), verbindet sich in den Cluster und rollt die Manifeste aus.

CI-Server sind beliebte Angriffsziele.

Wie wir allerdings am Anfang in Abschnitt 2.10 auf Seite 45 gesehen haben, sind CI-Server gern genutzte Angriffsvektoren für Eindringlinge. Sicherlich wird es nie ganz gelingen, Secrets aus CI-Servern fernzuhalten. Dennoch sehen wir es als erstrebenswert an, wenn so wenig Secrets wie möglich in CI-Servern liegen.

Außerdem benötigen wir mit diesem Ansatz wiederum Zugriff vom CI-Server auf das Zielsystem, um Secrets auszurollen – ein Sicherheitsrisiko, das wir durch den Einsatz von GitOps ursprünglich vermeiden wollten.

Secrets in dediziertem Service verwalten

Alternativ zu CI-Servern kann man einen dedizierten Service nutzen, um Secrets zu verwalten. Uns sind folgende Optionen häufig begegnet:

Externe Secret-Stores bieten bessere Zugriffskontrolle und dedizierte Secrets-Verwaltung.

Ohne Frage entstehen beim Integrieren eines externen Secret-Stores in eine bestehende Landschaft zusätzliche Aufwände. Der Aufwand kann zusätzlich steigen, wenn man sich entscheidet, den Store in der eigenen Infrastruktur selbst zu betreiben. Auch Lizenzkosten fallen bei manchen dieser Tools an. Viele dieser Werkzeuge bieten allerdings zusätzliche Vorteile, die in den vorherigen Szenarien nicht möglich sind:

An dieser Stelle kürzen wir ab und gehen noch nicht auf konkrete Tools zum Einbinden eines externen Secret-Stores ein. Erst in Abschnitt 5.2.1 auf Seite 108 greifen wir diese Thematik wieder auf, wenn wir betrachten, wie wir Secrets aus solchen externen Secret-Stores konsumieren können. Wir betrachten aber noch kurz ein Henne-Ei-Problem, das sich bei externen Secret-Stores auftut:

Wenn wir einen externen Secret-Store verwenden, wollen wir alle Secrets dort verwalten und keine Secrets mehr manuell im Cluster anlegen. Dennoch brauchen wir in den meisten Fällen einen initialen Zugriff auf den Secret-Store, den wir manuell hinterlegen müssen.

Mit Workload Identity sind keine Verbindungs-Secrets nötig.

In manchen Fällen können wir diese Notwendigkeit vollständig umgehen. Nehmen wir als Beispiel einen EKS-Cluster, der versucht, auf Secrets im Secrets Manager desselben AWS-Accounts zuzugreifen: Wenn dem EC2-Instanzprofil der Worker Nodes eine entsprechende IAM-Rolle hinzugefügt wird, dann kann der Zugriff auf den Secrets Manager unmittelbar erfolgen ohne weitere Zugangsdaten. Solche Vorgehensweisen sind oftmals sicherer, weil keine Zugangsdaten im Spiel sind, die im schlechtesten Fall kompromittiert werden könnten. Statt auf dem reinen Wissen von Zugangsdaten, in dessen Besitz auch ein nicht legitimer Akteur gelangen kann, basiert die Zugriffsberechtigung auf der Identität des Workloads , die schwerer zu fälschen ist.

Workload Identity über Cloud-Provider-Grenzen hinaus

Dieses Prinzip von Workload Identity ist dann am leichtesten zu implementieren, wenn ein Kubernetes-Cluster und der Secret-Store sich beim selben Cloud-Provider befinden. Darüber hinaus bieten aber auch einige Cloud-Provider Möglichkeiten, über die Grenzen ihres Territoriums hinaus sich als bestimmte Workloads auszuweisen. Als Beispiele nennen wir an dieser Stelle kube2iam für AWS17 und azwi für Azure18.

Manuelles Provisionieren und Rotieren als Fallback

Wenn wir jedoch keine andere Wahl haben, als Zugangsdaten zu provisionieren, dann sind wir zurück bei den Varianten »manuell deployen« und »per CI deployen« (siehe Abschnitt 5.1.2 auf Seite 102). Da es sich hier in der Regel um ein einziges Secret pro Cluster oder zumindest Namespace handelt, ist der manuelle Aufwand gering, und um die Menge an Secrets in CI-Servern zu reduzieren, ist der manuelle Weg hier meistens der gesündeste. Diese Zugangsdaten können dann logischerweise ausschließlich manuell rotiert werden.

Implikationen für den Umgang mit Secrets

Externe Secret-Stores verletzen GitOps-Prinzip 2.

Wenn wir einen externen Secret-Store verwenden, dann behandeln wir unsere Secrets fast ohne GitOps, denn das, was wir faktisch in Git hinterlegen, ist nicht das Secret selbst, sondern nur eine Referenz auf das Secret. Das verletzt vor allem Prinzip 2 hinsichtlich der Unveränderlichkeit, ähnlich wie es das Referenzieren eines Rolling-Image-Tags (zum Beispiel latest) tut: Die Manifeste sind dann nicht mehr deterministisch , weil das Anwenden derselben Manifeste zu unterschiedlichen Zeitpunkten unterschiedliche Ergebnisse erzielen kann. Ebenso leidet die Auditierbarkeit der Secrets darunter.

Einige Provider speichern tatsächlich unveränderliche Versionen beim Bearbeiten eines Secrets (darunter beispielsweise AWS Secrets Manager, AWS Parameter Store, HashiCorp Vault, Keeper Security, Scaleway, Delinea), sodass wir diese exakten Versionsreferenzen tendenziell nutzen könnten. Allerdings müssten wir dann auch sicherstellen, dass bei jedem Erzeugen einer neuen Version ein Commit auf ein Config-Repo getriggert wird. Solche Integrationen sind bisher in keinem der genannten Provider nativ verfügbar und würden zusätzlichen Implementierungsaufwand bedeuten.

Schlechtere Developer Experience bei lokal benötigten Secrets

Die Nutzung eines externen Secret-Stores kann auch Nachteile hinsichtlich der Developer Experience haben: Wenn Entwickelnde zum lokalen Entwickeln gewisse Secrets benötigen, können sie diese mit Ansätzen wie SOPS ohne viel Aufwand entschlüsseln. Und wenn ein Team bereits Tools zum Verschlüsseln von Secrets in Git verwendet, dann sind im besten Fall auch schon Mechanismen aufgesetzt, um zu verhindern, dass Secrets im Klartext committet werden. Ohne solche Ansätze müssen Entwickelnde erst Zugriff auf den Secret-Store haben, dort vielleicht manuell das passende Secret finden und es dann lokal in einer von Git ignorierten Konfigurationsdatei eintragen. Mit der CLI des jeweiligen Secret-Stores lassen sich manche Schritte sicherlich automatisieren, aber damit schafft man eine Trennung, bei der Entwickelnde und Kubernetes-Cluster unterschiedliche Schnittstellen auf die Nutzung von Secrets haben.

Unversionierte Ressourcen können nativ keinen automatischen Restart triggern.

Dass wir Secrets außerhalb von Git verwalten, bringt noch weitere Herausforderungen mit sich: Das Rotieren von Secrets ist auf der Provider-Seite komfortabel machbar (beispielsweise durch Ändern des Wertes in HashiCorp Vault), aber das Propagieren von geänderten Werten in die Workloads funktioniert nicht unbedingt automatisch, weil die Secrets ja dann außerhalb des Lebenszyklus des Config-Repos verwaltet werden. Sind Secrets als Volume gemountet und die Anwendung ist so implementiert, dass sie Dateien auf Änderungen überwacht, ist dies zwar möglich. Viele Anwendungen laden Dateien jedoch oft nur einmalig beim Start, und Umgebungsvariablen, über die Secrets in Prozesse gelangen (siehe Abschnitt 5.2), können generell nicht zur Laufzeit geändert werden.

Wer seine Secrets verschlüsselt in Git lagert, kann mit Kustomize oder Helm auf einfachste Weise Prüfsummen in die Ressourcennamen einbauen: Ändert sich der Dateiinhalt, dann ändert sich der Ressourcenname (automatisch bei Kustomize19) oder eine Annotation (manuell hinzugefügt bei Helm20) und damit auch das Manifest (beispielsweise ein Deployment-Manifest). Wird die Änderung ausgerollt, werden automatisch auch neue Pods erzeugt, die den neuen Secret-Wert auslesen.

Reloader ermöglicht automatische Restarts bei geänderten Kubernetes-Secrets.

Am einfachsten und skalierbarsten lässt sich dieses Problem lösen mit Tools wie Reloader von Stakater21: Bei jedem Pod-Controller (einem Deployment beispielsweise), der eine bestimmte statische Annotation trägt, werden alle eingebundenen ConfigMaps und Secrets überwacht, und bei Änderungen der Inhalte dieser Ressourcen werden die Pods automatisch neu erzeugt.

Separate Zugriffskontrollen

Ein weiterer Zusatzaufwand bei einem externen Secret-Store ist, dass wir RBAC für das Verwalten von Secrets komplett separat von Git implementieren müssen. Wir können Secrets dann nämlich nicht mehr über den exakt gleichen Mechanismus wie unsere restlichen Manifeste verwalten (beispielsweise über Berechtigungen auf Repositories und Arbeiten über PRs). Andererseits kann genau das durchaus gewollt sein, wenn wir für Secrets ein anderes Sicherheitsniveau als für unsere anderen Ressourcen haben wollen.

Trotz all der genannten Schwierigkeiten sind externe Secret-Stores aus unserer Sicht der beste Weg, um mit Secrets im Kontext von GitOps zu arbeiten.

5.2Secrets konsumieren

In Kubernetes können wir Secrets grundsätzlich in zwei Formen in unsere Workloads einbinden:

  1. Wir injizieren sie als Umgebungsvariablen.
  2. Wir mounten sie als Dateien in einem Filesystem.

Je nach Applikation sind womöglich sogar beide Wege relevant: Eine Spring-Boot-Anwendung kann beispielsweise viele Properties über Umgebungsvariablen entgegennehmen, aber die Logging-Konfiguration muss oftmals als XML-Datei übergeben werden.

Das Bereitstellen über Umgebungsvariablen ist meist einfacher in der Handhabung, das Bereitstellen über Dateien hingegen deutlich flexibler. Datei-Mounts haben auch einen Geschwindigkeitsvorteil: Solche gemounteten Dateien werden automatisch im Pod aktualisiert, wenn von außen Änderungen daran vorgenommen werden, sodass (wenn die Anwendung dazu fähig ist) ein direktes Laden der neuen Konfiguration möglich ist ohne einen vollständigen Neustart der Anwendung.

Bei Umgebungsvariablen hingegen muss der Container insgesamt neu gestartet werden, damit geänderte Werte wirksam werden. Solche Neustarts können allerdings auch durchaus gewünscht sein, um zunehmende Entropie und daraus folgenden Drift durch langlebige Container zu vermeiden.

Wir werden in diesem Abschnitt das Hauptaugenmerk nicht auf die Details zwischen Umgebungsvariablen und Datei-Mounts legen, sondern auf die Art der Bereitstellung dieser Secrets. Hier besprechen wir drei Möglichkeiten:

  1. native Kubernetes-Secrets
  2. über Pod-Annotationen injizierte Sidecar-Container, die Secrets als Dateien in einen gemeinsamen, temporären Volume-Mount ablegen
  3. Volume-Mounts über den Secret Store CSI Driver

Eine weitere Möglichkeit bieten native Integrationen der GitOps-Operator, die wir in Abschnitt 4.10 auf Seite 85 betrachten: Bei Argo CD erlaubt das Vault-Plugin, Secrets aus verschiedenen Backends per Templating direkt in Kubernetes-Ressourcen einzufügen. Flux kann per SOPS verschlüsselte Kubernetes-Secrets entschlüsseln.

5.2.1Secrets als native Kubernetes-Secrets konsumieren

Dies ist der Weg, der am natürlichsten zu Kubernetes passt: Das Secret wird als Kubernetes-Secret zur Verfügung gestellt und ein Container in einem Pod kann es als Umgebungsvariable oder Datei mounten.

Nachfolgend sehen wir ein Beispiel für ein Secret, dessen Keys als Umgebungsvariablen in einen Pod gemountet werden:

Listing 5–4
Ein Pod mountet Daten eines Secrets als Umgebungsvariablen.

1

apiVersion: v1

2

kind: Secret

3

metadata:

4

name: database-connection

5

data:

6

DB_USERNAME: bXktYXBw

7

DB_PASSWORD: Mzk1MjgkdmRnN0pi

8

---

9

apiVersion: v1

10

kind: Pod

11

metadata:

12

name: secret-test

13

spec:

14

containers:

15

- name: nginx

16

image: nginx

17

envFrom:

18

- secretRef:

19

name: database-connection

Beste DevEx und Performance

Wir empfehlen grundsätzlich diejenigen Ansätze, die Secrets als native Kubernetes-Secrets bereitstellen, weil es keine Umstellung in der Arbeitsweise benötigt und damit die Developer Experience nicht beeinträchtigt. Außerdem können Kubernetes-native Secrets als eine Art Cache dienen, falls ein externer Secret-Store zwischenzeitlich nicht erreichbar sein sollte.

ESO integriert sich mit den allermeisten Secret-Stores.

Viele Anbieter von externen Secret-Stores haben im Lauf der Zeit ihre eigene Implementierung entwickelt, um Secrets aus ihrem Store als Kubernetes-Secrets in einen Cluster zu synchronisieren. 2019 entschlossen sich einige der Entwickelnden hinter diesen separaten Tools dazu, ihre Anstrengungen zu vereinen22. Als Ergebnis entstand der ESO, der mittlerweile ein CNCF-Sandbox-Projekt ist. Mittels ESO lassen sich alle Secret-Stores, die wir unter Abschnitt 5.1.2 auf Seite 103 aufzählen, einbinden und noch weitere. Wir haben bei der Recherche für diesen Abschnitt kein anderes Produkt im Kubernetes-Umfeld gefunden, das in der Lage ist, ein derart breites Spektrum an Secret-Stores über eine einheitliche Schnittstelle als native Secrets in einen Cluster zu synchronisieren.

Einer der Provider, die über ESO auch angesprochen werden können, sind GitLab Variables23. Damit können Secrets ausgelesen werden, die in GitLab als Variablen hinterlegt sind. Wir raten stark ab von der Lagerung von Secrets in SCMs und CI-Servern (siehe Abschnitt 5.1.2 auf Seite 102) und empfehlen die Nutzung eines alternativen Secret-Stores!

Der ESO bringt mehrere CRDs mit, von denen zwei für uns besonders wichtig sind: den SecretStore und das ExternalSecret. Der SecretStore ermöglicht die Anbindung an den Secret-Store und benötigt gegebenenfalls ein initial erstelltes Secret für die Authentifizierung mit dem Store. (Den auf einen Namespace beschränkten SecretStore gibt es auch in einer clusterweit verfügbaren Geschmacksrichtung als ClusterSecretStore.) Das ExternalSecret wiederum synchronisiert ein konkretes Secret in den Namespace, in dem das ExternalSecret sich befindet.

Wir zeigen nachfolgend ein Beispiel für einen SecretStore, der mithilfe eines manuell erstellen Kubernetes-Secrets namens gcpsmcredentials auf den Google Cloud Secret Manager zugreift:

Listing 5–5
Ein SecretStore mit Zugriff auf Google Cloud Secret Manager

1

apiVersion: external-secrets.io/v1beta1

2

kind: SecretStore

3

metadata:

4

name: gcp-store

5

spec:

6

provider:

7

gcpsm:

8

auth:

9

secretRef:

10

secretAccessKeySecretRef:

11

name: gcpsm-credentials

12

key: secret-access-credentials

13

# Name of Google Cloud project

14

projectID: alphabet-123

Das folgende ExternalSecret wird das Secret database_password in Google Cloud Secret Manager referenzieren. Der Operator wird daraus ein Kubernetes-Secret namens database-credentials erzeugen, das alle 10 Minuten aktualisiert wird und unter dem Key password den Base64-kodierten Wert aus dem Secret-Store enthält:

Listing 5–6
Ein ExternalSecret, aus dem ein natives Secret entsteht

1

apiVersion: external-secrets.io/v1beta1

2

kind: ExternalSecret

3

metadata:

4

name: database-credentials

5

spec:

6

refreshInterval: 10m

7

secretStoreRef:

8

kind: SecretStore

9

name: gcp-store

10

target:

11

name: database-credentials

12

creationPolicy: Owner

13

data:

14

- secretKey: password

15

remoteRef:

16

key: database_password

Das resultierende Secret wird folgendermaßen aussehen:

Listing 5–7
Ein natives Secret, das auf Basis eines ExternalSecrets erzeugt wurde

1

apiVersion: v1

2

kind: Secret

3

metadata:

4

name: database-credentials

5

data:

6

password: ZGF0YWJhc2VfcGFzc3dvcmQ=

Dieses native Secret können wir dann wie gewohnt in einer Pod-Spezifikation als Umgebungsvariable oder Volume einbinden.

Kubernetes-Secrets lagern immer unverschlüsselt in etcd.

In manchen stark regulierten Kontexten kann es Anforderungen hinsichtlich der Datensicherheit geben, wonach keine Secrets unverschlüsselt in etcd, der Datenbank der Kubernetes Control Plane, gespeichert werden dürfen. In solchen Fällen können wir Secrets ausschließlich als Dateien mounten und nicht als Umgebungsvariablen. (Über Umwege kann man beim Start eines Containers gemountete Dateien wiederum als Umgebungsvariablen bereitstellen; siehe dazu den Kasten »Gemountete Secrets als Umgebungsvariablen« in Abschnitt 5.2.3 auf Seite 113).

Base64-Kodierung von Kubernetes-Secrets

Dass die Werte von Kubernetes-Secrets immer Base64-kodiert sind, hat auf den Grad der Sicherheit keine Auswirkung, weil Base64 eine reine (symmetrische) Kodierung ist und keine Verschlüsselung: Wer den Base64-Wert sieht, benötigt keine Zusatzinformationen, um den Originalwert zu erhalten; dafür reicht bereits ein einfacher Shell-Befehl wie echo $ENCODED_VALUE | base64 -d.

Die beiden folgenden Abschnitte beschäftigen sich mit diesem Szenario: Welche Optionen haben wir, wenn wir Secrets mounten wollen, ohne sie im Klartext in etcd zu speichern?

5.2.2Secrets über Sidecar-Container injizieren

image

Abb. 5–1
Arbeitsweise eines Agent Injectors

Agent Injectors mounten Secrets als Dateien über Sidecar-Container.

Unter diesen beschränkten Umständen kann ein Agent Injector helfen. In einem Aufbau mit Agent Injector wird ein Sidecar-Container parallel zu den laufenden Containern in einen Pod »injiziert«. Dieser Sidecar-Container lädt Secrets aus einem externen Secret-Store, erstellt ein Volume mit Dateien auf dieser Basis und mountet dieses Volume in den annotierten Container. Das Injizieren des Sidecar-Containers geschieht über einen Mutating Admission Webhook24, der Manifeste verändern kann, während sie noch vom API-Server des Clusters validiert werden und noch nicht in etcd persistiert wurden.

In Abb. 5–1 ist visuell dargestellt, wie ein Agent Injector arbeitet. Wichtig zu wissen ist dabei, dass wir nur den Hauptcontainer selbst explizit spezifieren müssen. Alle anderen Ressourcen, die für den Mechanismus des Agent Injectors relevant sind, werden über Annotations am Workload gesteuert und davon ausgehend automatisch in das Manifest eingefügt. Das gewährleistet eine möglichst nahtlose Integration, sodass wir bis auf Annotationen nichts grundlegend an unseren Manifesten verändern müssen, um einen Agent Injector zu nutzen.

Hier zeigen wir beispielhaft ein Deployment, bei dem wir die Annotationen für den HashiCorp Vault Agent Injector25 im Pod-Template einfügen:

Listing 5–8
Deployment mit Annotationen für HashiCorp Vault Agent Injector

1

apiVersion: apps/v1

2

kind: Deployment

3

metadata:

4

name: nginx

5

spec:

6

# ...

7

template:

8

metadata:

9

# ...

10

annotations:

11

# Activate injector on this PodSpec.

12

vault.hashicorp.com/agent-inject: "true"

13

# Create file "/vault/secrets/db-creds".

14

vault.hashicorp.com/agent-inject-secret-db-creds:

15

database/creds/db-app

16

# Generate file content based on template.

17

vault.hashicorp.com/agent-inject-template-db-creds: |

18

{{- with secret "database/creds/db-app" -}}

19

postgres://{{ .Data.username }}:{{ .Data.password

20

}}@postgres:5432/appdb?sslmode=disable

21

{{- end }}

22

# Set Vault Kubernetes authentication role.

23

vault.hashicorp.com/role: db-app

24

spec:

25

containers:

26

- name: nginx

27

image: nginx:1.25.2

Aufgrund dieser Annotationen fügt der Agent Injector Folgendes in den Pods des Deployments hinzu:

Als Endergebnis entsteht im Container app die Datei /vault/secrets/db-creds mit dem interpolierten Inhalt aus dem Secret.

5.2.3Secrets über ein CSI-Volume konsumieren

Will man das Speichern von Secrets in etcd vermeiden, kommt neben einem Agent Injector auch der Secrets Store CSI Driver (SSCD)26 infrage. Dieser Driver implementiert das Container Storage Interface (CSI)27.

CSI ist eine Initiative, um die enorme Vielfalt an Volume-Typen verschiedenster Anbieter generisch abzubilden, statt in Kubernetes und anderen Orchestratoren für jeden Volume-Typ eine neue Schnittstelle zu schaffen. Der SSCD wiederum nutzt dieses CSI und ermöglicht es dadurch, Secrets aus externen Secret-Stores als native Volume-Mounts in Kubernetes-Pods einzubinden.

Zur Nutzung müssen sowohl der SSCD als auch ein geeigneter Provider im Cluster installiert werden. Momentan existieren offizielle Provider für folgende Secret-Stores:

Wir schauen uns ein konkretes Beispiel für den AWS Parameter Store an. Zuerst definieren wir eine SecretProviderClass, die unter dem YAML-Pfad .spec.paremters.objects ein oder mehrere Secrets verfügbar machen kann:

Listing 5–9
SecretProviderClass für AWS Parameter Store

1

apiVersion: secrets-store.csi.x-k8s.io/v1alpha1

2

kind: SecretProviderClass

3

metadata:

4

name: nginx-aws-secrets

5

spec:

6

provider: aws

7

parameters:

8

objects: |

9

- objectName: MySecret

10

objectType: ssmparameter

In einem Deployment können wir das Secret, das in der SecretProviderClass per objectName verfügbar gemacht wird, folgendermaßen mounten:

Listing 5–10
Deployment mit CSI-Mount

1

apiVersion: apps/v1

2

kind: Deployment

3

metadata:

4

name: nginx

5

spec:

6

selector:

7

matchLabels:

8

app: nginx

9

template:

10

metadata:

11

labels:

12

app: nginx

13

spec:

14

volumes:

15

- name: secrets-store-inline

16

csi:

17

driver: secrets-store.csi.k8s.io

18

readOnly: true

19

volumeAttributes:

20

secretProviderClass: nginx-aws-secrets

21

containers:

22

- name: nginx

23

image: nginx

24

volumeMounts:

25

- name: secrets-store-inline

26

mountPath: /mnt/secrets-store

27

readOnly: true

Unter dem Dateipfad /mnt/secrets-store/MySecret ist dann im Container der Inhalt des Parameters »MySecret« aus dem AWS Parameter Store vorhanden. Ein Mounten als Umgebungsvariable ist mit dem CSI Driver nur dann möglich, wenn wir zusätzlich den Inhalt in ein Secret synchronisieren lassen, was wir ja ursprünglich verhindern wollten.

Gemountete Secrets als Umgebungsvariablen

Ein Workaround dafür, der sowohl bei Agent Injectors als auch CSI-Mounts funktioniert, ist das Überschreiben des Container-Befehls, sodass beim Ausführen des Containers zuerst Umgebungsvariablen aus einer Datei exportiert werden, um sie für alle Prozesse im Container sichtbar zu machen, und dann der ursprüngliche Startbefehl ausgeführt wird. Mit einem solchen Schritt werden allerdings die spezifische Implementierung des jeweiligen Container-Entrypoints und das Einbinden der Umgebungsvariablen sehr eng miteinander gekoppelt.

CSI-Mounts sind Agent Injectors überlegen.

Im Vergleich zu CSI-Mounts führen Agent Injectors meist zu einem höheren Ressourcenverbrauch, und zwar sowohl beim Secret-Store (durch die größere Anzahl an Requests) als auch im Cluster: Beim Agent Injector läuft sowohl ein zentrales Injector-Deployment (für den Webhook) als auch zusätzlich ein Container pro Pod, während beim CSI Driver nur ein zentrales DaemonSet, also ein Pod pro Node, läuft.

Agent Injectors haben zusätzlich den Nachteil, dass sie meist spezifisch vom Anbieter des jeweiligen Secret-Stores gebaut werden. Der Secrets Store CSI Driver hingegen ist eine generische Schnittstelle, und jeglicher Anbieter eines Secret-Stores kann durch Implementieren eines Providers sich damit integrieren. Einige Anbieter von Agent Injectors, zum Beispiel AWS, haben bereits deren Weiterentwicklung eingestellt und empfehlen die Nutzung des Secrets Store CSI Driver.

5.3Wir erweitern die Beispielimplementierung

Als nächsten Schritt wollen wir den Umgang mit Secrets im GitOps-Kontext praktisch einüben. Wir erweitern dazu die Beispielimplementierung aus Abschnitt 3.3 auf Seite 51. Dazu nutzen wir HashiCorp Vault in einer selbstgehosteten Variante als externen Secret-Store und deployen den ESO in den Cluster.

Um das Setup so einfach wie möglich zu halten, werden wir HashiCorp Vault in denselben lokalen Cluster deployen wie unsere Workloads und den ESO. Berechtigterweise könnte man hier kritisieren, dass wir damit eben keinen externen Secret-Store haben, weil wir ihn im Zielsystem betreiben.

Das Prinzip bleibt dennoch dasselbe: Wir haben einen Secret-Store und nutzen den ESO als generische Schnittstelle zu diesem Secret-Store – wo dieser Secret-Store läuft, ist dem ESO prinzipiell egal. HashiCorp Vault könnte genauso gut außerhalb des Clusters laufen, und dennoch würde die Schnittstelle (nämlich das ExternalSecret des ESO) identisch bleiben.

Ähnlich wie für Kapitel 2 auf Seite 25 haben wir auch für dieses Kapitel eine mögliche fertige Lösung bereitgestellt im Ordner ch05/ im Beispiel-Config-Repo29.

Eine Alternative ist der GitOps Playground, der es ebenfalls ermöglicht, mit ESO, Vault und einer Beispielanwendung zu experimentieren. Hier kann man mit Befehl eine lokale Testumgebung aufsetzen30.

5.3.1Ziele

Wir wollen am Ende dieses Kapitels die Beispielimplementierung folgendermaßen erweitert haben:

  1. Wir haben einen zusätzlichen Namespace platform, in dem wir den ESO, HashiCorp Vault und Reloader als Helm-Charts betreiben. Wir wollen einen dedizierten Namespace dafür, weil wir diese Anwendungen nicht einem einzelnen Environment zuordnen (beispielsweise Dev oder Staging), sondern sie im ganzen Cluster verfügbar machen wollen.
  2. Wir betreiben HashiCorp Vault (allerdings in einem nicht produktionsreifen Zustand).
  3. Wir haben in HashiCorp Vault ein Beispiel-Secret.
  4. Der ESO authentifiziert sich mit HashiCorp Vault über einen ServiceAccount. Damit können wir ein Setup simulieren, das Workload Identity ähnelt.
  5. Wir haben ein ExternalSecret, das den Inhalt des Vault-Secrets in ein Kubernetes-Secret synchronisiert.
  6. Unsere Beispielanwendung bindet dieses Kubernetes-Secret als Umgebungsvariable ein.
  7. Wenn sich das Secret in HashiCorp Vault ändert, dann ändert sich auch das Kubernetes-Secret, und unsere Beispielanwendung wird automatisch neu gestartet.

Um diese Ziele zu erreichen, werden wir konkret in diesem Abschnitt folgende Schritte durchführen:

  1. Wir bootstrappen initiale Inhalte in einen frischen Cluster, ähnlich wie wir es in Abschnitt 3.5 auf Seite 54 getan haben.
  2. Wir erstellen einen neuen Namespace und deployen unsere drei neuen Anwendungen hinein.
  3. Wir verbinden den ESO mit HashiCorp Vault über einen ClusterSecretStore.
  4. Wir erstellen das Beispiel-Secret in HashiCorp Vault und synchronisieren es über ein ExternalSecret in ein Kubernetes-Secret.
  5. Wir binden das Kubernetes-Secret in unsere Beispielanwendung ein und testen, ob es ankommt.
  6. Wir verändern unser Secret in HashiCorp Vault und verifizieren, dass die Änderung automatisch in unserer Beispielanwendung ankommt.

5.3.2Datenfluss von HashiCorp Vault über ESO in den Cluster

Die folgenden eher mechanischen Details sind relevant, um den Datenfluss zwischen HashiCorp Vault und ESO besser zu verstehen. Damit der ESO in der Lage ist, Secrets von HashiCorp Vault zu lesen, benötigen wir folgende Zutaten31:

Wir haben hierbei folgenden Informationsfluss, den wir in Abb. 5–2 zusätzlich grafisch darstellen:

  1. Das originale Secret wohnt in HashiCorp Vault.
  2. Der ESO ist durch einen ServiceAccount berechtigt, auf das Secret in HashiCorp Vault zuzugreifen.
  3. Der ESO überwacht ein ExternalSecret, das dieses Secret referenziert, und erzeugt deshalb ein Kubernetes-Secret mit dem Inhalt des originalen Secrets von HashiCorp Vault.
  4. Das Deployment mountet das Secret.
  5. Reloader überwacht das Secret und triggert einen Neustart des Deployments, sobald der Inhalt des Kubernetes-Secrets sich ändert.
image

Abb. 5–2
Architektur mit ESO, HashiCorp Vault, Custom Resources und Secrets

5.3.3Schritt 1: Das Config-Repo bootstrappen

Wir setzen an diesem Punkt unseren Cluster noch einmal von vorne auf, damit wir die ganzen Referenzen auf den Ordner ch03/ aufräumen können. Dazu kopieren wir die Manifeste von ch03/ nach ch05/, ersetzen die Ordnerreferenzen und stellen Argo CD im Cluster wieder her aus den neu erstellten Manifesten:

Listing 5–11
Wiederaufsetzen des lokalen Clusters

1

# Copy contents.

2

cp -a ch03 ch05

3

 

4

# Rename references.

5

find ch05 -type f \

6

\( -name '*.yaml' -or -name '*.json' \) \

7

-exec sed -i '' 's!ch03/!ch05/!g' {} \;

8

 

9

# Commit files.

10

git add ch05

11

git commit -m "feat: copy ch03 to ch05"

12

git push

13

 

14

# Recreate cluster.

15

minikube delete

16

minikube start

17

 

18

# Bootstrap config repo.

19

export GIT_USER=YOUR_GITLAB_USERNAME

20

export GIT_TOKEN=GITLAB_PAT

21

export GIT_REPO=https://gitlab.com/gitops-book/erp-gitops/ch05

22

argocd-autopilot repo bootstrap --recover

Anschließend sollte der lokale Cluster wieder nur Argo CD ausführen inklusive der Beispielanwendung. Mit dem Port-Forwarding-Befehl, der nach dem Bootstrap-Recover-Befehl angezeigt wird, kannst du dich wieder in die UI von Argo CD einloggen.

Feature Branch

Wenn du die Änderungen im Ordner ch05/ auf einem Feature Branch durchführst, musst du in folgenden Dateien das Feld revision beziehungsweise targetRevision anpassen:

  1. ch05/apps/podinfo/overlays/podinfo-dev/config.json
  2. ch05/bootstrap/argo-cd.yaml
  3. ch05/bootstrap/cluster-resources.yaml
  4. ch05/bootstrap/root.yaml
  5. ch05/projects/podinfo-dev.yaml

5.3.4Schritt 2: Anwendungen in neuen Namespace deployen

Plattform-Namespace erstellen

Wir erstellen für unsere Zwecke nun den Namespace platform.

Namespace-Konzipierung

Bei vielen Tools, die man per Helm installieren kann, wird geraten, einen Namespace zu erzeugen, der den Namen des Tools trägt – quasi einen Namespace pro Tool. Wir empfinden diese Aufteilung oftmals als zu unübersichtlich. Stattdessen empfehlen wir als Startpunkt das Erstellen eines generischen Namespace (oder generischer Namespaces für zusammenhängende Tools wie secrets oder monitoring), in dem oder denen clusterweit genutzte Workloads deployt werden. Eine Aufteilung in zusätzliche Namespaces kann später immer noch je nach Kontext erfolgen.

Ein Beispiel kann Argo CD sein: Aufgrund seiner Bauweise haben wir oft kaum eine andere Wahl, als einen zusätzlichen Namespace erstellen zu lassen, in dem Argo CD läuft und in dem auch alle Applications wohnen (außer man nutzt »Applications in any namespace«, wie Abschnitt 4.11 auf Seite 88 beschreibt).

Für den Plattform-Namespace erzeugen wir einen neuen Ordner im Bereich cluster-resources und darin ein Namespace-Manifest:

Listing 5–12
Erzeugen des Plattform-Namespace

1

cd ch05/bootstrap/cluster-resources/in-cluster

2

mkdir -p platform

3

kubectl create ns platform --dry-run -o=yaml \

4

> platform/namespace.yaml

5

cd -

Untergeordnete Ressourcen erfolgreich deployen

Wenn wir diese neue Datei einfach so committen und pushen, wird Argo CD den Namespace wider Erwarten nicht unmittelbar erzeugen. Das liegt daran, dass unser ApplicationSet cluster-resources noch nicht passend konfiguriert ist: In der zugehörigen Datei cluster-resources.yaml im Ordner ch05/bootstrap/ ist unter dem YAML-Pfad .spec.template.spec.source.path der Dateipfad ch05/bootstrap/cluster-resources/{{name}} eingetragen, der korrekterweise interpoliert wird zum Ordner in-cluster, in dem unser neu erzeugter Unterordner platform liegt. Argo CD aggregiert die Ressourcen unterhalb dieses Pfades allerdings nur eine Ebene tief und nicht automatisch rekursiv. Dies werden wir als Nächstes aktivieren.

Eine weitere Änderung, die sich in diesem ApplicationSet anbietet, ist das Aktivieren von Pruning auf dem eben bearbeiteten ApplicationSet in ch05/bootstrap/cluster-resources.yaml. Diese Änderung ist nicht zwingend nötig, aber sie hilft uns, falls wir zu Reparaturzwecken die Application cluster-resources-in-cluster löschen wollen.

Wir bearbeiten die Datei ch05/bootstrap/cluster-resources.yaml also wie im folgenden Strategic Merge Patch32 dargestellt:

Listing 5–13
Strategic Merge Patch für cluster-resources.yaml

1

spec:

2

template:

3

spec:

4

source:

5

directory:

6

recurse: true

7

syncPolicy:

8

automated:

9

prune: true

Wenn wir nun das Namespace-Manifest und das ApplicationSet-Manifest committen und pushen, sollte der Namespace erzeugt werden.

Applications deployen

Argo CD Applications erzeugen mit Copy & Paste

Anschließend wollen wir unsere drei neuen Anwendungen deployen. Dafür erstellen wir neue Applications.

Eigene Argo CD Custom Resources schreiben

Wir könnten Applications mithilfe der Argo CD CLI erstellen, beispielsweise

An dieser Stelle gehen wir allerdings mit Copy & Paste vor. Unserer Erfahrung nach ist der Lerneffekt höher, wenn wir nicht irgendwelche anwendungsspezifischen Befehle zur Codegenerierung erlernen, sondern uns stattdessen direkt mit der eigentlichen Schnittstelle auseinandersetzen, nämlich den Kubernetes-Manifesten. Auf diese Weise ist außerdem die Einstiegshürde geringer, weil man keine zusätzliche CLI installieren und erlernen muss. Dazu zwei Empfehlungen:

Wir kopieren also das Application-Manifest von Argo CD selbst in drei neue Manifeste:

Listing 5–14
Erzeugen der Application für HashiCorp Vault

1

cd ch05/bootstrap

2

for app in external-secrets-operator reloader vault; do

3

cp argo-cd.yaml \

4

cluster-resources/in-cluster/platform/${app}.yaml

5

done

6

cd -

Manifeste anpassen für Anwendungen

Anschließend nehmen wir folgende Änderungen an den Manifesten vor:

  1. Wir identifizieren die jeweilige Application eindeutig über die Metadaten.
  2. Wir setzen den richtigen Ziel-Namespace.
  3. Wir überschreiben den Block .spec.source und ersetzen ihn durch die korrekte Referenz auf das jeweilige Helm-Chart.
  4. Wir setzen, wenn nötig, Inline-Values für das jeweilige Helm-Chart unter .spec.source.helm.valuesObject.

Das Manifest für den ESO ändern wir folgendermaßen ab:

Listing 5–15
Strategic Merge Patch für external-secrets-operator.yaml

1

metadata:

2

labels:

3

app.kubernetes.io/managed-by: argo-cd

4

app.kubernetes.io/name: external-secrets

5

name: external-secrets

6

spec:

7

destination:

8

namespace: platform

9

source:

10

chart: external-secrets

11

repoURL: https://charts.external-secrets.io

12

targetRevision: 0.9.4

Beim Manifest für Reloader gehen wir sehr analog vor:

Listing 5–16
Strategic Merge Patch für reloader.yaml

1

metadata:

2

labels:

3

app.kubernetes.io/managed-by: argo-cd

4

app.kubernetes.io/name: reloader

5

name: reloader

6

spec:

7

destination:

8

namespace: platform

9

source:

10

chart: reloader

11

repoURL: https://stakater.github.io/stakater-charts

12

targetRevision: 1.0.38

Das Manifest für HashiCorp Vault verändern wir folgendermaßen:

Listing 5–17
Strategic Merge Patch für vault.yaml

1

metadata:

2

labels:

3

app.kubernetes.io/managed-by: argo-cd

4

app.kubernetes.io/name: hashicorp-vault

5

name: hashicorp-vault

6

spec:

7

destination:

8

namespace: platform

9

source:

10

chart: vault

11

repoURL: https://helm.releases.hashicorp.com

12

targetRevision: 0.25.0

5.3.5Schritt 3: ESO mit HashiCorp Vault verbinden

HashiCorp Vault konfigurieren mit Autorisierung für ESO

Im nächsten Schritt setzen wir einige Values auf dem Helm-Chart von HashiCorp Vault, um Folgendes zu erreichen:

  1. Wir wollen HashiCorp Vault im Dev-Modus ausführen.
  2. Wir wollen keinen Agent Injector.
  3. Wir wollen die notwendige Autorisierung erstellen, damit ESO sich mittels seines ServiceAccounts mit HashiCorp Vault verbinden kann.
    • Dabei wollen wir das Erstellen von expliziten Credentials zum Verbinden vermeiden.
    • Stattdessen erstellen wir eine Autorisierung, die den Namen und Namespace des ServiceAccounts von ESO mit einer Read-Only-Policy verknüpft. Mit diesem Vorgehen können wir das Prinzip von Workload Identity im kleinen Maßstab demonstrieren.

Spar dir die Mühe, das folgende Snippet abzutippen, und kopiere es lieber aus dem Beispiel-Config-Repo.

Listing 5–18
Zweiter Strategic Merge Patch für vault.yaml

1

spec:

2

source:

3

helm:

4

valuesObject:

5

injector:

6

enabled: false

7

server:

8

dev:

9

enabled: true

10

postStart:

11

- /bin/sh

12

- -c

13

- |-

14

sleep 5

15

vault auth enable kubernetes || true

16

vault write auth/kubernetes/config \

17

kubernetes_host=

18

image "https://$KUBERNETES_SERVICE_HOST:

19

image $KUBERNETES_SERVICE_PORT_HTTPS"

20

vault policy write read-secrets - <<'EOF'

21

path "secret/*" {

22

capabilities = ["read"]

23

}

24

EOF

25

vault write \

26

auth/kubernetes/role/external-secrets \

27

bound_service_account_names=external-secrets \

28

bound_service_account_namespaces=platform \

29

policies=read-secrets

Als Nächstes erstellen wir einen ClusterSecretStore, damit ESO auf HashiCorp Vault zugreifen kann. Dazu erzeugen wir folgende Datei:

Listing 5–19
ClusterSecretStore erzeugen

1

cat <<'EOF' > ch05/bootstrap/cluster-resources

2

image /in-cluster/platform/secret-store.yaml

3

apiVersion: external-secrets.io/v1beta1

4

kind: ClusterSecretStore

5

metadata:

6

name: hashicorp-vault

7

annotations:

8

argocd.argoproj.io/sync-options:

9

SkipDryRunOnMissingResource=true

10

spec:

11

provider:

12

vault:

13

server: http://hashicorp-vault:8200

14

path: secret

15

auth:

16

kubernetes:

17

role: external-secrets

18

serviceAccountRef:

19

name: external-secrets

20

namespace: platform

21

EOF

Mit dem Wert unter dem YAML-Pfad .spec.provider.vault.server adressieren wir den Kubernetes-Service von HashiCorp Vault. Mit dem Block .spec.provider.vault.auth.kubernetes setzen wir genau die Authentifizierungsparameter, die wir auch in HashiCorp Vault konfiguriert haben.

Reihenfolge von Custom Resources und CRDs beeinflussen

In den Annotations haben wir eine Zusatzoption namens SkipDryRunOnMissingResource aktiviert. So wie wir momentan alle Ressourcen Schritt für Schritt hintereinander deployen, würde der ClusterSecretStore auch ohne diese Annotation funktionieren.

Wenn wir aber den Cluster komplett neu provisionieren würden, würde die Application cluster-resources-in-cluster beständig fehlschlagen. Das rührt daher, dass diese Application sowohl den ESO beinhaltet, der neben anderen CRDs auch die CRD des ClusterSecretStores erzeugt, als auch einen ClusterSecretStore. Also ist beim Deployen von beidem zusammen die CRD noch gar nicht installiert, auf deren Basis der ClusterSecretStore erstellt werden könnte.

Wir könnten instinktiv nach den Sync Waves35 von Argo CD greifen, um dieses Problem zu lösen. Damit kann man die Reihenfolge beeinflussen, in der Ressourcen deployt werden.

Dummerweise erwischt uns der Fehler aber noch vor dem Punkt des Deployens, weil er beim Rendern und Validieren der Manifeste auftritt und nicht beim darauffolgenden Apply. Dadurch kommen wir in dieser Konstellation nie an den Punkt, dass der ESO installiert werden kann und beim anschließenden Durchlauf alle nötigen CRDs vorhanden sind. Als elegante Lösung können wir den ClusterSecretStore bewusst beim Validieren fehlschlagen lassen, weil wir wissen, dass das Validieren und anschließende Deployen beim zweiten Durchlauf funktionieren wird – und genau das tun wir mit der obigen Annotation.

Nach dem Committen und Pushen dieser vier Manifeste sollte die Application cluster-resources-in-cluster neben den drei neuen Applications nach wenigen Minuten auch einen ClusterSecretStore in gesundem Zustand zeigen, ähnlich wie in Abb. 5–3.

5.3.6Schritt 4: Beispiel-Secret erstellen

Sobald HashiCorp Vault läuft, werden wir ein Beispiel-Secret über die UI erstellen. Um Zugriff auf die UI von HashiCorp Vault zu bekommen, aktivieren wir Port-Forwarding:

Listing 5–20
Port-Forward für HashiCorp Vault

1

kubectl -n platform port-forward \

2

sts/hashicorp-vault 8200:8200

Wenn wir http://localhost:8200 aufrufen, können wir uns mit dem Standardtoken »root« einloggen. Nun können wir (Mitte oben) die Secrets Engine »secret« öffnen und mit (rechts oben) »Create secret +« ein neues Secret erzeugen. Als »Path for this secret« wählen wir die Bezeichnung erp-gitops/database. Als »Secret data« setzen wir die Keys »username« und »password« auf jeweils beliebige Werte.

image

Abb. 5–3
Ein gesunder ClusterSecretStore

Nach dem Speichern sollte das finale Secret ungefähr wie in Abb. 5–4 aussehen.

Das Secret in den Cluster synchronisieren

Nun erzeugen wir ein ExternalSecret, um das Secret aus HashiCorp Vault in den Namespace zu synchronisieren, in dem unsere Beispielanwendung lebt. Dafür erzeugen wir folgende Datei:

Listing 5–21
ExternalSecret erstellen

1

# Create ExternalSecret

2

cd ch05/apps/podinfo/base

3

cat <<'EOF' > external-secret.yaml

4

apiVersion: external-secrets.io/v1beta1

5

kind: ExternalSecret

6

metadata:

7

name: database-credentials

8

spec:

9

refreshInterval: 1m

10

secretStoreRef:

11

kind: ClusterSecretStore

12

name: hashicorp-vault

13

target:

14

name: database-credentials

15

data:

16

- secretKey: username

17

remoteRef:

18

key: secret/erp-gitops/database

19

property: username

20

- secretKey: password

21

remoteRef:

22

key: secret/erp-gitops/database

23

property: password

24

EOF

25

 

26

# Update Kustomization

27

rm -f kustomization.yaml

28

kustomize create --autodetect

29

cd -

image

Abb. 5–4
Erstelltes Secret in HashiCorp Vault

Nach dem Committen dieser Dateien sollte in der Application podinfo-dev-podinfo ein ExternalSecret erscheinen, aus dem ein natives Secret erzeugt wird, ähnlich wie in Abb. 5–5.

image

Abb. 5–5
Ein gesundes ExternalSecret und natives Secret

Über die Kubernetes-CLI können wir zusätzlich sicherstellen, dass die Werte korrekt gesetzt wurden:

Listing 5–22
ExternalSecret erstellen

1

$ kubectl get secret database-credentials \

2

--template='{{.data.username | base64decode}}

3

image :{{.data.password | base64decode}}'

4

admin:foobar

5.3.7Schritt 5: Das Secret integrieren

Wir sind in der Lage, ein Secret in HashiCorp Vault zu verwalten und in einen Namespace in unserem Cluster zu synchronisieren, sodass es dort als Kubernetes-Secret liegt. Jetzt wollen wir dieses Secret auch konkret verwenden. Als einfachsten Anwendungsfall werden wir jetzt das komplette Secret als Umgebungsvariablen in die Beispielanwendung mounten. Zusätzlich werden wir eine Annotation am Deployment setzen, damit bei zukünftigen Änderungen am Secret-Inhalt das Deployment automatisch neu gestartet wird.

Dementsprechend wenden wir folgenden Strategic Merge Patch an auf der Datei ch05/apps/podinfo/base/deployment.yaml:

Listing 5–23
Strategic Merge Patch für das Deployment der Beispielanwendung

1

spec:

2

template:

3

metadata:

4

annotations:

5

reloader.stakater.com/auto: "true"

6

spec:

7

containers:

8

- name: podinfo

9

envFrom:

10

- secretRef:

11

name: database-credentials

Nachdem diese Änderung ausgerollt wurde und unser Deployment sich erfolgreich neu gestartet hat, können wir einen Port-Forward starten und auf dem Endpunkt /env verifizieren, dass das Secret erfolgreich in unsere Anwendung integriert ist. Starten wir zuerst den Port-Forward:

Listing 5–24
Port-Forward für die Beispielanwendung

1

kubectl -n default port-forward \

2

deploy/podinfo 9898:9898

Und in einem separaten Terminal schicken wir einen Request an den Endpunkt:

Listing 5–25
Die Anwendung hat korrekt das aktuelle Secret gemountet
.

1

$ curl http://localhost:9898/env

2

[

3

# ...

4

"password=foobar",

5

"username=admin",

6

# ...

7

]

5.3.8Schritt 6: Das Secret ändern

Als letzten Schritt wollen wir die Werte im Secret ändern und sehen, wie sie automatisch in der Anwendung ankommen. Dafür starten wir wieder einen Port-Forward für HashiCorp Vault, öffnen die UI im Browser und navigieren zu dem Secret, wie wir es in Abschnitt 5.3.6 auf Seite 126 bereits getan haben.

Nun werden wir die Werte des Secrets ändern:

  1. Wähle (rechts oben) »Create new version +«, um eine neue Version des Secrets zu erzeugen.
  2. Ändere jetzt nur die Werte von »password« und »username«, aber nicht die Namen.
  3. Wähle »Save«.

Wenn du schnell genug bist, wirst du in der UI von Argo CD sehen können, wie die Pods des Beispiel-Deployments sich innerhalb einer Minute automatisch updaten (siehe Abb. 5–6). Sollte der Restart zu schnell passiert sein, wiederhole einfach die Schritte von gerade eben und halte nebenher die UI von Argo CD offen.

image

Abb. 5–6
Die Pods werden automatisch von Reloader neu gestartet
.

Anschließend kannst du mit einem erneuten Aufruf des Port-Forward der Beispielanwendung und einem erneuten curl-Aufruf (wie im vorherigen Schritt 5) verifizieren, dass die neuen Werte im Deployment angekommen sind:

Listing 5–26
Die Anwendung hat die geänderten Secrets-Werte erhalten
.

1

$ curl http://localhost:9898/env

2

[

3

# ...

4

"password=asdf",

5

"username=podinfo",

6

# ...

7

]

5.4Fazit

Wir haben in diesem Kapitel ausführlich beleuchtet, welche Möglichkeiten wir haben, im GitOps-Umfeld Secrets zu lagern und zu konsumieren. Unsere generelle Empfehlung lautet folgendermaßen:

  1. Nutze einen externen Secret-Store und binde ihn über den ESO an.
    • Nutze nach Möglichkeit einen Secret-Store, der im selben Cloud-Provider läuft wie deine Workloads.
    • Verwende Workload Identity zur Authentifizierung ohne sensible Zugangsdaten.
    • Verwende Reloader, um Änderungen an Secrets automatisch in Workloads hinein zu propagieren, die diese nicht ohne Neustart aktualisieren.
  2. Wenn du keine nativen Kubernetes-Secrets nutzen kannst, verwende den Secrets Store CSI Driver. (Hier gelten dieselben Hinweise zu Cloud-Providern und Workload Identity wie beim vorherigen Szenario mit ESO.)
  3. Wenn du keinen externen Secret-Store verwenden kannst, verschlüssle deine Secrets in Git mit SOPS (oder bei wenigen Clustern mit Sealed Secrets).

Der allerersten Empfehlungsstufe sind wir direkt gefolgt und haben die Beispielimplementierung erweitert um HashiCorp Vault als Beispiel eines externen Secret-Stores und zusätzlich ESO und Reloader. Wir haben ESO über seine Workload Identity (in diesem Fall den Service-Account) bei unserem Secret-Store authentifiziert und ein Secret aus diesem Store in einen Namespace synchronisiert.

Durch diese Schritte haben wir eine Secrets-Verwaltung bekommen, die hohen Ansprüchen genügen kann:

  1. Vermeidung von Secrets in Git: Wir benötigen keinerlei Secrets in Git, weder verschlüsselt noch unverschlüsselt.
  2. Reduzierung von Secrets: Durch Workload Identity benötigen wir keinerlei sensible Zugangsdaten zum Secret-Store.
  3. Vereinbarkeit mit GitOps: Bis auf die reinen Inhalte unserer Secrets können wir alles komplett über GitOps steuern. Selbst alle Authentifizierung und Autorisierung auf Secrets wird vollständig versioniert und automatisiert verwaltet.
    Wir verstoßen bezüglich der Secret-Referenzen zwar gegen das Prinzip der Unveränderlichkeit, aber das nehmen wir für die anderen Vorteile in Kauf.
  4. gute Zugriffskontrollen, passendes Feature-Set: Durch eine dedizierte Anwendung zur Secrets-Verwaltung können wir Zugriffe auf Secrets besser steuern und haben passende Features wie Secret-Rotation eingebaut.
  5. Performance: Das Einbinden von Secrets ist über kurze Manifeste möglich. Änderungen an Secrets werden in hoher Geschwindigkeit automatisiert ausgerollt.