mirror of
https://github.com/ovh/the-bastion.git
synced 2024-11-10 17:26:51 +08:00
1306 lines
51 KiB
Bash
Executable file
1306 lines
51 KiB
Bash
Executable file
#! /usr/bin/env bash
|
|
# vim: set filetype=sh ts=4 sw=4 sts=4 et:
|
|
|
|
basedir=$(readlink -f "$(dirname "$0")"/../..)
|
|
# shellcheck source=lib/shell/functions.inc
|
|
. "$basedir"/lib/shell/functions.inc
|
|
|
|
# instead of setting +e, count the errors we might have and report at the end,
|
|
# as it's better to go on instead of abrutly halting in the middle of the install
|
|
# for a potentially minor hiccup
|
|
trap_err_nb=0
|
|
trap_err_lines=''
|
|
script_end_reached=0
|
|
|
|
trap '(( ++trap_err_nb )); trap_err_lines="$trap_err_lines $LINENO"' ERR
|
|
|
|
trap_exit() {
|
|
if [ "$script_end_reached" = 0 ]; then
|
|
action_doing "Installation or upgrade interrupted!"
|
|
action_error "... as a result, the software might not be in a usable state!"
|
|
action_error "... Please run this script again to complete installation or upgrade!"
|
|
else
|
|
action_doing "Installation or upgrade done, ensuring no error occurred"
|
|
if [ "$trap_err_nb" != 0 ]; then
|
|
action_error "Some errors have occurred above, please review and advise!"
|
|
for line in $trap_err_lines; do
|
|
action_detail "... error on script source line $line"
|
|
done
|
|
exit 1
|
|
else
|
|
action_done
|
|
fi
|
|
fi
|
|
}
|
|
|
|
trap 'trap_exit' EXIT
|
|
|
|
declare -A opt
|
|
|
|
TTYREC_VERSION_NEEDED=1.1.7.0
|
|
|
|
set_default_options()
|
|
{
|
|
opt[modify-banner]=0
|
|
opt[modify-sshd-config]=0
|
|
opt[modify-ssh-config]=0
|
|
opt[modify-motd]=0
|
|
opt[modify-umask]=0
|
|
opt[modify-pam-sshd]=0
|
|
opt[modify-pam-lastlog]=0
|
|
opt[remove-weak-moduli]=0
|
|
opt[regen-hostkeys]=0
|
|
opt[logrotate]=1
|
|
opt[overwrite-logrotate]=1
|
|
opt[generate-mfa-secret]=1
|
|
|
|
# special case:
|
|
# If $autodetect_startup_system is 1, we'll autodetect whether we will install the
|
|
# systemd units (preferred) or the init.d files.
|
|
# Any specification of a --[no-]init or --[no]systemd-units option by the user inhibits
|
|
# this behavior. So even if both options below default to 0, one of those will in effect
|
|
# be set to 1 if $autodetect_startup_system is 1
|
|
autodetect_startup_system=1
|
|
opt[init]=0
|
|
opt[systemd-units]=0
|
|
# /special
|
|
|
|
# special case 2:
|
|
# If $autodetect_selinux is 1, we'll autodetect whether we're running on a system
|
|
# supporting SELinux, and if yes, we'll proceed in deploying the module to make it
|
|
# possible to use TOTP MFA on those systems.
|
|
# Any specification of --[no-]install-selinux-module will inhibit this behavior
|
|
autodetect_selinux=1
|
|
opt[install-selinux-module]=0
|
|
# /special case 2
|
|
|
|
opt[profile]=1
|
|
opt[cron]=1
|
|
opt[overwrite-cron]=1
|
|
opt[syslog-ng]=1
|
|
opt[overwrite-syslog-ng]=1
|
|
opt[check-ttyrec]=1
|
|
opt[install-fake-ttyrec]=0
|
|
}
|
|
set_default_options
|
|
|
|
show_help=0
|
|
[ -z "$1" ] && show_help=1
|
|
nothing=0
|
|
|
|
while [ -n "$1" ]; do
|
|
if [ "$1" = "--new-install" ]; then
|
|
set_default_options
|
|
opt[modify-banner]=1
|
|
opt[modify-sshd-config]=1
|
|
opt[modify-ssh-config]=1
|
|
opt[modify-motd]=1
|
|
opt[modify-umask]=1
|
|
opt[modify-pam-sshd]=1
|
|
opt[modify-pam-lastlog]=1
|
|
opt[remove-weak-moduli]=1
|
|
opt[regen-hostkeys]=1
|
|
elif [ "$1" = "--minimal" ] || [ "$1" = "--nothing" ]; then
|
|
set_default_options
|
|
opt[logrotate]=0
|
|
opt[overwrite-logrotate]=0
|
|
autodetect_startup_system=0
|
|
autodetect_selinux=0
|
|
opt[profile]=0
|
|
opt[cron]=0
|
|
opt[overwrite-cron]=0
|
|
opt[syslog-ng]=0
|
|
opt[overwrite-syslog-ng]=0
|
|
[ "$1" = "--nothing" ] && nothing=1
|
|
elif [ "$1" = "--managed-upgrade" ] || [ "$1" = "--puppet" ]; then
|
|
set_default_options
|
|
opt[logrotate]=1
|
|
opt[overwrite-logrotate]=0
|
|
autodetect_startup_system=1
|
|
autodetect_selinux=1
|
|
opt[profile]=0
|
|
opt[cron]=1
|
|
opt[overwrite-cron]=1
|
|
opt[syslog-ng]=0
|
|
opt[overwrite-syslog-ng]=0
|
|
elif [ "$1" = "--upgrade" ]; then
|
|
set_default_options
|
|
else
|
|
# "--[no]-wait" is no longer used, but to keep compatibility, we keep it here (ignored)
|
|
# same for --migration-grant-aclkeeper-to-gatekeepers
|
|
foundoption=0
|
|
for allowedopt in modify-banner modify-sshd-config modify-ssh-config modify-motd modify-umask \
|
|
modify-pam-lastlog remove-weak-moduli regen-hostkeys overwrite-logrotate overwrite-cron \
|
|
overwrite-syslog-ng logrotate cron syslog-ng migration-grant-aclkeeper-to-gatekeepers \
|
|
init systemd-units profile modify-pam-sshd wait check-ttyrec install-fake-ttyrec \
|
|
install-selinux-module generate-mfa-secret
|
|
do
|
|
if [ "$1" = "--no-$allowedopt" ]; then
|
|
opt[$allowedopt]=0
|
|
foundoption=1
|
|
elif [ "$1" = "--$allowedopt" ]; then
|
|
opt[$allowedopt]=1
|
|
foundoption=1
|
|
fi
|
|
if [ "$1" = "init" ] || [ "$1" = "systemd-units" ]; then
|
|
# see "special case" comment above for more information
|
|
autodetect_startup_system=0
|
|
fi
|
|
if [ "$1" = "install-selinux-module" ]; then
|
|
# see "special case 2" comment above for more information
|
|
autodetect_selinux=0
|
|
fi
|
|
done
|
|
if [ "$foundoption" != 1 ]; then
|
|
echo "$0: Unrecognized option '$1'"
|
|
show_help=1
|
|
fi
|
|
fi
|
|
shift
|
|
done
|
|
|
|
if [ "$show_help" = 1 ] ; then
|
|
cat <<EOF
|
|
This script installs (or upgrades) the necessary files and settings on the system for a bastion server
|
|
|
|
Usage:
|
|
You must specify one of the 5 modes below. Then possibly zero or more options:
|
|
|
|
MODES:
|
|
|
|
$0 --new-install
|
|
Will modify ssh/sshd, pam, default umask configs under $ETC_DIR/ and regenerate host keys.
|
|
Use this if you're installing a new bastion. This is the equivalent of specifying all
|
|
the options below that are tagged [N].
|
|
Mostly useful for new installations, avoid using if you already have users.
|
|
If your bastion system configuration is partly handled by another system such as Puppet/Chef/Ansible,
|
|
you might want to specify which parts you want this script to handle and which others to
|
|
avoid touching, by using --upgrade instead and eventually some of the options below. You
|
|
can also use --new-install but override some options with their --no-* form.
|
|
|
|
$0 --upgrade
|
|
Will avoid touching most non-bastion configuration under $ETC_DIR/, mainly useful when applying
|
|
a new code release, to ensure the potential modifications to bastion configuration is done
|
|
without touching the other parts of the system. This is the equivalent of specifying all
|
|
the options below that are tagged [U]. Recommended in most cases.
|
|
|
|
$0 --managed-upgrade
|
|
Use this when you're using some kind of software configuration manager such as Puppet/Chef/Ansible
|
|
to manage most of the system-related configuration of your bastion. This is a light --upgrade mode,
|
|
and the equivalent of specifying all the options below that are tagged [M].
|
|
These might change in the future.
|
|
|
|
$0 --minimal
|
|
Use when you want to prevent this script from touching anything not directly bastion-related.
|
|
This is the equivalent of disabling all the below options (you can then enable them one by one
|
|
where needed). You can see it as a yet lighter --upgrade mode. Useful if you want to handle
|
|
yourself satellite configuration files such as logrotate or cron.
|
|
|
|
$0 --nothing
|
|
Don't do anything at all, not even what we normally do when all below options are disabled (as
|
|
in the minimal mode). Can be useful if you want to do only one precise modification that you
|
|
can explicitly specify using the options below.
|
|
|
|
OPTIONS:
|
|
|
|
--[no-]modify-banner install a default sshd banner [N]
|
|
--[no-]modify-sshd-config apply a hardened ssh server config from our template [N]
|
|
--[no-]modify-ssh-config apply a hardened ssh client config from our template [N]
|
|
--[no-]modify-motd empty the motd file so that users don't get nagged [N]
|
|
--[no-]modify-umask enforce a default umask of 077 on the system [N]
|
|
--[no-]modify-pam-sshd overwrite current system PAM configuration for sshd with our template [N]
|
|
--[no-]modify-pam-lastlog modify PAM configuration to write lastlog for bastion users [N]
|
|
--[no-]init copy satellite daemon scripts for startup in system init.d/ [N, M]
|
|
--[no-]systemd-units copy satellite daemon systemd unit files for startup [N, M]
|
|
--[no-]profile apply our hardened configuration in system profile.d/ [N]
|
|
--[no-]remove-weak-moduli remove weak (<4096) moduli from the ssh server moduli file [N]
|
|
--[no-]regen-hostkeys generate new host keys (and trash the old ones) for ssh server [N]
|
|
--[no-]generate-mfa-secret generate a new MFA token secret, will never overwrite a preexisting one [N, U]
|
|
|
|
--[no-]logrotate put our logrotate config files in system logrotate.d/ [N, U, M]
|
|
--[no-]overwrite-logrotate overwrite possibly existing files in system logrotate.d/ with our templates [N, U]
|
|
--[no-]cron put our cron config files in system cron.d/ [N, U, M]
|
|
--[no-]overwrite-cron overwrite possibly existing files in system cron.d/ with our templates [N, U]
|
|
--[no-]syslog-ng put our syslog-ng config files in syslog-ng.d/ [N, U]
|
|
--[no-]overwrite-syslog-ng overwrite possibly existing files in system syslog-ng.d/ with our templates [N, U]
|
|
|
|
--[no-]check-ttyrec verify that the ttyrec installed version is compatible with our code [N, U, M]
|
|
--[no-]install-fake-ttyrec install a fake ttyrec binary if ttyrec is not present; useful mainly for tests,
|
|
or if you *really* don't want to use the real ttyrec
|
|
|
|
EOF
|
|
exit 1
|
|
fi
|
|
|
|
# if we're under FreeBSD, we must have ACLs enabled on /home (or whatever partition contains it),
|
|
# so check for that first
|
|
|
|
if [ "$OS_FAMILY" = FreeBSD ]; then
|
|
action_doing "Running under FreeBSD, verifying if /home is mounted with ACLs..."
|
|
home_mp="$(df /home 2>/dev/null | awk '{ if(NR==2) {print $6;exit} }')"
|
|
if [ -z "$home_mp" ]; then
|
|
# no /home? nevermind, / will probably do
|
|
home_mp=/
|
|
fi
|
|
if ! mount | awk '{ if($3=="'"$home_mp"'"){print;exit} }' | grep -q -w acls; then
|
|
if mount | awk '{ if($3=="'"$home_mp"'"){print;exit} }' | grep -q -w zfs; then
|
|
action_error "No. This is a ZFS mount, which might not be POSIX acls compliant (nfsv4acls supported only)."
|
|
action_error "You can still try to enable POSIX acls by running \`mount -u -o acls $home_mp' to apply the change on-the-fly."
|
|
else
|
|
action_error "No. Please modify your /etc/fstab accordingly, and run \`mount -u -o acls $home_mp' to apply the change on-the-fly."
|
|
fi
|
|
exit 1
|
|
else
|
|
action_done
|
|
fi
|
|
fi
|
|
|
|
if [ "${opt[install-fake-ttyrec]}" = 1 ]; then
|
|
action_doing "Installing fake ttyrec (use this only for tests!)"
|
|
if [ ! -e "/usr/bin/ttyrec" ] && [ ! -e "/usr/local/bin/ttyrec" ]; then
|
|
install -o "$UID0" -g "$GID0" -m 0755 "$basedir/tests/functional/fake_ttyrec.sh" "/usr/local/bin/ttyrec"
|
|
action_done
|
|
else
|
|
action_na
|
|
fi
|
|
fi
|
|
|
|
if [ "${opt[modify-ssh-config]}" = 1 ] || [ "${opt[modify-sshd-config]}" = 1 ] ; then
|
|
action_doing "Find which ssh/sshd config templates to install on $OS_FAMILY $LINUX_DISTRO $DISTRO_VERSION"
|
|
short_suffix_name=$(echo "$LINUX_DISTRO$DISTRO_VERSION_MAJOR" | sed -re "s/[^a-z0-9]//")
|
|
[ "$short_suffix_name" = "rocky8" ] && short_suffix_name=centos8
|
|
[ "$short_suffix_name" = "rocky9" ] && short_suffix_name=centos9
|
|
filesuffix=default
|
|
if [ -e "$basedir/etc/ssh/sshd_config.$short_suffix_name" ] && [ -e "$basedir/etc/ssh/ssh_config.$short_suffix_name" ]; then
|
|
filesuffix=$short_suffix_name
|
|
elif [ "$OS_FAMILY" = Linux ]; then
|
|
if [ "$LINUX_DISTRO" = ubuntu ]; then
|
|
if [ "$DISTRO_VERSION_MAJOR" -le 16 ]; then
|
|
filesuffix=debian9
|
|
elif [ "$DISTRO_VERSION_MAJOR" -le 18 ]; then
|
|
filesuffix=debian10
|
|
elif [ "$DISTRO_VERSION_MAJOR" -le 20 ]; then
|
|
filesuffix=debian11
|
|
else
|
|
filesuffix=debian12
|
|
fi
|
|
elif echo "$DISTRO_LIKE" | grep -q -w suse; then
|
|
filesuffix=opensuse15
|
|
fi
|
|
elif [ "$OS_FAMILY" = FreeBSD ]; then
|
|
filesuffix=freebsd
|
|
fi
|
|
|
|
action_done "Will use the $filesuffix templates"
|
|
|
|
if [ "${opt[modify-ssh-config]}" = 1 ] ; then
|
|
action_doing "Install hardened configuration for ssh to $SSH_DIR/ssh_config"
|
|
install -o "$UID0" -g "$GID0" -m 0644 "$basedir/etc/ssh/ssh_config.$filesuffix" $SSH_DIR/ssh_config
|
|
fi
|
|
if [ "${opt[modify-sshd-config]}" = 1 ] ; then
|
|
action_doing "Install hardened configuration for sshd to $SSH_DIR/sshd_config"
|
|
install -o "$UID0" -g "$GID0" -m 0644 "$basedir/etc/ssh/sshd_config.$filesuffix" $SSH_DIR/sshd_config
|
|
fi
|
|
action_done
|
|
fi
|
|
|
|
if [ "${opt[modify-banner]}" = 1 ] ; then
|
|
action_doing "Install default sshd banner"
|
|
install -o "$UID0" -g "$GID0" -m 0644 "$basedir/etc/ssh/banner" $SSH_DIR/
|
|
action_done
|
|
fi
|
|
|
|
if [ "${opt[modify-motd]}" = 1 ] ; then
|
|
action_doing "Empty the /etc/motd file"
|
|
if [ -f /etc/motd ] && [ ! -s /etc/motd ] ; then
|
|
mv /etc/motd /etc/motd.bastion-backup
|
|
cat /dev/null > /etc/motd
|
|
chmod 644 /etc/motd
|
|
action_done
|
|
else
|
|
action_na "File already empty or non existing"
|
|
fi
|
|
fi
|
|
|
|
if [ "${opt[regen-hostkeys]}" = 1 ] ; then
|
|
action_doing "Change sshd host keys (this can take a while)"
|
|
rm -f $SSH_DIR/ssh_host_{dsa,rsa,ecdsa,ed25519}_key{,.pub}
|
|
ssh-keygen -q -t rsa -b 4096 -N '' -f $SSH_DIR/ssh_host_rsa_key >/dev/null
|
|
ssh-keygen -q -t ecdsa -b 521 -N '' -f $SSH_DIR/ssh_host_ecdsa_key >/dev/null || true
|
|
ssh-keygen -q -A >/dev/null || true
|
|
action_done
|
|
|
|
action_doing "Restart sshd"
|
|
ret=-1
|
|
if [ -e /etc/rc.d/sshd ] ; then
|
|
/etc/rc.d/sshd restart; ret=$?
|
|
elif [ -e /etc/init.d/ssh ] ; then
|
|
/etc/init.d/ssh restart; ret=$?
|
|
fi
|
|
if [ $ret -eq 0 ]; then
|
|
action_done
|
|
else
|
|
action_error "You might want to check if the sshd config is valid!"
|
|
fi
|
|
fi
|
|
|
|
if [ "$nothing" = 0 ]; then
|
|
action_doing "Add .ssh in /etc/skel if needed"
|
|
if [ ! -d /etc/skel/.ssh ] && [ -d /etc/skel ]; then
|
|
if mkdir /etc/skel/.ssh; then
|
|
action_done
|
|
else
|
|
action_error "Couldn't create /etc/skel/.ssh, nevermind, proceeding"
|
|
fi
|
|
else
|
|
action_na
|
|
fi
|
|
|
|
action_doing "Create $BASTION_ETC_DIR if needed"
|
|
if [ -d $BASTION_ETC_DIR ]; then
|
|
action_na
|
|
else
|
|
mkdir $BASTION_ETC_DIR
|
|
action_done
|
|
fi
|
|
|
|
chmod 751 $BASTION_ETC_DIR
|
|
|
|
# to make the otp pam module of our default config happy
|
|
action_doing "Create /var/otp if needed"
|
|
if [ ! -d /var/otp ]; then
|
|
if mkdir /var/otp; then
|
|
action_done
|
|
else
|
|
action_error
|
|
fi
|
|
else
|
|
action_na
|
|
fi
|
|
|
|
list="bastion"
|
|
[ "${opt[logrotate]}" = 1 ] && list="$list logrotate"
|
|
[ "${opt[cron]}" = 1 ] && list="$list cron"
|
|
[ "${opt[syslog-ng]}" = 1 ] && list="$list syslog"
|
|
for todo in $list
|
|
do
|
|
case "$todo" in
|
|
bastion) subdir="bastion"; destdir="$BASTION_ETC_DIR";;
|
|
logrotate) subdir="logrotate.d"; destdir="$ETC_DIR/logrotate.d";;
|
|
cron) subdir="cron.d"; destdir="$CRON_DIR";;
|
|
syslog) subdir="syslog-ng/conf.d"; destdir="$ETC_DIR/syslog-ng/conf.d";;
|
|
*) continue;;
|
|
esac
|
|
# don't try to copy file in nonexistent dirs (i.e. syslog-ng if rsyslog is installed)
|
|
# our own specific dirs have already been created above, so they exist
|
|
action_doing "Check files in $destdir..."
|
|
[ -d "$destdir" ] || continue
|
|
|
|
for file in "$basedir/etc/$subdir"/*.dist ; do
|
|
destfile="$destdir/$(basename "$file" .dist)"
|
|
if [ -e "$destfile" ]; then
|
|
# if the target already exist, check if we're asked to overwrite it
|
|
if [ "$todo" = "logrotate" ] && [ "${opt[overwrite-logrotate]}" = 1 ]; then
|
|
: # we'll overwrite
|
|
elif [ "$todo" = "cron" ] && [ "${opt[overwrite-cron]}" = 1 ]; then
|
|
: # we'll overwrite
|
|
elif [ "$todo" = "syslog" ] && [ "${opt[overwrite-syslog-ng]}" = 1 ]; then
|
|
: # we'll overwrite
|
|
else
|
|
# in all other cases, don't overwrite
|
|
action_detail "... won't overwrite $destfile"
|
|
continue
|
|
fi
|
|
action_detail "... we will overwrite $destfile"
|
|
elif [ "$(basename "$file")" = "osh-encrypt-rsync.conf.dist" ] || \
|
|
[ "$(basename "$file")" = "osh-backup-acl-keys.conf.dist" ] || \
|
|
[ "$(basename "$file")" = "osh-remove-empty-folders.conf.dist" ]; then
|
|
# special case for those files: if we have the $file.d dir available, don't do anything
|
|
if [ -d "$destfile".d ]; then
|
|
action_detail "... won't copy file to $destfile as we have $destfile.d"
|
|
continue
|
|
fi
|
|
fi
|
|
# if the dest file didn't exist, or it does but we've been asked to overwrite it, do it
|
|
# copy our template .dist to proper location in system
|
|
action_detail "... create $destfile"
|
|
install -o "$UID0" -g "$GID0" -m 0644 -b "$file" "$destfile"
|
|
# actually don't do a backup for cron files: we would get double-executions...
|
|
[ "$todo" = "cron" ] && rm -f "$destfile"\~
|
|
|
|
# special case if the file contains %RANDOMX%N:M%, with X between 1 and 9,
|
|
# we replace it by a random number between N and M (for crons)
|
|
if grep -qE '%RANDOM[1-9]%[0-9]+:[0-9]+%' "$destfile"; then
|
|
for i in $(seq 1 9)
|
|
do
|
|
placeholder=$(grep -Eo "%RANDOM$i%[0-9]+:[0-9]+%" "$destfile" | head -n1)
|
|
[ -n "$placeholder" ] || continue
|
|
# we have a match, compute the random and do the replace
|
|
n=$(echo "$placeholder" | cut -d% -f3 | cut -d: -f1)
|
|
m=$(echo "$placeholder" | cut -d% -f3 | cut -d: -f2)
|
|
if [ $(( m-n )) -eq 0 ]; then
|
|
action_detail "... we would divide by zero! fallback to a non-random random, such as $n"
|
|
random=$n
|
|
else
|
|
# shellcheck disable=SC2119
|
|
random=$(( ( 0x$(echo "$(hostname -f) $placeholder $file" | md5sum_compat | cut -c1-4) % (m-n) ) + n ))
|
|
fi
|
|
action_detail "... in above file, replacing $placeholder by $random"
|
|
sed_compat "s/$placeholder/$random/g" "$destfile"
|
|
done
|
|
fi
|
|
done
|
|
done
|
|
|
|
for base in osh-encrypt-rsync.conf osh-backup-acl-keys.conf osh-remove-empty-folders.conf; do
|
|
if [ -f "$BASTION_ETC_DIR/$base" ]; then
|
|
chmod 0600 "$BASTION_ETC_DIR/$base"
|
|
fi
|
|
if [ -d "$BASTION_ETC_DIR/$base.d" ]; then
|
|
chmod 0700 "$BASTION_ETC_DIR/$base.d"
|
|
fi
|
|
done
|
|
|
|
# remove deprecated cron files
|
|
if [ "${opt[cron]}" = 1 ]; then
|
|
rm -f "$CRON_DIR/osh-compress-old-logs"
|
|
fi
|
|
|
|
# ensure the includedir is present in global sudoers conf
|
|
action_doing "Add include directory in sudoers if needed"
|
|
if [ ! -e $SUDOERS_FILE ] ; then
|
|
action_error "$SUDOERS_DIR doesn't exist, is sudo installed?"
|
|
else
|
|
if grep -Eq "^[#@]includedir $SUDOERS_DIR$" $SUDOERS_FILE ; then
|
|
action_na "sudoers.d already added in config"
|
|
else
|
|
echo '# added by the-bastion:' >> $SUDOERS_FILE
|
|
echo "#includedir $SUDOERS_DIR" >> $SUDOERS_FILE
|
|
action_done
|
|
fi
|
|
fi
|
|
|
|
action_doing "Create sudoers.d if needed"
|
|
if [ ! -d "$SUDOERS_DIR" ]; then
|
|
mkdir -p "$SUDOERS_DIR"
|
|
action_done
|
|
else
|
|
action_na
|
|
fi
|
|
|
|
# List all sudoers.d files matching osh-*. Then, each time we regenerate one below,
|
|
# we'll delete it from the list. This way, when we're done, and if any file name remains,
|
|
# it means we should delete it because it's obsolete. This way of upgrading in place
|
|
# tries to avoid a race condition in sudo where it stat()s all the sudoers.d files, then
|
|
# opens them one by one, if some files have disappeared in the meantime,
|
|
# it'll log an error to syslog.
|
|
action_doing "Listing pre-existing sudoers.d files before in-place regeneration"
|
|
oldsudoers=$(mktemp)
|
|
find "$SUDOERS_DIR/" -name "osh-*" -type f > "$oldsudoers"
|
|
nbfiles=$(wc -l < "$oldsudoers")
|
|
action_done "Found $nbfiles sudoers.d files"
|
|
|
|
# copy new sudoers files
|
|
action_doing "Copy sudoers files to $SUDOERS_DIR"
|
|
for file in "$basedir/etc/sudoers.d"/osh-*; do
|
|
filename=$(basename "$file")
|
|
sed_compat "/\/$filename$/d" "$oldsudoers"
|
|
action_detail "... copying $filename"
|
|
# don't use install(1) because it doesn't overwrite in place (it unlinks then copies data)
|
|
# this can lead to a race condition if somebody uses sudo exactly at this moment, where it'll
|
|
# log a bunch of errors because files it has listed from sudoers.d have disappeared
|
|
# use a .tmp file while we're setting the proper perms, files containing '.' are ignored by sudo
|
|
if ! cp "$file" "$SUDOERS_DIR/$filename.tmp"; then
|
|
action_error "Failed copying $file to $SUDOERS_DIR/$filename.tmp"
|
|
continue
|
|
fi
|
|
if ! chown "$UID0":"$GID0" "$SUDOERS_DIR/$filename.tmp"; then
|
|
action_error "Failed chowing $SUDOERS_DIR/$filename.tmp to $UID0:$GID0"
|
|
# attempt to continue
|
|
fi
|
|
if ! chmod 0440 "$SUDOERS_DIR/$filename.tmp"; then
|
|
action_error "Failed chmoding $SUDOERS_DIR/$filename.tmp to 0440"
|
|
# attempt to continue
|
|
fi
|
|
# then overwrite in place
|
|
if ! mv -f "$SUDOERS_DIR/$filename.tmp" "$SUDOERS_DIR/$filename"; then
|
|
action_error "Failed to move $SUDOERS_DIR/$filename.tmp to $SUDOERS_DIR/$filename"
|
|
fi
|
|
done
|
|
action_done
|
|
|
|
# regenerate all group sudoers files
|
|
OLD_SUDOERS="$oldsudoers" "$basedir/bin/sudogen/generate-sudoers.sh" create group
|
|
|
|
# regenerate all accounts sudoers files
|
|
OLD_SUDOERS="$oldsudoers" "$basedir/bin/sudogen/generate-sudoers.sh" create account
|
|
|
|
action_doing "Removing obsolete sudoers.d files if any..."
|
|
nbtoremove=$(wc -l < "$oldsudoers")
|
|
if [ "$nbtoremove" = 0 ]; then
|
|
action_na
|
|
else
|
|
for toremove in $(< "$oldsudoers")
|
|
do
|
|
action_detail "removing $toremove"
|
|
rm -f "$toremove"
|
|
done
|
|
action_done "removed $nbtoremove obsolete files"
|
|
fi
|
|
rm -f "$oldsudoers"
|
|
|
|
# create the bastionsync account (needed for master/slave)
|
|
action_doing "Creating the bastionsync account"
|
|
if getent passwd bastionsync >/dev/null 2>&1; then
|
|
action_na
|
|
else
|
|
if ! groupadd_compat bastionsync 333; then
|
|
action_error "Error while adding bastionsync group"
|
|
elif ! useradd_compat bastionsync 333 '' "$basedir/bin/shell/bastion-sync-helper.sh" 333; then
|
|
action_error "Error while adding bastionsync user"
|
|
else
|
|
action_done
|
|
fi
|
|
fi
|
|
|
|
# create some needed accounts
|
|
action_doing "Regenerating groups sudoers files from current template"
|
|
for i in keykeeper allowkeeper keyreader proxyhttp
|
|
do
|
|
action_doing "Create $i if needed"
|
|
if getent passwd $i >/dev/null ; then
|
|
action_na "this account already exists"
|
|
else
|
|
action_detail "account doesn't exist, creating"
|
|
useradd_compat "$i" "" "/home/$i" "/bin/false"
|
|
action_done
|
|
fi
|
|
done
|
|
chmod 0750 /home/keyreader /home/proxyhttp
|
|
chmod 0755 /home/allowkeeper /home/keykeeper
|
|
|
|
chmod 0755 "$basedir"/bin/admin/fixrights.sh
|
|
"$basedir"/bin/admin/fixrights.sh
|
|
|
|
# create passkeeper
|
|
action_doing "Create /home/passkeeper if needed"
|
|
if [ ! -d /home/passkeeper ] ; then
|
|
mkdir /home/passkeeper
|
|
action_done
|
|
else
|
|
action_na
|
|
fi
|
|
chmod 0755 /home/passkeeper
|
|
|
|
# add groups for specific modules
|
|
action_doing "Create needed system groups"
|
|
at_least_one_changed=0
|
|
for i in superowner admin auditor \
|
|
$(find "$basedir/bin/plugin/restricted" -mindepth 1 -maxdepth 1 -type f -perm -u+x -print | sed -e "s=$basedir/bin/plugin/restricted/==")
|
|
do
|
|
i="osh-$i"
|
|
if getent group "$i" >/dev/null ; then
|
|
:
|
|
else
|
|
at_least_one_changed=1
|
|
groupadd_compat "$i"
|
|
action_detail "$i created"
|
|
fi
|
|
done
|
|
if [ "$at_least_one_changed" = 1 ]; then
|
|
action_done
|
|
else
|
|
action_na
|
|
fi
|
|
|
|
# lastoshuser
|
|
# ensures that users created without specifying IDs will be created
|
|
# with higher IDs than the lastoshuser UID
|
|
|
|
# first, we check if it exists with the old name
|
|
action_doing "Checking if user lastoshuser exists with a deprecated name"
|
|
if getent passwd lastovhuser >/dev/null; then
|
|
# yes, ok, do we have also the new name?
|
|
if getent passwd lastoshuser >/dev/null; then
|
|
# yes, ok, just delete the old name
|
|
action_detail "... yes, but also exists with the new name, deleting the old one"
|
|
if userdel lastovhuser; then
|
|
action_done
|
|
else
|
|
action_error
|
|
fi
|
|
else
|
|
# no, ok, rename it
|
|
action_detail "... no, renaming it to the new name"
|
|
if usermod -l lastoshuser lastovhuser; then
|
|
action_done
|
|
else
|
|
action_error
|
|
fi
|
|
fi
|
|
else
|
|
action_na
|
|
fi
|
|
|
|
# lastoshuser
|
|
# ... do the same for lastoshuser's main gid
|
|
|
|
# first, we check if it exists with the old name
|
|
action_doing "Checking if group lastoshuser exists with a deprecated name"
|
|
if getent group lastovhuser >/dev/null; then
|
|
# yes, ok, do we have also the new name?
|
|
if getent group lastoshuser >/dev/null; then
|
|
# yes, ok, just delete the old name
|
|
action_detail "... yes, but also exists with the new name, deleting the old one"
|
|
if groupdel lastovhuser; then
|
|
action_done
|
|
else
|
|
action_error
|
|
fi
|
|
else
|
|
# no, ok, rename it
|
|
action_detail "... no, renaming it to the new name"
|
|
if group_rename_compat lastovhuser lastoshuser; then
|
|
action_done
|
|
else
|
|
action_error
|
|
fi
|
|
fi
|
|
else
|
|
action_na
|
|
fi
|
|
|
|
# if user exists already, change its ID (old versions had uid 2999)
|
|
action_doing "Create user lastoshuser"
|
|
lastoshuseruid=$(getent passwd lastoshuser | cut -d: -f3)
|
|
if [ -n "$lastoshuseruid" ] && [ "$lastoshuseruid" != "10000" ] ; then
|
|
action_detail "user exists but with bad UID ($lastoshuseruid), fixing"
|
|
if usermod_changeuid_compat lastoshuser 10000; then
|
|
action_done
|
|
else
|
|
action_error "Error while attempting to change lastoshuser UID"
|
|
fi
|
|
elif [ -n "$lastoshuseruid" ] && [ "$lastoshuseruid" = "10000" ] ; then
|
|
action_na "user exists with proper UID"
|
|
else
|
|
action_detail "doesn't exist, creating it"
|
|
useradd_compat lastoshuser 10000 /nonexistent /bin/false
|
|
action_done
|
|
fi
|
|
|
|
# ensure lastoshuser main group has a gid of 10000
|
|
action_doing "Adjust gid of lastoshuser if needed"
|
|
lastoshgid=$(getent group lastoshuser | cut -d: -f3)
|
|
if [ -n "$lastoshgid" ] && [ "$lastoshgid" != "10000" ] ; then
|
|
action_detail "group exists but with bad GID ($lastoshgid), fixing"
|
|
if group_change_gid_compat lastoshuser 10000; then
|
|
action_done
|
|
else
|
|
action_error "Error while attempting to change lastoshuser GID"
|
|
fi
|
|
elif [ -n "$lastoshgid" ] && [ "$lastoshgid" = "10000" ] ; then
|
|
action_na "user exists with proper GID"
|
|
else
|
|
action_error "No group lastoshuser was found (!)"
|
|
fi
|
|
|
|
action_doing "Create /var/log/bastion if needed"
|
|
if [ ! -d /var/log/bastion ] ; then
|
|
mkdir -p /var/log/bastion
|
|
action_done
|
|
else
|
|
action_na
|
|
fi
|
|
|
|
action_doing "Set proper rights on /var/log/bastion"
|
|
chown "$UID0":allowkeeper /var/log/bastion
|
|
chmod 0710 /var/log/bastion
|
|
action_done
|
|
|
|
# ensuring proper ACLs on group homes
|
|
action_doing "Ensuring proper ACLs on group homes and allowed.ip"
|
|
for grp in $(getent group | cut -d: -f1 | grep -- '-gatekeeper$' | sed -e 's/-gatekeeper$//'); do
|
|
test -f "/home/$grp/allowed.ip" || continue
|
|
if [ "$OS_FAMILY" = "Linux" ] || [ "$OS_FAMILY" = "FreeBSD" ]; then
|
|
setfacl -m "group:osh-whoHasAccessTo:--x" "/home/$grp"
|
|
setfacl -m "group:osh-auditor:--x" "/home/$grp"
|
|
setfacl -m "group:osh-superowner:--x" "/home/$grp"
|
|
setfacl -m "group:$grp-gatekeeper:--x" "/home/$grp"
|
|
setfacl -m "group:$grp-aclkeeper:--x" "/home/$grp"
|
|
setfacl -m "group:$grp-owner:--x" "/home/$grp"
|
|
fi
|
|
chgrp "$grp-aclkeeper" "/home/$grp/allowed.ip"
|
|
chmod 0664 "/home/$grp/allowed.ip"
|
|
done
|
|
action_done
|
|
|
|
# ensuring proper ACLs on account homes
|
|
action_doing "Ensuring proper ACLs on account homes"
|
|
for accounthome in $(getent passwd | grep ":$basedir/bin/shell/osh.pl$" | cut -d: -f6); do
|
|
if test -d "$accounthome"; then
|
|
chmod 0750 "$accounthome"
|
|
setfacl -m g:osh-auditor:x "$accounthome"
|
|
fi
|
|
if test -d "$accounthome/.ssh"; then
|
|
setfacl -m g:osh-auditor:x "$accounthome/.ssh"
|
|
fi
|
|
done
|
|
action_done
|
|
|
|
# v2.27.02+ (bastion-users), v2.30.00+ (mfa-*)
|
|
action_doing "Creating missing system groups where needed"
|
|
at_least_one_changed=0
|
|
at_least_one_error=0
|
|
for group in bastion-users \
|
|
mfa-password-reqd mfa-password-bypass mfa-password-configd \
|
|
mfa-totp-reqd mfa-totp-bypass mfa-totp-configd bastion-nopam osh-pubkey-auth-optional
|
|
do
|
|
if getent group "$group" >/dev/null 2>&1; then
|
|
:
|
|
else
|
|
action_detail "... $group"
|
|
if groupadd_compat "$group"; then
|
|
at_least_one_changed=1
|
|
else
|
|
at_least_one_error=1
|
|
fi
|
|
fi
|
|
done
|
|
if [ "$at_least_one_error" != 0 ]; then
|
|
action_error
|
|
elif [ "$at_least_one_changed" = 0 ]; then
|
|
action_na
|
|
else
|
|
action_done
|
|
fi
|
|
|
|
# create logkeeper
|
|
action_doing "Create /home/logkeeper if needed"
|
|
if [ ! -d /home/logkeeper ] ; then
|
|
mkdir /home/logkeeper
|
|
action_done
|
|
else
|
|
action_na
|
|
fi
|
|
chown root:bastion-users /home/logkeeper
|
|
chmod 0730 /home/logkeeper
|
|
|
|
# create the healthcheck account for the monitoring probe (doesn't harm even if not used)
|
|
action_doing "Creating the healthcheck account"
|
|
if getent passwd healthcheck >/dev/null 2>&1; then
|
|
action_na
|
|
else
|
|
ssh-keygen -q -t ed25519 -N '' -f "$UID0HOME/id_healthcheck" || \
|
|
ssh-keygen -q -t ecdsa -N '' -f "$UID0HOME/id_healthcheck" || \
|
|
ssh-keygen -q -t rsa -b 4096 -N '' -f "$UID0HOME/id_healthcheck"
|
|
if [ ! -e "$UID0HOME/id_healthcheck.pub" ]; then
|
|
action_error "Error while generating the SSH key"
|
|
else
|
|
chmod 0444 "$UID0HOME/id_healthcheck.pub"
|
|
USER="$UID0" HOME="$UID0HOME" "$basedir"/bin/plugin/restricted/accountCreate '' '' '' '' --account healthcheck --uid-auto --always-active --immutable-key --osh-only --force-key-from "127.0.0.1" < "$UID0HOME/id_healthcheck.pub"
|
|
if ! getent passwd healthcheck >/dev/null 2>&1; then
|
|
action_error "Couldn't create the healthcheck account"
|
|
else
|
|
mv "$UID0HOME/id_healthcheck" ~healthcheck/.ssh/id_healthcheck
|
|
mv "$UID0HOME/id_healthcheck.pub" ~healthcheck/.ssh/id_healthcheck.pub
|
|
chown healthcheck:healthcheck ~healthcheck/.ssh/id_healthcheck ~healthcheck/.ssh/id_healthcheck.pub
|
|
chmod 400 ~healthcheck/.ssh/id_healthcheck
|
|
action_done
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# ensure the system bastion accounts are in bastion-nopam group
|
|
at_least_one_changed=0
|
|
at_least_one_error=0
|
|
action_doing "Ensuring bastionsync and healthcheck are in bastion-nopam group"
|
|
for account in bastionsync healthcheck; do
|
|
if ! getent group bastion-nopam | grep -q -E "[:,]$account(,|$)"; then
|
|
if add_user_to_group_compat $account bastion-nopam; then
|
|
at_least_one_changed=1
|
|
else
|
|
at_least_one_error=1
|
|
fi
|
|
fi
|
|
done
|
|
if [ "$at_least_one_error" != 0 ]; then
|
|
action_error
|
|
elif [ "$at_least_one_changed" = 0 ]; then
|
|
action_na
|
|
else
|
|
action_done
|
|
fi
|
|
|
|
# create the master2slave ssh key (only needed for master, useless but not harmful for slaves)
|
|
action_doing "Generating the master/slave SSH key"
|
|
if [ -e "$UID0HOME/.ssh/id_master2slave" ]; then
|
|
action_na
|
|
else
|
|
ssh-keygen -q -t ed25519 -N '' -f "$UID0HOME/.ssh/id_master2slave" || \
|
|
ssh-keygen -q -t ecdsa -N '' -f "$UID0HOME/.ssh/id_master2slave" || \
|
|
ssh-keygen -q -t rsa -b 4096 -N '' -f "$UID0HOME/.ssh/id_master2slave"
|
|
if [ ! -e "$UID0HOME/.ssh/id_master2slave.pub" ]; then
|
|
action_error "Error while generating the SSH key"
|
|
else
|
|
action_done
|
|
fi
|
|
fi
|
|
|
|
# create the ssh key for backups
|
|
action_doing "Generating the backup SSH key"
|
|
if [ -e "$UID0HOME/.ssh/id_backup" ]; then
|
|
action_na
|
|
else
|
|
ssh-keygen -q -t ed25519 -N '' -f "$UID0HOME/.ssh/id_backup" || \
|
|
ssh-keygen -q -t ecdsa -N '' -f "$UID0HOME/.ssh/id_backup" || \
|
|
ssh-keygen -q -t rsa -b 4096 -N '' -f "$UID0HOME/.ssh/id_backup"
|
|
if [ ! -e "$UID0HOME/.ssh/id_backup.pub" ]; then
|
|
action_error "Error while generating the SSH key"
|
|
else
|
|
action_done
|
|
fi
|
|
fi
|
|
|
|
# grant the bastion admins to all the restricted commands, as we might have added new ones
|
|
action_doing "Granting all the restricted commands to the bastion admins"
|
|
admins=$(perl -I"$basedir/lib/perl" -MOVH::Bastion -e 'my $admins = OVH::Bastion::config("adminAccounts")->value; print join("\n", @{ $admins || [] })')
|
|
grant_status=0
|
|
for account in $(getent group osh-admin | cut -d: -f4 | tr "," " ")
|
|
do
|
|
# we must also check that this account is declared in bastion.conf (or it's not really an admin),
|
|
if ! echo "$admins" | grep -qw "$account"; then
|
|
action_detail "$account is not really an admin, skipping"
|
|
continue
|
|
fi
|
|
action_detail "$account"
|
|
_before_change=$(id "$account")
|
|
"$basedir/bin/admin/grant-all-restricted-commands-to.sh" "$account" >/dev/null; ret=$?
|
|
_after_change=$(id "$account")
|
|
if [ "$ret" -ne 0 ]; then
|
|
action_detail "... failed!"
|
|
grant_status=-1
|
|
elif [ "$_before_change" = "$_after_change" ]; then
|
|
action_detail "... nothing to do"
|
|
else
|
|
action_detail "... done"
|
|
[ "$grant_status" != -1 ] && grant_status=1
|
|
fi
|
|
done
|
|
if [ "$grant_status" = 0 ]; then
|
|
action_na
|
|
elif [ "$grant_status" = -1 ]; then
|
|
action_error
|
|
else
|
|
action_done
|
|
fi
|
|
|
|
action_doing "Ensuring all bastion accounts are in the bastion-users group"
|
|
bastion_users_members=$(getent group bastion-users | cut -d: -f4 | tr , " ")
|
|
at_least_one_changed=0
|
|
at_least_one_error=0
|
|
for account in $(getent passwd | grep ":$basedir/bin/shell/osh.pl$" | cut -d: -f1) proxyhttp
|
|
do
|
|
if ! echo "$bastion_users_members" | grep -q -w "$account"; then
|
|
action_detail "... $account"
|
|
add_user_to_group_compat "$account" "bastion-users" || at_least_one_error=1
|
|
at_least_one_changed=1
|
|
fi
|
|
done
|
|
if [ "$at_least_one_error" != 0 ]; then
|
|
action_error
|
|
elif [ "$at_least_one_changed" = 0 ]; then
|
|
action_na
|
|
else
|
|
action_done
|
|
fi
|
|
|
|
action_doing "Archiving old account and group files"
|
|
at_least_one_changed=0
|
|
at_least_one_error=0
|
|
acls_param=''
|
|
[ "$OS_FAMILY" = "Linux" ] && acls_param='--acls'
|
|
[ "$OS_FAMILY" = "FreeBSD" ] && acls_param='--acls'
|
|
while IFS= read -r -d '' dir
|
|
do
|
|
at_least_one_changed=1
|
|
if command -v chattr &>/dev/null; then
|
|
find "$dir" -name "*.log" -print0 | xargs -r0 chattr -a || true
|
|
fi
|
|
tarfile="$dir.tar.gz"
|
|
while [ -e "$tarfile" ]; do
|
|
# archive already exists? append some randomness to the name, we don't want to overwrite it!
|
|
tarfile="$dir.$(date +%s)-$RANDOM.tar.gz"
|
|
done
|
|
if tar czf "$tarfile" $acls_param --one-file-system -p --remove-files "$dir"; then
|
|
chmod 0 "$tarfile"
|
|
else
|
|
at_least_one_error=1
|
|
fi
|
|
done < <(find /home/oldkeeper/accounts/ /home/oldkeeper/groups/ -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null)
|
|
if [ "$at_least_one_error" != 0 ]; then
|
|
action_error
|
|
elif [ "$at_least_one_changed" = 0 ]; then
|
|
action_na
|
|
else
|
|
action_done
|
|
fi
|
|
|
|
action_doing "Replacing legacy o+w by bastion-users/g+w"
|
|
at_least_one_changed=0
|
|
for file in \
|
|
$(test -f /home/osh.log && find /home/osh.log \( -perm -o+w -o ! -user root \) ) \
|
|
$(find /home/logkeeper/ -mindepth 1 -maxdepth 1 -type f -name "global-log-*" \( -perm -o+w -o ! -user root \) )
|
|
do
|
|
if echo "$file" | grep -Eq '\.log$' && command -v chattr &>/dev/null; then
|
|
chattr -a "$file" 2>/dev/null || true
|
|
fi
|
|
chown root:bastion-users "$file"
|
|
chmod 660 "$file"
|
|
if echo "$file" | grep -Eq '\.log$' && command -v chattr &>/dev/null; then
|
|
chattr +a "$file" 2>/dev/null || true
|
|
fi
|
|
at_least_one_changed=1
|
|
done
|
|
if [ "$at_least_one_changed" = 0 ]; then
|
|
action_na
|
|
else
|
|
action_done
|
|
fi
|
|
|
|
action_doing "Ensuring symlinked plugins have their json config"
|
|
at_least_one_changed=0
|
|
at_least_one_error=0
|
|
while IFS= read -r -d '' plugin
|
|
do
|
|
if [ -e "${plugin}.json" ]; then
|
|
# the config is there, ok
|
|
continue
|
|
fi
|
|
# the config is not there, is there a corresponding .json at the other end of the symlink?
|
|
source=$(readlink "$plugin")
|
|
sourcecanon="$(dirname "$plugin")/$source"
|
|
sourcecanon="$(readlink -f "$sourcecanon")"
|
|
if [ -e "${sourcecanon}.json" ]; then
|
|
action_detail "... ${sourcecanon}.json found, symlinking"
|
|
ln -s "${source}.json" "${plugin}.json"; ret=$?
|
|
if [ $ret -ne 0 ]; then
|
|
at_least_one_error=1
|
|
else
|
|
at_least_one_changed=1
|
|
fi
|
|
fi
|
|
done < <(find "$basedir"/bin/plugin -type l ! -name "*.*" -print0)
|
|
if [ "$at_least_one_error" != 0 ]; then
|
|
action_error
|
|
elif [ "$at_least_one_changed" = 0 ]; then
|
|
action_na
|
|
else
|
|
action_done
|
|
fi
|
|
|
|
# The Bastion > v3.04.00
|
|
action_doing "Migrating account creation metadata to new format where applicable"
|
|
at_least_one_changed=0
|
|
at_least_one_error=0
|
|
for accounthome in $(getent passwd | grep ":$basedir/bin/shell/osh.pl$" | cut -d: -f6); do
|
|
# if the old AND the new files exists, don't do anything: we won't delete the old file
|
|
# because we want our users to be able to roll back to an old version, and as this file doesn't
|
|
# hurt anything (it'll just get ignored by new versions), no point in inserting an incompatible change here.
|
|
if [ -f "$accounthome/accountCreate.comment" ] && ! [ -f "$accounthome/config.creation_info" ]; then
|
|
at_least_one_changed=$((at_least_one_changed + 1))
|
|
# build a config.creation_info file from the data stored in the legacy accountCreate.comment
|
|
perl -Mstrict -MJSON -mPOSIX -e '
|
|
my %h;
|
|
open(my $oldfh, "<", $ARGV[0]) or die $!;
|
|
while (<$oldfh>) {
|
|
chomp;
|
|
my @f = split("=", $_, 2);
|
|
$h{$f[0]}=$f[1] if @f == 2;
|
|
}
|
|
close($oldfh);
|
|
open(my $newfh, ">", $ARGV[1]) or die $!;
|
|
print $newfh encode_json({
|
|
by => $h{CREATED_BY},
|
|
bastion_version => $h{BASTION_VERSION},
|
|
datetime_utc => POSIX::strftime("%a %Y-%m-%d %H:%M:%S UTC", gmtime($h{CREATION_TIMESTAMP})),
|
|
datetime_local => POSIX::strftime("%a %Y-%m-%d %H:%M:%S %Z", localtime($h{CREATION_TIMESTAMP})),
|
|
timestamp => $h{CREATION_TIMESTAMP}+0,
|
|
comment => (($h{COMMENT} && $h{COMMENT} ne "(no_comment_provided)") ? $h{COMMENT} : undef),
|
|
});
|
|
close($newfh);
|
|
' "$accounthome/accountCreate.comment" "$accounthome/config.creation_info"; ret=$?
|
|
[ -f "$accounthome/config.creation_info" ] && chmod 644 "$accounthome/config.creation_info"
|
|
[ "$ret" != 0 ] && at_least_one_error=1
|
|
fi
|
|
done
|
|
if [ "$at_least_one_error" != 0 ]; then
|
|
action_error
|
|
elif [ "$at_least_one_changed" = 0 ]; then
|
|
action_na
|
|
else
|
|
action_done "Migrated $at_least_one_changed files"
|
|
fi
|
|
|
|
# checking whether we have things to install from the install.d directory
|
|
# shellcheck disable=SC2034
|
|
STARTED_BY_MAIN_INSTALL=1
|
|
while IFS= read -r -d '' installfile
|
|
do
|
|
action_doing "Starting module install ($installfile)..."
|
|
# shellcheck disable=SC1090
|
|
if . "$installfile"; then
|
|
action_done
|
|
else
|
|
action_error
|
|
fi
|
|
done < <(test -d "$basedir"/install/modules && find "$basedir"/install/modules -mindepth 2 -maxdepth 2 -name install -type f -print0)
|
|
unset STARTED_BY_MAIN_INSTALL
|
|
fi
|
|
|
|
if [ "${opt[syslog-ng]}" = 1 ] && [ "${opt[overwrite-syslog-ng]}" = 1 ]; then
|
|
if [ -e /etc/syslog-ng/syslog-ng.conf ] && ! grep -q s_src /etc/syslog-ng/syslog-ng.conf && grep -q s_sys /etc/syslog-ng/syslog-ng.conf ; then
|
|
sed_compat 's/s_src/s_sys/g' /etc/syslog-ng/conf.d/20-bastion.conf
|
|
fi
|
|
fi
|
|
|
|
if [ "$autodetect_startup_system" = 1 ]; then
|
|
action_doing "Autodetecting startup system..."
|
|
if command -v systemctl >/dev/null && [ -d "/var/run/systemd" ]; then
|
|
action_done "found systemd"
|
|
opt[systemd-units]=1
|
|
opt[init]=0
|
|
else
|
|
action_done "found sysV-style init"
|
|
opt[systemd-units]=0
|
|
opt[init]=1
|
|
fi
|
|
fi
|
|
|
|
if [ "${opt[init]}" = 1 ]; then
|
|
initd=rc.d
|
|
[ "$OS_FAMILY" = Linux ] && initd=init.d
|
|
action_doing "Copy init scripts to /etc/$initd"
|
|
for file in "$basedir/etc/init.d"/osh-*; do
|
|
servicename=$(basename "$file")
|
|
if [ -e "/etc/$initd/$servicename" ]; then
|
|
isnew=0
|
|
else
|
|
isnew=1
|
|
fi
|
|
if install -o "$UID0" -g "$GID0" -m 0755 "$file" "/etc/$initd/"; then
|
|
if [ "$isnew" = 1 ]; then
|
|
action_detail "$servicename installed, ${WHITE_ON_BLUE}please use \`update-rc.d $servicename defaults' if you want to enable this service${NOC}"
|
|
else
|
|
action_detail "$servicename updated"
|
|
fi
|
|
else
|
|
action_error "failed installing $file"
|
|
fi
|
|
done
|
|
action_done
|
|
|
|
# remove obsolete init.d files if needed
|
|
action_doing "Remove obsolete init.d files..."
|
|
at_least_one_changed=0
|
|
# shellcheck disable=SC2043
|
|
for obsolete in osh-proxy-http
|
|
do
|
|
if [ -e "/etc/$initd/$obsolete" ]; then
|
|
at_least_one_changed=1
|
|
action_detail "removing $obsolete"
|
|
update-rc.d -f $obsolete disable || true
|
|
rm -f "/etc/$initd/$obsolete"
|
|
fi
|
|
done
|
|
if [ "$at_least_one_changed" = 1 ]; then
|
|
action_done
|
|
else
|
|
action_na
|
|
fi
|
|
fi
|
|
|
|
if [ "${opt[systemd-units]}" = 1 ]; then
|
|
action_doing "Copy systemd unit files to /etc/systemd/system"
|
|
for file in "$basedir/etc/systemd"/osh-*.service; do
|
|
servicename="$(basename "$file")"
|
|
if [ -e "/etc/systemd/system/$servicename" ]; then
|
|
isnew=0
|
|
else
|
|
isnew=1
|
|
fi
|
|
if cp "$file" /etc/systemd/system/; then
|
|
if [ "$isnew" = 1 ]; then
|
|
action_detail "$servicename installed, ${WHITE_ON_BLUE}please use \`systemctl enable $servicename' if you want to enable this service${NOC}"
|
|
else
|
|
action_detail "$servicename updated"
|
|
fi
|
|
else
|
|
action_error "failed installing $file"
|
|
fi
|
|
done
|
|
systemctl daemon-reload || true
|
|
action_done
|
|
fi
|
|
|
|
if [ "$autodetect_selinux" = 1 ]; then
|
|
action_doing "Autodetecting SELinux..."
|
|
if command -v setenforce >/dev/null; then
|
|
action_done "found"
|
|
opt[install-selinux-module]=1
|
|
else
|
|
action_done "not found"
|
|
opt[install-selinux-module]=0
|
|
fi
|
|
fi
|
|
|
|
if [ "${opt[install-selinux-module]}" = 1 ]; then
|
|
action_doing "Installing SELinux module"
|
|
if ! command -v semodule >/dev/null; then
|
|
action_error "Missing \`semodule' tool, please install it (it's usually found in the 'policycoreutils' package)"
|
|
else
|
|
if semodule -l | grep -q the-bastion; then
|
|
action_na "module is already installed"
|
|
else
|
|
if semodule -i "$basedir/etc/selinux/the-bastion.pp"; then
|
|
action_done "module installed"
|
|
else
|
|
action_error "semodule returned an error"
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [ "${opt[profile]}" = 1 ]; then
|
|
action_doing "Copy profile.d files if applicable"
|
|
if [ -d $ETC_DIR/profile.d ]; then
|
|
for file in "$basedir/etc/profile.d"/*.sh; do
|
|
action_detail "$file"
|
|
install -o "$UID0" -g "$GID0" -m 0755 "$file" "$ETC_DIR/profile.d/"
|
|
done
|
|
action_done
|
|
else
|
|
action_na "$ETC_DIR/profile.d not found"
|
|
fi
|
|
fi
|
|
|
|
if [ "${opt[modify-umask]}" = 1 ]; then
|
|
# umask
|
|
action_doing "Adjust umask in /etc/login.defs if applicable"
|
|
if [ -e /etc/login.defs ]; then
|
|
sed_compat 's/^(\s*UMASK\s+).+/\1027/g' /etc/login.defs
|
|
action_done
|
|
else
|
|
action_na
|
|
fi
|
|
|
|
action_doing "Adjust umask in $PAM_DIR/common-session if applicable"
|
|
if [ -e $PAM_DIR/common-session ]; then
|
|
if ! grep -Eq '^\s*session\s+optional\s+pam_umask.so\s+umask=0?027' \
|
|
$PAM_DIR/common-session ; then
|
|
action_detail "missing umask config in file, adjusting"
|
|
echo "# bastion config: umask needs to be at 0027" >> $PAM_DIR/common-session
|
|
echo "session optional pam_umask.so umask=0027" >> $PAM_DIR/common-session
|
|
action_done
|
|
else
|
|
action_na "umask was already OK"
|
|
fi
|
|
else
|
|
action_na "no file found"
|
|
fi
|
|
fi
|
|
|
|
if [ "${opt[modify-pam-sshd]}" = 1 ]; then
|
|
action_doing "Use our template for pam.d/sshd"
|
|
if grep -Eiq '^[[:space:]]*AuthenticationMethods[[:space:]]+publickey,keyboard-interactive:pam' "$SSH_DIR/sshd_config"; then
|
|
echo "$DISTRO_LIKE" | grep -q -w debian && pamsuffix=debian
|
|
echo "$DISTRO_LIKE" | grep -q -w rhel && pamsuffix=rhel
|
|
[ "$OS_FAMILY" = FreeBSD ] && pamsuffix=freebsd
|
|
if [ "$LINUX_DISTRO" = ubuntu ] && [ "$DISTRO_VERSION_MAJOR" -ge 22 ]; then
|
|
pamsuffix="ubuntu2204"
|
|
elif [ -n "$pamsuffix" ] && [ -e "$basedir/etc/pam.d/sshd.$pamsuffix$DISTRO_VERSION" ]; then
|
|
pamsuffix="$pamsuffix$DISTRO_VERSION"
|
|
fi
|
|
if [ -n "$pamsuffix" ] && [ -e "$PAM_SSHD" ] && [ -e "$basedir/etc/pam.d/sshd.$pamsuffix" ]; then
|
|
cp -a "$PAM_SSHD" "$PAM_SSHD.backup_$(date +%s)"
|
|
cat "$basedir/etc/pam.d/sshd.$pamsuffix" > "$PAM_SSHD"
|
|
action_done "sshd.$pamsuffix"
|
|
else
|
|
action_error "couldn't use our pam.d/sshd template (no template for $OS_FAMILY/$DISTRO_LIKE)"
|
|
fi
|
|
else
|
|
action_na "the currently installed sshd_config file doesn't have a forced 'AuthenticationMethods publickey', we can't install our pam.d template safely (it could turn this machine into an allow-all accesses without auth through ssh!)"
|
|
fi
|
|
fi
|
|
|
|
if [ "${opt[modify-pam-lastlog]}" = 1 ]; then
|
|
# pam.d lastlogin
|
|
action_doing "Adjust lastlog in pam.d/sshd if applicable"
|
|
if [ -e "$PAM_SSHD" ] ; then
|
|
if ! grep -Eq '^\s*session\s+optional\s+pam_lastlog.so' "$PAM_SSHD" ; then
|
|
action_detail "missing lastlog config in file, adjusting"
|
|
# shellcheck disable=SC1004
|
|
sed_compat '/^[[:space:]]*@include[[:space:]]+common-session/a\
|
|
# bastion config: lastlog needs to be updated on connection\
|
|
session optional pam_lastlog.so silent' "$PAM_SSHD"
|
|
action_done
|
|
else
|
|
action_na "lastlog config was already ok"
|
|
fi
|
|
else
|
|
action_na "no file found"
|
|
fi
|
|
fi
|
|
|
|
if [ "${opt[remove-weak-moduli]}" = 1 ]; then
|
|
# remove low moduli
|
|
action_doing "Remove weak moduli (less than 4K)"
|
|
if [ -e $SSH_DIR/moduli ] ; then
|
|
tmpmod=$(mktemp)
|
|
awk '$5 >= 4095' $SSH_DIR/moduli > "$tmpmod"
|
|
if cmp -s $SSH_DIR/moduli "$tmpmod"; then
|
|
action_na "no weak moduli found"
|
|
else
|
|
cat "$tmpmod" > $SSH_DIR/moduli
|
|
action_done
|
|
fi
|
|
rm -f "$tmpmod"
|
|
fi
|
|
fi
|
|
|
|
if [ "${opt[generate-mfa-secret]}" = 1 ]; then
|
|
action_doing "Generate the MFA HMAC secret if needed"
|
|
if [ -e "$BASTION_ETC_DIR/mfa-token.conf" ]; then
|
|
action_na
|
|
else
|
|
secret=$(env LANG=C tr -dc A-Za-z0-9 < /dev/urandom 2>/dev/null | head -c32)
|
|
touch "$BASTION_ETC_DIR/mfa-token.conf"
|
|
chown 0:bastion-users "$BASTION_ETC_DIR/mfa-token.conf"
|
|
chmod 640 "$BASTION_ETC_DIR/mfa-token.conf"
|
|
echo '{ "secret": "'"$secret"'" }' > "$BASTION_ETC_DIR/mfa-token.conf"
|
|
action_done
|
|
fi
|
|
fi
|
|
|
|
# lastly, check for ttyrec version and yell if it's not the proper one
|
|
if [ "${opt[check-ttyrec]}" = 1 ] ; then
|
|
action_doing "Checking ttyrec version"
|
|
if ! command -v ttyrec >/dev/null 2>&1; then
|
|
action_error "ttyrec is not installed, the bastion will not work! Please either install ovh-ttyrec (/opt/bastion/bin/admin/install-ttyrec.sh) or run this script a second time with \`$0 --nothing --install-fake-ttyrec'"
|
|
else
|
|
ttyrec_version=$(ttyrec -V 2>/dev/null | grep -Eo 'ttyrec v[0-9.]+' | cut -c9-)
|
|
if [ -z "$ttyrec_version" ]; then
|
|
action_error "Incompatible ttyrec version installed, the bastion will not work! Please either install ovh-ttyrec (/opt/bastion/bin/admin/install-ttyrec.sh) or run this script again with \`$0 --nothing --install-fake-ttyrec'"
|
|
else
|
|
action_detail "found v$ttyrec_version"
|
|
action_detail "expected v$TTYREC_VERSION_NEEDED"
|
|
if [ "$(printf "%s\\n%s\\n" "$ttyrec_version" "$TTYREC_VERSION_NEEDED" | sort | head -1)" = "$TTYREC_VERSION_NEEDED" ]; then
|
|
action_done
|
|
else
|
|
action_error "The installed ttyrec version is too old, the bastion will not work! Please update ovh-ttyrec to at least v$TTYREC_VERSION_NEEDED (/opt/bastion/bin/admin/install-ttyrec.sh can help)"
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# used in trap_exit
|
|
script_end_reached=1
|