Implement backups, commit and restoration
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
# Backup archive format
|
||||
|
||||
Backups are uncompressed tar archives with a versioned TOML manifest. Version 1
|
||||
uses only fixed logical member names and is parsed without extracting files.
|
||||
The archive is an opaque saved-state copy; its tar checksum detects accidental
|
||||
header damage but does not authenticate contents or make the archive tamper-proof.
|
||||
|
||||
## Members and limits
|
||||
|
||||
The first member is always `manifest.toml`. The only permitted payload names are
|
||||
`settings.toml`, `server.conf`, `internal-state.toml`, and `vault.toml`, in that
|
||||
order when present. Settings and server configuration are required. Internal
|
||||
state and vault are optional components: a missing component is recorded as
|
||||
`present = false` and `length = none`, which means restore removes that owned
|
||||
component. Absence is not represented by an empty placeholder file.
|
||||
|
||||
| Member | Maximum bytes | Presence |
|
||||
| --- | ---: | --- |
|
||||
| `manifest.toml` | 64 KiB | Required, first |
|
||||
| `settings.toml` | 1 MiB | Required |
|
||||
| `server.conf` | 4 MiB | Required |
|
||||
| `internal-state.toml` | 2 MiB | Optional |
|
||||
| `vault.toml` | 16 MiB | Optional |
|
||||
|
||||
The archive is limited to 26 MiB and five entries. Only checksummed UStar regular
|
||||
files are accepted. Absolute names, path separators, traversal, links, device or
|
||||
special entries, sparse entries, GNU/PAX extension headers, nonzero path prefixes,
|
||||
unexpected metadata fields, duplicate/missing/unexpected members, size mismatches,
|
||||
truncation, nonzero padding, and nonzero trailing data are rejected. The reader
|
||||
checks declared sizes before allocating payload buffers and never follows a
|
||||
tar-supplied path. It does not restore files or validate their application
|
||||
semantics; restoration must run settings, configuration, state, and vault
|
||||
validators before mutation.
|
||||
|
||||
## Manifest v1
|
||||
|
||||
The manifest is UTF-8 TOML with unknown fields denied:
|
||||
|
||||
```toml
|
||||
version = 1
|
||||
created_at_utc = "2026-10-19T12:30:00Z"
|
||||
reason = "pre-commit"
|
||||
|
||||
[historical_apply]
|
||||
status = "unknown"
|
||||
|
||||
[[members]]
|
||||
name = "settings.toml"
|
||||
present = true
|
||||
length = 420
|
||||
|
||||
[[members]]
|
||||
name = "server.conf"
|
||||
present = true
|
||||
length = 640
|
||||
|
||||
[[members]]
|
||||
name = "internal-state.toml"
|
||||
present = false
|
||||
|
||||
[[members]]
|
||||
name = "vault.toml"
|
||||
present = true
|
||||
length = 8192
|
||||
```
|
||||
|
||||
`reason` is one of `manual`, `pre-commit`, `pre-restore`, or `pre-restart`.
|
||||
Timestamps are canonical RFC 3339 UTC values with whole-second precision. The
|
||||
historical apply record is either `status = "unknown"` or
|
||||
`status = "verified"` with a `verified_at_utc` timestamp. It is historical
|
||||
metadata only: a pre-operation reason or a verified marker does not establish
|
||||
that these bytes match the currently running interface. Application-created
|
||||
backups derive this record from the timestamp stored in the internal-state bytes
|
||||
captured with the saved set. A successful Commit or restore clears that timestamp
|
||||
for its new saved generation; a failed operation leaves it intact. Reopening may
|
||||
show the saved generation's historical verification time, but runtime observation
|
||||
always begins as unknown.
|
||||
|
||||
The inventory always has exactly four records in the specified order. Required
|
||||
members have `present = true` and an exact byte `length`; optional absence has
|
||||
`present = false` and omits `length` (TOML has no null value). Present optional members follow the required
|
||||
members in fixed order. The manifest itself is excluded from its payload
|
||||
inventory. Readers reject unsupported versions and any discrepancy between
|
||||
inventory and tar entries.
|
||||
|
||||
## Publication and trust boundary
|
||||
|
||||
Creation receives exact saved bytes from a trusted caller. It does not discover
|
||||
files, inspect the current working directory, recurse into the archive directory,
|
||||
or read client source/output directories. The caller must supply installed bytes,
|
||||
never a proposed draft or decrypted vault records. New archives are fully
|
||||
serialized and synced in a private temporary file, then published without
|
||||
replacement under a timestamped collision-safe name; the archive directory is
|
||||
synced after publication. The archive file is mode `0600` from temporary-file
|
||||
creation. A write or file-sync failure removes the unpublished temporary file;
|
||||
a publication or directory-sync failure returns an error and blocks the protected
|
||||
operation. A complete archive may already be visible if failure occurs after its
|
||||
atomic publication, and a failed temporary cleanup may leave a hidden `0600`
|
||||
temporary file. Neither condition is reported as backup success. Readers should
|
||||
expose the list and preview without passwords, but must keep payload bytes out of
|
||||
Debug output and logs.
|
||||
|
||||
The application restore preview validates both the archive structure and its
|
||||
settings, server document, optional internal state, and opaque vault before it
|
||||
returns an opaque confirmation value. That value owns the validated payload
|
||||
bytes, so confirmation applies the exact content shown during preview even if
|
||||
the selected archive path changes afterward. Confirmation first completes a
|
||||
`pre-restore` archive and then uses the saved-file transaction journal for the
|
||||
full replacement. Dropping the preview is cancellation and does not discard a
|
||||
draft. Historical verification metadata is informational; opening or restoring
|
||||
an archive always leaves current runtime state unknown.
|
||||
|
||||
This module only creates, lists, and validates archives. It does not write saved
|
||||
state, apply WireGuard changes, or restore content. A backup is not encrypted:
|
||||
it can contain the server's private configuration and the encrypted vault, so
|
||||
filesystem access must remain restricted to root.
|
||||
@@ -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.
|
||||
@@ -7,12 +7,11 @@ unknown fields, malformed labels, invalid Base64, and oversized bundles.
|
||||
|
||||
## Version 1 structure
|
||||
|
||||
For M4, the current encrypted bundle is intended to live at
|
||||
The current encrypted bundle lives at
|
||||
`ResolvedSettings::data_directory.join("vault.toml")`. This stable filename is
|
||||
relative to the resolved data directory, whose default is `Backup/data` beside
|
||||
the executable. M3 only prepares and serializes vault bytes in memory; it does
|
||||
not create this file or install application state. M4 persistence will own that
|
||||
write.
|
||||
the executable. The application installs the opaque serialized bytes through
|
||||
the saved-file transaction after completing its mandatory pre-Commit backup.
|
||||
|
||||
The top level contains:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user