Back to posts
Post

How to Encrypt Kubernetes Secrets in Git with SOPS and Age

Learn to securely store encrypted secrets in Git using SOPS and Age for Helm charts in under 10 minutes — practical setup with commands and pitfalls.

DevopsKubernetesSOPSAgeHelmGitOps

If you're managing Helm charts in Git and need to keep secrets out of plain text, encrypting Kubernetes secrets with SOPS and Age is a straightforward, auditable way to do it. I’ve used this setup across multiple production clusters to avoid accidental leaks while keeping GitOps workflows intact. The process takes less than 10 minutes once you have the tools installed, and it integrates cleanly with Helm and flux or Argo CD.

Why SOPS and Age for Secret Encryption

SOPS (Secrets OPerationS) lets you encrypt files using various backends — including Age, PGP, AWS KMS, and GCP KMS. Age is simple, modern, and doesn’t require key servers, making it ideal for self-hosted or air-gapped environments. Unlike sealed-secrets which ties decryption to a controller, SOPS-encrypted files can be decrypted anywhere with the key, giving you flexibility in CI/CD pipelines and disaster recovery.

I prefer Age over PGP for this use case because key management is simpler: just a public key for encryption and a private key for decryption. No keyring complexity, no expiration headaches.

Installing SOPS and Age

First, install the tools. On Linux, you can use the package manager or download binaries directly.

# Install Age (via package manager)
sudo apt-get install -y age

# Or download latest binary
curl -sSfL https://github.com/FiloSottile/age/releases/latest/download/age-v1.1.1-linux-amd64.tar.gz | tar -xzv
sudo mv age/age /usr/local/bin/
sudo mv age/age-keygen /usr/local/bin/

# Install SOPS
curl -sSfL https://github.com/getsops/sops/releases/latest/download/sops-v3.8.1.linux.amd64 | sudo tee /usr/local/bin/sops > /dev/null
sudo chmod +x /usr/local/bin/sops

Verify both are in your PATH:

age-keygen --version
sops --version

Generate Your Age Key Pair

Create a key pair. The public key encrypts; the private key decrypts. Store the private key securely — ideally in a password manager or HSM.

age-keygen -o key.txt

This creates a file like:

# public key: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
AGE-PRIVATE-KEY-1XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Copy the public key (the line starting with # public key:) — you’ll need it to encrypt files. Keep key.txt private and backed up.

Encrypt a Secret File for Helm

Let’s say you have a Kubernetes secret for your app’s database password:

# secret-plain.yaml
apiVersion: v1
kind: Secret
metadata:
  name: app-db-secret
  namespace: production
type: Opaque
stringData:
  DB_PASSWORD: super-secret-password

Encrypt it with SOPS using your Age public key:

sops --encrypt --age <YOUR_PUBLIC_KEY> secret-plain.yaml > secret.enc.yaml

Replace <YOUR_PUBLIC_KEY> with the actual key from key.txt (without the comment). The output secret.enc.yaml will look like:

apiVersion: v1
kind: Secret
metadata:
  name: app-db-secret
  namespace: production
type: Opaque
stringData:
  DB_PASSWORD: ENC[AES256_GCM,data:...,tag:...,type:str]
sops:
  kms: []
  gcp_kms: []
  azure_kms: []
  hc_vault: []
  age: []
  lastmodified: "2024-05-10T12:34:56Z"
  mac: ENC[...]
  pgp: []
  unencrypted_suffix: _enc
  version: 3.8.1

Now you can safely commit secret.enc.yaml to Git. The secret is encrypted; only someone with the private key can decrypt it.

Decrypting Secrets for Helm or kubectl

When deploying, decrypt the file on the fly. You don’t need to store decrypted versions.

With Helm, use --values and process the file through SOPS:

helm upgrade --install my-app ./chart \
  -f <(sops -d secret.enc.yaml)

Or if you're using flux or Argo CD, configure them to run sops -d as a decrypt strategy. For Argo CD, you can use a [SOPS plugin](https://argo-cd.readthedocs.io/en/stable/user-guide/helm/sops/).

To view the decrypted content locally:

sops -d secret.enc.yaml

Best Practices and Pitfalls

  • Never commit the private key. Treat key.txt like a root password. Use git-crypt or SOPS itself to encrypt it if you must store it in a repo (not recommended).
  • Rotate keys periodically. If you suspect a key is compromised, re-encrypt all secrets with a new key pair.
  • Use a .sops.yaml config to avoid typing the key every time:
  creation_rules:
    - age: >-
        age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Then run sops -e secret-plain.yaml without specifying --age.

  • Test decryption in CI. Add a step that runs sops -d on encrypted files to catch corruption early.
  • Don’t encrypt non-secret files. SOPS adds overhead — only encrypt what needs it (like secrets, TLS certs, or config with API keys).

Integrating with Your Existing GitOps Flow

If you’re already using Helm with GitOps, this adds minimal friction. Encrypted secrets sit alongside your Chart.yaml and values.yaml. Your CI pipeline decrypts them at deploy time using the private key injected as a secret (e.g., via GitHub Actions secrets or GitLab CI variables).

As I mentioned before in my [GitOps infrastructure post](https://furkanikkan.com/urun/gitops-ile-altyapi-yonetimi-manuel-deploy-lara-son-58), keeping your desired state in Git only works if that state is trustworthy — and that includes secrets you can’t afford to leak.

SOPS + Age gives you a lightweight, transparent way to achieve that without introducing a sidecar or controller just for secret decryption. It’s worked reliably for me across EKS, AKS, and bare-metal Kubernetes clusters — and it takes less than 10 minutes to set up once you know the steps.

If you’re looking for a no-nonsense way to stop committing plain-text secrets to Git, this is it.


Cover image: HD Wallpapers · CC0 (Openverse / kamu malı) · https://stocksnap.io/photo/light-abstract-V9L6XXK3LB