Production secrets management with HashiCorp Vault, Mozilla SOPS, and cloud-native solutions. Covers rotation, access control, emergency procedures, and the mistakes that cause breaches.
This document describes practical techniques for managing secrets across environments using tools that actually work in production. It focuses on how to store, rotate, access, and audit secrets with minimal operational overhead and maximum security.
Use SOPS to encrypt secrets at rest in Git repositories. The encryption is applied per-key, not full-file:
# example-secrets.yaml
database_password: ENC[AES256_GCM,data:...,iv:...,tag:...]
api_key: ENC[AES256_GCM,data:...,iv:...,tag:...]
Encrypt with:
sops --encrypt --encrypted-regex '^(database_password|api_key)$' example-secrets.yaml
SOPS uses age keys or AWS KMS for encryption. The encrypted file remains readable as YAML, but the values are base64-encoded and wrapped in a structured format.
Vault provides a consistent secret storage interface with TTLs and access control:
vault kv put secret/myapp/config \
database_password="supersecret" \
api_key="anothersecret"
Access via HTTP API:
curl -H "X-Vault-Token: $VAULT_TOKEN" \
https://vault.example.com/v1/secret/data/myapp/config
Vault returns JSON with data and metadata keys:
{
"data": {
"data": {
"database_password": "supersecret",
"api_key": "anothersecret"
},
"lease_id": "secret/myapp/config/...",
"lease_duration": 2592000,
"renewable": true
}
}
Vault also supports dynamic secrets like database credentials or SSH keys.
For static secrets, use a two-step process:
vault kv put secret/myapp/config \
database_password="newpassword123" \
api_key="newkey456"
sops --decrypt example-secrets.yaml > decrypted.yaml
# Manually update the plaintext fields
sops --encrypt decrypted.yaml > example-secrets.yaml
Vault allows reading without rotation:
vault kv get secret/myapp/config
Use Vault's AppRole authentication:
vault auth enable approle
vault write auth/approle/role/myapp \
policies="myapp-policy" \
token_policies="myapp-policy"
vault read auth/approle/role/myapp/role-id
vault write -f auth/approle/role/myapp/secret-id
curl -H "X-Vault-Token: $VAULT_TOKEN" \
https://vault.example.com/v1/secret/data/myapp/config
Use a sidecar container that decrypts files on mount:
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
containers:
- name: app
image: myapp:latest
volumeMounts:
- name: secrets
mountPath: /etc/secrets
volumes:
- name: secrets
emptyDir: {}
initContainers:
- name: decrypt-secrets
image: mozilla/sops:3.7.1
command:
- /bin/sh
- -c
- |
sops --decrypt /etc/sops/secrets.yaml > /tmp/secrets.yaml
cp /tmp/secrets.yaml /etc/secrets/
volumeMounts:
- name: sops-secrets
mountPath: /etc/sops
- name: secrets
mountPath: /etc/secrets
Policies are written in HCL:
# myapp-policy.hcl
path "secret/data/myapp/*" {
capabilities = ["read", "list"]
}
path "secret/metadata/myapp/*" {
capabilities = ["read", "list"]
}
Apply to a token:
vault policy write myapp-policy myapp-policy.hcl
vault token create -policy="myapp-policy"
Vault returns errors like:
Error: permission denied on secret/myapp/config
SOPS supports multiple encryption keys. Each team member has their own age key:
# List current encryptors
sops --decrypt example-secrets.yaml | grep -A 10 "encryptors"
Adding a new key:
sops --encrypt \
--add-encryption-key age1xyz \
example-secrets.yaml > new-secrets.yaml
Enable audit logging:
vault audit enable file file_path=/var/log/vault-audit.log
Vault logs entries like:
{"time":"2023-01-01T00:00:00Z","type":"request","path":"secret/data/myapp/config","operation":"read"}
SOPS files are binary, so git diff shows raw base64:
git diff example-secrets.yaml
Output:
- database_password: ENC[AES256_GCM,data:...,iv:...,tag:...]
+ database_password: ENC[AES256_GCM,data:...,iv:...,tag:...]
Use sops --decrypt to see plaintext content:
sops --decrypt example-secrets.yaml
Vault returns error on expired tokens:
Error: invalid token
Applications must handle this by acquiring new tokens via AppRole or Vault agent.
If a team member loses their age key, they cannot decrypt files:
sops --decrypt example-secrets.yaml
This produces:
Error: failed to decrypt data: unable to decrypt with any key
Recovery requires:
SOPS allows mixing encrypted and plaintext fields. If a field is not encrypted, it can leak:
# This is safe
database_password: ENC[AES256_GCM,...]
api_key: ENC[AES256_GCM,...]
# This leaks plaintext
plaintext_field: "should be encrypted but isn't"
Use --encrypted-regex to enforce encryption:
sops --encrypt --encrypted-regex '^(database_password|api_key)$' example.yaml
Reusing tokens across services creates a single point of failure:
# Bad: Reuse token across multiple services
vault token create -token-policies="common-policy"
Better:
# Good: Per-service tokens
vault token create -token-policies="myapp-policy"
vault token create -token-policies="database-policy"
SOPS files are not versioned by default. A new encryption key creates a new file:
sops --encrypt example-secrets.yaml > example-secrets.yaml
To preserve history, commit both old and new versions:
git add example-secrets.yaml
git commit -m "Update secrets"
Vault paths should be consistent. Avoid ambiguous names:
# Good
vault kv put secret/myapp/config/database_password="..."
# Bad
vault kv put secret/myapp/database_password="..."
The combination of Vault and SOPS provides a robust foundation for secrets management where Vault handles dynamic secrets and SOPS manages static ones, with clear separation of concerns and minimal operational overhead.
This page was rewritten on 15 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy