3
\$\begingroup\$

I wrote the following Bash scripts to make pull-mode backups with restic, a backup solution written in Golang, similar to Borg Backup. The main use case for this script is pulling backups into a central backup server that you trust, especially if you don't want to create a shell account or sftp user for every machine you backup, while still using a tool like restic on your own infrastructure.

This script is mostly just a wrapper for restic to talk to rest-server over SSH, using port forwarding. Rest-server allows for enforcing append-only mode, so clients shouldn't be able to delete their past backups.

The main developer of restic is interested in, or at least considering, making restic support pull-mode without rest-server. If that is released at some point, then it would probably make more sense to use restic directly, rather than this script.

A coworker with more BASH experience than me reviewed the codebase, and I merged in most of those patches, with some changes.

My main concern regarding this code is security, and was wondering if there are any ways I could improve it. One weakness is that restic's encryption password has to be stored somewhere, and so I store it in plain text on the backup server, with the expectation that the backups are encrypted by a LUKS partition and a password.

restic also uses its encryption password to verify backups, so I don't know if that complicates the security of this script. More details about my design choices are in the readme of the source code repository.

I'm interested in hearing your thoughts. Thanks! : )

kaya:

#! /bin/bash

# This file is part of Kaya.
#
# Kaya is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Kaya is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Kaya.  If not, see <https://www.gnu.org/licenses/>.
#
# Copyright 2021 Andrew Engelbrecht
#

if ! test "$BASH_VERSION"; then echo "error: shell is not bash" >&2; exit 1; fi
shopt -s inherit_errexit 2>/dev/null ||: # ignore fail in bash < 4.4
set -eE -o pipefail
trap 'echo "$0:$LINENO:error: \"$BASH_COMMAND\" exit status: $?, PIPESTATUS: ${PIPESTATUS[*]}" >&2' ERR


# first stable version of API between kaya and kaya-client
kaya_protocol_version=1

kaya-usage() {
    cat << EOF >&2

kaya usage:
kaya [-c CONFIG_PATH] [-u REMOTE_USER] HOST_TO_BACKUP backup -- RESTIC_ARGS

REMOTE_USER          Default is root
CONFIG_PATH          Default is /etc/kaya.conf
HOST_TO_BACKUP       For example www1.example.com
RESTIC_ARGS          See possible args by running "restic backup --help"
EOF
    exit "$1"
}

set-defaults() {
    global_conf="/etc/kaya.conf"
    remote_user="root"
}

get-params() {
    local -a positional
    local key
    while (( $# )); do
        key="$1"

        case $key in
            -c|--conf)
                global_conf="$2"
                shift 2
                ;;
            -u|--user)
                remote_user="$2"
                shift 2
                ;;
            --)
                shift
                backup_options=("$@")
                break
                ;;
            -*)
                echo "kaya: error: unrecognized argument: $key" >&2
                kaya-usage 1
                ;;
            *)
                positional+=("$key")
                shift
                ;;
        esac
    done

    set -- "${positional[@]}"

    hostname="$1"
    action="$2"

    if [[ -z $hostname ]]; then
        echo "kaya: error: missing hostname argument" >&2
        kaya-usage 1
    fi

    case $action in
        backup)
            # currently the only option is to backup
            ;;
        *)
            echo "kaya: error: invalid action argument: $action" >&2
            kaya-usage 1
            ;;
    esac
}

## create restic backup repo, store the new password in plaintext
create-backup-dir() {
    echo "kaya: Creating backup directory..."

    mkdir -p "${backup_dir}"; chmod 700 "${backup_dir}"
    touch "${password_file}"; chmod 600 "${password_file}"; pwgen 30 1 > "${password_file}"; chmod 400 "${password_file}"

    RESTIC_PASSWORD="$(cat "${password_file}")" restic -r "${backup_dir}" init > /dev/null
}

# check / create the hashed password in .htpasswd
update-htpasswd-file() {
    if [[ ! -e $htpasswd_file ]] ; then
        touch "${htpasswd_file}"
    fi
    chmod 600 "${htpasswd_file}"

    # check to see if the password is already there and correct
    # otherwise, update it
    if ! grep -q "^${hostname}" "${htpasswd_file}" || ! htpasswd -i -v "${htpasswd_file}" "${hostname}" < "${password_file}" &> /dev/null ; then

        flock -w 10 "${htpasswd_file}.flock" -c "htpasswd -i -B '${htpasswd_file}' '${hostname}'" < "${password_file}" \
            |& { grep -E -v "(Adding|Updating) password for user" >&2 ||:; }

        echo "kaya: Waiting for rest-server to reload .htpasswd... (mostly for new backups)"
        sleep 32
    fi
}

start-backup() {
    echo "kaya: Starting backup of ${hostname}"
    echo

    local password
    password="$(head -n1 "${password_file}")"

    # make the backup over a forwarded port
    cat << EOF | ssh -R "${remote_port}:localhost:${local_port}" "${remote_user}@${hostname}" kaya-client \
        "${remote_port@Q}" "${hostname@Q}" "${backup_options[@]@Q}"
${kaya_protocol_version}
${password}
EOF

}

main() {
    local program
    local missing_prog
    missing_prog=false
    for program in flock pwgen; do
        if ! type -p $program &>/dev/null; then
            echo "kaya: error: install $program (package $program on debian based systems)" >&2
        fi
    done
    if ! type -p htpasswd &>/dev/null; then
        echo "kaya: error: install htpasswd (package apache2-utils on debian based systems)" >&2
    fi
    if $missing_prog; then
        exit 1
    fi
    set-defaults
    get-params "$@"

    # shellcheck source=/etc/kaya.conf
    source "${global_conf}"
    if [[ -z $local_port ]]; then
        local_port=8000
    fi

    backup_dir="${backuproot}/${hostname}"
    password_file="${backup_dir}/repo-password-keep"
    htpasswd_file="${backuproot}/.htpasswd"

    if [[ ! -d $backup_dir ]]; then
        create-backup-dir
    fi

    if [[ ! -e $password_file ]]; then
        echo "kaya: error: the repo password file is missing:" >&2
        echo "${password_file}" >&2
        exit 1
    fi

    update-htpasswd-file

    if [[ $action == backup ]]; then
        start-backup
    fi
}

main "$@"

kaya-client:

#!/bin/bash

# This file is part of Kaya.
#
# Kaya is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Kaya is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Kaya.  If not, see <https://www.gnu.org/licenses/>.
#
# Copyright 2021 Andrew Engelbrecht
#

if ! test "$BASH_VERSION"; then echo "error: shell is not bash" >&2; exit 1; fi
shopt -s inherit_errexit 2>/dev/null ||: # ignore fail in bash < 4.4
set -eE -o pipefail
trap 'echo "$0:$LINENO:error: \"$BASH_COMMAND\" exit status: $?, PIPESTATUS: ${PIPESTATUS[*]}" >&2' ERR

read -r kaya_protocol_version

if [[ $kaya_protocol_version != 1 ]] ; then
    echo "kaya-client: error: mismatched protocol version with kaya" >&2
    echo "kaya-client: error: kaya protocol version: ${kaya_protocol_version} kaya-client protocol version: 1" >&2
    exit 1
fi

read -r password

port="$1"
hostname="$2"
shift 2

username="${hostname}"
backupdir="${hostname}"

export RESTIC_REPOSITORY="rest:http://${username}:${password}@localhost:${port}/${backupdir}/"
export RESTIC_PASSWORD="${password}"

cmd=( restic backup "$@")
ret=0
"${cmd[@]}" || ret=$?

if (( ret )); then
    echo "kaya-client: command exit code $ret: ${cmd[*]}" >&2
    exit $ret
fi

kaya.conf:

#!/bin/bash
#
# Config file for Kaya, a restic backup wrapper
#
# This will be sourced by bash

backuproot=/srv/backups/kaya

# set if using non-default port as in 'rest-server --listen :PORT'
# local_port=8000
remote_port=8777    # some aribtrary port
```
\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.