Files
wireguard-admin/docs/PERSISTENCE.md
T

119 lines
7.1 KiB
Markdown

# Saved state persistence
The persistence adapter reads and installs the application's saved files at the
paths in `config::ResolvedSettings`. Settings remain beside the executable,
the server configuration stays at its configured invariant path, and internal
state and the opaque encrypted vault live in `data_directory`. No path is
resolved from the process working directory. Archive member names never become
filesystem paths.
## Internal state v1
`internal-state.toml` is optional. Its absence is valid only during initial
setup; once an installed state file exists, malformed or unsupported data is an
error and cannot be treated as an empty reservation set. Version 1 contains no
alias registry and does not duplicate server client metadata:
```toml
version = 1
reserved_addresses = ["10.0.0.8", "fd00::8"]
last_verified_at_utc = "2026-10-19T12:30:00Z"
```
`reserved_addresses` records addresses retained after saved deletion until a
later explicit runtime verification. It is canonically sorted and duplicate
free. `last_verified_at_utc` records when this saved generation was last verified
against the runtime. It is historical bookkeeping only: loading it always
produces `RuntimeObservation::Unknown` and never establishes that the current
interface matches saved files. The timestamp is stored in the same journaled
internal-state component as the saved set it describes. Successful Commit and
restore write a new generation with this timestamp cleared; failed operations do
not advance or clear it. M5 records verification only after a completed
pre-restart backup, successful runtime verification, and successful journaled
state write. If that write fails, the in-memory applied state remains unchanged.
This binding follows the application's single-administrator save boundary; it is
not a digest that detects manual edits made outside the application.
Unknown fields, duplicate addresses, invalid addresses/timestamps, and
unsupported versions are rejected.
Settings bytes are parsed and semantically validated, server bytes are parsed
with the byte-preserving `ConfigDocument`, and a present vault is parsed by
`VaultBundle::from_toml` without unlocking it. Saved reads are bounded and
return exact bytes; their `Debug` representations redact content. The server
parser reports its existing anomalies without rewriting or normalizing bytes.
The server configuration and a present vault must already have mode `0600` or
stricter when read. A broader mode fails closed with an actionable permission
error; the application does not silently chmod an existing secret-bearing file.
The operator must restrict its mode and retry. Newly installed replacements
are created as `0600`.
During restore, operational settings such as endpoint, DNS, systemd unit, and
timeout may come from the archive. The configured interface name remains fixed
for this installation, along with the resolved server configuration, client
source, and data-root paths (including their derived data/archive directories).
This preserves the single-interface mapping while allowing an archive to restore
other settings without redirecting writes.
## Destination and path safety
The adapter accepts only the resolved settings path, configured server path,
`data_directory/internal-state.toml`, and `data_directory/vault.toml`. These
paths are checked for overlap/aliasing before a write. Every existing path
component is checked without following symlinks; saved files must be regular
files. Destination directories are private (`0700`), and replacement and
journal files are created `0600` from their creation. Symlinks, non-regular
files, unsafe directory components, and path aliasing fail closed. No archive
metadata controls destination names, permissions, or ownership.
## Multi-file installation and recovery
Installation is not a cross-directory atomic operation. The caller must first
complete the mandatory operation backup and supply the adapter's
`BackupComplete` capability. The adapter then validates the complete proposed
saved set and checks that no earlier transaction needs recovery. It creates a
fixed-name private transaction directory under the trusted data directory and
writes the prior bytes (including explicit absence) and proposed bytes under
fixed component names. Each payload and the transaction directory are synced,
then the journal metadata and a `prepared` marker are atomically published and
the journal and parent data directory are synced. Only then may destination
files be changed. An interrupted journal without a valid prepared marker is
safe to clean because the adapter cannot have started replacements yet.
Replacement order is settings, server configuration, internal state, then
vault. A deletion of an optional state or vault is part of that order. Each
new file is written completely to a same-directory `0600` temporary file,
flushed and synced, atomically renamed, and followed by a parent-directory
sync. After all replacements, the adapter syncs all affected parents, records
the transaction as committed, syncs that marker and its parent, then removes
the transaction directory and syncs the data directory. Only a fully completed
sequence returns success.
On reopen, an uncommitted journal means rollback: prior bytes are reinstalled
in reverse replacement order, previously absent optional files are removed,
and all affected parent directories are synced. If rollback or cleanup fails,
the adapter returns `RecoveryRequired` and rejects every new mutation. Recovery
can be retried after correcting the filesystem problem. A committed journal
means all replacements finished; reopening performs cleanup only and never
rolls back the completed set. If writing/syncing the committed marker or
cleaning the journal fails during installation, the result is explicitly
`RecoveryRequired`, not success. The next open determines rollback versus
cleanup from the durable marker.
This journal repairs interrupted saved-file writes only. It never invokes a
WireGuard or systemd operation, does not provide concurrent-editor locking,
and is not automatic rollback after a failed runtime restart. The separate
pre-operation archive remains the recovery source if local journal data is
unusable; the adapter does not silently discard an invalid journal.
For an explicit restart, the application creates the pre-restart archive before
calling the service adapter. After a successful terminal service job, it checks
the complete observed peer set against the saved server document. A match is
then recorded by installing the updated `internal-state.toml` through this same
journal, with reservations removed only from the proposed state as appropriate.
The in-memory saved/applied state advances only after installation succeeds. If
that write or journal recovery fails after the VPN may have changed, the
application reports the persistence stage, retains in-memory reservations, and
requires manual recovery; it does not roll back or restart again. Default tests
exercise this orchestration with private fake adapters and synthetic files.
Actual interface and systemd behavior remain deferred to the isolated Alpine
and disposable VM acceptance environments described in `RUNTIME.md`.