Managing SSH chroots to backup a lot of machines
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

451 lines
13 KiB

#!/bin/sh
#
# Config for bkctld
#
# shellcheck disable=SC2034
[ -f /etc/default/bkctld ] && . /etc/default/bkctld
VERSION="2.7.0"
LIBDIR=${LIBDIR:-/usr/lib/bkctld}
CONFDIR="${CONFDIR:-/etc/evobackup}"
BACKUP_DISK="${BACKUP_DISK:-}"
BACKUP_PARTITION="${BACKUP_PARTITION:-/backup}"
JAILDIR="${JAILDIR:-${BACKUP_PARTITION}/jails}"
INCDIR="${INCDIR:-${BACKUP_PARTITION}/incs}"
TPLDIR="${TPLDIR:-/usr/share/bkctld}"
LOCALTPLDIR="${LOCALTPLDIR:-/usr/local/share/bkctld}"
LOCKDIR="${LOCKDIR:-/run/lock/bkctld}"
INDEX_DIR="${INDEX_DIR:-${BACKUP_PARTITION}/index}"
IDX_FILE="${IDX_FILE:-${INDEX_DIR}/bkctld-jails.idx}"
SSHD_PID="${SSHD_PID:-/run/sshd.pid}"
SSHD_CONFIG="${SSHD_CONFIG:-/etc/ssh/sshd_config}"
AUTHORIZED_KEYS="${AUTHORIZED_KEYS:-/root/.ssh/authorized_keys}"
FIREWALL_RULES="${FIREWALL_RULES:-}"
LOGLEVEL="${LOGLEVEL:-6}"
CRITICAL="${CRITICAL:-48}"
WARNING="${WARNING:-24}"
DUC=$(command -v duc-nox || command -v duc)
FORCE="${FORCE:-0}"
show_version() {
cat <<END
bkctld version ${VERSION}
Copyright 2004-2020 Evolix <info@evolix.fr>,
Victor Laborie <vlaborie@evolix.fr>,
Jérémy Lecour <jlecour@evolix.fr>
and others.
bkctld comes with ABSOLUTELY NO WARRANTY. This is free software,
and you are welcome to redistribute it under certain conditions.
See the GNU Affero General Public License v3.0 for details.
END
}
show_help() {
cat <<EOF
Usage: bkctld [options] <subcommand> [arguments]
Options
-h|--help|-? Display help
-V|--version Display version, authors and license
Subcommands:
EOF
for filename in ${LIBDIR}/bkctld-*; do
desc=$(grep -E "^# Description:" "${filename}"|sed "s/^# Description: //")
usage=$(grep -E "^# Usage:" "${filename}"|sed "s/^# Usage: //")
printf " %- 32s %s\n" "${usage}" "${desc}"
done
printf "\n"
}
log_date() {
echo "[$(date +"%Y-%m-%d %H:%M:%S")]"
}
process_name() {
basename $0
}
debug() {
msg="${1:-$(cat /dev/stdin)}"
if [ "${LOGLEVEL}" -ge 7 ]; then
echo "$(log_date) DEBUG $(process_name) ${msg}"
logger -t bkctld -p daemon.debug "$(process_name) ${msg}"
fi
}
info() {
msg="${1:-$(cat /dev/stdin)}"
if [ "${LOGLEVEL}" -ge 6 ]; then
tty -s && echo "$(log_date) INFO $(process_name) ${msg}"
logger -t bkctld -p daemon.info "$(process_name) ${msg}"
fi
}
notice() {
msg="${1:-$(cat /dev/stdin)}"
tty -s && echo "$(log_date) NOTICE $(process_name) ${msg}"
[ "${LOGLEVEL}" -ge 5 ] && logger -t bkctld -p daemon.notice "$(process_name) ${msg}"
}
warning() {
msg="${1:-$(cat /dev/stdin)}"
tty -s && echo "$(log_date) WARNING $(process_name) ${msg}" >&2
if [ "${LOGLEVEL}" -ge 4 ]; then
tty -s || echo "$(log_date) WARNING $(process_name) ${msg}" >&2
logger -t bkctld -p daemon.warning "$(process_name) ${msg}"
fi
}
# Return codes
# 1 : generic error
# 2 : jail not found
# > 100 : subcommands specific errors
error() {
msg="${1:-$(cat /dev/stdin)}"
rc="${2:-1}"
tty -s && echo "$(log_date) ERROR $(process_name) ${msg}" >&2
if [ "${LOGLEVEL}" -ge 5 ]; then
tty -s || echo "$(log_date) ERROR $(process_name) ${msg}" >&2
logger -t bkctld -p daemon.error "$(process_name) ${msg}"
fi
exit ${rc}
}
dry_run() {
test "$DRY_RUN" = "1"
}
current_time() {
date +"%H:%M:%S"
}
# Returns true if the given path is on a btrfs filesystem
is_btrfs() {
path=$1
inode=$(stat --format=%i "${path}")
test $inode -eq 256
}
# Returns the list of jails found in the "jails" directory (default)
jails_list() {
# TODO: try if this command works the same :
# find "${JAILDIR}" -mindepth 1 -maxdepth 1 -type d -printf '%f\n'
find "${JAILDIR}" -mindepth 1 -maxdepth 1 -type d | sed 's!.*/!!' | sort -h
}
# Returns the list of jails found in the "incs" directory
jails_with_incs_list() {
find "${INCDIR}" -mindepth 1 -maxdepth 1 -type d | sed 's!.*/!!' | sort -h
}
# Returns the complete path of a jail
jail_path() {
jail_name=${1:?}
echo "${JAILDIR}/${jail_name}"
}
jail_config_dir() {
jail_name=${1:?}
echo "${CONFDIR}/${jail_name}.d"
}
jail_incs_policy_file() {
jail_name=${1:?}
jail_config_dir=$(jail_config_dir "${jail_name}")
echo "${jail_config_dir}/incs_policy"
}
# Returns the path of incs for a jail
incs_path() {
jail_name=${1:?}
echo "${INCDIR}/${jail_name}"
}
# Returns the path of a specific inc for a jail
inc_path() {
jail_name=${1:?}
inc_name=${2:?}
echo "${INCDIR}/${jail_name}/${inc_name}"
}
# Test the existence of an inc pattern for a jail
inc_exists() {
jail_name=${1-?}
inc_pattern=${2-?}
inc_path=$(inc_path "${jail_name}" "${inc_pattern}")
# inc_path must not be quoted because it can contain globs
ls -d ${inc_path} > /dev/null 2>&1
}
# Returns the list of all incs for a jail
incs_list() {
jail_name=${1:?}
find "$(incs_path "${jail_name}")" -mindepth 1 -maxdepth 1 -type d | sed 's!.*/!!' | sort -h
}
# Return the list of empty incs directories
empty_incs_list() {
find ${INCDIR} -mindepth 1 -maxdepth 1 -type d -empty
}
current_jail_incs_policy_file() {
jail_name=${1:?}
new_file="$(jail_incs_policy_file "${jail_name}")"
old_file="${CONFDIR}/${jail_name}"
if [ -f "${new_file}" ]; then
echo "${new_file}"
elif [ -f "${old_file}" ]; then
echo "${old_file}"
else
echo ""
fi
}
jail_check_policy_file() {
jail_name=${1:?}
jail_config_dir=$(jail_config_dir "${jail_name}")
echo "${jail_config_dir}/check_policy"
}
current_jail_check_policy_file() {
jail_name=${1:?}
new_file="$(jail_check_policy_file "${jail_name}")"
# old_file="${JAILDIR}/${jail_name}/etc/bkctld-check"
if [ -f "${new_file}" ]; then
echo "${new_file}"
# elif [ -f "${old_file}" ]; then
# echo "${old_file}"
else
echo ""
fi
}
# relative_date "+%Y-%m-%d.-2day"
relative_date() {
format=$(echo $1 | cut -d'.' -f1)
time_jump=$(echo $1 | cut -d'.' -f2)
reference_date=$(date "${format}")
past_date=$(date --date "${reference_date} ${time_jump}" +"%Y-%m-%d")
echo ${past_date}
}
new_tmp_file() {
name=${1:-}
mktemp --tmpdir=/tmp "bkctld.${$}.${name}.XXXXX"
}
new_tmp_dir() {
name=${1:-}
mktemp --directory --tmpdir=/tmp "bkctld.${$}.${name}.XXXXX"
}
cleanup_tmp() {
find /tmp -name "bkctld.${$}.*" -delete
}
new_lock_file() {
lock_file=${1:-}
lock_dir=$(dirname "${lock_file}")
mkdir --parents "${lock_dir}" && echo $$ > ${lock_file} || error "Failed to acquire lock file '${lock_file}'"
}
pkg_version() {
# $(command -v ssh) -V 2>&1 | grep -iEo 'OpenSSH_(\S+)' | cut -d '_' -f2
dpkg-query -W -f='${Version}\n' $1 \
| sed 's/[~+-].\+//' \
| sed 's/.\+://' \
| sed 's/p.*//' \
| cut -d. -f1,2
}
ssh_keygen_with_prefix() {
# openssh-client 7.9 provides ssh-keygen with "-f prefix_path" option
dpkg --compare-versions "$(pkg_version 'openssh-client')" ge "7.9"
}
setup_jail_chroot() {
jail_name=${1:?}
jail_path=$(jail_path "${jail_name}")
passwd="${TPLDIR}/passwd"
shadow="${TPLDIR}/shadow"
group="${TPLDIR}/group"
sshrc="${TPLDIR}/sshrc"
[ -f "${LOCALTPLDIR}/passwd" ] && passwd="${LOCALTPLDIR}/passwd"
[ -f "${LOCALTPLDIR}/shadow" ] && shadow="${LOCALTPLDIR}/shadow"
[ -f "${LOCALTPLDIR}/group" ] && group="${LOCALTPLDIR}/group"
[ -f "${LOCALTPLDIR}/sshrc" ] && group="${LOCALTPLDIR}/sshrc"
cd "${jail_path}" || error "${jail_name}: failed to change directory to ${jail_path}."
umask 077
info "1 - Creating the chroot"
rm -rf ./bin
rm -rf ./lib
rm -rf ./lib64
rm -rf ./run
rm -rf ./usr
rm -rf ./var/run
# Let's not delete the existing SSH host keys,
# otherwise the clients will have to accept the new keys
mkdir -p ./dev
mkdir -p ./proc
mkdir -p ./usr/bin
mkdir -p ./usr/sbin
mkdir -p ./usr/lib
mkdir -p ./usr/lib/x86_64-linux-gnu
mkdir -p ./usr/lib/openssh
mkdir -p ./usr/lib64
mkdir -p ./etc/ssh
mkdir -p ./var/log
mkdir -p ./run/sshd
# shellcheck disable=SC2174
mkdir -p ./root/.ssh --mode 0700
# shellcheck disable=SC2174
mkdir -p ./var/backup --mode 0700
ln -s ./usr/bin ./bin
ln -s ./usr/lib ./lib
ln -s ./usr/lib64 ./lib64
ln -s --target-directory=./var ../run
touch ./var/log/lastlog ./var/log/wtmp ./run/utmp
info "2 - Copying essential files"
#
if ssh_keygen_with_prefix; then
# Generate SSH host keys if missing in jail
ssh-keygen -A -f "${jail_path}"
else
# Copy SSH host keys from host if missing in jail
for key in /etc/ssh/*_key; do
cp --no-clobber ${key} ${jail_path}${key};
done
fi
touch "./${AUTHORIZED_KEYS}"
chmod 600 "./${AUTHORIZED_KEYS}"
cp "${passwd}" ./etc
cp "${shadow}" ./etc
cp "${group}" ./etc
cp "${sshrc}" ./etc/ssh
info "3 - Copying binaries"
cp -f /lib/ld-linux.so.2 ./lib 2>/dev/null || cp -f /lib64/ld-linux-x86-64.so.2 ./lib64
cp /lib/x86_64-linux-gnu/libnss* ./lib/x86_64-linux-gnu
for dbin in \
/bin/sh \
/bin/ls \
/bin/mkdir \
/bin/cat \
/bin/rm \
/bin/sed \
/usr/bin/rsync \
/usr/bin/lastlog \
/usr/bin/touch \
/usr/sbin/sshd \
/usr/lib/openssh/sftp-server\
; do
cp -f "${dbin}" "./${dbin}";
for lib in $(ldd "${dbin}" | grep -Eo "/.*so.[0-9\.]+"); do
cp -p "${lib}" "./${lib}"
done
done
}
setup_jail_config() {
jail_name=${1:?}
jail_path=$(jail_path "${jail_name}")
jail_sshd_config="${jail_path}/${SSHD_CONFIG}"
sshd_config_tpl="${TPLDIR}/sshd_config"
test -f "${LOCALTPLDIR}/sshd_config" && sshd_config_tpl="${LOCALTPLDIR}/sshd_config"
info "4 - Copie default sshd_config"
install -m 0640 "${sshd_config_tpl}" "${jail_sshd_config}"
info "5 - Copie default inc configuration"
incs_policy_tpl="${TPLDIR}/incs_policy.tpl"
test -f "${LOCALTPLDIR}/incs_policy.tpl" && incs_policy_tpl="${LOCALTPLDIR}/incs_policy.tpl"
jail_incs_policy_file=$(jail_incs_policy_file "${jail_name}")
mkdir --parents "$(dirname "${jail_incs_policy_file}")"
install -m 0640 "${incs_policy_tpl}" "${jail_incs_policy_file}"
check_policy_tpl="${TPLDIR}/check_policy.tpl"
test -f "${LOCALTPLDIR}/check_policy.tpl" && check_policy_tpl="${LOCALTPLDIR}/check_policy.tpl"
jail_check_policy_file=$(jail_check_policy_file "${jail_name}")
mkdir --parents "$(dirname "${jail_check_policy_file}")"
install -m 0640 "${check_policy_tpl}" "${jail_check_policy_file}"
"${LIBDIR}/bkctld-port" "${jail_name}" auto
}
is_mounted_inside_jail() {
target=${1:?}
# TODO: try to find why it doesn't work with this findmnt(8) command
# findmnt --target "${target}" --tab-file /proc/mounts
grep -q "${target}" /proc/mounts
}
mount_jail_fs() {
jail_name=${1:?}
jail_path=$(jail_path "${jail_name}")
is_mounted_inside_jail "${jail_path}/dev" || mount -nt tmpfs "dev-${jail_name}" "${jail_path}/dev"
[ -e "dev/console" ] || mknod -m 622 "${jail_path}/dev/console" c 5 1
chown root:tty "${jail_path}/dev/console"
[ -e "dev/null" ] || mknod -m 666 "${jail_path}/dev/null" c 1 3
[ -e "dev/zero" ] || mknod -m 666 "${jail_path}/dev/zero" c 1 5
[ -e "dev/ptmx" ] || mknod -m 666 "${jail_path}/dev/ptmx" c 5 2
chown root:tty "${jail_path}/dev/ptmx"
[ -e "dev/tty" ] || mknod -m 666 "${jail_path}/dev/tty" c 5 0
chown root:tty "${jail_path}/dev/tty"
[ -e "dev/random" ] || mknod -m 444 "${jail_path}/dev/random" c 1 8
[ -e "dev/urandom" ] || mknod -m 444 "${jail_path}/dev/urandom" c 1 9
mkdir -p "${jail_path}/dev/pts"
is_mounted_inside_jail "${jail_path}/dev/pts" || mount -t devpts -o gid=4,mode=620 none "${jail_path}/dev/pts"
mkdir -p "${jail_path}/dev/shm"
is_mounted_inside_jail "${jail_path}/dev/shm" || mount -t tmpfs none "${jail_path}/dev/shm"
is_mounted_inside_jail "${jail_path}/proc" || mount -t proc "proc-${jail_name}" "${jail_path}/proc"
ln -fs "${jail_path}/proc/self/fd" "${jail_path}/dev/fd"
ln -fs "${jail_path}/proc/self/fd/0" "${jail_path}/dev/stdin"
ln -fs "${jail_path}/proc/self/fd/1" "${jail_path}/dev/stdout"
ln -fs "${jail_path}/proc/self/fd/2" "${jail_path}/dev/stderr"
ln -fs "${jail_path}/proc/kcore" "${jail_path}/dev/core"
}
read_variable() {
file=${1:?}
var_name=${2:?}
pattern="^\s*${var_name}=.+"
grep --extended-regexp --only-matching "${pattern}" "${file}" | cut -d= -f2
}
read_numerical_variable() {
file=${1:?}
var_name=${2:?}
pattern="^\s*${var_name}=-?[0-9]+"
grep --extended-regexp --only-matching "${pattern}" "${file}" | cut -d= -f2
}