SOPS: The Elegantly Simple Way to Store Secrets in Git

SOPS: The Elegantly Simple Way to Store Secrets in Git

One of the most persistent questions in GitOps is: “How do I handle secrets?” Git repositories are often shared, sometimes public, and definitely logged. Putting plaintext database passwords in your repository is obviously wrong. But your deployment configuration lives in Git, and your deployments need secrets.

The approaches:

  1. External secrets management (Vault, AWS SSM, Azure Key Vault, Google Secret Manager): Secrets live outside Git, a Kubernetes operator pulls them at runtime.
  2. Sealed Secrets: A controller encrypts secrets using a cluster-resident key; the encrypted form is stored in Git.
  3. SOPS: Secrets are encrypted in your Git repository using age or PGP keys. The encrypted file is committed to Git; the deployment tool decrypts at apply time.

For homelab and SMB production use, SOPS is the most elegant solution. It’s simple, requires no external infrastructure, and integrates natively with Flux.

How SOPS Works

SOPS is a tool that encrypts YAML/JSON/ENV/INI files. It’s smart about what it encrypts: in a YAML file, it encrypts the values but leaves the keys unencrypted. This means you can see that a file contains database_password without seeing what the password is.

# Before SOPS encryption
database_password: mysecretpassword123
api_key: sk-abcdefghijklmnop

# After SOPS encryption (simplified)
database_password: ENC[AES256_GCM,data:xyz123==,iv:abc,aad:,tag:def==]
api_key: ENC[AES256_GCM,data:ghi456==,iv:jkl,aad:,tag:mno==]
sops:
    kms: []
    age:
        - recipient: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
          enc: |
              -----BEGIN AGE ENCRYPTED FILE-----
              <encrypted SOPS data key>
              -----END AGE ENCRYPTED FILE-----
    lastmodified: "2026-02-22T10:00:00Z"
    mac: ENC[AES256_GCM,data:mac==,...]
    version: 3.8.0

The file is committed to Git in this encrypted form. When Flux encounters a SOPS-encrypted file, it decrypts it using the key it has access to and applies the plaintext values as a Kubernetes Secret.

age is the preferred key type—simpler than PGP, modern cryptography, no key management complexity.

# Generate an age key pair
age-keygen -o key.txt

# Output:
# Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
# The private key is in key.txt - protect this!

The private key (key.txt) is secret. The public key is safe to share and put in your .sops.yaml.

Configuring SOPS

Create .sops.yaml in your repository root:

creation_rules:
  # All YAML files in secrets/ directories
  - path_regex: .*/secrets/.*\.yaml$
    age: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p

  # Specific secret files anywhere in the repo
  - path_regex: .*secret.*\.yaml$
    age: >-
      age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p,
      age1backup987...

Using multiple age recipients means the same encrypted file can be decrypted by multiple keys—useful for team environments or when you need a backup key.

Encrypting Files

# Encrypt a file in place
SOPS_AGE_RECIPIENTS=age1ql3z7... sops --encrypt --in-place secret.yaml

# Or with .sops.yaml configured:
sops --encrypt --in-place secret.yaml

# Decrypt temporarily to edit
sops secret.yaml  # Opens in $EDITOR, saves encrypted

# Decrypt to stdout (for piping)
sops --decrypt secret.yaml

Encrypted files go in Git. The private key stays out of Git.

Kubernetes Secrets with SOPS

For Kubernetes Secrets, create the YAML file and encrypt it:

# secrets/database.yaml (before encryption)
apiVersion: v1
kind: Secret
metadata:
  name: database-credentials
  namespace: my-app
type: Opaque
stringData:
  password: "mysecretpassword123"
  connection-string: "postgresql://user:mysecretpassword123@postgres:5432/mydb"
sops --encrypt --in-place secrets/database.yaml

The encrypted file is committed. Flux decrypts and applies.

Flux Integration

Flux has native SOPS support. Configure it to use your age key:

# Store the private key as a Kubernetes Secret in flux-system
cat key.txt | kubectl create secret generic sops-age \
  --namespace=flux-system \
  --from-file=age.agekey=/dev/stdin

Tell Flux to use this key for decryption in your Kustomization:

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

When Flux processes files in ./clusters/production/my-app/secrets, it automatically decrypts any SOPS-encrypted files using the age key stored in sops-age.

Team Workflows

For teams, use multiple age recipients so multiple team members can decrypt:

# .sops.yaml
creation_rules:
  - path_regex: .*secret.*\.yaml$
    age: >-
      age1alice...,
      age1bob...,
      age1cicd...

When Alice encrypts a file, both Bob and the CI/CD system can decrypt it. New team member? Add their public key to .sops.yaml and re-encrypt existing files:

# Re-encrypt all secrets with updated recipients
sops updatekeys secrets/database.yaml

When a team member leaves, remove their key and re-encrypt. Their old private key can no longer decrypt new secrets, and since you’ve re-encrypted existing secrets, their old key can no longer decrypt those either.

What SOPS Is Not

SOPS is not a secret rotation tool. It doesn’t integrate with identity providers for dynamic credentials. If you need:

  • Automatic credential rotation
  • Dynamic secrets that expire
  • Integration with enterprise identity (LDAP, Active Directory)
  • Multi-cloud secret synchronization

…then External Secrets Operator with Vault or a cloud secret manager is the right choice.

SOPS is for the common case: static secrets (TLS certs, API keys, database passwords) that don’t change frequently, in a GitOps repository, managed by a small team. For this use case, SOPS is the simplest possible approach that maintains security without requiring additional infrastructure.

Security Properties

SOPS with age provides:

  • Confidentiality: Values encrypted with AES-256-GCM. Only key holders can decrypt.
  • Integrity: MAC (message authentication code) protects against tampering. SOPS will refuse to decrypt a modified file.
  • Key separation: The data encryption key is wrapped with the age key. You can add/remove recipients without re-encrypting the data.
  • Audit trail: All changes to the encrypted secret file are tracked in git history.

What SOPS doesn’t provide:

  • Access logging (who decrypted what, when)
  • Fine-grained key access (if you can decrypt any secret encrypted with a key, you can decrypt all of them)
  • Key rotation automation

For regulated environments needing access logging and fine-grained control, use Vault. For everyone else, SOPS is exactly enough security without excess complexity.

Share this post: LinkedIn Reddit WhatsApp Mastodon
Jesse Borden

Jesse Borden

Software Engineer with an interest in hands on learning

I have several years of professional Information Technology (IT) experience leading staff and projects within the Department of War (DOW). I have managed Service Desk, Web Application Development, and System Administration teams. My two greatest passions are learning and conti...