Secrets Workflow¶
Your daily cheat sheet for managing secrets with kstlib and SOPS.
TL;DR (3 commands to get started)¶
# 1. Quick setup (generates age key + .sops.yaml config)
kstlib secrets init
# 2. Check everything is ready
kstlib secrets doctor
# 3. Encrypt your secrets
kstlib secrets encrypt secrets.yml --out secrets.sops.yml --shred
Setup¶
Prerequisites¶
Install sops and age. See Binary Dependencies for platform-specific instructions.
Quick setup¶
kstlib secrets init # Global (home directory)
kstlib secrets init --local # Project-local
Or follow the manual steps below for more control.
Generate your age key (manual)¶
# Create directory
mkdir -p ~/.config/sops/age
# Generate key pair
age-keygen -o ~/.config/sops/age/keys.txt
# Note your public key (starts with "age1...")
age-keygen -y ~/.config/sops/age/keys.txt
# or grep "public key" ~/.config/sops/age/keys.txt
# Create directory
New-Item -ItemType Directory -Force -Path "$env:APPDATA\sops\age"
# Generate key pair
age-keygen -o "$env:APPDATA\sops\age\keys.txt"
# IMPORTANT: Tell SOPS where to find the key (add to your PowerShell profile)
$env:SOPS_AGE_KEY_FILE = "$env:APPDATA\sops\age\keys.txt"
# To make permanent: [Environment]::SetEnvironmentVariable("SOPS_AGE_KEY_FILE", "$env:APPDATA\sops\age\keys.txt", "User")
# Note your public key (starts with "age1...")
age-keygen -y "$env:APPDATA\sops\age\keys.txt"
# or Select-String "public key" "$env:APPDATA\sops\age\keys.txt"
Important
What matters:
Private key → Back up securely. Lose it = lose access to your secrets forever.
Encrypted
.sops.yml→ Commit to git. Contains secrets + decryption metadata.Plaintext file → Destroy after encryption (
--shred). Reconstructible from encrypted file..sops.yamlconfig → Nice to have, but metadata is embedded in encrypted files.
Default key locations:
Windows:
%APPDATA%\sops\age\keys.txtmacOS/Linux:
~/.config/sops/age/keys.txt
Configure SOPS¶
Create a .sops.yaml file (project root or global):
Location |
Scope |
|---|---|
|
This repo only |
|
All projects |
Note
On Windows, ~/.sops.yaml resolves to C:\Users\<username>\.sops.yaml (NOT %APPDATA%).
# .sops.yaml
creation_rules:
# Match any .sops.yml or .sops.yaml file
- path_regex: .*\.(yml|yaml)$
encrypted_regex: .*(?:sops|key|password|secret|token|credentials?).*
age: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Replace with YOUR age public key (from the generate step above)
Verify setup¶
kstlib secrets doctor
All components should show available.
Daily Usage¶
Create a secrets file¶
Start with a plaintext template:
# secrets.yml (destroy after encryption with --shred)
mail:
smtp:
host: smtp.gmail.com
port: 587
username: alice@example.com
password: "my-secret-password"
api:
stripe_key: "sk_live_xxxxx"
openai_key: "sk-xxxxx"
webhook_url: "https://..."
database:
host: localhost
port: 5432
credentials:
username: dbuser
password: dbpass
Encrypt¶
# Encrypt and keep the original
kstlib secrets encrypt secrets.yml --out secrets.sops.yml
# Encrypt and securely delete the original (recommended)
kstlib secrets encrypt secrets.yml --out secrets.sops.yml --shred
What the encrypted file looks like¶
# secrets.sops.yml (safe to commit)
mail:
smtp:
host: smtp.gmail.com
port: 587
username: alice@example.com
password: ENC[AES256_GCM,data:...,type:str] # ENCRYPTED (matches "password")
api:
stripe_key: ENC[AES256_GCM,data:...,type:str] # ENCRYPTED (matches "key")
openai_key: ENC[AES256_GCM,data:...,type:str] # ENCRYPTED (matches "key")
webhook_url: https://... # NOT encrypted
database:
host: localhost
port: 5432
credentials: # ⚠️ matches "credentials"
username: ENC[AES256_GCM,data:...,type:str] # ENCRYPTED (child of credentials)
password: ENC[AES256_GCM,data:...,type:str] # ENCRYPTED (matches "password")
sops:
age:
- recipient: age1zsnz8l28tjg9gcxe3rgt5pycvuzwvwnjz55840875v2aagwgsg5s7sgpp9
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
...
-----END AGE ENCRYPTED FILE-----
encrypted_regex: .*(?:sops|key|password|secret|token|credentials?).*
version: 3.11.0
Warning
Notice that database.credentials.username is encrypted even though “username” doesn’t match the regex.
When a parent key matches (like credentials), all children are encrypted too.
To avoid this, restructure your YAML or adjust encrypted_regex.
Note
The ENC[...] values change on every re-encryption (random nonces).
This is normal - git diff will show changes even if the plaintext didn’t change.
Decrypt (view)¶
# Print to stdout (safe for quick peek)
kstlib secrets decrypt secrets.sops.yml
# Write to file (careful!)
kstlib secrets decrypt secrets.sops.yml --out secrets.yml
Edit encrypted file¶
# Opens in $EDITOR with decrypted content, re-encrypts on save
sops secrets.sops.yml
Use in code¶
from kstlib import secrets
# Simple: get a secret by dotted path
record = secrets.resolve_secret("mail.smtp.password")
print(record.value) # "my-secret-password"
print(record.source) # SecretSource.SOPS
# With fallback
record = secrets.resolve_secret(
"api.missing_key",
required=False,
default="fallback-value",
)
Secure context manager¶
Minimize how long secrets stay in memory:
from kstlib.secrets import resolve_secret, sensitive
record = resolve_secret("mail.smtp.password")
with sensitive(record) as secret_value:
send_email(password=secret_value)
# record.value is now None - secret purged from memory
Secret Rotation¶
Rotate a secret value¶
Decrypt to plaintext:
kstlib secrets decrypt secrets.sops.yml --out secrets.yml
Edit
secrets.ymlwith new valuesRe-encrypt and shred:
kstlib secrets encrypt secrets.yml --out secrets.sops.yml --force --shred
Rotate encryption keys¶
When you need to change the age key (compromised, employee left, etc.):
Generate new key:
age-keygen -o ~/.config/sops/age/keys-new.txt
Update
.sops.yamlwith the new public keyRe-encrypt all files:
# Decrypt with old key, re-encrypt with new sops updatekeys secrets.sops.yml
Replace old key file:
mv ~/.config/sops/age/keys-new.txt ~/.config/sops/age/keys.txt
Troubleshooting¶
“SOPS binary not found”¶
# Check if sops is installed
which sops
# If missing, install it (see Prerequisites above)
“No age key detected”¶
# Verify key file exists
ls -la ~/.config/sops/age/keys.txt
# Or set environment variable
export SOPS_AGE_KEY_FILE=~/.config/sops/age/keys.txt
“Failed to decrypt”¶
Common causes:
Wrong key: The file was encrypted with a different age key
Corrupted file: The .sops.yml file was manually edited incorrectly
Missing .sops.yaml: SOPS cannot find the configuration
# Check which keys were used to encrypt
sops --show-metadata secrets.sops.yml
“Permission denied” on key file¶
# Key file should be read-only by you (no write needed)
chmod 400 ~/.config/sops/age/keys.txt
Run full diagnostics¶
kstlib secrets doctor
This checks: sops binary, age-keygen, age key file, keyring backend, and kstlib configuration.
Quick Reference¶
Task |
Command |
|---|---|
Quick setup |
|
Local setup |
|
Check setup |
|
Encrypt |
|
Encrypt + delete original |
|
Decrypt to stdout |
|
Decrypt to file |
|
Edit in place |
|
Secure delete |
|
Files to .gitignore¶
# .gitignore additions
# Never commit plaintext secrets
secrets.yml
*.secret
*.cleartext
# Keep encrypted versions
!*.sops.yml
!*.sops.yaml