Skip to content

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:

spec:
  decryption:
    provider: sops
    secretRef:
      name: sops-gpg

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 secret needed
  • 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-gpg is lost on minikube 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.