persistent-mount-coverage
Audit persistent mount coverage
Local implementation for hs-recovery-0145; repository completion is still open.
The October 7 handoff
records 45 passing scoped tests, passing web/Go typecheck, failed root lint and an
unrun full test phase. No live Docker collection or recovery certification has been performed.
Run the offline audit on a saved evidence packet:
node scripts/node/persistent-mount-audit.mjs /path/to/evidence.json
node scripts/node/persistent-mount-audit.mjs /path/to/evidence.json --text
Exit 0 means every supplied runtime mount is accounted for and all expected containers were inspected. Exit 1 means missing coverage, inspection gaps or a conflicting policy. Exit 2 means invalid/unreadable input. The CLI reads at most 16 MiB, accepts regular files, and performs no Docker, network or backup operation.
This is a coverage audit, not a recovery certificate. Filesystem selections do
not prove a completed backup, transactional database consistency or offsite recovery.
Native receipts are supplied evidence, not independently authenticated artifacts.
Every report sets recoveryVerified: false and every row restoreVerified: false.
Collect a packet from Docker
The optional collector reads the Docker context named in a policy file. It runs
only docker --context <name> ps and inspect; it never stops a container, copies
data, starts a backup or changes configuration. Choose the context deliberately:
it may refer to a remote Docker engine. The collector lists running containers;
stopped containers, unmounted volumes and writable container layers are outside its inventory.
{
"schema": 1,
"node": "node-identity-used-by-your-receipts",
"dockerContext": "reviewed-context",
"selections": [],
"exclusions": [],
"nativeReceipts": []
}
Populate those policy arrays using the contracts below. Empty arrays are valid: they make unprotected runtime mounts visible rather than inventing selections. The supplied node name is an operator label, not a verified hardware identity.
umask 077
node scripts/node/persistent-mount-collect.mjs /path/to/policy.json > packet.json
node scripts/node/persistent-mount-audit.mjs packet.json --text
Collector exit 0 means the inventory was collected completely; coverage can still
be missing. Exit 1 writes a partial packet for inspection failures, inventory
changes or collection deadlines. Keep and audit that packet. Exit 2 means invalid
policy/input, with no valid packet. Do not chain the commands with && if you need
the audit to show partial collection failures.
Collection lists full immutable container IDs before and after inspection. A missing
inspect result keeps its ID. Added/removed IDs retain an inventory-changed issue.
The collector projects only identity/mount fields, drops unrelated policy fields,
and never returns Docker stderr. Native receipt fields must be nonempty bounded
strings. A 30-second shared command budget, 5-second per-command deadline, 2 MiB
command output limit and 1,000-container limit bound collection. Large or slow fleets
produce incomplete evidence rather than a false success. The observed timestamp is
the end of collection; a backwards wall-clock jump makes the packet incomplete.
These checks establish two-observation inventory stability, not a transactional
snapshot of changing application data. Recorded collection issues are validated by
the auditor, remain visible in reports, and prevent inventoryComplete and
policyComplete from becoming true. Empty runtime is distinct from failed discovery.
Evidence packet
The packet uses Docker inspect's existing identity/mount fields. The abbreviated example below uses placeholders; supply full 64-character IDs and real evidence. Do not place credentials in evidence labels or exclusion rationale.
{
"schema": 1,
"node": "node-identity",
"observedAt": "2026-10-07T12:00:00.000Z",
"expectedContainerIds": ["<64-character container ID>"],
"inspectionFailures": [],
"containers": [{
"Id": "<64-character container ID>",
"Name": "/example-app",
"Image": "sha256:<64-character image digest>",
"Mounts": [{
"Type": "volume",
"Name": "example-data",
"Source": "/var/lib/docker/volumes/example-data/_data",
"Destination": "/data",
"RW": true
}]
}],
"selections": [{
"source": "/var/lib/docker/volumes/example-data/_data",
"scope": "tree",
"method": "live-files",
"evidence": "reviewed-full-backup-selection"
}],
"exclusions": [],
"nativeReceipts": []
}
expectedContainerIds must come from the same observed runtime listing as the
inspections. Do not derive it from the successful inspections alone: doing that
would conceal failed inspections. Keep failed IDs in inspectionFailures and
retain successful containers. Identity drift and unsupported mount types make the
inventory incomplete. Duplicate identities, invalid paths and malformed mounts
reject the packet. A volume shared by containers appears once per consumer.
selections describes actual backup selection policy, not an intended future
policy. Exact scope matches only the mount's source; tree scope also matches
descendants at directory boundaries. Supported methods are live-files and
paused-files. Both remain filesystem-selection-only until stronger evidence
exists. Bind sources may be files or directories; neither read-only mounts nor
opaque/cache-looking volume names imply expendability. tmpfs is counted separately.
An exclusion requires an exact source, nonempty rationale and evidence.
Overlapping exclusions and coverage produce a missing row with
conflicting-coverage-policy. Review the conflict; the audit does not choose for you.
Native database and queue evidence
Each normalized native receipt needs node, containerId, immutable image, exact
mount source and destination, method, status: "verified", capturedAt,
artifactSha256 and evidence. Methods are postgres-logical, mariadb-logical,
redis-rdb and queue-native. A successful native export must cover the declared
data scope; configuration in other mounts needs its own selection.
The receipt's age is measured against observedAt, with a 36-hour default budget.
This supports historical audits; it does not label old inventory as current health.
The library API auditMountCoverage(packet, { maxReceiptAgeMs }) accepts a positive
integer budget up to 366 days. Future, malformed, old and mismatched receipts cannot
cover a mount. Of valid receipts, the newest is chosen deterministically.
Contradictory digests for the same latest method/time produce a visible missing row,
even when a filesystem selection exists; the audit will not silently choose evidence.
Existing Redis manifests contain containerId, image, sha256, capturedAt and,
in newer versions, storage.source and storage.mountDestination. Normalize only
after checking native verification and the node identity. Older manifests without
storage binding cannot be joined by container name as though the binding were known.
Do not claim queue replay safety merely because an RDB file exists.
The audit never emits Docker Config.Env, Cmd or labels. Its report does include
container names, mount paths, image identities and your evidence labels; treat it
as internal operator metadata. This tool is not an authorization or signature boundary.
Verification
node --test --test-concurrency=1 scripts/node/persistent-mount-*.test.mjs
Unmounted volumes, mutable container layers and application-level recovery remain outside this audit. Collector integration has executable fake-Docker proof; real fleet collection has not been run as part of this change. An empty explicitly observed runtime can have complete policy accounting without proving that an app is protected.