Implement backups, commit and restoration
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user