|
|
@@ -0,0 +1,305 @@ |
|
|
|
#!/bin/bash |
|
|
|
|
|
|
|
BORG_DIR=${BORG_DIR:-/opt/borg} |
|
|
|
BACKUP_HOST=${BACKUP_HOST:-backup.jim.sh} |
|
|
|
BACKUP_USER=${BACKUP_USER:-jim-backups} |
|
|
|
BACKUP_REPO=${BACKUP_REPO:-borg/$(hostname)} |
|
|
|
|
|
|
|
# Use stable host ID in case MAC address changes |
|
|
|
HOSTID="$(hostname -f)@$(python -c 'import uuid;print(uuid.getnode())')" |
|
|
|
|
|
|
|
function error_handler() { |
|
|
|
echo "Error at $1 line $2:" |
|
|
|
echo -n '>>> ' ; cat $1 | tail -n +$2 | head -1 |
|
|
|
echo "... exited with code $3" |
|
|
|
exit $3 |
|
|
|
} |
|
|
|
trap 'error_handler ${BASH_SOURCE} ${LINENO} $?' ERR |
|
|
|
set -o errexit |
|
|
|
set -o errtrace |
|
|
|
set -o nounset |
|
|
|
|
|
|
|
if [ -e $BORG_DIR ]; then |
|
|
|
echo "Error: BORG_DIR $BORG_DIR already exists; giving up" |
|
|
|
exit 1 |
|
|
|
fi |
|
|
|
|
|
|
|
# Make a temp dir to work in |
|
|
|
mkdir $BORG_DIR |
|
|
|
TMP=$(mktemp -d --tmpdir=$BORG_DIR) |
|
|
|
|
|
|
|
# Install some cleanup handlers |
|
|
|
cleanup() |
|
|
|
{ |
|
|
|
set +o errexit |
|
|
|
set +o errtrace |
|
|
|
set +o nounset |
|
|
|
trap - ERR |
|
|
|
ssh -o ControlPath=$TMP/ssh-control -O exit x >/dev/null 2>&1 |
|
|
|
rm -rf -- $TMP |
|
|
|
} |
|
|
|
cleanup_int() |
|
|
|
{ |
|
|
|
echo |
|
|
|
cleanup |
|
|
|
exit 1 |
|
|
|
} |
|
|
|
trap cleanup 0 |
|
|
|
trap cleanup_int 1 2 15 |
|
|
|
|
|
|
|
error() |
|
|
|
{ |
|
|
|
echo Error: "$@" |
|
|
|
exit 1 |
|
|
|
} |
|
|
|
|
|
|
|
log() |
|
|
|
{ |
|
|
|
echo -ne "\033[1;33m===\033[0;33m" "$@" |
|
|
|
echo -e "\033[0m" |
|
|
|
} |
|
|
|
|
|
|
|
notice() |
|
|
|
{ |
|
|
|
echo -ne "\033[1;32m===\033[0;32m" "$@" |
|
|
|
echo -e "\033[0m" |
|
|
|
} |
|
|
|
|
|
|
|
# Install required packages |
|
|
|
install_dependencies() |
|
|
|
{ |
|
|
|
NEED= |
|
|
|
check() { |
|
|
|
command -v $1 >/dev/null || NEED+=" $2" |
|
|
|
} |
|
|
|
check borg borgbackup |
|
|
|
if [ ! -z ${NEED:+x} ]; then |
|
|
|
log "Need to install packages: $NEED" |
|
|
|
apt install --no-upgrade $NEED |
|
|
|
fi |
|
|
|
} |
|
|
|
|
|
|
|
# Create wrapper to execute borg |
|
|
|
create_borg_wrapper() |
|
|
|
{ |
|
|
|
BORG=${BORG_DIR}/borg.sh |
|
|
|
BORG_REPO="ssh://${BACKUP_USER}@${BACKUP_HOST}/./${BACKUP_REPO}" |
|
|
|
|
|
|
|
cat >$BORG <<EOF |
|
|
|
#!/bin/sh |
|
|
|
|
|
|
|
export BORG_REPO=${BORG_REPO} |
|
|
|
export BORG_PASSCOMMAND=${BORG_DIR}/print-passphrase |
|
|
|
export BORG_HOST_ID=${HOSTID} |
|
|
|
export BORG_BASE_DIR=${BORG_DIR} |
|
|
|
export BORG_CACHE_DIR=${BORG_DIR}/cache |
|
|
|
export BORG_CONFIG_DIR=${BORG_DIR}/config |
|
|
|
export BORG_RSH="ssh -F ${BORG_DIR}/ssh/config" |
|
|
|
exec borg "\$@" |
|
|
|
EOF |
|
|
|
chmod +x $BORG |
|
|
|
if ! $BORG -h >/dev/null ; then |
|
|
|
error "Can't run the new borg wrapper; does borg work?" |
|
|
|
fi |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
print_random_key() |
|
|
|
{ |
|
|
|
dd if=/dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 16 |
|
|
|
} |
|
|
|
|
|
|
|
generate_keys() |
|
|
|
{ |
|
|
|
PASS_SSH=$(print_random_key) |
|
|
|
PASS_REPOKEY=$(print_random_key) |
|
|
|
cat >${BORG_DIR}/print-passphrase <<EOF |
|
|
|
#!/bin/sh |
|
|
|
echo $PASS_REPOKEY |
|
|
|
EOF |
|
|
|
chmod 700 ${BORG_DIR}/print-passphrase |
|
|
|
} |
|
|
|
|
|
|
|
# Run a command on the remote host over an existing SSH tunnel |
|
|
|
run_ssh_command() |
|
|
|
{ |
|
|
|
ssh -o ControlPath=$TMP/ssh-control use-existing-control-tunnel "$@" |
|
|
|
} |
|
|
|
|
|
|
|
# Configure SSH key-based login |
|
|
|
configure_ssh() |
|
|
|
{ |
|
|
|
SSH=$BORG_DIR/ssh |
|
|
|
mkdir $SSH |
|
|
|
|
|
|
|
# Create keys |
|
|
|
log "Creating SSH keys" |
|
|
|
ssh-keygen -N "" -t ecdsa \ |
|
|
|
-C backup-appendonly@$HOSTID -f $SSH/id_ecdsa_appendonly |
|
|
|
ssh-keygen -N "$PASS_SSH" -t ecdsa \ |
|
|
|
-C backup@$HOSTID -f $SSH/id_ecdsa |
|
|
|
|
|
|
|
# Create config snippets |
|
|
|
log "Creating SSH config and wrapper script" |
|
|
|
cat >> $SSH/config <<EOF |
|
|
|
User $BACKUP_USER |
|
|
|
ControlPath none |
|
|
|
ServerAliveInterval 120 |
|
|
|
Compression no |
|
|
|
UserKnownHostsFile $SSH/known_hosts |
|
|
|
ForwardX11 no |
|
|
|
ForwardAgent no |
|
|
|
BatchMode yes |
|
|
|
IdentityFile $SSH/id_ecdsa_appendonly |
|
|
|
|
|
|
|
Host backup-appendonly |
|
|
|
HostName $BACKUP_HOST |
|
|
|
IdentityFile $SSH/id_ecdsa_appendonly |
|
|
|
|
|
|
|
Host backup |
|
|
|
HostName $BACKUP_HOST |
|
|
|
IdentityFile $SSH/id_ecdsa |
|
|
|
EOF |
|
|
|
|
|
|
|
# Connect to backup host, using persistent control socket |
|
|
|
log "Connecting to server" |
|
|
|
log "Please enter password; look in Bitwarden for: ${BACKUP_USER}@${BACKUP_HOST}" |
|
|
|
ssh -F $SSH/config -o BatchMode=no -o PubkeyAuthentication=no \ |
|
|
|
-o ControlMaster=yes -o ControlPath=$TMP/ssh-control \ |
|
|
|
-o StrictHostKeyChecking=accept-new \ |
|
|
|
-f backup sleep 600 |
|
|
|
if ! run_ssh_command true >/dev/null 2>&1 </dev/null ; then |
|
|
|
error "SSH failed" |
|
|
|
fi |
|
|
|
log "Connected to ${BACKUP_USER}@${BACKUP_HOST}" |
|
|
|
|
|
|
|
# Since we now have an SSH connection, check that the repo doesn't exist |
|
|
|
if run_ssh_command "test -e $BACKUP_REPO" ; then |
|
|
|
error "$BACKUP_REPO already exists on the server, bailing out" |
|
|
|
fi |
|
|
|
|
|
|
|
# Copy SSH keys to the server's authorized_keys file, removing any |
|
|
|
# existing keys with this HOSTID. |
|
|
|
log "Setting up SSH keys on remote host" |
|
|
|
cmd="borg serve --restrict-to-repository ~/$BACKUP_REPO" |
|
|
|
|
|
|
|
keys=".ssh/authorized_keys" |
|
|
|
backup="${keys}.old-$(date +%Y%m%d-%H%M%S)" |
|
|
|
run_ssh_command "mkdir -p .ssh; chmod 700 .ssh; touch $keys" |
|
|
|
run_ssh_command "mv $keys $backup; sed '/@$HOSTID\$/d' < $backup > $keys" |
|
|
|
run_ssh_command "if cmp -s $backup $keys; then rm $backup ; fi" |
|
|
|
run_ssh_command "cat >> .ssh/authorized_keys" <<EOF |
|
|
|
command="$cmd --append-only",restrict $(cat $SSH/id_ecdsa_appendonly.pub) |
|
|
|
command="$cmd",restrict $(cat $SSH/id_ecdsa.pub) |
|
|
|
EOF |
|
|
|
|
|
|
|
# Test that everything worked |
|
|
|
log "Testing SSH login with new key" |
|
|
|
if ! ssh -F $SSH/config -T backup-appendonly borg --version </dev/null ; then |
|
|
|
error "Logging in with a key failed -- is server set up correctly?" |
|
|
|
fi |
|
|
|
log "Remote connection OK!" |
|
|
|
} |
|
|
|
|
|
|
|
# Create the repository on the server |
|
|
|
create_repo() |
|
|
|
{ |
|
|
|
log "Creating repo $BACKUP_REPO" |
|
|
|
# Create repo |
|
|
|
$BORG init --make-parent-dirs --encryption repokey |
|
|
|
} |
|
|
|
|
|
|
|
# Export keys as HTML page |
|
|
|
export_keys() |
|
|
|
{ |
|
|
|
log "Exporting keys" |
|
|
|
$BORG key export --paper '' ${BORG_DIR}/key.txt |
|
|
|
chmod 600 ${BORG_DIR}/key.txt |
|
|
|
cat >>${BORG_DIR}/key.txt <<EOF |
|
|
|
|
|
|
|
Repository: ${BORG_REPO} |
|
|
|
Passphrase: ${PASS_REPOKEY} |
|
|
|
EOF |
|
|
|
} |
|
|
|
|
|
|
|
# Create a script that can be run to make a new backup |
|
|
|
create_backup_script() |
|
|
|
{ |
|
|
|
SCRIPT=${BORG_DIR}/backup.sh |
|
|
|
|
|
|
|
log "Creating backup script" |
|
|
|
|
|
|
|
cat > $SCRIPT <<EOF |
|
|
|
#!/bin/bash |
|
|
|
|
|
|
|
# Generated by borg-setup |
|
|
|
|
|
|
|
BORG=$BORG_DIR/borg.sh |
|
|
|
|
|
|
|
set -e |
|
|
|
|
|
|
|
if [ -z \${DIRS:+x} ]; then |
|
|
|
DIRS=/ |
|
|
|
# Include mountpoints that we care about, since we otherwise don't |
|
|
|
# follow mounts |
|
|
|
for DIR in /usr /var /home /boot ; do |
|
|
|
if [ \$(stat -c%m \$DIR) = \$DIR ] ; then |
|
|
|
DIRS="\$DIRS \$DIR" |
|
|
|
fi |
|
|
|
done |
|
|
|
fi |
|
|
|
|
|
|
|
echo "Backing up ${DIRS}" |
|
|
|
|
|
|
|
ionice -c 2 -n 6 \\ |
|
|
|
nice -n 10 \\ |
|
|
|
\$BORG create \\ |
|
|
|
--verbose \\ |
|
|
|
--list \\ |
|
|
|
--filter E \\ |
|
|
|
--stats \\ |
|
|
|
--exclude-caches \\ |
|
|
|
--one-file-system \\ |
|
|
|
--checkpoint-interval 300 \\ |
|
|
|
--compression zstd,3 \\ |
|
|
|
::'{hostname}-{now:%Y%m%d-%H%M%S}' \\ |
|
|
|
\$DIRS |
|
|
|
|
|
|
|
\$BORG check --verbose |
|
|
|
|
|
|
|
# Run manual commands like this: |
|
|
|
# sudo $BORG_DIR/borg.sh list |
|
|
|
EOF |
|
|
|
chmod +x $SCRIPT |
|
|
|
log "Script created: $SCRIPT" |
|
|
|
} |
|
|
|
|
|
|
|
log "Configuration:" |
|
|
|
log " Backup server host: ${BACKUP_HOST}" |
|
|
|
log " Backup server user: ${BACKUP_USER}" |
|
|
|
log " Repository path: ${BACKUP_REPO}" |
|
|
|
|
|
|
|
install_dependencies |
|
|
|
create_borg_wrapper |
|
|
|
generate_keys |
|
|
|
configure_ssh |
|
|
|
create_repo |
|
|
|
export_keys |
|
|
|
create_backup_script |
|
|
|
|
|
|
|
log "All done" |
|
|
|
notice "" |
|
|
|
notice "Add these two passwords to Bitwarden:" |
|
|
|
notice "" |
|
|
|
notice " Name: borg $(hostname)" |
|
|
|
notice " Username: repo key" |
|
|
|
notice " Password: $PASS_REPOKEY" |
|
|
|
notice "" |
|
|
|
notice " Name: borg $(hostname)" |
|
|
|
notice " Username: read-write ssh key" |
|
|
|
notice " Password: $PASS_SSH" |
|
|
|
notice "" |
|
|
|
notice "Full repo key should be printed out: ${BORG_DIR}/key.txt" |
|
|
|
notice "" |
|
|
|
|
|
|
|
#create_crontab_entry |