diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 1d63df3..9dea2fa 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -347,8 +347,8 @@ export default function SettingsPage() {

Backup token

- Enables encrypted settings + credentials backups to the master - (its INSTANCE_BACKUP_TOKEN). Blank = backups off. + Backups push automatically — this token (the master's + INSTANCE_BACKUP_TOKEN) is only needed to restore one.

{backupStatus.at diff --git a/lib/backup-sync.ts b/lib/backup-sync.ts index 07cb1d3..8769d0b 100644 --- a/lib/backup-sync.ts +++ b/lib/backup-sync.ts @@ -5,9 +5,11 @@ * instance's uuid. * * This is the deliberate exception to "credentials never leave the machine": - * the payload goes over HTTPS to the bearer-gated /api/instances/backup and is - * AES-encrypted at rest on the master. It never rides the unauthenticated - * control socket. + * the payload goes over HTTPS to /api/instances/backup and is AES-encrypted + * at rest on the master. It never rides the control socket. Pushing needs no + * token — every instance backs up automatically once it has its uuid; only + * PULLING a backup back out (lib/backup-restore.ts) is token-gated, because + * that is the direction credentials come out in the clear. * * Push policy: on bootstrap, then every POLL_INTERVAL, but only when the * content actually changed (hash comparison) — plus an immediate debounced @@ -85,19 +87,20 @@ function contentHash(payload: BackupPayload): string { .digest('hex'); } -function preconditions(): { masterUrl: string; id: string; token: string } | null { +function preconditions(): { masterUrl: string; id: string } | null { const masterUrl = getSetting('master_dashboard_url'); - const token = getSetting('master_backup_token'); // No uuid yet = the master has never said welcome; there is nothing stable - // to key the backup by, so wait for the first hello to complete. + // to key the backup by, so wait for the first hello to complete. No token + // needed: pushes are open on the master (only PULLING a backup is + // token-gated), so every instance backs up automatically. const id = getSetting('instance_id'); - if (!masterUrl || !token || !id) return null; - return { masterUrl, id, token }; + if (!masterUrl || !id) return null; + return { masterUrl, id }; } export async function pushBackup(force = false): Promise<{ ok: boolean; skipped?: boolean; error?: string }> { const pre = preconditions(); - if (!pre) return { ok: false, skipped: true, error: 'master url, backup token or instance uuid not set' }; + if (!pre) return { ok: false, skipped: true, error: 'master url or instance uuid not set' }; const payload = collectBackup(); const hash = contentHash(payload); @@ -113,10 +116,7 @@ export async function pushBackup(force = false): Promise<{ ok: boolean; skipped? try { const res = await fetch(url, { method: 'PUT', - headers: { - 'content-type': 'application/json', - authorization: `Bearer ${pre.token}`, - }, + headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: pre.id, name: getSetting('instance_name') ?? '', @@ -151,7 +151,10 @@ export function scheduleBackupPush(): void { export function startBackupSync(): void { stopBackupSync(); const tick = async () => { - await pushBackup(); + // Forced: pushes need no token, so anyone who reaches the master and + // knows this uuid could overwrite our backup there. Re-asserting the + // real state every tick bounds that poisoning window to 5 minutes. + await pushBackup(true); // Jitter so a fleet restarting together doesn't push in lockstep. g.__backupSyncTimer = setTimeout(tick, POLL_INTERVAL_MS + Math.floor(Math.random() * 30_000)); }; diff --git a/lib/db.ts b/lib/db.ts index c81a5f8..b763018 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -197,8 +197,9 @@ seedSetting.run('twocaptcha_api_key', ''); // Blank falls back to the proxy hardcoded in app/api/autobuyer/captcha; `none` // solves without one. seedSetting.run('twocaptcha_proxy', ''); -// Bearer for pushing/pulling the encrypted settings+credentials backup on the -// master (its INSTANCE_BACKUP_TOKEN). Blank = backups off. +// Bearer for PULLING an encrypted settings+credentials backup back from the +// master (its INSTANCE_BACKUP_TOKEN) — pushing needs no token and happens +// automatically. Blank = restore unavailable from this machine's UI. seedSetting.run('master_backup_token', ''); // Routes broker traffic (TopstepX, Tradovate, Yahoo quotes) through a proxy: // http(s):// or socks5(h)://, user:pass ok. The master link stays direct on diff --git a/setup-linux.sh b/setup-linux.sh index b66d720..5a5d0a5 100755 --- a/setup-linux.sh +++ b/setup-linux.sh @@ -86,10 +86,11 @@ read -r -p " Master dashboard URL [$DEFAULT_MASTER]: " MASTER_URL MASTER_URL="${MASTER_URL:-$DEFAULT_MASTER}" read -r -p " Instance name [$(hostname)]: " INSTANCE INSTANCE="${INSTANCE:-$(hostname)}" -read -r -p " Backup token (blank = no master backups): " BACKUP_TOKEN -RESTORE_FROM="" -if [ -n "$BACKUP_TOKEN" ]; then - read -r -p " Restore a dead instance? Its uuid (blank = fresh install): " RESTORE_FROM +# Backups push automatically; the token is only needed to pull one back. +read -r -p " Restore a dead instance? Its uuid (blank = fresh install): " RESTORE_FROM +BACKUP_TOKEN="" +if [ -n "$RESTORE_FROM" ]; then + read -r -p " Backup token (the master's INSTANCE_BACKUP_TOKEN): " BACKUP_TOKEN fi echo info "installing to: $TARGET"