Automating Kubernetes Secrets with ArgoCD and SOPS (Day 30)
Automating Kubernetes secret management with ArgoCD, SOPS and helmfile for a fully GitOps-driven workflow.
So I've been using SOPS to encrypt secrets and storing them in git but the catch was I had to manually decrypt and pipe them to kubectl each time I needed to apply them to the cluster.
# The manual way
sops -d secret.yaml | kubectl apply -f -
Secret Management
ArgoCD is unopinionated about secret management, which is both a blessing and a curse. The blessing: flexibility. The curse: you have to figure it out yourself.
Since I was already using Helmfile, I decided to leverage the helm-secrets plugin that comes bundled with the Helmfile container image.
Setting It Up
First, I checked what plugins were available in the Helmfile container:
docker run --rm -it ghcr.io/helmfile/helmfile:v0.171.0 helm plugin list
NAME VERSION DESCRIPTION
diff 3.9.14 Preview helm upgrade changes as a diff
helm-git 0.16.0 Get non-packaged Charts directly from Git.
s3 0.16.2 Provides AWS S3 protocol support for charts and repos.
secrets 4.6.0 This plugin provides secrets values encryption for Helm charts
Perfect! The secrets plugin is already there.
Step 1: Create an AGE Key
Generate an age key (you could use your existing keys too):
age-keygen > key.txt
kubectl -n argocd create secret generic age --from-file=./key.txt
Step 2: Register the Plugin with ArgoCD
ArgoCD uses ConfigManagementPlugin system that can be configured in the helm values:
configs:
cmp:
create: true
plugins:
helmfile:
allowConcurrency: true
discover:
fileName: helmfile.yaml
generate:
command:
- bash
- "-c"
- |
if [[ -v ENV_NAME ]]; then
helmfile -n "$ARGOCD_APP_NAMESPACE" -e $ENV_NAME template --include-crds -q
elif [[ -v ARGOCD_ENV_ENV_NAME ]]; then
helmfile -n "$ARGOCD_APP_NAMESPACE" -e "$ARGOCD_ENV_ENV_NAME" template --include-crds -q
else
helmfile -n "$ARGOCD_APP_NAMESPACE" template --include-crds -q
fi
lockRepo: false
This config tells ArgoCD: "If you find a helmfile.yaml, use the helmfile command to process it."
Step 3: Add the Helmfile Container to the Repo Server
Then I added the helmfile container to ArgoCD's repo server:
repoServer:
extraContainers:
- name: helmfile
image: ghcr.io/helmfile/helmfile:v0.171.0
command: ["/var/run/argocd/argocd-cmp-server"]
env:
- name: SOPS_AGE_KEY_FILE
value: /app/config/age/key.txt
- name: HELM_CACHE_HOME
value: /tmp/helm/cache
- name: HELM_CONFIG_HOME
value: /tmp/helm/config
- name: HELMFILE_CACHE_HOME
value: /tmp/helmfile/cache
- name: HELMFILE_TEMPDIR
value: /tmp/helmfile/tmp
securityContext:
runAsNonRoot: true
runAsUser: 999
volumeMounts:
- mountPath: /var/run/argocd
name: var-files
- mountPath: /home/argocd/cmp-server/plugins
name: plugins
- mountPath: /home/argocd/cmp-server/config/plugin.yaml
subPath: helmfile.yaml
name: argocd-cmp-cm
- mountPath: /tmp
name: cmp-tmp
- mountPath: /app/config/age/
name: age
Note the SOPS_AGE_KEY_FILE
and the mounted the age secret. SOPS checks for this environment variable when decrypting secrets.
I also had to direct all the cache folders to /tmp
, otherwise I'd get:
Error: mkdir /helm/.config: permission denied COMBINED OUTPUT: Error: mkdir /helm/.config: permission denied
Managing the Secrets in Helmfile
With the ArgoCD setup complete, I then structured my Helmfile to handle the secrets. In the releases block add a secrets section:
releases:
- name: grafana
namespace: monitoring
createNamespace: true
chart: grafana/grafana
version: 8.10.4
values:
- ./values.yaml.gotmpl
needs:
- monitoring/grafana-auth
- name: grafana-auth
namespace: monitoring
createNamespace: true
chart: ../../../../../charts/secrets/
version: 0.1.0
secrets:
- ../../../../secrets/sealed-grafana-auth-secret.yaml
The trick is to have a minimal Helm chart that takes these decrypted values and creates a Kubernetes secret:
apiVersion: v1
kind: Secret
type: Opaque
metadata:
name: {{ include "secrets.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "secrets.labels" . | nindent 6}}
{{- with .Values.data }}
data:
{{- range $key, $value := .}}
{{$key }}: {{ $value | b64enc }}
{{- end }}
{{- end }}
{{- with .Values.stringData }}
stringData:
{{- range $key, $value := .}}
{{$key }}: {{ $value | b64enc }}
{{- end }}
{{- end }}
Then in the Grafana admin block values, I reference this secret:
existingSecret: grafana-auth-secret
userKey: admin-user
passwordKey: admin-password
So now
- No more manual decryption: ArgoCD now handles the secret decryption and application automatically.
- GitOps all the things: Everything—including secrets—is now managed declaratively through git.