#! /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 < /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