Skip to content
Codeloom
DevOps

Secrets Management with SOPS and Age Encryption

Encrypt secrets in Git safely using Mozilla SOPS and age encryption for Kubernetes, Terraform, and CI/CD workflows.

·5 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • Why storing encrypted secrets in Git is a valid pattern
  • How SOPS encrypts values while keeping keys readable
  • Setting up age key pairs for team encryption
  • Integrating SOPS with Kubernetes and Flux
  • Rotating keys and managing access securely

Prerequisites

None — this post is self-contained.

Every team eventually faces the secrets problem: how do you store database passwords, API keys, and certificates so that your infrastructure code is reproducible without committing plaintext credentials to Git? Vault and cloud KMS services solve this at scale, but they require infrastructure of their own. Mozilla SOPS offers a lighter-weight alternative: encrypt secrets in place, store them in Git alongside your code, and decrypt them at deploy time.

Why SOPS

SOPS (Secrets OPerationS) encrypts the values in structured files (YAML, JSON, ENV, INI) while leaving the keys in plaintext. This means you can review a diff and see which secrets changed without seeing the actual values:

apiVersion: v1
kind: Secret
metadata:
  name: api-credentials
stringData:
  DATABASE_URL: ENC[AES256_GCM,data:8f3k2...,type:str]
  API_KEY: ENC[AES256_GCM,data:j29dk...,type:str]
sops:
  age:
    - recipient: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
      enc: |
        -----BEGIN AGE ENCRYPTED FILE-----
        YWdlLWVuY3J5cHRpb24...
        -----END AGE ENCRYPTED FILE-----
  lastmodified: "2026-07-02T10:00:00Z"
  version: 3.9.0

You can see that DATABASE_URL and API_KEY exist, but the values are encrypted. The sops metadata block tells SOPS which keys can decrypt the file.

Setting Up Age Encryption

Age is a simple, modern encryption tool that replaces PGP for SOPS workflows. It is easier to use, has fewer footguns, and the key format is simpler.

Generate a key pair:

age-keygen -o keys.txt
# Output:
# Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p

Store the private key file securely. Each team member and each CI system gets its own key pair. The public keys go into the SOPS configuration; the private keys stay on the machines that need to decrypt.

Create a .sops.yaml configuration file in your repository root:

creation_rules:
  - path_regex: .*\.secret\.yaml$
    age: >-
      age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p,
      age1an6grz70cesmtxlkt2kerdmz0re24yr8axr8apm5pq0verxmyxlq8n6dqy
  - path_regex: environments/production/.*
    age: >-
      age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p

This configuration encrypts any file matching *.secret.yaml with two age recipients (two team members or systems can decrypt). Production secrets use a separate, restricted key.

Encrypting and Decrypting

Create a plaintext secrets file and encrypt it:

# Create the plaintext file
cat > db.secret.yaml << 'EOF'
apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
stringData:
  username: admin
  password: supersecretpassword123
  host: db.internal.example.com
EOF

# Encrypt in place
sops --encrypt --in-place db.secret.yaml

SOPS reads the .sops.yaml rules, matches the filename, and encrypts the values using the specified age public keys. The file is now safe to commit.

To edit an encrypted file:

sops db.secret.yaml

SOPS decrypts the file into a temporary buffer, opens your editor, and re-encrypts when you save and close. You never see decrypted content on disk.

To decrypt for use in a script:

sops --decrypt db.secret.yaml | kubectl apply -f -

Integrating with Kubernetes and Flux

Flux, the GitOps toolkit, has native SOPS support. It decrypts secrets during reconciliation so you can store encrypted Kubernetes Secrets in your Git repository.

Configure the Flux Kustomization to use SOPS:

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

Create the age private key as a Kubernetes Secret in the flux-system namespace:

kubectl create secret generic sops-age \
  --namespace=flux-system \
  --from-file=age.agekey=keys.txt

Now Flux automatically decrypts any SOPS-encrypted file it finds in the ./secrets path and applies the plaintext Secret to the cluster. The decrypted values never touch Git.

Using SOPS with Terraform

SOPS works with Terraform through the sops provider:

terraform {
  required_providers {
    sops = {
      source  = "carlpett/sops"
      version = "~> 1.0"
    }
  }
}

data "sops_file" "secrets" {
  source_file = "secrets.secret.yaml"
}

resource "aws_db_instance" "main" {
  engine   = "postgres"
  username = data.sops_file.secrets.data["db_username"]
  password = data.sops_file.secrets.data["db_password"]
  # ...
}

The secrets file is encrypted in Git. Terraform decrypts it at plan and apply time using the age key available on the machine.

CI/CD Integration

In GitHub Actions, store the age private key as a repository secret and make it available to SOPS:

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup SOPS
        run: |
          curl -LO https://github.com/getsops/sops/releases/download/v3.9.0/sops-v3.9.0.linux.amd64
          chmod +x sops-v3.9.0.linux.amd64
          sudo mv sops-v3.9.0.linux.amd64 /usr/local/bin/sops
      - name: Decrypt and deploy
        env:
          SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }}
        run: |
          sops --decrypt secrets.secret.yaml | kubectl apply -f -

The SOPS_AGE_KEY environment variable contains the private key content. SOPS reads it automatically without needing a key file.

Key Rotation

When a team member leaves or a key is compromised, rotate by adding the new key and removing the old one from .sops.yaml, then re-encrypt all affected files:

# Update .sops.yaml with the new key list
# Then re-encrypt all secret files
find . -name '*.secret.yaml' -exec sops updatekeys {} \;

The updatekeys command re-encrypts the data key with the updated recipient list without changing the secret values. Commit the re-encrypted files and the old key can no longer decrypt them.

Wrap-up

SOPS with age encryption gives you encrypted secrets in Git without the operational overhead of running a secrets management server. It integrates cleanly with Kubernetes, Flux, Terraform, and CI/CD pipelines. Start with age keys for simplicity, use .sops.yaml creation rules to enforce encryption policies, and rotate keys promptly when access changes. For larger organizations with dynamic secrets and lease management, complement SOPS with a tool like Vault, but for many teams, SOPS is the right balance of security and simplicity.