Chapter 5. Encrypt Kubernetes Secrets in Git

Why this

You don’t need a cloud KMS to stop plaintext secrets in Git. SOPS + age lets you encrypt fields in YAML, commit safely, and decrypt only when applying on your machine.

1) Install SOPS and age (one-time)

  • macOS: brew install sops age
  • Windows (PowerShell): winget install -e --id Mozilla.SOPS and winget install -e --id FiloSottile.OpenSSH (age may be in package managers you use)
  • Linux (Debian/Ubuntu): sudo apt install -y sops age

If your platform uses different packages, install SOPS and age via your package manager of choice.

2) Generate an age key (keep local)

age-keygen -o .sops.agekey
echo 'export SOPS_AGE_KEY_FILE=.sops.agekey' >> .env.sops
source .env.sops

Do not commit .sops.agekey. Add it to .gitignore.

3) Define SOPS rules for Kubernetes Secrets

Create .sops.yaml at repo root:

# Encrypt Kubernetes Secret data/stringData by default
creation_rules:
  - path_regex: infra/k8s/.*\.secret\.ya?ml$
    encrypted_regex: '^(data|stringData)$'
    age: []
    # age recipient(s) will be added automatically from your key when you first encrypt

4) Create a sample Secret (unencrypted working copy)

Create infra/k8s/secret.secret.yaml:

apiVersion: v1
kind: Secret
metadata:
  name: demo-secret
  namespace: ship
type: Opaque
stringData:
  API_TOKEN: "replace-me"

5) Encrypt it in-place

sops -e -i infra/k8s/secret.secret.yaml
git add infra/k8s/secret.secret.yaml .sops.yaml
git commit -m "chore(secrets): add encrypted Secret via SOPS"

Open the file—you should see ciphertext, plus a sops: metadata block.

6) Apply to the cluster (decrypt on the fly)

sops -d infra/k8s/secret.secret.yaml | kubectl apply -f -
kubectl -n ship get secret demo-secret -o yaml | grep -A3 data:

You’ll see base64 fields at runtime (normal for K8s), but Git still holds ciphertext.

7) Audit evidence (prove it’s encrypted)

grep -n "sops:" -n infra/k8s/secret.secret.yaml > artifacts/sops-audit.txt
echo "Wrote artifacts/sops-audit.txt"

Makefile helpers (optional)

sops-keygen:
	@test -f .sops.agekey || age-keygen -o .sops.agekey
	@echo "export SOPS_AGE_KEY_FILE=.sops.agekey" > .env.sops
	@echo "Run: source .env.sops"

sops-encrypt:
	sops -e -i infra/k8s/secret.secret.yaml

sops-decrypt:
	sops -d infra/k8s/secret.secret.yaml > /tmp/secret.plain.yaml && \
	echo "Decrypted to /tmp/secret.plain.yaml (do not commit)"

k8s-apply-sops:
	sops -d infra/k8s/secret.secret.yaml | kubectl apply -f -

Add .sops.agekey to .gitignore:

../setonmpvps./.ssaoegpcesrkeety.plain.yaml

Checklist

  • .sops.agekey generated and not committed
  • .sops.yaml present with Kubernetes rules
  • infra/k8s/secret.secret.yaml shows ciphertext in Git
  • sops -d … | kubectl apply -f - works
  • artifacts/sops-audit.txt saved