#! /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 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.ok" $SSH_DIR/
    install -o "$UID0" -g "$GID0" -m 0644 "$basedir/etc/ssh/banner.sealed" $SSH_DIR/
    if [ -f $SSH_DIR/banner ] && [ ! -L $SSH_DIR/banner ] ; then
        rm $SSH_DIR/banner
    fi
    ln -sf $SSH_DIR/banner.ok $SSH_DIR/banner
    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"
    # don't do it for Ubuntu 24.04, as they removed pam_lastlog.so entirely
    if [ "$LINUX_DISTRO" = ubuntu ] && [ "$DISTRO_VERSION_MAJOR" -ge 24 ]; then
        action_na "Not supported under $LINUX_DISTRO $DISTRO_VERSION_MAJOR"
    elif [ -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
