What Key Rotation Actually Stops (and What It Doesn't)
Rotating an age key re-encrypts secrets/db.enc.env to a new recipient list going forward. It does nothing to the plaintext an old key already read, and nothing to the ciphertext still sitting in earlier git commits.
The nextcloud-selfhosted README states the rule in one sentence, in the middle of a bullet list, easy to skim past: "Rotating a key stops future reads; anything already decrypted stays compromised." That sentence is the whole argument for why "we rotated the key" and "we're safe now" are not the same claim.
Rotation is a forward-only control. Anything already exposed needed a different response.
What sops updatekeys actually does
Adding or removing a machine from the trust set follows a fixed procedure: generate a new age key, add (or delete) its public half in every age: list in .sops.yaml, then run:
./scripts/secrets-rotate.sh # sops updatekeys — re-encrypts to the new listThis re-encrypts the current secrets/db.enc.env so it can only be decrypted by the keys now listed as recipients. That's real and it works — a key removed from .sops.yaml genuinely cannot decrypt the file produced after that point. But look closely at what got re-encrypted: the current file. One artifact, one point in time.
Two things rotation cannot touch
Already-decrypted values. If an age private key was exposed — copied off a laptop, leaked in a misconfigured backup, pasted somewhere it shouldn't have been — then whoever holds it has already been able to run sops -d against every secret in the repo, for as long as they've had it. Rotating the key going forward does not revoke knowledge that already left the system. The database password that key could decrypt is still that password until it is changed — a completely separate action from rotating the encryption key that protected it.
Git history. SOPS encrypts files, and git keeps every version of every file it has ever committed. sops updatekeys re-encrypts the file at the current commit. Every earlier commit in the repository's history still holds ciphertext that the old key can decrypt — because that ciphertext was never touched by the rotation; only the newest version was. git log -p -- secrets/db.enc.env will show a trail of old, still-readable-by-the-old-key blobs going back to the file's first commit.
Verifying a removal actually worked
Because sops silently falls back to a key in a default location if one is present, testing rotation naively can produce a false pass — the old key file might still be sitting in ~/.config/sops/age/keys.txt on the machine doing the test, and sops will happily use it even though you meant to test as if it were gone. The only reliable check runs under an isolated environment that can't see any key but the one under test:
env -i PATH=/usr/bin:/bin:/usr/local/bin HOME=/tmp/empty \
SOPS_AGE_KEY_FILE=/path/to/old.key sops -d secrets/db.enc.env
# must FAILIf that command succeeds, the removal didn't take — usually because the old key is still listed somewhere in .sops.yaml, or the file wasn't actually re-encrypted after the edit.
The rule this generalizes to
This is not specific to age or SOPS. Every rotation mechanism for every kind of credential — API tokens, TLS certificates, SSH keys, database passwords — shares the same shape: rotation is a forward-looking control on future access, not a retroactive control on past exposure. The response to "this key may have leaked" is never rotation alone. It's rotation of the key and rotation of every secret that key could read, because only the second half actually invalidates what an attacker might already hold.
Series: The Self-Hosting Stack. Next: a deploy key that can only pull.