Off-box backup: pin the host key, guarantee the remote dir, page on failure

PHASE 1 — HOST KEY STATICALLY PINNED. ssh-keyscan -p 23 returned an
ED25519 key whose fingerprint EQUALS the out-of-band value
SHA256:XqONwb1S0zuj5A1CDxpOSuD2hnAArV1A3wKY7Z3sdgM, so it is safe to pin.
scripts/storagebox_known_hosts now carries that verified line and ships to
the container (Dockerfile already COPYs scripts/). backup-db.sh uses
StrictHostKeyChecking=yes + UserKnownHostsFile=<pin> instead of
accept-new, which was trust-on-first-use and would have accepted an
impostor on the very first run. A missing pin file REFUSES the push rather
than silently falling back. Never weakened to accept-new/=no//dev/null —
a test asserts that on executable lines.

PHASE 1b — REMOTE DIR GUARANTEED. The box has only .ssh/, and rsyncing a
file into a missing parent either fails or silently writes the dump AS the
directory name — one file, overwritten nightly, reading as "backups exist"
while retaining exactly one. Uses rsync --mkpath when available, else an
explicit remote mkdir -p ahead of the push.

PHASE 2b — FAILED OFF-BOX PUSH IS NOW LOUD. Off-box is required, so the
failed-push path pages at "urgent" (was "low"/deferred) and the script
emits a machine-readable OFFBOX_OK=1/0/deferred that
POST /api/internal/backup/run surfaces as a distinct offbox_ok field.
Exit code deliberately still reflects ON-BOX durability — a good on-box
dump must not raise a false total-failure alarm. Surfacing the truth, not
manufacturing a failure.

No key material is echoed anywhere; only the PUBLIC host key is committed.

Suite 280/3338 green, build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SmNjJAwEnqHPtXbvSZR8kA
This commit is contained in:
Kev
2026-07-20 01:10:23 -04:00
parent 7e150f9342
commit c4c9b97604
4 changed files with 147 additions and 6 deletions
+8
View File
@@ -255,8 +255,16 @@ router.post('/backup/run', async (req, res) => {
try {
const started = Date.now();
const result = await runBackup();
// Session 64 Phase 2b — off-box is REQUIRED now, so its outcome is reported
// SEPARATELY from the exit code. The script deliberately still exits 0 on a
// failed push (a durable on-box dump must not raise a false total-failure
// alarm) — so without this field a failed off-box push reads as success.
const tail = result.tail || '';
const offboxOk = /OFFBOX_OK=1/.test(tail) ? true
: (/OFFBOX_OK=0/.test(tail) ? false : null); // null = deferred/not attempted
return res.json({
ok: result.ok,
offbox_ok: offboxOk,
exit_code: result.code,
duration_ms: Date.now() - started,
durability_warning: durabilityWarning() || null,