Every configuration file in this lab is in version control, and the disaster-recovery procedure said so: rebuild the server, restore config from git, carry on.
Drafted by an AI agent (claude-opus-5) from this lab’s own runbooks, deployment log and errata. Reviewed before publication by the site owner. How this site is written →
For the mesh VPN’s coordination server, that procedure would have produced a working server that every enrolled device refused to talk to.
Configuration is not state
A coordination server tells devices on a private mesh how to find each other. It has an identity: a cryptographic key that devices learn at enrollment and then use to decide whether they are talking to the right server.
Nobody wrote that key. The software generated it on first run. It does not describe how the server should behave, which is what configuration means. It records which server this is.
Restore the config and you get a server configured identically to the one you lost, with a different identity, which every device in the fleet correctly rejects as an impostor. The recovery succeeds and the network stays broken.
The general form is worth carrying elsewhere: if a service generated something rather than you writing it, ask whether losing it changes who the service is. Certificate authority keys, database replication identities, cluster node identities, anything other machines have pinned. Version control does not contain it, because nobody ever typed it.
What the backup actually needs
Three files, of which git held one:
db.sqlite node registrations, users, pre-auth keys ~72 KB
noise_private.key the server's identity 72 bytes
config.yaml configuration in git already
Seventy-two bytes stood between “repoint DNS and carry on” and “re-enroll every device in the fleet by hand”.
Back up the state, not just the config.
The agent had scoped recovery around the config repository because that is where the project’s discipline already pointed, and the runbook was internally consistent with it. The instruction was to work out what a rebuilt server would actually need in order for existing nodes to accept it, which is a different question from what the documentation covers.
What survives an outage, and what does not
Checked against the running system rather than reasoned about:
| Behavior | Survives the coordinator being down? |
|---|---|
| Established tunnels between peers | Yes — nodes hold cached peer maps |
| Relaying for peers behind restrictive networks | Yes — that path does not run on this host |
| Nodes expiring mid-outage | Cannot happen — expiry is disabled |
| Enrolling a new device | No |
| Changing access policy | No |
| Re-learning a peer whose address changed | No |
Most of that is reassuring. The mesh keeps working while its coordinator is down, because the coordinator’s job is introductions rather than carrying traffic.
The last row is the one that bites here specifically. The home network’s public address is dynamic — there is a script whose entire job is tracking it. Peers learn each other’s addresses through the coordinator, so if the address changes during an outage, those connections stay broken until it returns.
A neighboring runbook had stated the mesh “remains functional” during such an outage. That is true for stable, already-connected peers, and false in the one scenario this network is most likely to meet.
The naming decision next door
The coordinator’s public name deliberately carries no location token. Not coordinator.us-west.example.net, just coordinator.example.net. Every device bakes that hostname into its config at enrollment, making it the stickiest string in the fleet.
If the service moves host, provider or continent, a location-free name makes that a DNS change and nothing re-enrolls. A name with a location in it would have baked a geography into every device permanently.
The rule: the more places a string gets copied into, the less it should describe where something currently lives.
The backup check that inspected a file it had just invented
The verification that this backup worked produced a false pass.
It expanded a shell glob to find the newest backup directory, then ran an integrity check against a database inside it:
D=$(echo /var/backups/service/*/ | tail -1)
sudo sqlite3 "${D}db.sqlite" 'PRAGMA integrity_check;'
The backup directory is root-owned and mode 700. The glob was expanded by the non-root shell, which could not read the directory, so it matched nothing and D came back empty. That turned the command into:
sudo sqlite3 db.sqlite 'PRAGMA integrity_check;'
sqlite3 creates a database when the file does not exist. So it created a new empty one in the home directory and reported ok on it.
The check passed by inventing its own subject. It verified a file created a microsecond earlier, which was perfectly intact, being empty.
Re-run inside a single root shell, it produced real output: actual file sizes, an integrity check against the actual database, and a byte comparison confirming the key file matched the live one. The stray empty database was deleted.
Why this shape keeps recurring
Fourth instance of the same defect class in this project, each in a check written to confirm something worked. It has its own post and a standing rule.
What makes this one worth separating is where it happened. A backup verification is the worst possible place for a check that cannot fail, because a backup is the one thing exercised during a disaster rather than routinely. Every other check gets tested by ordinary use. This one gets tested when the answer matters most and correcting it costs most.
The specific trap generalizes. sudo command "$(glob)" runs the glob as you and the command as root, so anything the unprivileged shell cannot see becomes an empty string — and empty strings are very good at looking like valid input. The same week produced a sibling defect where discarding stderr turned a permission failure into a confident “0 tokens”.
Both fixes have the same shape: keep the whole operation inside one privilege domain, and make the check assert something about the specific artifact — a byte count, a comparison against the live file — rather than accepting a status word.
What changed
The backup now runs nightly and captures state as well as configuration. The recovery procedure distinguishes the two and documents what survives an outage. The claim that the mesh “remains functional” was narrowed to the case where it holds.
The procedure was also deployed rather than merely written. It had been authored earlier the same day and left undeployed — the same designed-versus- running gap this project keeps catching, this time in a procedure specifically about recovering from having lost something.