Verify off-box presence by reading the remote dir back

Exit 0 from the backup script is deliberately tied to ON-BOX durability,
so it is not proof the off-box copy landed. GET /api/internal/backup/offbox
runs rsync --list-only against BACKUP_REMOTE using the SAME pinned
known_hosts as the push (checking never disabled) and returns the dumps
actually present, with size and timestamp — so off-box presence is a
verified fact rather than an inference from an exit code.

Needed because the dev box cannot authenticate to the Storage Box: the
authorized key installed there is Kev's ~/vyndr-backup-key, not the
keypair generated in-session, so independent verification has to run from
the container that does hold working credentials.

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:14:38 -04:00
parent c4c9b97604
commit 2bfae804da
2 changed files with 75 additions and 1 deletions
+59 -1
View File
@@ -174,7 +174,65 @@ function latestDump(dir = process.env.BACKUP_DIR || '/var/backups/vyndr', deps =
}
}
/**
* List the OFF-BOX copies actually present on the Storage Box.
*
* Exit 0 from the backup script is NOT proof the dump landed — the script
* deliberately keeps its exit code tied to on-box durability. This reads the
* remote directory back, so "the off-box copy exists" is a verified fact rather
* than an inference. Uses the SAME pinned known_hosts as the push; host-key
* checking is never disabled.
*/
function listOffbox(deps = {}) {
const spawn = deps.spawn || require('child_process').spawn;
const remote = process.env.BACKUP_REMOTE;
const port = process.env.BACKUP_SSH_PORT || '23';
const knownHosts = process.env.BACKUP_KNOWN_HOSTS
|| path.join(__dirname, '..', 'scripts', 'storagebox_known_hosts');
return new Promise((resolve) => {
if (!remote) return resolve({ ok: false, error: 'BACKUP_REMOTE unset' });
if (!process.env.BACKUP_SSH_KEY) return resolve({ ok: false, error: 'BACKUP_SSH_KEY unset' });
const fs = require('fs');
const os = require('os');
let keyFile;
try {
// Same decode contract as backup-db.sh: base64 preferred, raw PEM fallback.
const raw = process.env.BACKUP_SSH_KEY;
let decoded;
try {
const d = Buffer.from(raw, 'base64').toString('utf8');
decoded = d.includes('PRIVATE KEY') ? d : raw.replace(/\\n/g, '\n');
} catch { decoded = raw.replace(/\\n/g, '\n'); }
keyFile = path.join(os.tmpdir(), `vyndr-offbox-${Date.now()}`);
fs.writeFileSync(keyFile, decoded.endsWith('\n') ? decoded : `${decoded}\n`, { mode: 0o600 });
} catch (e) {
return resolve({ ok: false, error: `key write failed: ${e.message}` });
}
const sshCmd = `ssh -p ${port} -o StrictHostKeyChecking=yes -o UserKnownHostsFile=${knownHosts} -o BatchMode=yes -i ${keyFile}`;
const child = spawn('rsync', ['--list-only', '-e', sshCmd, remote], { stdio: ['ignore', 'pipe', 'pipe'] });
let out = '';
let err = '';
child.stdout.on('data', (b) => { out += b.toString(); });
child.stderr.on('data', (b) => { err = (err + b.toString()).slice(-1500); });
const done = (result) => {
try { require('fs').unlinkSync(keyFile); } catch { /* best effort */ }
resolve(result);
};
child.on('error', (e) => done({ ok: false, error: e.message }));
child.on('close', (code) => {
const files = out.split('\n')
.map((l) => l.trim())
.filter((l) => /vyndr-.*\.dump$/.test(l))
.map((l) => {
const parts = l.split(/\s+/);
return { size: Number(parts[1].replace(/,/g, '')) || null, date: parts[2], time: parts[3], file: parts[parts.length - 1] };
});
done({ ok: code === 0, code, count: files.length, files, error: code === 0 ? null : (err || `exit ${code}`) });
});
});
}
module.exports = {
startBackupScheduler, runBackup, shouldArm, durabilityWarning, SCRIPT,
countRowsInDump, latestDump,
countRowsInDump, latestDump, listOffbox,
};