Secrets Management in ArgoCD mit Kustomize und Helm

CH

Cedrik Hoffmann

7. August 2024

Stellen wir uns vor, wir möchten eine Anwendung mittels GitOps kontinuierlich ausliefern. Der Code ist ein absoluter Blickfang, die Helm Charts oder Kustomize-Dateien sind ein Traum und die Anwendung ist startklar zum Deployment. Aber da ist noch ein kleines Thema offen, das wir unbedingt klären müssen: Wie handhaben wir Secrets in unserem GitOps-Workflow? Lasst uns das in diesem Artikel zu Secrets Management in ArgoCD angehen und die Lösung finden!

Secrets in einem Projekt zu verwalten, ist wie das Aufbewahren der Haustürschlüssel. Du möchtest sicherstellen, dass sie sicher sind, aber im Notfall sollten die richtigen Personen Zugang haben, ohne die Tür eintreten zu müssen. ArgoCD - das GitOps CD-Tool, das wir hier bei PROGEEK verwenden - möchte sich nicht auf eine Lösung beschränken und beschreibt verschiedene Referenzen, wie man Passwörter, API-Schlüssel, Private Keys etc. mit externen Tools verwalten kann. In der Auflistung sind viele verschiedene Lösungen ersichtlich, welche verschiedene Use Cases abdecken. Deshalb sollten wir erstmal unsere Anforderungen definieren.

Was sind die Anforderungen?

Wir verwenden Helm Charts und Kustomize, um Anwendungen bereitzustellen. Daher müssen diese beiden Werkzeuge unterstützt werden. Die Secrets werden alle als Manifestdateien zum Code im Repository abgelegt. Einige Secrets - wie z.B. Access Tokens - werden zur Build-Zeit der Anwendung generiert und dann ebenfalls im Repository abgelegt. Es sollte möglich sein, dass jeder Mitarbeiter Secrets verschlüsseln kann, aber nicht jeder sollte sie auch wieder entschlüsseln können. Der Workflow sollte "Developer friendly" sein und eine einfache und einheitliche Lösung bieten. Eine weitere Überlegung, die allerdings nichts mehr mit dem Titel dieses Artikels zu tun hat, ist, dass es im besten Fall auch eine einheitliche Lösung außerhalb von ArgoCD zum Speichern von Secrets in Repositories gibt. Fassen wir dies mal in einer Liste zusammen:

  • Secrets werden im Git Repository gespeichert
  • Es können Secrets zur Build-Zeit generiert werden
  • Es werden hauptsächlich Helm Charts und Kustomize verwendet
  • Jeder Mitarbeiter soll verschlüsseln können, nicht aber entschlüsseln
  • Leichte und einfache Bedienung
  • Einfache Integration in GitHub Actions
  • Optional: auch außerhalb von ArgoCD / k8s Cluster verwendbar

Nach einigen Recherchen und Tests blieben zwei Kandidaten übrig: Bitnami Sealed Secrets und SOPS. Die Wahl zwischen Bitnami Sealed Secrets und SOPS für unser Projekt zu treffen, fühlte sich an wie die Entscheidung zwischen einem SUV und einem Geländewagen für eine Weltreise: Beide bringen dich ans Ziel, aber nur einer gibt dir die Flexibilität, jeden noch so rauen Pfad zu befahren, während du im Komfort sitzt. Die Wahl fiel schließlich auf SOPS, da SOPS im Vergleich zu Sealed Secrets eine flexiblere und vielseitigere Variante zur Verschlüsselung von Secrets bietet. SOPS ist plattformunabhängig und kann unter anderem nicht nur zur Verschlüsselung von Kubernetes Secrets, sondern auch zur Verschlüsselung von Kubernetes-unabhängigen Dateien wie z.B. .env Dateien verwendet werden. Verschiedene Erweiterungen wie KSOPS für Kustomize und Helm Secrets für Helm bieten eine komfortable Anbindung.

Was genau ist SOPS?

SOPS, was für Secret OPerationS steht, ist ein Open-Source-Tool, mit dem geheimen Daten sicher verschlüsselt werden können. SOPS wurde von Mozilla entwickelt und verschlüsselt Datensätze auf der Ebene der kleinsten möglichen Komponente. Das bedeutet, dass nicht die gesamte Datei verschlüsselt wird, sondern nur die tatsächlichen Werte. In einer YAML-Datei beispielsweise bleiben die Schlüssel sichtbar, während die Werte verschlüsselt werden. Dies hat den Vorteil, dass aus der verschlüsselten Datei immer noch ersichtlich ist, was sie beschreibt bzw. wie sie aufgebaut ist, während die Werte unkenntlich bleiben. SOPS zu nutzen, um Secrets zu verschlüsseln, ist wie einen Safe in deinem Haus zu haben, der nicht nur deine Wertsachen schützt, sondern auch sicherstellt, dass du ganz genau weißt, was drinnen ist, ohne den Safe öffnen zu müssen – dank des durchsichtigen Displays an der Front. Dabei setzt SOPS hinter den Kullissen auf bereits bestehende Verschlüsselungsmethoden, was es vielseitig macht. Hier könnten externe Cloud Lösungen verwendet werden wie AWS KMS, Azure Key Vault oder GCP KMS. Aber auch für lokale Verschlüsselungsmethoden können bewerte Tools wie GPG oder Age verwendet werden. Bei PROGEEK haben wir uns für die Verwendung von Age entschieden.

Unser Testaufbau mit SOPS in minikube

Vor der Integrierung der neuen Methode, habe ich dies erst einmal lokal in minikube getestet, um einen Proof of Concept zu erstellen und zu verstehen, wie SOPS innerhalb eines Kubernetes Cluster funktioniert. Mein Ziel war es, unabhängig von ArgoCD ein Kubernetes Secret mittels KSOPS für Kustomize und Helm Secrets für Helm zu verschlüsseln und die verschlüsselte Datei in Kubernetes bereitzustellen. Alle Dateien, welche ich hier beschreibe, können in einen bereitgestellten GitHub Repository https://github.com/choffmann/sops-showcase eingesehen werden. Jetzt wird's technisch, legen wir los!

Vorbereitung

Neben der Installation von SOPS und Age wird für die Verschlüsselung eines Secrets mit SOPS ein Schlüsselpaar für den Private Key und den Public Key benötigt. Wie funktioniert die Asymmetrische Verschlüsselung? Stellt euch vor, ihr wollt eine supergeheime Nachricht verschicken, ohne dass irgendwer anders sie schnappen und lesen kann. Asymmetrische Verschlüsselung ist wie ein trickreicher digitaler Briefkasten, der das möglich macht. Ihr habt einen öffentlichen Schlüssel, den ihr mit der ganzen Welt teilen könnt - quasi wie die Adresse eures Briefkastens. Jeder, der euch etwas Geheimes schicken möchte, verwendet diesen öffentlichen Schlüssel, um die Nachricht zu verschlüsseln. Jetzt kommt der Clou: Nur ihr habt den privaten Schlüssel, quasi den Schlüssel zum Briefkasten, um den Brief (also die verschlüsselte Nachricht) zu öffnen und zu lesen. Niemand anders kann die Nachricht entschlüsseln, selbst wenn sie den öffentlichen Schlüssel haben. So bleiben eure Geheimnisse sicher, egal wohin sie auf ihrer digitalen Reise gehen. Wie bereits erwähnt, wird hier Age verwendet. Die Erzeugung des Schlüsselpaares kann mit folgenden Befehlen durchgeführt werden

$ age-keygen -o age.agekey

In der Ausgabe der Konsole wird direkt der Public Key ausgegeben. Die erstelle Datei age.agekey enthält den Private Key, wo aber auch der Public Key hinterlegt ist.

$ cat age.agekey
# created: 2024-08-05T12:29:04+02:00
# public key: age1tmmzfjmkv8me7eeu4m3pyunr39twmwjzuhgyg4qjffnsq4ap0ussxnhhdf
AGE-SECRET-KEY-1MFUZ60RLCSMCVEA9GLH8M82Y8JTT76J8VTDSEJJ056CUSZZUZZTSM3TYC9

Jetzt haben wir unseren Public Key, welchen auch die Mitarbeiter haben werden oder in Pipelines hinterlegt wird, um unsere Datei zu verschlüsseln. Dazu kann man diesen Key entweder beim Aufruf mit SOPS angeben oder in einer seperaten Datei .sops.yaml hinterlegen. Da wir nicht SOPS direkt verwenden, sondern mit Erweiterungen wie KSOPS oder Helm Secrets, ist es schlau, diese und eventuelle weitere Informationen in eine seperate Datei auszulagern. Die .sops.yaml sieht dann so aus:

creation_rules:
  - unencrypted_regex: '^(apiVersion|metadata|kind|type)$'
    age: age1tmmzfjmkv8me7eeu4m3pyunr39twmwjzuhgyg4qjffnsq4ap0ussxnhhdf

Diese Datei wird im Verzeichniss abgelegt, wo SOPS ausgeführt wird. SOPS erkennt dann automatisch diese Datei und verwendet diese. Age gibt dabei den Public Key an. Mittels unencrypted_regex kann angegeben werden, welche Schlüssel in der YAML Datei nicht verschlüsselt werden sollen. Da apiVersion, metadata, kind und type in der Manifestdatei nicht geheim gehalten werden müssen, werden diese dort angegeben. Um nun ein Kubernetes Secret zu verschlüsseln, wird SOPS wie folgt aufgerufen. Die Datei secrets.yaml wird mit dem Age schlüssel verschlüsselt, welche dann in eine neue Datei secrets.enc.yaml gespeichert wird.

sops -e secret.yaml > secret.enc.yaml

Nun wollen wir entlich die super geheimen Werte aus dem Secret Manifest verschlüsseln. Dazu wird SOPS im Terminal aufgerufen. Dieser Befehl nimmt sich die secret.yaml Datei und gibt den Wert in stdout zurück. Um die Ausgabe in einer Datei zu speichern, wird diese in secret.enc.yaml übergeben. Dieses Secret ist nun sicher verschlüsselt und die Datei secret.enc.yaml könnte in einem Git Repository hochgeladen werden.

KSOPS

KSOPS ist ein Kustomize Plugin, um SOPS Ressourcen zu verschlüsseln. Dafür wird ein Kustomize Generator erstellt, welcher die angebene Datei mit dem Private Key wieder entschlüsseln kann. Der Generator dient nur dazu, eine verschlüsselte Datei wieder zu entschlüsseln, verschlüsselt oder bearbeitet wird diese weiter mit SOPS. Der Generator sieht wie folgt aus:

apiVersion: viaduct.ai/v1
kind: ksops
metadata:
  # Specify a name
  name: example-secret-generator
  annotations:
    config.kubernetes.io/function: |
      exec:
        # if the binary is in your PATH, you can do
        path: ksops
        # otherwise, path should be relative to manifest files, like
        # path: /path/to/ksops
files:
  - ./secret.enc.yaml

KSOPS ist eine Binary, welche zuerst geladen werden muss. Befindet sich die Binary in der Umgebungsvariable PATH, kann dies direkt aufgerufen werden. Ansonsten kann der Pfad im Generator spezifisch angegeben werden. SOPS kennt aktuell nur den Public Key. Damit die Secrets allerdings auch wieder entschlüsselt werden können, wird der Private Key benötigt. Diese Aufgabe hat am Ende ArgoCD. Den Private Key haben wir bereits erstellt, welcher in der Datei age.agekey enthalten ist. Der gesamte Output der Datei kann unter $XDG_CONFIG_HOME/sops/age/key.txt hinterlegt werden.

Nun kann Kustomize mit den darauf folgenden Befehl die Kubernetes Manifest Dateien generieren. Dabei ist es wichtig, dass die Flags --enable-alpha-plugins und --enable-exec mitangegeben werden. Diese werden für KSOPS benötigt. Eine vollständige Übersicht, wie die Kustomize Dateien aufgebaut sind und zusammenhängen, kann im bereitgestellten GitHub Repository eingesehen werden.

kustomize build --enable-alpha-plugins --enable-exec .

INFO: Dadurch, da SOPS nun den Private Key hat, können die verschlüsselte Dateien direkt mittels SOPS bearbeitet werden. Durch den Aufruf von sops edit <file> entschlüsselt SOPS die Datei und öffnet ein Text Editor im Terminal, wo die Datei entschlüsselt bearbeitet werden kann. Wurde die Datei bearbeitet und der Text Editor geschlossen, wird die Datei wieder von SOPS verschlüsselt.

Helm Secrets

Um Secrets in einem Helm Chart zu verschlüsseln, wird das Helm Plugin Helm Secrets verwendet. Helm Secrets ist dabei die Schnittstelle zwischen Helm und SOPS. Im Gegensatz zu KSOPS, wo nur die Manifeste entschlüsselt werden, können mit Helm Secrets auch die Secrets verschlüsselt werden. Hier war mein Ansatz, die Secrets in gepflegter Helm-Manier in einer values.yaml Datei zu speichern. Somit wird hier nicht das Kubernetes Secrets Manifest verschlüsselt, sondern nur die YAML Datei, welche die Werte enthält. Das Kubernetes Secrets Manifest wird als Template definiert, welches dann mit den Werten aus den values.yaml Dateien gefüllt wird. Helm bietet die Möglichkeit, die Werte in verschiedene Dateien aufzuteilen. Dabei wird unterschieden zwischen Werten, die verschlüsselt werden (die Secrets) und nicht geheimen Werten, die offen einsehbar sind. Daraus ergibt sich folgende Ordnerstruktur:

.
├── README.md
├── templates
│   └── ...
├── secrets
│   ├── dev
│   │   ├── values.yaml     # unverschlüsselte Secrets (nicht im Repository)
│   │   └── values.enc.yaml # verschlüsselte Secrets (im Repository)
│   ├── stage
│   │   ├── values.yaml     # unverschlüsselte Secrets (nicht im Repository)
│   │   └── values.enc.yaml # verschlüsselte Secrets (im Repository)
│   └── prod
│       ├── values.yaml     # unverschlüsselte Secrets (nicht im Repository)
│       └── values.enc.yaml # verschlüsselte Secrets (im Repository)
├── values
│   ├── dev.yaml    # values.yaml für dev
│   ├── stage.yaml  # values.yaml für stage
│   └── prod.yaml   # values.yaml für prod
├── .sops.yaml
└── values.yaml     # globale values.yaml

Die Secrets werden in einem seperaten Ordner secrets/ abgelegt - optional auch für verschiedene Staging Umgebungen (dev, stage, prod). Durch diese Struktur muss allerdings auch die .sops.yaml angepasst werden.

creation_rules:
  - path_regex: 'secrets/dev/(.*).yaml'
    age: age1fcm7m3zekz38agqr2wyv62n5xuwhnyu4yq9unj3lftwervtq2d6qgc7auk # dev age key

  - path_regex: 'secrets/stage/(.*).yaml'
    age: age1fcm7m3zekz38agqr2wyv62n5xuwhnyu4yq9unj3lftwervtq2d6qgc7auk # stage age key

  - path_regex: 'secrets/prod/(.*).yaml'
    age: age1fcm7m3zekz38agqr2wyv62n5xuwhnyu4yq9unj3lftwervtq2d6qgc7auk # prod age key

Hier wird nun nicht mehr Key unencrypted_regex benötigt, da hier die Gesamte YAML Datei verschlüsselt wird. Dafür wird der Key path_regex verwendet, um die unterschiedlichen Staging Umgebungen anzugeben. Durch die Aufteilung in verschiedene Unterordner, ist es möglich, verschiedene Age Keys für die Staging Umgebungen zu definieren, sollten diese aufgeteilt werden und verschiedene Schlüssel verwenden.

Um nun die Werte zu verschlüsseln, wird helm secrets encrypt aufgerufen. Auch das direkte Bearbeiten ist mit helm secrets verfügbar. Dies benötigt allerdings auch die Einrichtung des Private Keys in $XDG_CONFIG_HOME/sops/age/key.txt. Der Befehl helm secrets edit ist equivalent zu dem SOPS Befehl sops edit. Mit Helm Secrets können die Secrets direkt in das Cluster mit dem Befehl helm secrets upgrade veröffentlicht werden.

# encrypt secrets
helm secrets encrypt secrets/dev/values.yaml > secrets/dev/values.enc.yaml # encrypt von dev secrets
helm secrets encrypt secrets/stage/values.yaml > secrets/stage/values.enc.yaml # encrypt von stage secrets
helm secrets encrypt secrets/prod/values.yaml > secrets/prod/values.enc.yaml # encrypt von prod secrets

# edit secrets
helm secrets edit secrets/dev/values.yaml > secrets/dev/values.enc.yaml # edit dev secrets
helm secrets edit secrets/stage/values.yaml > secrets/stage/values.enc.yaml # edit stage secrets
helm secrets edit secrets/prod/values.yaml > secrets/prod/values.enc.yaml # edit prod secrets

# upgrade secrets
helm secrets upgrade --install my-release . -f secrets/dev/values.enc.yaml # upgrade dev secrets
helm secrets upgrade --install my-release . -f secrets/stage/values.enc.yaml # upgrade stage secrets
helm secrets upgrade --install my-release . -f secrets/prod/values.enc.yaml # upgrade prod secrets

SOPS in ArgoCD einbinden

Um nun ArgoCD so umzubauen, dass die Secrets mit SOPS entschlüsselt und im Cluster veröffentlicht werden können, werden Patches als Manifestdateien für ArgoCD erstellt. Diese können dann mittels kubectl patch die Manifestdateien von ArgoCD überarbeiten. Aber was genau muss in ArgoCD geändert werden? Zunächst muss die ConfigMap mit dem Namen argocd-cm angepasst werden. Hier werden spezifische Angaben zum Kustomize Build Prozess gemacht, wie die benötigten Flags --enable-alpha-plugins --enable-exec oder spezifische Angaben zu Helm Secrets. Weiterhin muss das Deployment vom Repo Server argocd-repo-server angepasst werden. Hier werden mittels initContainers die benötigten Applikationen wie SOPS, KSOPS und Helm Secrets installiert. Zusätzlich werden Umgebungsvariablen für helm-secrets und SOPS gesetzt. ArgoCD benötigt außerdem Zugriff auf den privaten Schlüssel. Dieser kann als Secret im Cluster veröffentlicht werden. Die genauen Manifestdateien der Patches sind im GitHub Repository hinterlegt. Die Patches auf ArgoCD können mit folgendem Kommando eingespielt werden:

# deploy age key secret
age-keygen -o age.agekey
cat age.agekey | kubectl create secret generic sops-age --namespace argocd --from-file=keys.txt=/dev/stdin

# patch argocd
kubectl patch configmap argocd-cm -n argocd \
  --patch "$(curl https://raw.githubusercontent.com/choffmann/sops-showcase/main/argocd-cm-patch.yaml)"
kubectl patch deployment argocd-repo-server -n argocd \
  --patch "$(curl https://raw.githubusercontent.com/choffmann/sops-showcase/main/argocd-deployment-patch.yaml)"

Ready for Production

Und jetzt geht's los! Wir implementieren diese Methode und schauen, was passiert! Dank der gründlichen Vorbereitung konnte ich die Patches in unser Cluster anwenden, wie es ArgoCD erfordert – und es hat alles geklappt! Nach dem Neustart des Repo-Servers in ArgoCD hieß es Daumen drücken – und tatsächlich: Er lief ohne Probleme wieder hoch! ArgoCD läuft also einwandfrei! Und auch der erste Test, ein Demoprojekt für Helm und Kubernetes zu veröffentlichen, hat geklappt.

Und damit runden wir unsere aufschlussreiche Reise durch die verborgenen Winkel des Secret Managements in der GitOps-Welt mit einem speziellen Fokus auf die Nutzung von ArgoCD und SOPS ab. Wir sind tief in die Geheimnisse eingetaucht (im wahrsten Sinne des Wortes), wie SOPS zusammen mit Age Secrets verschlüsselt, unterstützt durch KSOPS für Kustomize und Helm Secrets für Helm Charts. Zusätzlich haben wir uns angeschaut, wie ArgoCD feinjustiert werden kann, um sicherzustellen, dass die verschlüsselten Secrets aus unserem Repository problemlos im Kubernetes Cluster entschlüsselt werden. Jetzt, ausgestattet mit diesem Wissen, können wir bei PROGEEK unsere Secrets sorgenfrei verschlüsseln und in Repositories hochladen, ohne Angst haben zu müssen, dass neugierige Augen sie entziffern könnten. Als Hinweis nochmal das GitHub Repository, wo die Gesamten Schritte nochmal zugänglicher sind: https://github.com/choffmann/sops-showcase.