ADR-010: SOPS + GPG for Secrets Management¶
Status¶
Accepted
Date¶
2026-03-21
Context¶
Phase 3 introduced four secrets that were initially committed to Git in plaintext:
- Keycloak admin password (
identity/keycloak-deployment.yaml) - Grafana admin password (
observability/kube-prometheus-stack.yaml) - Grafana OIDC client secret (
observability/grafana-oidc-secret.yaml) - Keycloak realm JSON containing client secret and user passwords (
identity/realm-config.yaml)
Committing plaintext secrets to Git is unacceptable even for a local sandbox — the repo is public and the pattern should reflect production practice. A secrets management solution is needed that integrates with Flux's GitOps reconciliation model.
Decision¶
Use SOPS (Secrets OPerationS) with a dedicated GPG key to encrypt data and stringData fields in Kubernetes Secret and ConfigMap manifests committed to Git. Flux decrypts them at apply time using a GPG private key stored as a Kubernetes Secret in flux-system.
Key components:
.sops.yaml # Encryption rules (path regex + PGP fingerprint)
*.sops.yaml files # Encrypted manifests (K8s Secrets / ConfigMaps)
flux-system/sops-gpg # K8s Secret holding the GPG private key
Encryption scope — only data and stringData YAML fields are encrypted; metadata stays readable:
# .sops.yaml
creation_rules:
- path_regex: \.sops\.yaml$
encrypted_regex: ^(data|stringData)$
pgp: CF7169E94481219626AF34290D18AE7E58FB2D45
GPG key: A dedicated HomeKube SOPS <homekube@local> key was generated without a passphrase so Flux can decrypt unattended. The private key is stored in flux-system/sops-gpg.
Flux integration: decryption.provider: sops added to identity and observability Kustomizations:
Alternatives Considered¶
| Option | Reason Not Chosen |
|---|---|
| age (modern encryption tool) | Requires installing an additional tool (age, age-keygen); GPG is already installed and has existing keys |
| Vault + External Secrets Operator | Significant operational overhead for a local sandbox; requires running HashiCorp Vault |
| Kubernetes-native ExternalSecrets | Requires an external secret store backend (AWS Secrets Manager, GCP Secret Manager, etc.) — overkill for local dev |
| Sealed Secrets | Requires a cluster-side controller; controller key must be backed up separately; less portable than SOPS |
| Plaintext (status quo) | Unacceptable — secrets visible in public Git history |
Consequences¶
Positive¶
- Secrets are encrypted in Git — safe to commit and share
- Flux decrypts automatically at apply time — no manual
kubectl create secretneeded - Edit secrets with one command:
sops <file>(opens in$EDITOR, re-encrypts on save) - GPG is already present on most developer machines
- Pattern is identical to production SOPS workflows
Negative¶
- The GPG private key stored in
flux-system/sops-gpgis lost onminikube delete— must be re-added after each fresh cluster (see SOPS how-to) - Any machine that needs to create or edit secrets must have the GPG private key imported
- GPG key has no passphrase (intentional for unattended Flux decryption) — the key file itself must be kept secure
Encrypted Files¶
| File | Contents |
|---|---|
identity/keycloak-secret.sops.yaml |
Keycloak admin password |
identity/realm-config.sops.yaml |
Keycloak realm JSON (client secrets, user passwords) |
observability/grafana-admin-secret.sops.yaml |
Grafana admin username and password |
observability/grafana-oidc-secret.sops.yaml |
Grafana OIDC client secret |
Trade-offs¶
Familiarity and zero additional installs are prioritised over newer tooling (age). GPG is already present and the pattern is well-understood. The no-passphrase key is a pragmatic trade-off for a local sandbox where Flux must decrypt unattended.