Skip to main content

Secrets Bootstrap — sops + age

OWASAKA encrypts every plaintext secret out of git via sops with age recipients. This document walks an operator through the first-time setup on a fresh machine.

See ADR-0059 §"Secrets management" for the design rationale and threat model.

Quick path

nix develop # ensures sops + age are available
./scripts/bootstrap-secrets.sh
# Follow the printed instructions to register your age recipient,
# then edit secrets.yaml:
sops secrets.yaml

That's it for most operators. The rest of this document explains what the script does and how to handle edge cases.


Manual walkthrough

1. Generate an age keypair

Each operator and each service that needs to decrypt secrets gets its own keypair. Never share private keys.

mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
chmod 600 ~/.config/sops/age/keys.txt

The file contains:

  • Your private key (AGE-SECRET-KEY-1...) — keep secret.
  • A comment line with the public recipient (# public key: age1...).

To print the recipient on demand:

age-keygen -y ~/.config/sops/age/keys.txt

2. Register the recipient in .sops.yaml

Open .sops.yaml at the repo root and add your age1... recipient to the appropriate creation_rules entry. Multiple recipients are comma-separated.

creation_rules:
- path_regex: ^secrets\.yaml$
age: >-
age1qy3rrjkxr5d3z2lz7eajrqkpdv2mhxq2hd6scnumgj97vp0p55sqp4lqsr,age1q9...

Commit the change. New encryptions will use all listed recipients.

If secrets.yaml already exists with different recipients, re-key it:

sops updatekeys secrets.yaml

3. Author secrets.yaml

Copy the template and fill in values:

cp secrets.example.yaml secrets.yaml
sops secrets.yaml # opens $EDITOR; sops re-encrypts on save

Or encrypt an existing plaintext in place:

sops --encrypt --in-place secrets.yaml

Commit secrets.yaml — it is encrypted at rest, safe in git.

4. Verify

sops -d secrets.yaml | head -5 # should print the YAML content

Multi-recipient pattern

For team operations, register every operator's recipient plus at least one breakglass recipient stored offline (printed and locked). If a primary operator's machine is lost, the breakglass recipient recovers access.

creation_rules:
- path_regex: ^secrets\.yaml$
age: >-
age1alice...,age1bob...,age1charlie...,age1BREAKGLASS_OFFLINE...

Rotate recipients with sops updatekeys secrets.yaml after any team change.


Runtime decryption

The application binary decrypts at startup via the sops library — you do not decrypt to a file on disk. The NixOS module wires the age private key in via LoadCredential so the running process is the only reader.

For ad-hoc CLI use (debugging, exporting to docker-compose):

sops -d secrets.yaml > secrets.dec.yaml # NEVER commit
# ... use it ...
shred -u secrets.dec.yaml

secrets.dec.yaml and secrets.dec.*.yaml are explicitly gitignored.


Threat model recap

Per ADR-0059 §"Threat model (defense surface)":

  • Supply chain: age private key lives outside the build, loaded at runtime via systemd credential. Compromised dependencies cannot read it unless they also achieve process injection.
  • Insider: every read of secrets.yaml is auditable via git history (the encrypted blob changes when re-encrypted). Combine with the transparency log (Sprint 3) for full attestation.
  • Network-adjacent: secrets never leave the host in plaintext.
  • Credential theft: rotate via sops updatekeys immediately on suspicion. Pull encrypted file again after rotation.

Common operations

# Edit
sops secrets.yaml

# Rotate recipients (after `.sops.yaml` change)
sops updatekeys secrets.yaml

# Decrypt one-off
sops -d secrets.yaml

# Encrypt a new file from plaintext
sops --encrypt --in-place secrets.staging.yaml

# Verify which recipients can decrypt
sops --decrypt --extract '[]' secrets.yaml >/dev/null && echo OK