Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions docs/runbooks/back-up-the-age-key.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,19 +68,29 @@ make secrets-verify-backup KEY=/path/to/the/copy
Expect:

```console
-- recipient matches .sops.yaml: age1yrdu996…
-- recipient age1yrdu996…
-- this file has 2 recipient(s); this run proves one of them
-- decrypting observability.sops.yaml with the backup key only
ok — keys.txt decrypts secrets/observability.sops.yaml (6/6 keys)
ok — keys.txt decrypts secrets/observability.sops.yaml (9/9 keys)
Proven: this key, on its own, recovers every secret in the repo.
Not proven: the other 1 recipient(s) of this file. Each needs its own
run against its own copy — SecretsKeyBackupUnproven names any that go
ninety days without one.
Not proven: that where you keep it will still exist after a fire,
a theft, or a forgotten password. That part is your judgement.
```

What it proves: the key parses, its public half is the recipient the file was
encrypted to, it decrypts the real ciphertext, and the result contains all six
keys `render-config.sh` requires. No secret value is printed, written to a
temporary file, or passed to another process.
What it proves: the key parses, its public half is one of the recipients the
file is *actually* encrypted to — read out of the ciphertext rather than out of
`.sops.yaml`, which can drift from it — it decrypts that ciphertext, and the
result contains all nine keys `render-config.sh` requires. No secret value is
printed, written to a temporary file, or passed to another process.

What it deliberately does not prove: that where you put the copy will still
exist after a fire, a theft, or a forgotten master password. That part is
judgement, not a check.
exist after a fire, a theft, or a forgotten master password — that part is
judgement, not a check — and nothing whatever about the *other* recipient. Each
copy is proved on its own, which is what the second `--` line above is counting;
see [Adding a second recipient](#adding-a-second-recipient).

> **Why not just run `sops -d` by hand.** Because on the host that already holds
> the key, it passes no matter what. `SOPS_AGE_KEY_FILE=<backup> sops -d …` also
Expand Down Expand Up @@ -301,8 +311,8 @@ recipient of their own, generated where they keep it and never on this host,
which satisfies ADR-0023's off-estate constraint and the handover case in one
move. The location stays out of this repository, as the first key's does.

Whether their key is in `.sops.yaml` yet is read from the file
(`grep -A3 creation_rules .sops.yaml`), not from this paragraph. Adding it is
Whether their key can open the secrets yet is read from the ciphertext
(`./scripts/key-recipients.sh --list`), not from this paragraph. Adding it is
the procedure above — `make secrets-add-recipient` with their public half — and
then `make secrets-verify-backup` with *each* key, because a re-key that drops
a recipient is the failure mode here. What this still does not solve: their
Expand Down
31 changes: 21 additions & 10 deletions docs/runbooks/successor-handover.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,21 +255,32 @@ and the recovery path is re-deriving each credential from the device it belongs
to: four SNMP rotations on hardware, one of which cannot persist a community
deletion and needs the switch rebooted to change.

Check first whether it is actually gone. `.sops.yaml` may list more than one
recipient — [ADR-0024](../adr/0024-hold-a-second-age-recipient-and-prove-each-one-separately.md)
Check first whether it is actually gone. The secrets may be encrypted to more
than one recipient — [ADR-0024](../adr/0024-hold-a-second-age-recipient-and-prove-each-one-separately.md)
made a second one the design, and a second *copy* may exist that is not the one
you were handed:
you were handed. Ask the ciphertext, which needs no key to read:

```bash
grep -A3 creation_rules .sops.yaml # every key that can open the secrets
./scripts/key-recipients.sh --list # every key that can open the secrets
```

If a recipient there is one somebody else holds, the secrets are recoverable and
this is a phone call rather than a rotation. Since 2026-09-08 the design is that
there is one: the **technical second named on the break-glass card** holds a
recipient of their own ([#294](https://github.com/Gerrrt/HomeLab/issues/294),
[`back-up-the-age-key.md`](back-up-the-age-key.md)). If `.sops.yaml` lists two
keys, the second is theirs, and the call is to them.
That reads the recipients out of `secrets/observability.sops.yaml` itself, which
is the only list that decides anything. `.sops.yaml` says which keys *future*
encryptions will use, and the two can disagree — a key added there by hand
without a re-key opens nothing. Compare them if you want to know whether that
has happened:

```bash
grep -oE 'age1[a-z0-9]+' .sops.yaml # what the repo advertises
```

If any recipient in the first list is one somebody else holds, the secrets are
recoverable and this is a phone call rather than a rotation. Since 2026-09-08
the design is that there is such a person: the **technical second named on the
break-glass card** holds a recipient of their own
([#294](https://github.com/Gerrrt/HomeLab/issues/294),
[`back-up-the-age-key.md`](back-up-the-age-key.md)). If the list has a key that
is not the one you were handed, that is theirs, and the call is to them.

**A handover is the moment to add yours.** ADR-0024 built the mechanism and
[#294](https://github.com/Gerrrt/HomeLab/issues/294) named the second holder;
Expand Down
Loading