Flux CD Flux CD

Manage Secrets in Git with SOPS and age

Encrypt Kubernetes Secrets before committing them, and let Flux decrypt them in the cluster.

12 min Intermediate

GitOps puts your cluster’s configuration in Git, and that includes Secrets. Committing a plain Kubernetes Secret exposes its values to anyone who can read the repository, because Secret data is only base64-encoded. Flux’s kustomize-controller can decrypt files encrypted with Mozilla SOPS, so you can commit encrypted Secrets and keep the whole system in Git. This guide uses age keys, the simplest option.

Install the tools and create a key

Install age and sops (for example brew install age sops), then generate a key pair:

age-keygen -o age.agekey

The output file holds the private key, and the command prints the public key (it starts with age1). Keep the private key out of Git. Back it up somewhere safe, because if you lose it you can’t decrypt your secrets. Flux 2.9 also adds support for age’s post-quantum cipher, if your policies call for it.

Give the cluster the private key

Store the private key as a Secret in the same namespace as the Flux Kustomization that will use it:

kubectl create secret generic sops-age \
  --namespace=flux-system \
  --from-file=age.agekey=/dev/stdin < age.agekey

The key in the Secret must end in .agekey. Delete the local age.agekey file or move it to a password manager once it’s safely stored elsewhere.

Configure SOPS in the repository

Create .sops.yaml at the root of your repository so everyone encrypts the same way. Replace the key with your public key:

creation_rules:
  - path_regex: .*\.yaml
    encrypted_regex: ^(data|stringData)$
    age: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

encrypted_regex encrypts only the data and stringData fields, so everything else in the manifest stays readable for review and for Flux to process. You can list several age recipients (comma-separated) so teammates and the cluster key can all decrypt.

Encrypt a Secret

Write the Secret manifest locally, for example apps/webapp/db-credentials.yaml:

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
  namespace: webapp
stringData:
  username: webapp
  password: s3cr3t-value

Encrypt it in place:

sops --encrypt --in-place apps/webapp/db-credentials.yaml

Open the file: the username and password values are now ciphertext and a sops: block has been added with metadata. It’s safe to commit. To edit later, run sops apps/webapp/db-credentials.yaml, which decrypts in your editor and re-encrypts on save. To check what you’re about to commit, make sure the values are no longer readable.

Tell Flux to decrypt

Add a decryption block to the Flux Kustomization that applies the folder:

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: webapp
  namespace: flux-system
spec:
  interval: 10m
  sourceRef:
    kind: GitRepository
    name: flux-system
  path: ./apps/webapp
  prune: true
  decryption:
    provider: sops
    secretRef:
      name: sops-age

Commit and push. During reconciliation, kustomize-controller decrypts the file in memory and applies a normal Secret to the cluster. Verify it:

flux reconcile kustomization webapp --with-source
kubectl get secret db-credentials -n webapp

Troubleshooting and good practice

If the Kustomization reports a decryption error, check that the sops-age Secret exists in the same namespace as the Kustomization, that its key name ends in .agekey, and that the file was encrypted to the matching public key. kubectl describe kustomization webapp -n flux-system shows the error.

  • Use a different key per environment so a dev key can’t read production secrets.
  • Rotate keys by adding the new recipient, re-encrypting (sops updatekeys), then removing the old one.
  • Cloud KMS keys (AWS KMS, Azure Key Vault, Google Cloud KMS) and HashiCorp Vault or OpenBao work too, and avoid distributing key files. Configure them with workload identity and skip secretRef where the platform provides credentials.
  • If you’d rather keep secrets out of Git entirely, use the External Secrets Operator to sync them from a vault, and let Flux manage only the ExternalSecret objects.

Popular Guides