diff --git a/controller/internal/backup/offbox.go b/controller/internal/backup/offbox.go index d0af191..0236833 100644 --- a/controller/internal/backup/offbox.go +++ b/controller/internal/backup/offbox.go @@ -131,13 +131,15 @@ func (m *Manager) offboxBaseArgs(t *settings.OffboxTarget) ([]string, []string) if port == 0 { port = 22 } - // One -o sftp.args token; restic splits it on spaces. Our paths have no spaces (data dir). The - // ConnectTimeout makes a dead NAS fail in ~N s; StrictHostKeyChecking + a pinned known_hosts avoid - // blind TOFU; BatchMode prevents any interactive prompt from hanging the runner. - sftpArgs := fmt.Sprintf("-oBatchMode=yes -oConnectTimeout=%d -oStrictHostKeyChecking=yes -oUserKnownHostsFile=%s -oPort=%d -i %s", - offboxConnectTimeoutSec, m.offboxKnownHosts(), port, m.offboxKeyPath()) + // restic's sftp backend connects via the `-o sftp.command` SSH invocation (the portable form across + // restic versions — `sftp.args` is not recognized by restic 0.14). The ConnectTimeout makes a dead NAS + // fail in ~N s (the load-bearing spike Q8 knob); StrictHostKeyChecking + a pinned known_hosts avoid + // blind TOFU; BatchMode prevents any interactive prompt from hanging the runner. The value is one -o + // token (restic takes everything after `sftp.command=`); our paths have no spaces (data dir). + sftpCmd := fmt.Sprintf("ssh %s@%s -p %d -oBatchMode=yes -oConnectTimeout=%d -oStrictHostKeyChecking=yes -oUserKnownHostsFile=%s -i %s -s sftp", + t.User, t.Host, port, offboxConnectTimeoutSec, m.offboxKnownHosts(), m.offboxKeyPath()) repo := "sftp:" + t.User + "@" + t.Host + ":" + t.RepoPath - args := []string{"-r", repo, "-o", "sftp.args=" + sftpArgs} + args := []string{"-r", repo, "-o", "sftp.command=" + sftpCmd} env := []string{"RESTIC_PASSWORD_FILE=" + m.offboxPwPath()} return args, env }