[Drafted by Claude]
Production credentials (database passwords, API keys, etc.) are stored in plaintext in the private pillar repository. This means:
- Anyone with read access to the repository has every production credential.
- A leaked personal access token, or a compromised laptop with a clone, exposes all secrets at once.
- Secrets persist in git history, so removing one requires rotation, not just deletion.
- GitHub itself is in the trust boundary: a GitHub account or infrastructure compromise is a credential compromise.
Since we deploy with salt-ssh (no master), pillar rendering happens on the machine running run.py, so encryption at rest in git protects the repository without changing the deployment trust model — whoever can run salt-ssh already has root SSH to all targets.
Proposal
Encrypt secret values with sops using age keys.
sops encrypts at the value level, so pillar files keep readable keys (db_password: ENC[...]) — diffs and PR review stay meaningful. age keypairs are single short strings (no GPG keyring), and multi-recipient encryption means each deployer's key can decrypt; offboarding is re-encrypting without that key (plus rotating what they could read).
- Add
.sops.yaml listing deployers' age public keys and a path regex matching secret pillar files.
- Each deployer keeps their age private key in
~/.config/sops/age/keys.txt (outside the repo).
- Editing workflow:
sops edit pillar/private/foo.sls.
Integration with run.py
Options:
- Decrypt in the wrapper. Before invoking salt-ssh, decrypt matching pillar files into a gitignored directory; move the
subprocess.run + sys.exit(returncode) into a try/finally that deletes the decrypted files.
sops exec-file. Let sops own the temp-file lifecycle and keep execvp untouched. Awkward with multiple secret pillar files.
- Salt's GPG renderer instead of sops. Zero wrapper changes (
#!yaml|gpg header, gpg_keydir in Saltfile), but whole-value GPG blobs, no meaningful diffs, and GPG key management for every deployer.
Option 1 is preferred.
Migration
- Generate age keys for each deployer; commit
.sops.yaml.
- Encrypt existing secret pillar files with
sops encrypt --in-place.
- Update
run.py per option 1; document setup in the deploy docs.
- (TBD) Rotate all credentials currently in git history — they remain exposed to anyone who has ever cloned the repository, and encryption going forward doesn't retroactively protect them.
Non-goals
- Protecting the deployment path itself: salt-ssh copies rendered pillar data to targets over SSH regardless. The threat addressed here is repository/GitHub exposure.
- Vault or cloud secret managers: dynamic credentials and audit logs aren't worth running another critical service at our scale.
[Drafted by Claude]
Production credentials (database passwords, API keys, etc.) are stored in plaintext in the private pillar repository. This means:
Since we deploy with salt-ssh (no master), pillar rendering happens on the machine running
run.py, so encryption at rest in git protects the repository without changing the deployment trust model — whoever can run salt-ssh already has root SSH to all targets.Proposal
Encrypt secret values with sops using age keys.
sops encrypts at the value level, so pillar files keep readable keys (
db_password: ENC[...]) — diffs and PR review stay meaningful. age keypairs are single short strings (no GPG keyring), and multi-recipient encryption means each deployer's key can decrypt; offboarding is re-encrypting without that key (plus rotating what they could read)..sops.yamllisting deployers' age public keys and a path regex matching secret pillar files.~/.config/sops/age/keys.txt(outside the repo).sops edit pillar/private/foo.sls.Integration with run.py
Options:
subprocess.run+sys.exit(returncode)into atry/finallythat deletes the decrypted files.sops exec-file. Let sops own the temp-file lifecycle and keepexecvpuntouched. Awkward with multiple secret pillar files.#!yaml|gpgheader,gpg_keydirin Saltfile), but whole-value GPG blobs, no meaningful diffs, and GPG key management for every deployer.Option 1 is preferred.
Migration
.sops.yaml.sops encrypt --in-place.run.pyper option 1; document setup in the deploy docs.Non-goals