Docs/Hearth/Secrets

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:

FileContains
hearth/env/<stage>/<service>.yamlPlaintext config — URLs, ports, public flags, inline documentation
hearth/env/<stage>/<service>.enc.yamlsops-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:

YAML
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.

shell
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

shell
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

shell
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

shell
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:

  1. Generate a new value at the upstream provider (Discord, Google, AWS, etc.).
  2. hearth secret edit <stage> <service> to update.
  3. Deploy the new value (hearth ship for prod, hearth up for 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

shell
# 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

shell
hearth team remove age1leaving...
# Commit + push, then rotate any secrets they could see.

Adding a new secret

shell
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

shell
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.