389565021081, the GitHub repo amarcin/homelab, and physical/SSH access to the hardware.
Recovery Keys You Need
All stored outside the homelab (Apple Passwords / 1Password / hardware token):- AWS credentials for the backup IAM user (S3 bucket
augustin-backups) - Restic repository password
- GitHub SSH key (to clone the repo to a replacement node)
- sealed-secrets sealing key — the private key that decrypts every
SealedSecretin the repo. It lives in the cluster (nskube-system), and a monthly CronJob age-encrypts it tos3://augustin-backups/sealed-secrets-keys(cluster/infra/sealed-secrets/key-backup-cronjob.yaml). The private age key that decrypts those backups is in 1Password — without it a lost control-plane means every SealedSecret is undecryptable. - Cloudflare tunnel token (for
cloudflared)
How Recovery Works Now
Desired state is thecluster/ tree in git; Flux reconciles it. So recovery is mostly: get a k3s cluster back, point Flux at the repo, restore the sealed-secrets sealing key and the restic data, and let Flux converge. There is no per-service docker stack deploy anymore.
Scenario 1: Single Service / DB Corrupted
Two backup layers restore application data:- Longhorn S3 backups (primary) — every
platform/bulkvolume is snapshotted daily to S3 by a Longhorn RecurringJob (StorageClasses auto-enroll new volumes). Restore a volume from the Longhorn UI (Backup → restore) and re-attach it, or clone into a new PVC. - restic CronJob (
cluster/infra/backup/) — covers what Longhorn snapshots can’t capture consistently:pg_dumpallof Postgres (miniflux, temporal) plus the legacy hostPath / local-path trees. Restore from S3:
- Postgres (miniflux, temporal): restore the
pg_dumpalldump from/tmp/restore, thenkubectl execinto the DB pod andpsql < dump. - Everything else: restore the Longhorn volume backup rather than a file copy.
Scenario 2: Worker Node Loss (i3 or i5)
Longhorn’splatform class keeps 2 replicas across i3+i5, so losing one worker does not lose platform data — the surviving replica serves it and Longhorn rebuilds onto the replacement.
- Reinstall Debian on the replacement, install
open-iscsi+nfs-common(Longhorn needs them). - Join it to the cluster as a k3s agent (token from the control-plane:
cat /var/lib/rancher/k3s/server/node-tokenon pentium). - Re-apply the node label (
kubectl label node <name> kind=storage|compute) and, if it bears a Longhorn disk, add the disk in the Longhorn UI. - Longhorn rebuilds the missing replica automatically.
bulkdata (the media library) is single-replica — if the node holding it is lost, that data is gone and must be re-downloaded. Only the media library is onbulk.
Scenario 3: Control-Plane Loss (pentium)
pentium is diskless and holds no persistent data — it’s the control-plane + public door (cloudflared + Envoy proxy).- Reinstall Debian, install k3s server (control-plane). Restore the same cluster token / datastore if you want the same cluster identity; otherwise a fresh control-plane + re-join workers also works but is more disruptive.
registries.yamlis retired (2026-07-19) — nodes pull from GHCR directly, so there is nothing to restore here.- Recreate the local-storage default-class override (node-local, not in git):
sudo touch /var/lib/rancher/k3s/server/manifests/local-storage.yaml.skip, thenkubectl annotate sc local-path storageclass.kubernetes.io/is-default-class=false --overwrite. Without the.skipsentinel, k3s re-applies its bundledlocal-storage.yamlon every restart and re-markslocal-pathas the cluster-default StorageClass. See “No default StorageClass” under Known Gaps. - Re-enable Secrets encryption-at-rest (node-local, not in git — see “Secrets encryption-at-rest” under Known Gaps). If you restored the original datastore, the AES key travels with it (it’s persisted as cluster bootstrap data), so you only need
secrets-encryption: trueback in/etc/rancher/k3s/config.yamlbefore starting k3s. On a fresh datastore, re-run the enable flow:echo "secrets-encryption: true" | sudo tee /etc/rancher/k3s/config.yaml→sudo k3s secrets-encrypt enable→sudo systemctl restart k3s(wait for/healthz= ok) →sudo k3s secrets-encrypt rotate-keys→sudo systemctl restart k3s. Verify withsudo k3s secrets-encrypt status(Enabled, stagereencrypt_finished, an AES-CBC active key). - Ensure the
NoScheduletaint is back so workloads don’t land on it. - cloudflared + the Envoy Gateway proxy pods reschedule from git; verify
https://<anything>.augustin.airesolves again.
Scenario 4: Full Cluster Rebuild
- Install k3s server on pentium, agents on i3 + i5. Label the workers, taint pentium.
- Install Longhorn’s node prereqs (
open-iscsi,nfs-common) and add disks on i3 + i5. registries.yamlis retired — nodes pull from GHCR directly (no mirror to restore).flux bootstrap githubagainstamarcin/homelab(branchmain). Restore the sealed-secrets sealing key into nskube-systembefore Flux tries to unseal: fetch the latest age-encrypted bundle froms3://augustin-backups/sealed-secrets-keys, decrypt it with the private age key from 1Password, andkubectl applythe key Secret (labelsealedsecrets.bitnami.com/sealed-secrets-key). The controller then decrypts everySealedSecretin the repo. (A fresh key means every SealedSecret must be re-sealed — restore the backed-up key to avoid that.)- Flux reconciles the whole
cluster/tree. Longhorn PVCs come up empty. - Restore application data from restic (Scenario 1 pattern) into the freshly-provisioned PVCs.
- Media library (
bulk) is re-downloaded, not restored.
Scenario 5: Restic Repo Corruption
Detected byrestic check.
- Don’t
restic forget --pruneuntil fixed — it makes it worse. - S3 versioning has your back: recover prior pack-file versions via
aws s3api list-object-versions --bucket augustin-backups --prefix cluster/data/. - Worst case: start a new repo, accept data loss to the last good snapshot; keep the old repo read-only until the new one is proven healthy.
Scenario 6: Cloudflared Tunnel Dead
All public URLs andssh.augustin.ai stop working.
kubectl -n mesh get pods -l app=cloudflaredandkubectl -n mesh logs— running? registration errors?- Cloudflare dashboard → Zero Trust → Networks → Tunnels — tunnel “healthy”? origin still
http://192.168.1.16:8890? - If the token is rotated/compromised: re-seal the
cloudflaredSealedSecret with the new token, commit, let Flux roll it. - Out-of-band access with the tunnel down: SSH into a node on its LAN IP from a machine on the LAN.
Known Gaps
- Node-level config is not in git. The k3s install itself and the Longhorn disk config are node-local, as is the local-storage default-class override on pentium (see next). (
registries.yamlwas retired 2026-07-19 — nodes pull from GHCR directly, nothing to restore.) - No default StorageClass — enforced node-level on pentium. Callers must name a class explicitly (
platformfor precious state,bulkfor media); a class-less PVC intentionally fails to bind rather than silently landing on node-local single-copylocal-path. k3s shipslocal-pathas the cluster-default via its bundledlocal-storageaddon manifest and re-applies it on every restart, so the override is two node-local pieces on pentium: (1) asudo touch /var/lib/rancher/k3s/server/manifests/local-storage.yaml.skipsentinel that tells k3s to stop managing that addon (the running provisioner is left intact, solocal-pathstays available for anyone who names it), and (2)kubectl annotate sc local-path storageclass.kubernetes.io/is-default-class=false --overwrite. The.skipmakes the annotation durable across k3s restarts. This is deliberately NOT in git — the StorageClass is owned by the k3s addon controller, and a Flux-managed copy would fight it; the sentinel is the k3s-sanctioned way to bow the addon out. - Secrets encryption-at-rest is enabled but node-local on pentium. Kubernetes Secrets are encrypted at rest in the k3s datastore via k3s’s built-in AES-CBC provider (
secrets-encryption: truein/etc/rancher/k3s/config.yaml, which points the apiserver at/var/lib/rancher/k3s/server/cred/encryption-config.json). Neither the config flag nor the AES key is in git — the key lives in the datastore’s cluster bootstrap data and travels with a datastore restore, but a fresh control-plane must re-run the enable flow (Scenario 3, step 4). Consumers read secrets transparently; encryption is invisible to workloads. Note: enabling only encrypts secrets going forward + a one-time reencrypt pass; stale pre-encryption plaintext revisions linger in the SQLite datastore until kine compaction purges them. bulk(media library) is single-replica — a disk/node loss there means re-download. This is deliberate (media is cheap to refetch); precious state is on 2-replicaplatform.- The sealed-secrets sealing key is load-bearing. Without it the controller cannot decrypt any
SealedSecret, so secret-bearing services stay down. It lives in nskube-systemand is age-encrypted monthly tos3://augustin-backups/sealed-secrets-keys; keep the private age key (1Password) off-site.