Recover, rebuild or bootstrap¶
Getting the secrets back when something is gone: your workstation, GitHub, the Swarm, or everything. Also how the store was first created. Background is in BACKGROUND.md.
Pick your situation¶
| You have | Section |
|---|---|
| a Swarm manager and GitHub access, just not your workstation | 1. Read the store from anywhere |
| a Swarm manager and an Entra login, but no GitHub | 2. Restore the store from Key Vault |
| GitHub, but no Key Vault, and the age key on paper | 3. Without Key Vault |
| a brand-new Swarm to put the secrets back into | 4. Rebuild a Swarm |
| no store at all yet | 5. Bootstrap a new store |
The container carries the tools. The one thing it can't carry is
secrets/secrets.enc.yaml, the encrypted store, because that file changes. So
recovery is always two questions: where the store comes from, and where the key
to decrypt it comes from.
1. Read the store from anywhere¶
On any Swarm manager, from a workstation with ssh or on the manager itself.
1.1 Start the container. Use keyman if you have the alias, otherwise run
this on the manager:
docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock --dns-search mydomain.com -e AKV_VAULT=YourKeyVaultName -e AKV_TENANT=00000000-0000-0000-0000-000000000000 -e HOMELAB_REPO_URL=https://github.com/you/your-repo.git ghcr.io/scyto/key-manager:latest
1.2 At No repo in /repo. Sign in to GitHub and clone it now? [Y/n], press
Enter, then complete the GitHub device sign-in.
1.3 Sign in to Key Vault, which is where the age identity comes from:
1.4 Confirm the store decrypts:
2. Restore the store from Key Vault¶
When GitHub is unreachable. Key Vault holds the encrypted store and the age identity, so one Entra login gets both.
2.1 Start the container as in 1.1.
2.2 At No repo in /repo. Sign in to GitHub and clone it now? [Y/n], type
n.
2.3 Sign in to Key Vault:
2.4 Fetch the encrypted store into /repo:
2.5 Confirm it decrypts:
/repois not a git clone in this situation, so nothing you change here can be committed. Use it to read the store or rebuild a Swarm (section 4), not to add or rotate secrets.If you hold accounts in more than one Entra tenant: the code only completes against the tenant
az-loginprints. An account from another tenant fails withAADSTS50020rather than authorising anything.
3. Without Key Vault¶
The store comes from GitHub and the key from your paper copy. The key is typed into memory inside the container and never reaches a disk.
3.1 Start the container as in 1.1, and at the clone prompt press Enter and complete the GitHub sign-in.
3.2 Type the key into the container's memory-backed /work:
Type or paste the AGE-SECRET-KEY-1... line, press Enter, then Ctrl-D.
3.3 Point sops at it and check it decrypts:
identity did not match any of the recipients, orno identity matched any of the recipientswhen sops tried more than one key, means the key is a valid age key, but not one this store is encrypted to: an old key, or another store's. It is not a typo. A mistyped key never gets that far: age keys carry a checksum, so it fails first, as amalformed secret key, usually withinvalid checksum.Do not use
SOPS_AGE_KEYfor this. sops accepts the key in that environment variable, but setting it leaves the key behind. Passed withdocker run -e, it is in the container's configuration, whichdocker inspectshows to anyone with the Docker socket, and on thedocker runcommand line, whichpsshows on the host. Typed asexport SOPS_AGE_KEY=...at the prompt, it is on a command line inside the container, and bash saves that to~/.bash_history, on the container's disk, when you exit.catinto/workputs it in neither place.
/workis a tmpfs: the file is in RAM and gone when the container exits.
4. Rebuild a Swarm¶
Everything except the secrets comes from this repo. The secrets come from the store.
4.1 Create the Swarm and join the other managers.
4.2 Recreate the out-of-band networks, from a clone of this repo on each node. First on every node:
Then once, on a manager:
swarm makes the AdGuard macvlans from the per-node configs, so it needs
config-only done everywhere first. overlay makes discovery, the
attachable overlay that Homepage and Gatus use to reach dockerproxy, and
depends on neither.
4.3 On a manager, start the container and read the store, as in section 1 or 2.
4.4 Read the new Swarm's cluster ID:
4.5 Record it in the store, so provision will write to this Swarm:
4.6 Push the changed store to Key Vault straight away:
4.7 Create every secret:
4.8 If you have a git clone (section 1), commit and push the change on a
branch and open the PR. It prints the PR's URL; the number at the end is 130
here:
git checkout -b secrets/new-swarm-cluster-id
git add -A
git commit -m "secrets: record the rebuilt Swarm's cluster id"
git push -u origin secrets/new-swarm-cluster-id
gh pr create --fill
4.9 Merge it:
4.10 Recreate the Portainer stacks from their deploy branches.
Why 4.5: the store records the one Swarm it may write to, and every write checks it. A rebuilt Swarm has a new cluster ID, so without 4.5,
provisionstops withWRONG SWARM.Why 4.6 comes before anything else: the container is thrown away when you exit. Until the change is in Key Vault or merged into
main, it exists only inside the container, and a laterrestoreor clone would bring back the old ID. In section 2's situation there is no clone, so 4.6 is the only durable copy until GitHub is back and you can do 4.8 and 4.9.4.4 to 4.9 follow the tool's code and have not been run for real, because this Swarm has not been rebuilt.
5. Bootstrap a new store¶
How the store was first created, kept for a rebuild from nothing.
Written before key operations moved into the container. These steps create the age identity on a workstation, which the rules now forbid. Doing the same inside the container, with the identity in
/workand then uploaded to Key Vault bybackup, is the intended replacement and has not been tried.
5.1 Create the identity:
mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
chmod 600 ~/.config/sops/age/keys.txt
age-keygen -o prints only the public key. The identity goes to the file.
| Platform | Where sops looks for the key |
|---|---|
| Linux | ~/.config/sops/age/keys.txt |
| macOS | ~/Library/Application Support/sops/age/keys.txt |
| Windows | %AppData%\sops\age\keys.txt |
SOPS_AGE_KEY_FILE overrides all three.
5.2 Record the recipient in .sops.yaml at the repo root:
creation_rules:
- path_regex: ^secrets/secrets\.enc\.yaml$
encrypted_regex: '^value$'
age: >-
age1... your public key ...
encrypted_regex: '^value$' encrypts the values and leaves names, notes and
flags readable.
5.3 Keep the identity in at least two independent places. To check a paper
copy, type it back, press Ctrl-D, and confirm it prints your age1...:
A typo gives invalid checksum rather than a silently wrong key, because the
key format carries a checksum.
5.4 Create the vault, with Azure RBAC, soft-delete and purge protection:
az keyvault create --name <vault> --resource-group <resource-group> --location <region> --enable-rbac-authorization true --enable-purge-protection true --retention-days 90
RBAC is the CLI's default now, but older versions default to access policies, and 5.5 needs RBAC. Saying it keeps the command right on either.
5.5 Give yourself the right to read and write its secrets. Creating an RBAC vault grants you nothing inside it, so until this every read or write of a secret is refused:
az role assignment create --role "Key Vault Secrets Officer" --assignee <your-sign-in-name> --scope /subscriptions/<subscription-id>/resourceGroups/<resource-group>/providers/Microsoft.KeyVault/vaults/<vault>
A new role assignment can take a few minutes to apply.
5.6 In the key-manager container, started and cloned as in 1.1 and 1.2 so
the new store is in /repo, put the identity in /work as in
3.2 and 3.3.
5.7 Push everything offsite and check it:
backupuploads each secret, the age identity, so a bare machine can bootstrap from the vault, and the whole encrypted store, so a restore reproduces notes and flags exactly.The identity goes up only from a file, which is why 5.6 puts it in
/work. Without one,backupprintsidentity skipped: no local key fileand uploads the rest.
backup --applysigns you in to Key Vault itself.az-loginwould too, but on a vault with noage-identityyet it ends withcould not read age-identity: 5.7 is what creates it.Without purge protection, one admin action destroys the backup permanently.
What a container leaves behind¶
Nothing. With --rm and only the Docker socket mounted, the container takes
everything with it when it exits: the clone, the GitHub login, the Entra token,
a typed age key, and /work.
That is why add, rotate and retire changes have to be pushed before you
exit. If rotate or retire could not push, it says NOT PUSHED and exits
non-zero. That is the one time not to type exit.