Transport via SCP through a Bastion Host
Wire transfer.sh so every successful backup is copied through a bastion (jump host) to a final backup target inside an isolated network. Use this when corporate or compliance policy requires that the Operaide host cannot reach the backup target directly, only via a controlled intermediate host.
The pattern uses ssh -J (ProxyJump) and standard scp. No agents, no extra services on the bastion.
Prerequisites
- A bastion host you can reach over SSH from the Operaide host.
- A backup target host reachable over SSH from the bastion (and only from there).
- A directory on the target host where backups should land (e.g.
/srv/operaide-backups/). - Backup setup already enabled on the Operaide host. See Enable Backups first.
Step 1: generate a dedicated SSH key
On the Operaide host, as root:
sudo -u root ssh-keygen -t ed25519 -f /root/.ssh/operaide_backup -N "" -C "operaide-backup@$(hostname)"
This is a passphrase-less key for unattended cron use. Cron cannot type passphrases. Protect the file with chmod 600 (ssh-keygen does that already).
Step 2: install the public key on both hops
The cron job authenticates twice: once at the bastion, once at the target. Both hosts must accept the same public key.
# Display the public key:
sudo cat /root/.ssh/operaide_backup.pub
Copy the output and append it to ~/.ssh/authorized_keys of the backup user on:
- The bastion, for the user the cron will SSH as (e.g.
jump). - The target, for the user the cron will eventually copy files as (e.g.
backup).
Restrict the keys with from= (limit source IP) and command= (lock to scp/rsync only) when your security policy requires it. Example for the target host's authorized_keys:
restrict,from="<bastion-IP>",command="rsync --server -vlogDtpre.iLsfxCIvu . /srv/operaide-backups/" ssh-ed25519 AAAA... operaide-backup@operaide-host
The command= value depends on the exact rsync flags used by transfer.sh. Capture it once with ssh -v to the target and copy it verbatim.
Step 3: configure the SSH client
Make the multi-hop transparent via ~/.ssh/config of the user that runs cron (root):
# /root/.ssh/config
Host operaide-bastion
HostName bastion.example.com
User jump
Port 22
IdentityFile /root/.ssh/operaide_backup
IdentitiesOnly yes
Host operaide-backup-target
HostName backup.internal.example.com
User backup
Port 22
IdentityFile /root/.ssh/operaide_backup
IdentitiesOnly yes
ProxyJump operaide-bastion
Replace the hostnames, usernames, and ports with your own.
ProxyJump operaide-bastion is the magic. From now on, any ssh operaide-backup-target or scp ... operaide-backup-target:... automatically tunnels through the bastion.
chmod 600 /root/.ssh/config. SSH refuses to read world-readable config files.
Step 4: verify the multi-hop connection
Before editing transfer.sh, confirm the path works manually:
sudo ssh operaide-backup-target "mkdir -p /srv/operaide-backups && ls -la /srv/"
This should connect (through the bastion) and list the target's /srv without prompts.
Step 5: edit transfer.sh
Open <project>/backup/transfer.sh and replace the placeholder body:
#!/usr/bin/env bash
# transfer.sh — rsync via ProxyJump through a bastion to the backup target.
#
# Called by operaide-outer-backup.sh with $1 = path to the new tar.
# `--remove-source-files` deletes the local tar after a successful copy
# so the backup volume does not fill up. The deletion is the
# "transferred" signal the backup-status UI watches for.
set -euo pipefail
REMOTE_HOST="operaide-backup-target" # Host alias from /root/.ssh/config
REMOTE_DIR="/srv/operaide-backups"
rsync -av --remove-source-files \
-e "ssh -F /root/.ssh/config" \
"$1" "$1.sha256" \
"$REMOTE_HOST:$REMOTE_DIR/"
Replace REMOTE_HOST and REMOTE_DIR to match your config.
rsync is preferred over plain scp because it preserves attributes, supports --remove-source-files, and resumes interrupted transfers. The -e "ssh -F /root/.ssh/config" makes rsync use root's SSH config and therefore the ProxyJump rule.
If you want to keep the local tar instead of having rsync delete it, drop --remove-source-files and append touch "$1.transferred" after a successful run.
Make the script executable and check syntax:
chmod +x <project>/backup/transfer.sh
bash -n <project>/backup/transfer.sh
Step 6: dry-run against an existing tar
Run the transfer once by hand against the most recent backup tar:
cd <project>
sudo bash backup/transfer.sh backup/backups/full-backup-YYYYMMDD-HHMMSS.tar
Expected: rsync logs both endpoints (bastion and target) during the connection, transfers the tar plus its sha256, and removes the local copies.
Verify on the target side, going through the same bastion:
sudo ssh operaide-backup-target "ls -la /srv/operaide-backups/"
Step 7: confirm the transferred badge
Refresh System Admin -> Backups. The Outer-block card should show Transferred (green check) because the local tar is gone (rsync --remove-source-files).
Step 8: retention on the target
transfer.sh only uploads. Decide where retention runs:
- A separate cron on the target host with
find /srv/operaide-backups -name 'full-backup-*.tar' -mtime +30 -delete. Adjust+30to your policy. - Snapshot-based retention if the target host's filesystem supports it (ZFS, Btrfs).
- Manual rotation for very small deployments. Easy to forget; not recommended at any scale.
The Operaide host's local eviction (OPERAIDE_BACKUP_MIN_FREE_BYTES) is independent of remote retention.
Common mistakes
- Passphrase on the SSH key. Cron cannot type. Use a passphrase-less key. Protect with
chmod 600and key-restrictiveauthorized_keyslines. - Same key for interactive and unattended use. Generate a dedicated transport key. If it leaks, you remove its access from two
authorized_keysfiles and you are done. - Bastion that does not pin the key. Without
restrict,command=...inauthorized_keys, the transport key can also start interactive shells on bastion or target. Lock it to the rsync invocation. ProxyJumpthat opens an interactive bastion shell. Rare with stock OpenSSH, but some hardened bastions force a menu. ProxyJump only works against bastions that allow plain SSH forwarding. If yours forces a TUI, use a proper SSH gateway like Teleport or talk to the security team.- No retention. Even compliance-mandated backup targets fill up. Set retention before the disk is full.
- No drill. See Recovery Drill. A bundle on a target you cannot reach during an incident is the same as no bundle.