Secrets
KATforge uses sops and age for secret management. Encrypted files are committed to git; the security boundary is the age private key on each contributor's laptop.
File model
Every stage has two kinds of env file per service, both committed:
| File | Contains |
|---|---|
hearth/env/<stage>/<service>.yaml | Plaintext config — URLs, ports, public flags, inline documentation |
hearth/env/<stage>/<service>.enc.yaml | sops-encrypted secrets — API keys, signing keys, OAuth client secrets |
Both file types live in the hearth repo. The split keeps URLs and feature flags reviewable as plain diff while gating real secrets behind decryption.
Recipients (.sops.yaml)
hearth/.sops.yaml lists who can decrypt what. Each entry pairs a path pattern with an age recipient list:
creation_rules:
- path_regex: env/.*\.enc\.yaml$
age: >-
age1abc...,
age1def...,
age1xyz...
When you encrypt or re-encrypt a file, sops uses every recipient on this list. Any one of those private keys can decrypt the resulting file. Adding or removing a recipient re-encrypts every matching file.
The ~/.katforge/age.txt key
Your age private key, generated by the installer. The shell rc block sets SOPS_AGE_KEY_FILE=$KATFORGE_ROOT/age.txt, which sops reads transparently — you never type the path.
hearth team whoami
# → age1abc...
That's your public key. Share it freely; it's the lookup token for becoming a recipient.
If you lose age.txt, ask a maintainer to remove your public key from .sops.yaml and rotate any compromised secrets — see Rotation.
Editing secrets
hearth secret edit dev api
Decrypts hearth/env/dev/api.enc.yaml to a temp file, opens $EDITOR, then re-encrypts on save. The temp file is deleted regardless of editor exit code.
Reading secrets
hearth secret list dev # all keys per service
hearth secret get dev api OAUTH_DISCORD_ID # one value
hearth secret diff dev prod # what differs between stages
secret diff is useful when bringing up a new stage — it shows which keys are missing or different.
Team membership
hearth team list # current recipients
hearth team whoami # this machine's public key
hearth team add age1abc... # authorize someone
hearth team remove age1abc... # de-authorize
team add and team remove rewrite .sops.yaml and re-encrypt every secret file in one transaction. Commit and push the resulting diff so other contributors pick up the new recipient list.
Key rotation
hearth team remove re-encrypts every file without the removed recipient — but the old ciphertext (and any plaintext they already extracted) remain valid against their key. Always rotate the underlying values too.
After hearth team remove, rotate any secret the removed recipient had access to:
- Generate a new value at the upstream provider (Discord, Google, AWS, etc.).
hearth secret edit <stage> <service>to update.- Deploy the new value (
hearth shipfor prod,hearth upfor dev).
For high-impact secrets — APP_SECRET, JWT_SECRET_KEY, HMAC_SECRET — a rotation also invalidates outstanding sessions and signed tokens. Plan accordingly.
Why sops + age, not Kubernetes secrets
k8s Secret resources solve a different problem (in-cluster credential delivery) and don't compose well with multi-contributor laptops. Hearth uses sops + age for the source of truth (committed in git, reviewable, history) and projects values into k8s Secret objects on demand via hearth secret apply <stage>. The git-tracked .enc.yaml is what gets edited; the Secret resource in the cluster is downstream and rebuildable from the file at any time. See Production-specific vars for the full path.
Common workflows
Onboarding a new contributor
# Them, after install:
hearth team whoami
# → age1newperson...
# A maintainer:
hearth team add age1newperson...
git -C ~/.katforge/hearth add .sops.yaml env/
git -C ~/.katforge/hearth commit -m "team: add new contributor"
git -C ~/.katforge/hearth push
Offboarding
hearth team remove age1leaving...
# Commit + push, then rotate any secrets they could see.
Adding a new secret
hearth secret edit dev api
# Add KEY: value in your editor, save.
hearth secret edit qa api # mirror to other stages
hearth secret edit prod api
git -C ~/.katforge/hearth diff env/ # ciphertext diff is fine to commit
Migrating a secret to a new value
hearth secret edit prod api # update the value, save
hearth secret apply prod # push the new value to the api-secrets k8s Secret
kubectl rollout restart deployment/api -n default # pod picks it up on restart
hearth secret apply updates the in-cluster Secret but doesn't restart pods (envFrom values are read at container start). A rolling restart is the simplest way to make running pods see the new value.