2 / 8
Encryption: age and sops
Set agents and MCP aside for a moment — everything on top rests on a simple question at the bottom: how do you store a secret on disk so the file can be copied, committed, even lost on a flash drive, and none of it matters without one specific thing?
age: a key pair instead of a password
age encrypts data to a key pair, not a password:
- the public key (
age1...) can only encrypt. Handing it out is fine; - the private key (
AGE-SECRET-KEY-1...) can only decrypt. It's the one thing worth protecting.
A useful analogy: the public key is a mailbox slot anyone can drop a letter into; the private key is the key to the door — only it gets the letters back out.
Athanor sets this up as follows: athanor init generates a pair once
(internal/crypto/identity.go, age.GenerateX25519Identity()) and stores
the private half at ~/.config/athanor/identity.txt — one per developer by
default, not one per project, so a single key backup covers every project
at once. The public half (recipient) goes into athanor.yaml, where it's
meant to be seen and safe to commit.
sops: encrypting values, not the whole file
age on its own turns data into one solid blob — you can't tell what's
inside, and you can't diff it in git. sops fixes that: it encrypts
values in a YAML/JSON document while leaving key names readable.
DB_PASSWORD: ENC[AES256_GCM,data:xqmOcd0=,iv:...,tag:...,type:str]
STRIPE_TEST_KEY: ENC[AES256_GCM,data:...,type:str]
sops:
age:
- recipient: age1...
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
...
You can see which secrets exist without seeing what they're worth. That's
exactly what athanor list gives you — names, no values.
How Athanor actually calls sops: no plaintext on disk
The important engineering decision here isn't what gets used — age and
sops — it's how they're called. The usual interactive mode,
sops file.yaml, writes a temporary decrypted file to disk, opens it in an
editor, then re-encrypts it. That's exactly what Athanor avoids: a
temporary plaintext file is a leak waiting to happen.
Instead, internal/crypto/sops.go routes everything through stdin/stdout:
READ (Decrypt):
sops --decrypt --input-type yaml --output-type json secrets.enc.yaml
(SOPS_AGE_KEY_FILE=identity.txt in the sops process's environment)
│
▼
JSON arrives on the sops process's stdout
│
▼
Athanor parses it straight into memory (map[string]string)
— nothing touches disk
WRITE (Encrypt):
Athanor serializes the map to JSON, in memory
│
▼ (passed via stdin, not a file)
sops --encrypt --input-type json --output-type yaml
--age <recipient> /dev/stdin
│
▼
the encrypted YAML arrives on the sops process's stdout
│
▼
Athanor writes it to secrets.enc.yaml with one os.WriteFile (chmod 600)
If secrets.enc.yaml doesn't exist yet, Decrypt just returns an empty
map — a normal "new store," not an error.
Who owns what
- age is only responsible for the key pair existing and for
sopsknowing how to use it — Athanor never callsagedirectly to encrypt a file, only thefilippo.io/ageGo library, to generate an identity duringathanor init. - sops is an external binary, called via
os/exec, and it does all the real cryptographic work. Athanor doesn't hold or reinvent any of it — "don't write your own crypto" is worth following everywhere, not just here. - Athanor decides when to call sops, what to feed it, and who gets the result — that's the access model, and it's the subject of the next chapter.
Checkpoint question. Why doesn't interactive sops file.yaml mode fit
Athanor, and what replaces it?
Answer: interactive mode writes its own temporary decrypted file to disk
for the editor, while Athanor guarantees plaintext never touches disk at
all. It uses non-interactive sops --decrypt/--encrypt instead, with data
moving through stdin/stdout and living only in Athanor's own process
memory.