Browse Source

Initial commit

master
Jim Paris 10 months ago
commit
cec72d0dcb
3 changed files with 322 additions and 0 deletions
  1. +7
    -0
      Makefile
  2. +10
    -0
      README.md
  3. +305
    -0
      borg-setup.sh

+ 7
- 0
Makefile View File

@@ -0,0 +1,7 @@
test:
rm -rf /tmp/test-borg
BORG_DIR=/tmp/test-borg ./borg-setup.sh
ls -al /tmp/test-borg

dist:
scp borg-setup.sh psy:/www/psy

+ 10
- 0
README.md View File

@@ -0,0 +1,10 @@
# Design

On bucket:

- New user account "jim-backups" with password in bitwarden
- Each client has 2 ssh keys in /home/jim-backups/.ssh/authorized_keys
(1) no password, for append-only operation

backup key in authorized_keys that allows append-only backups


+ 305
- 0
borg-setup.sh View File

@@ -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

Loading…
Cancel
Save