Initial commit
This commit is contained in:
commit
cec72d0dcb
7
Makefile
Normal file
7
Makefile
Normal 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
README.md
Normal file
10
README.md
Normal 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
borg-setup.sh
Executable file
305
borg-setup.sh
Executable 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…
Reference in New Issue
Block a user