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:
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:
Die externe Verwaltung kann im Zielsystem, im CI-Server oder in einem dedizierten Tool erfolgen.
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.
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:
Wie Abschnitt 4.10 auf Seite 85 zeigt, bietet Flux sogar native Unterstützung für SOPS. In Argo CD können wir diese über den ebenfalls dort erwähnten CM-Plugin-Mechanismus mittels KSOPS nachrüsten8.
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
ENC[AES256_GCM,data:mAds...,iv:XOHV...,tag:FLgL...,type:str]
12
pgp:
13
- created_at: "2023-02-24T11:06:45Z"
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
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.
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.
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.
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.
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.
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.
In Kubernetes können wir Secrets grundsätzlich in zwei Formen in unsere Workloads einbinden:
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:
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.
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).
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?
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.
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.
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.
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.
Wir wollen am Ende dieses Kapitels die Beispielimplementierung folgendermaßen erweitert haben:
Um diese Ziele zu erreichen, werden wir konkret in diesem Abschnitt folgende Schritte durchführen:
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:
Abb. 5–2
Architektur mit ESO, HashiCorp Vault, Custom Resources und Secrets
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.
Wir erstellen für unsere Zwecke nun den Namespace platform.
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.
Argo CD Applications erzeugen mit Copy & Paste
Anschließend wollen wir unsere drei neuen Anwendungen deployen. Dafür erstellen wir neue Applications.
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:
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
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:
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
"https://$KUBERNETES_SERVICE_HOST:
19
$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
/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.
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.
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.
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 -
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.
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
:{{.data.password | base64decode}}'
4
admin:foobar
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
]
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:
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.
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
]
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:
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: