diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml new file mode 100644 index 0000000..49f0b95 --- /dev/null +++ b/.github/workflows/freebsd.yml @@ -0,0 +1,37 @@ +name: FreeBSD tests + +on: + pull_request: + types: [labeled, synchronize] + +jobs: + freebsd: + runs-on: macos-latest + name: FreeBSD + if: contains(github.event.pull_request.labels.*.name, 'tests:full') + steps: + - uses: actions/checkout@v2 + - name: Functional tests under FreeBSD + uses: vmactions/freebsd-vm@v0.0.8 + with: + usesh: true + run: | + set -ex + freebsd-version + mount -o acls / + pkg install -y bash unzip rsync ca_root_nss jq fping screen flock gmake + mkdir -p /opt/bastion + rsync -a . /opt/bastion/ + fetch https://github.com/ovh/ovh-ttyrec/archive/master.zip + unzip master.zip + cd ovh-ttyrec-master/ + ./configure + gmake + gmake install + cd .. + /opt/bastion/bin/admin/packages-check.sh -i + /opt/bastion/bin/admin/install --new-install --no-wait + ssh-keygen -t ed25519 -f id_user + ssh-keygen -t ed25519 -f id_root + NO_SLEEP=1 user_pubkey=$(cat id_user.pub) root_pubkey=$(cat id_root.pub) TARGET_USER=user5000 /opt/bastion/tests/functional/docker/target_role.sh + HAS_MFA=0 HAS_MFA_PASSWORD=1 HAS_PAMTESTER=1 nocc=1 /opt/bastion/tests/functional/launch_tests_on_instance.sh 127.0.0.1 22 user5000 id_user id_root /usr/local/etc/bastion diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 708b6f0..f75794a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,6 +5,17 @@ on: types: [labeled, synchronize] jobs: + tests_short: + name: Short (Debian 10 only) + runs-on: ubuntu-latest + if: contains(github.event.pull_request.labels.*.name, 'tests:short') + steps: + - uses: actions/checkout@v2 + - name: run tests inside a debian10 docker + run: tests/functional/docker/docker_build_and_run_tests.sh debian10 + env: + DOCKER_TTY: false + tests_full: name: Full strategy: @@ -18,14 +29,3 @@ jobs: run: tests/functional/docker/docker_build_and_run_tests.sh ${{ matrix.platform }} env: DOCKER_TTY: false - - tests_short: - name: Full on Debian 10 - runs-on: ubuntu-latest - if: contains(github.event.pull_request.labels.*.name, 'tests:short') - steps: - - uses: actions/checkout@v2 - - name: run tests inside a debian10 docker - run: tests/functional/docker/docker_build_and_run_tests.sh debian10 - env: - DOCKER_TTY: false diff --git a/bin/admin/check-consistency.pl b/bin/admin/check-consistency.pl index c3d0386..7e0de32 100755 --- a/bin/admin/check-consistency.pl +++ b/bin/admin/check-consistency.pl @@ -542,13 +542,16 @@ sub _tocheck { my $gid = $stat[5]; my $wantuser = (split / /, $tocheck{'FILEOWN'}[0])[0]; my $wantgroup = (split / /, $tocheck{'FILEOWN'}[0])[1]; - $wantuser = $UID0 if $wantuser eq 'root'; - $wantgroup = $GID0 if $wantgroup eq 'root'; - if ($uid ne getpwnam($wantuser)) { - _err "bad owner on file $file (got $uid but wanted $wantuser)"; + $wantuser = $UID0 if ($wantuser eq 'root' || $wantuser eq '0'); + $wantgroup = $GID0 if ($wantgroup eq 'root' || $wantgroup eq '0'); + my $wantuid = getpwnam($wantuser); + my $wantgid = getgrnam($wantgroup); + + if ($uid ne $wantuid) { + _err "bad owner on file $file (got $uid but wanted $wantuid aka $wantuser)"; } - if ($gid ne getgrnam($wantgroup)) { - _err "bad group on file $file (got $gid but wanted $wantgroup)"; + if ($gid ne $wantgid) { + _err "bad group on file $file (got $gid but wanted $wantgid aka $wantgroup)"; } } if (exists $tocheck{'SUDOERS'}) { diff --git a/bin/admin/install b/bin/admin/install index 8e1ce20..e9f7d38 100755 --- a/bin/admin/install +++ b/bin/admin/install @@ -202,8 +202,8 @@ fi if [ "${opt[install-fake-ttyrec]}" = 1 ]; then action_doing "Installing fake ttyrec (use this only for tests!)" - if [ ! -e "/usr/bin/ttyrec" ]; then - install -o "$UID0" -g "$GID0" -m 0755 "$basedir/tests/functional/fake_ttyrec.sh" "/usr/bin/ttyrec" + 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 @@ -228,6 +228,8 @@ if [ "${opt[modify-ssh-config]}" = 1 ] || [ "${opt[modify-sshd-config]}" = 1 ] ; 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" @@ -345,9 +347,9 @@ if [ "$nothing" = 0 ]; then osh-lingeringSessionsReaper osh-orphanedHomedir osh-pivGraceReaper \ osh-protectLogs osh-rotateTtyrec osh-activeUsers do - if [ -e "$ETC_DIR/cron.d/$obsolete" ]; then + if [ -e "$CRON_DIR/$obsolete" ]; then at_least_one_changed=1 - rm -f "$ETC_DIR/cron.d/$obsolete" + rm -f "$CRON_DIR/$obsolete" fi done fi @@ -381,26 +383,33 @@ if [ "$nothing" = 0 ]; then action_na fi - dirstocheck='bastion' - [ "${opt[logrotate]}" = 1 ] && dirstocheck="$dirstocheck logrotate.d" - [ "${opt[cron]}" = 1 ] && dirstocheck="$dirstocheck cron.d" - [ "${opt[syslog-ng]}" = 1 ] && dirstocheck="$dirstocheck syslog-ng/conf.d" - for subdir in $dirstocheck + 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 $ETC_DIR/$subdir..." - [ -d "$ETC_DIR/$subdir" ] || continue + action_doing "Check files in $destdir..." + [ -d "$destdir" ] || continue for file in "$basedir/etc/$subdir"/*.dist ; do - destfile="$ETC_DIR/$subdir/$(basename "$file" .dist)" + destfile="$destdir/$(basename "$file" .dist)" if [ -e "$destfile" ]; then # if the target already exist, check if we're asked to overwrite it - if [ "$subdir" = "logrotate.d" ] && [ "${opt[overwrite-logrotate]}" = 1 ]; then + if [ "$todo" = "logrotate" ] && [ "${opt[overwrite-logrotate]}" = 1 ]; then : # we'll overwrite - elif [ "$subdir" = "cron.d" ] && [ "${opt[overwrite-cron]}" = 1 ]; then + elif [ "$todo" = "cron" ] && [ "${opt[overwrite-cron]}" = 1 ]; then : # we'll overwrite - elif [ "$subdir" = "syslog-ng/conf.d" ] && [ "${opt[overwrite-syslog-ng]}" = 1 ]; then + elif [ "$todo" = "syslog" ] && [ "${opt[overwrite-syslog-ng]}" = 1 ]; then : # we'll overwrite else # in all other cases, don't overwrite @@ -420,7 +429,7 @@ if [ "$nothing" = 0 ]; then 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... - [ "$subdir" = "cron.d" ] && rm -f "$destfile"\~ + [ "$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) @@ -436,7 +445,8 @@ if [ "$nothing" = 0 ]; then action_detail "... we would divide by zero! fallback to a non-random random, such as $n" random=$n else - random=$(( ( 0x$(echo "$(hostname -f) $placeholder $file" | md5sum | cut -c1-4) % (m-n) ) + n )) + # 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" @@ -459,7 +469,7 @@ if [ "$nothing" = 0 ]; then if [ ! -e $SUDOERS_FILE ] ; then action_error "$SUDOERS_DIR doesn't exist, is sudo installed?" else - if grep -q "^#includedir $SUDOERS_DIR$" $SUDOERS_FILE ; then + 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 @@ -1187,13 +1197,13 @@ if [ "${opt[modify-umask]}" = 1 ]; then action_na fi - action_doing "Adjust umask in $ETC_DIR/pam.d/common-session if applicable" - if [ -e $ETC_DIR/pam.d/common-session ]; then + 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' \ - $ETC_DIR/pam.d/common-session ; then + $PAM_DIR/common-session ; then action_detail "missing umask config in file, adjusting" - echo "# bastion config: umask needs to be at 0027" >> $ETC_DIR/pam.d/common-session - echo "session optional pam_umask.so umask=0027" >> $ETC_DIR/pam.d/common-session + 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" @@ -1205,15 +1215,16 @@ 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' /etc/ssh/sshd_config; then - echo "$DISTRO_LIKE" | grep -q -w debian && pamsuffix=debian - echo "$DISTRO_LIKE" | grep -q -w rhel && pamsuffix=rhel - if [ -n "$pamsuffix" ] && [ -e $ETC_DIR/pam.d/sshd ] && [ -e "$basedir/etc/pam.d/sshd.$pamsuffix" ]; then - cp -a "$ETC_DIR/pam.d/sshd" "$ETC_DIR/pam.d/sshd.backup_$(date +%s)" - cat "$basedir/etc/pam.d/sshd.$pamsuffix" > $ETC_DIR/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 [ -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 else - action_error "couldn't use our pam.d/sshd template" + 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!)" @@ -1223,12 +1234,12 @@ fi if [ "${opt[modify-pam-lastlog]}" = 1 ]; then # pam.d lastlogin action_doing "Adjust lastlog in pam.d/sshd if applicable" - if [ -e "$ETC_DIR/pam.d/sshd" ] ; then - if ! grep -Eq '^\s*session\s+optional\s+pam_lastlog.so' "$ETC_DIR/pam.d/sshd" ; then + 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 '/^\s*@include\s+common-session/a\ - # bastion config: lastlog needs to be updated on connection\nsession optional pam_lastlog.so silent' "$ETC_DIR/pam.d/sshd" + # bastion config: lastlog needs to be updated on connection\nsession optional pam_lastlog.so silent' "$PAM_SSHD" action_done else action_na "lastlog config was already ok" diff --git a/bin/admin/packages-check.sh b/bin/admin/packages-check.sh index a88eace..72deaf8 100755 --- a/bin/admin/packages-check.sh +++ b/bin/admin/packages-check.sh @@ -125,8 +125,18 @@ elif echo "$DISTRO_LIKE" | grep -q -w suse; then installed="FIXME" install_cmd="zypper install" elif [ "$OS_FAMILY" = FreeBSD ]; then + wanted_list="base64 coreutils rsync bash sudo pamtester p5-JSON p5-JSON-XS p5-common-sense p5-Net-IP p5-GnuPG p5-DBD-SQLite p5-Net-Netmask p5-Term-ReadKey expect fping p5-Net-Server p5-CGI p5-LWP-Protocol-https" + install_cmd="pkg add" + installed="" + for i in $wanted_list + do + if pkg info -e "$i"; then + installed="$installed $i" + fi + done if [ "$opt_install" = 1 ]; then - pkg install -y rsync bash sudo p5-JSON p5-JSON-XS p5-common-sense p5-Net-IP p5-GnuPG p5-DBD-SQLite p5-Net-Netmask p5-Term-ReadKey expect fping p5-Net-Server p5-CGI p5-LWP-Protocol-https + # shellcheck disable=SC2086 + pkg install -y $wanted_list exit $? fi else diff --git a/bin/helper/osh-accountAddGroupServer b/bin/helper/osh-accountAddGroupServer index 5b100c4..a244090 100755 --- a/bin/helper/osh-accountAddGroupServer +++ b/bin/helper/osh-accountAddGroupServer @@ -3,7 +3,7 @@ # KEYSUDOERS # as a gatekeeper, to be able to add the servers to /home/allowkeeper/ACCOUNT/allowed.partial.%GROUP% file # KEYSUDOERS SUPEROWNERS, %%GROUP%-gatekeeper ALL=(allowkeeper) NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-accountAddGroupServer --group %GROUP% * # FILEMODE 0750 -# FILEOWN root allowkeeper +# FILEOWN 0 allowkeeper #>HEADER use common::sense; diff --git a/bin/helper/osh-accountCreate b/bin/helper/osh-accountCreate index f3c24fc..730fa45 100755 --- a/bin/helper/osh-accountCreate +++ b/bin/helper/osh-accountCreate @@ -3,7 +3,7 @@ # NEEDGROUP osh-accountCreate # SUDOERS %osh-accountCreate ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountCreate --type normal * # FILEMODE 0700 -# FILEOWN root root +# FILEOWN 0 0 #>HEADER use common::sense; diff --git a/bin/helper/osh-accountDelete b/bin/helper/osh-accountDelete index 2025814..0d78293 100755 --- a/bin/helper/osh-accountDelete +++ b/bin/helper/osh-accountDelete @@ -3,7 +3,7 @@ # NEEDGROUP osh-accountDelete # SUDOERS %osh-accountDelete ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountDelete * # FILEMODE 0700 -# FILEOWN root root +# FILEOWN 0 0 #>HEADER use common::sense; diff --git a/bin/helper/osh-accountGeneratePassword b/bin/helper/osh-accountGeneratePassword index 13c8d73..e9e91e4 100755 --- a/bin/helper/osh-accountGeneratePassword +++ b/bin/helper/osh-accountGeneratePassword @@ -3,7 +3,7 @@ # SUDOERS # to be able to generate an egress password for accounts # SUDOERS %osh-accountGeneratePassword ALL=(%bastion-users) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountGeneratePassword * # FILEMODE 0755 -# FILEOWN root root +# FILEOWN 0 0 #>HEADER use common::sense; diff --git a/bin/helper/osh-accountGetPasswordInfo b/bin/helper/osh-accountGetPasswordInfo index 1dc8436..f1eaea0 100755 --- a/bin/helper/osh-accountGetPasswordInfo +++ b/bin/helper/osh-accountGetPasswordInfo @@ -3,7 +3,7 @@ # NEEDGROUP osh-auditor # SUDOERS %osh-auditor ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountGetPasswordInfo * # FILEMODE 0700 -# FILEOWN root root +# FILEOWN 0 0 #>HEADER use common::sense; diff --git a/bin/helper/osh-accountListEgressKeys b/bin/helper/osh-accountListEgressKeys index 52cd1af..5eeaec7 100755 --- a/bin/helper/osh-accountListEgressKeys +++ b/bin/helper/osh-accountListEgressKeys @@ -3,7 +3,7 @@ # NEEDGROUP osh-accountListEgressKeys # SUDOERS %osh-accountListEgressKeys ALL=(keyreader) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountListEgressKeys * # FILEMODE 0750 -# FILEOWN root keyreader +# FILEOWN 0 keyreader #>HEADER use common::sense; diff --git a/bin/helper/osh-accountListIngressKeys b/bin/helper/osh-accountListIngressKeys index 49c5bcd..251a199 100755 --- a/bin/helper/osh-accountListIngressKeys +++ b/bin/helper/osh-accountListIngressKeys @@ -3,7 +3,7 @@ # NEEDGROUP osh-accountListIngressKeys # SUDOERS %osh-accountListIngressKeys ALL=(keyreader) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountListIngressKeys * # FILEMODE 0750 -# FILEOWN root keyreader +# FILEOWN 0 keyreader #>HEADER use common::sense; diff --git a/bin/helper/osh-accountListPasswords b/bin/helper/osh-accountListPasswords index 97125d6..5b531a0 100755 --- a/bin/helper/osh-accountListPasswords +++ b/bin/helper/osh-accountListPasswords @@ -3,7 +3,7 @@ # NEEDGROUP osh-accountListPasswords # SUDOERS %osh-accountListPasswords ALL=(%bastion-users) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountListPasswords * # FILEMODE 0755 -# FILEOWN root root +# FILEOWN 0 0 #>HEADER use common::sense; diff --git a/bin/helper/osh-accountMFAResetPassword b/bin/helper/osh-accountMFAResetPassword index 2afc439..7a5c3d0 100755 --- a/bin/helper/osh-accountMFAResetPassword +++ b/bin/helper/osh-accountMFAResetPassword @@ -3,7 +3,7 @@ # NEEDGROUP osh-accountMFAResetPassword # SUDOERS %osh-accountMFAResetPassword ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountMFAResetPassword --account * # FILEMODE 0700 -# FILEOWN root root +# FILEOWN 0 0 #>HEADER use common::sense; diff --git a/bin/helper/osh-accountMFAResetTOTP b/bin/helper/osh-accountMFAResetTOTP index af4a359..16c2213 100755 --- a/bin/helper/osh-accountMFAResetTOTP +++ b/bin/helper/osh-accountMFAResetTOTP @@ -3,7 +3,7 @@ # NEEDGROUP osh-accountMFAResetTOTP # SUDOERS %osh-accountMFAResetTOTP ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountMFAResetTOTP --account * # FILEMODE 0700 -# FILEOWN root root +# FILEOWN 0 0 #>HEADER use common::sense; diff --git a/bin/helper/osh-accountModify b/bin/helper/osh-accountModify index 77fbd98..8506d47 100755 --- a/bin/helper/osh-accountModify +++ b/bin/helper/osh-accountModify @@ -4,7 +4,7 @@ # SUDOERS # modify parameters/policy of an account # SUDOERS %osh-accountModify ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountModify * # FILEMODE 0700 -# FILEOWN root root +# FILEOWN 0 0 #>HEADER use common::sense; diff --git a/bin/helper/osh-accountModifyCommand b/bin/helper/osh-accountModifyCommand index a68269b..5d41e84 100755 --- a/bin/helper/osh-accountModifyCommand +++ b/bin/helper/osh-accountModifyCommand @@ -7,7 +7,7 @@ # SUDOERS # revoke access to a command # SUDOERS %osh-accountRevokeCommand ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountModifyCommand --action revoke * # FILEMODE 0755 -# FILEOWN root root +# FILEOWN 0 0 #>HEADER use common::sense; diff --git a/bin/helper/osh-accountModifyPersonalAccess b/bin/helper/osh-accountModifyPersonalAccess index b19d6be..8492c6e 100755 --- a/bin/helper/osh-accountModifyPersonalAccess +++ b/bin/helper/osh-accountModifyPersonalAccess @@ -3,22 +3,22 @@ # NEEDGROUP osh-selfAddPersonalAccess # SUDOERS %osh-selfAddPersonalAccess ALL=(allowkeeper) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountModifyPersonalAccess --target self --action add * # FILEMODE 0750 -# FILEOWN root allowkeeper +# FILEOWN 0 allowkeeper # # NEEDGROUP osh-accountAddPersonalAccess # SUDOERS %osh-accountAddPersonalAccess ALL=(allowkeeper) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountModifyPersonalAccess --target any --action add * # FILEMODE 0750 -# FILEOWN root allowkeeper +# FILEOWN 0 allowkeeper # # NEEDGROUP osh-selfDelPersonalAccess # SUDOERS %osh-selfDelPersonalAccess ALL=(allowkeeper) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountModifyPersonalAccess --target self --action del * # FILEMODE 0750 -# FILEOWN root allowkeeper +# FILEOWN 0 allowkeeper # # NEEDGROUP osh-accountDelPersonalAccess # SUDOERS %osh-accountDelPersonalAccess ALL=(allowkeeper) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountModifyPersonalAccess --target any --action del * # FILEMODE 0750 -# FILEOWN root allowkeeper +# FILEOWN 0 allowkeeper #>HEADER use common::sense; diff --git a/bin/helper/osh-accountPIV b/bin/helper/osh-accountPIV index 20188f9..46e11e9 100755 --- a/bin/helper/osh-accountPIV +++ b/bin/helper/osh-accountPIV @@ -5,7 +5,7 @@ # SUDOERS %osh-accountPIV ALL=(allowkeeper) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountPIV --step 1 --account * # SUDOERS %osh-accountPIV ALL=(%bastion-users) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountPIV --step 2 --account * # FILEMODE 0755 -# FILEOWN root root +# FILEOWN 0 0 #>HEADER use common::sense; diff --git a/bin/helper/osh-accountUnexpire b/bin/helper/osh-accountUnexpire index b15f611..afb90b8 100755 --- a/bin/helper/osh-accountUnexpire +++ b/bin/helper/osh-accountUnexpire @@ -3,7 +3,7 @@ # NEEDGROUP osh-accountUnexpire # SUDOERS %osh-accountUnexpire ALL=(%bastion-users) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountUnexpire * # FILEMODE 0755 -# FILEOWN root root +# FILEOWN 0 0 #>HEADER use common::sense; diff --git a/bin/helper/osh-adminMaintenance b/bin/helper/osh-adminMaintenance index fcfe278..8d31783 100755 --- a/bin/helper/osh-adminMaintenance +++ b/bin/helper/osh-adminMaintenance @@ -3,7 +3,7 @@ # NEEDGROUP osh-admin # SUDOERS %osh-admin ALL=(allowkeeper) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-adminMaintenance * # FILEMODE 0750 -# FILEOWN root allowkeeper +# FILEOWN 0 allowkeeper #>HEADER use common::sense; diff --git a/bin/helper/osh-groupAddServer b/bin/helper/osh-groupAddServer index 8a363a5..cd06cde 100755 --- a/bin/helper/osh-groupAddServer +++ b/bin/helper/osh-groupAddServer @@ -3,7 +3,7 @@ # KEYSUDOERS # as an aclkeeper, we can add/del a server from the group server list in /home/%GROUP%/allowed.ip # KEYSUDOERS SUPEROWNERS, %%GROUP%-aclkeeper ALL=(%GROUP%) NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-groupAddServer --group %GROUP% * # FILEMODE 0755 -# FILEOWN root root +# FILEOWN 0 0 #>HEADER use common::sense; diff --git a/bin/helper/osh-groupAddSymlinkToAccount b/bin/helper/osh-groupAddSymlinkToAccount index 0ee0063..9d5d6a2 100755 --- a/bin/helper/osh-groupAddSymlinkToAccount +++ b/bin/helper/osh-groupAddSymlinkToAccount @@ -3,7 +3,7 @@ # KEYSUDOERS # as a gatekeeper, to be able to symlink in /home/allowkeeper/ACCOUNT the /home/%GROUP%/allowed.ip file # KEYSUDOERS SUPEROWNERS, %%GROUP%-gatekeeper ALL=(allowkeeper) NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-groupAddSymlinkToAccount --group %GROUP% * # FILEMODE 0750 -# FILEOWN root allowkeeper +# FILEOWN 0 allowkeeper #>HEADER use common::sense; diff --git a/bin/helper/osh-groupCreate b/bin/helper/osh-groupCreate index 22a88ee..08384a7 100755 --- a/bin/helper/osh-groupCreate +++ b/bin/helper/osh-groupCreate @@ -3,7 +3,7 @@ # NEEDGROUP osh-groupCreate # SUDOERS %osh-groupCreate ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-groupCreate * # FILEMODE 0700 -# FILEOWN root root +# FILEOWN 0 0 #>HEADER use common::sense; diff --git a/bin/helper/osh-groupDelete b/bin/helper/osh-groupDelete index 2e97e53..12eeb98 100755 --- a/bin/helper/osh-groupDelete +++ b/bin/helper/osh-groupDelete @@ -3,7 +3,7 @@ # NEEDGROUP osh-groupDelete # SUDOERS %osh-groupDelete ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-groupDelete * # FILEMODE 0700 -# FILEOWN root root +# FILEOWN 0 0 #>HEADER use common::sense; diff --git a/bin/helper/osh-groupGeneratePassword b/bin/helper/osh-groupGeneratePassword index af3ebcd..0650120 100755 --- a/bin/helper/osh-groupGeneratePassword +++ b/bin/helper/osh-groupGeneratePassword @@ -3,7 +3,7 @@ # KEYSUDOERS # as an owner, we can generate an egress password for the group # KEYSUDOERS SUPEROWNERS, %%GROUP%-owner ALL=(%GROUP%) NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-groupGeneratePassword --group %GROUP% * # FILEMODE 0755 -# FILEOWN root root +# FILEOWN 0 0 #>HEADER use common::sense; diff --git a/bin/helper/osh-groupModify b/bin/helper/osh-groupModify index e24a14e..7667707 100755 --- a/bin/helper/osh-groupModify +++ b/bin/helper/osh-groupModify @@ -3,7 +3,7 @@ # KEYSUDOERS # as an owner, we can modify the group settings # KEYSUDOERS SUPEROWNERS, %%GROUP%-owner ALL=(%GROUP%) NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-groupModify --group %GROUP% * # FILEMODE 0755 -# FILEOWN root root +# FILEOWN 0 0 #>HEADER use common::sense; diff --git a/bin/helper/osh-groupSetRole b/bin/helper/osh-groupSetRole index 7b906e5..234ce1a 100755 --- a/bin/helper/osh-groupSetRole +++ b/bin/helper/osh-groupSetRole @@ -11,7 +11,7 @@ # KEYSUDOERS # as a gatekeeper, we can grant/revoke a guest access # KEYSUDOERS SUPEROWNERS, %%GROUP%-gatekeeper ALL=(root) NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-groupSetRole --type guest --group %GROUP% * # FILEMODE 0700 -# FILEOWN root root +# FILEOWN 0 0 #>HEADER use common::sense; diff --git a/bin/helper/osh-selfMFASetupPassword b/bin/helper/osh-selfMFASetupPassword index a8758a4..b09fdc1 100755 --- a/bin/helper/osh-selfMFASetupPassword +++ b/bin/helper/osh-selfMFASetupPassword @@ -1,7 +1,7 @@ #! /usr/bin/perl -T # vim: set filetype=perl ts=4 sw=4 sts=4 et: # FILEMODE 0700 -# FILEOWN root root +# FILEOWN 0 0 #>HEADER use common::sense; diff --git a/bin/helper/osh-selfMFASetupTOTP b/bin/helper/osh-selfMFASetupTOTP index 57417a5..7a88c96 100755 --- a/bin/helper/osh-selfMFASetupTOTP +++ b/bin/helper/osh-selfMFASetupTOTP @@ -1,7 +1,7 @@ #! /usr/bin/perl -T # vim: set filetype=perl ts=4 sw=4 sts=4 et: # FILEMODE 0700 -# FILEOWN root root +# FILEOWN 0 0 #>HEADER use common::sense; diff --git a/bin/plugin/open/selfMFASetupPassword.json b/bin/plugin/open/selfMFASetupPassword.json index 9646d13..b6e718a 100644 --- a/bin/plugin/open/selfMFASetupPassword.json +++ b/bin/plugin/open/selfMFASetupPassword.json @@ -3,5 +3,6 @@ "selfMFASetupPassword" , {"pr" : [""]} ], "master_only": true, + "execution_mode_on_freebsd": "system", "terminal_mode": "noecho" } diff --git a/bin/shell/connect.pl b/bin/shell/connect.pl index 49011cf..1b60d45 100755 --- a/bin/shell/connect.pl +++ b/bin/shell/connect.pl @@ -100,7 +100,12 @@ else { } # in any case, force this -$command[0] = '/usr/bin/ttyrec'; +if (-e '/usr/local/bin/ttyrec') { + $command[0] = '/usr/local/bin/ttyrec'; +} +else { + $command[0] = '/usr/bin/ttyrec'; +} # then finally launch the command ! my $sysret = system(@command); diff --git a/bin/shell/osh.pl b/bin/shell/osh.pl index 2960392..216e588 100755 --- a/bin/shell/osh.pl +++ b/bin/shell/osh.pl @@ -872,58 +872,32 @@ if ($osh_command) { if ($MFArequiredForPlugin ne 'none' && !$skipMFA) { print "As this is required to run this plugin, entering MFA phase.\n"; - # use system() instead of OVH::Bastion::execute() because we need it to grab the term - my $pamtries = 3; - while (1) { - my $pamsysret = system('pamtester', 'sshd', $sysself, 'authenticate'); - if ($pamsysret < 0) { - main_exit(OVH::Bastion::EXIT_MFA_FAILED, 'mfa_failed', "MFA is required for this plugin, but this bastion is missing the `pamtester' tool, aborting"); - } - elsif ($pamsysret != 0) { - if (--$pamtries <= 0) { - main_exit(OVH::Bastion::EXIT_MFA_FAILED, 'mfa_failed', "Sorry, but Multi-Factor Authentication failed, aborting"); - } - next; - } - - # success, if we are configured to launch a external command on pamtester success, do it. - # see the bastion.conf.dist file for usage example. - my $MFAPostCommand = OVH::Bastion::config('MFAPostCommand')->value; - if (ref $MFAPostCommand eq 'ARRAY' && @$MFAPostCommand) { - s/%ACCOUNT%/$self/g for @$MFAPostCommand; - $fnret = OVH::Bastion::execute(cmd => $MFAPostCommand, must_succeed => 1); - if (!$fnret) { - warn_syslog("MFAPostCommand returned a non-zero value: " . $fnret->msg); - } - } - last; - } + $fnret = OVH::Bastion::do_pamtester(self => $self, sysself => $sysself); + $fnret or main_exit(OVH::Bastion::EXIT_MFA_FAILED, 'mfa_failed', $fnret->msg); } OVH::Bastion::set_terminal_mode_for_plugin(plugin => $osh_command, action => 'set'); - if (OVH::Bastion::is_bsd() && $osh_command eq 'selfMFASetupPassword') { - system(@cmd); - $fnret = R('OK', value => {sysret => $?}); - } - else { - # some plugins need to be called with system() instead of ::execute - my $is_binary; - my $system; + + # get the execution mode required by the plugin + my $is_binary; + my $system; + $fnret = OVH::Bastion::plugin_config(plugin => $osh_command, key => "execution_mode_on_$^O"); + if (!$fnret || !$fnret->value) { $fnret = OVH::Bastion::plugin_config(plugin => $osh_command, key => "execution_mode"); - if ($fnret && $fnret->value) { - $system = 1 if $fnret->value eq 'system'; - $is_binary = 1 if $fnret->value eq 'binary'; - } - $ENV{'OSH_IP_FROM'} = $ipfrom; # used in some plugins for is_access_granted() - $fnret = OVH::Bastion::execute( - cmd => \@cmd, - noisy_stdout => 1, - noisy_stderr => 1, - expects_stdin => 1, - system => $system, - is_binary => $is_binary, - ); } + if ($fnret && $fnret->value) { + $system = 1 if $fnret->value eq 'system'; + $is_binary = 1 if $fnret->value eq 'binary'; + } + $ENV{'OSH_IP_FROM'} = $ipfrom; # used in some plugins for is_access_granted() + $fnret = OVH::Bastion::execute( + cmd => \@cmd, + noisy_stdout => 1, + noisy_stderr => 1, + expects_stdin => 1, + system => $system, + is_binary => $is_binary, + ); OVH::Bastion::set_terminal_mode_for_plugin(plugin => $osh_command, action => 'restore'); if (defined $log_insert_id and defined $log_db_name) { @@ -1306,32 +1280,8 @@ if ($JITMFARequired) { print "... skipping as your account is exempt from MFA\n"; } else { - # use system() instead of OVH::Bastion::execute() because we need it to grab the term - my $pamtries = 3; - while (1) { - my $pamsysret = system('pamtester', 'sshd', $sysself, 'authenticate'); - if ($pamsysret < 0) { - main_exit(OVH::Bastion::EXIT_MFA_FAILED, 'mfa_failed', "MFA is required for this host, but this bastion is missing the `pamtester' tool, aborting"); - } - elsif ($pamsysret != 0) { - if (--$pamtries <= 0) { - main_exit(OVH::Bastion::EXIT_MFA_FAILED, 'mfa_failed', "Sorry, but Multi-Factor Authentication failed, I can't connect you to this host"); - } - next; - } - - # success, if we are configured to launch a external command on pamtester success, do it. - # see the bastion.conf.dist file for usage example. - my $MFAPostCommand = OVH::Bastion::config('MFAPostCommand')->value; - if (ref $MFAPostCommand eq 'ARRAY' && @$MFAPostCommand) { - s/%ACCOUNT%/$self/g for @$MFAPostCommand; - $fnret = OVH::Bastion::execute(cmd => $MFAPostCommand, must_succeed => 1); - if (!$fnret) { - warn_syslog("MFAPostCommand returned a non-zero value: " . $fnret->msg); - } - } - last; - } + $fnret = OVH::Bastion::do_pamtester(self => $self, sysself => $sysself); + $fnret or main_exit(OVH::Bastion::EXIT_MFA_FAILED, 'mfa_failed', $fnret->msg); } } diff --git a/bin/sudogen/generate-sudoers.sh b/bin/sudogen/generate-sudoers.sh index 28cf9a9..bd3a230 100755 --- a/bin/sudogen/generate-sudoers.sh +++ b/bin/sudogen/generate-sudoers.sh @@ -32,7 +32,7 @@ generate_account_sudoers() normalized_account=$(sed -re 's/[^A-Z0-9_]/_/gi' <<< "$account") # as we're reducing the amount of possible chars in normalized_account # we could have collisions: use MD5 to generate a uniq suffix - account_suffix=$(md5sum - <<< "$account" | cut -c1-6) + account_suffix=$(md5sum_compat - <<< "$account" | cut -c1-6) normalized_account="${normalized_account}_${account_suffix}" # lowercase is prohibited normalized_account=$(tr '[:lower:]' '[:upper:]' <<< "$normalized_account") @@ -42,8 +42,16 @@ generate_account_sudoers() chmod 0440 "${dst}.tmp" { echo "# generated from install script" - for template in $(find "$basedir/etc/sudoers.account.template.d/" -type f | sort) + for template in $(find "$basedir/etc/sudoers.account.template.d/" -type f -name "*.sudoers" | sort) do + # if $template has two dots, then it's of the form XXX-name.$os.sudoers, + # in that case we only include this template if $os is our current OS + if [ "$(echo "$template" | cut -d. -f3)" = "sudoers" ]; then + if [ "$(echo "$template" | cut -d. -f2 | tr '[:upper:]' '[:lower:]')" != "$(echo "$OS_FAMILY" | tr '[:upper:]' '[:lower:]')" ]; then + # not the same OS, skip it + continue + fi + fi echo echo "# $template:" perl -pe "s!%ACCOUNT%!$account!g;s!%NORMACCOUNT%!$normalized_account!g;s!%BASEPATH%!$basedir!g" "$template" diff --git a/etc/pam.d/sshd.freebsd b/etc/pam.d/sshd.freebsd new file mode 100644 index 0000000..e474ebd --- /dev/null +++ b/etc/pam.d/sshd.freebsd @@ -0,0 +1,13 @@ +# PAM configuration for the Secure Shell service + +auth optional pam_echo.so Your account has Multi-Factor Authentication enabled, an additional authentication factor is required (password). +auth optional pam_exec.so capture_stdout /opt/bastion/bin/shell/pam_exec_pwd_info.sh +auth required pam_unix.so + +account required pam_nologin.so +account required pam_login_access.so +account required pam_unix.so + +session required pam_permit.so + +password required pam_unix.so no_warn try_first_pass diff --git a/etc/ssh/ssh_config.freebsd b/etc/ssh/ssh_config.freebsd new file mode 100644 index 0000000..c74f7eb --- /dev/null +++ b/etc/ssh/ssh_config.freebsd @@ -0,0 +1,114 @@ +# Hardened SSH bastion config -- modify wisely! +# Based on https://wiki.mozilla.org/Security/Guidelines/OpenSSH +# With modifications where applicable/needed + +# hardened params follow. every non-needed feature is disabled by default, +# following the principle of least rights and least features (more enabled +# features mean a more important attack surface). + +# === FEATURES === + +# disable non-needed sshd features +# mitigates CVE-0216-0778 +UseRoaming no +# other unwanted features +Tunnel no +ForwardAgent no +ForwardX11 no +GatewayPorts no +ControlMaster no + +# === CRYPTOGRAPHY === + +# enforce the use of ssh version 2 protocol, version 1 is disabled. +# all sshd_config options regarding protocol 1 are therefore omitted. +Protocol 2 + +# list of allowed ciphers. +# chacha20-poly1305 is a modern cipher, considered very secure +# aes is still the standard, we prefer gcm cipher mode, but also +# allow ctr cipher mode for compatibility (ctr is considered secure) +# we deny arcfour(rc4), 3des, blowfish and cast +# for older remote servers (or esoteric hardware), we might need to add: aes256-cbc,aes192-cbc,aes128-cbc +# known gotchas: +# - BSD (https://lists.freebsd.org/pipermail/freebsd-bugs/2013-June/053005.html) needs aes256-gcm@openssh.com,aes128-gcm@openssh.com DISABLED +# - Old Cisco IOS (such as v12.2) only supports aes128-cbc,3des-cbc,aes192-cbc,aes256-cbc +# - Ancient Debians (Sarge) and RedHats (7) only support aes128-cbc,3des-cbc,blowfish-cbc,cast128-cbc,arcfour,aes192-cbc,aes256-cbc,rijndael-cbc@lysator.liu.se,aes128-ctr,aes192-ctr,aes256-ctr +Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr + +# list of allowed message authentication code algorithms. +# etm (encrypt-then-mac) are considered the more secure, we +# prefer umac (has been proven secure) then sha2. +# for older remote servers, fallback to the non-etm version of +# the algorithms. we deny md5 entirely. +# for older remote servers (or esoteric hardware), we might need to add: hmac-sha1 +# Known gotchas: +# - Old Cisco IOS (such as v12.2) only supports hmac-sha1,hmac-sha1-96,hmac-md5,hmac-md5-96 +# - Ancient Debians (Sarge) and RedHats (7) only support hmac-md5,hmac-sha1,hmac-ripemd160,hmac-ripemd160@openssh.com,hmac-sha1-96,hmac-md5-96 +MACs umac-128-etm@openssh.com,umac-64-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128@openssh.com,umac-64@openssh.com,hmac-sha2-512,hmac-sha2-256 + +# List of allowed key exchange algorithms. +# we prefer curve25519-sha256 which is considered the most modern/secure, +# and still allow diffie hellman with group exchange using sha256 which is +# the most secure dh-based kex. +# we avoid algorithms based on the disputed NIST curves, and anything based +# on sha1. +# known gotchas: +# - Windows needs diffie-hellman-group14-sha1 and also needs to NOT have diffie-hellman-group-exchange-sha1 present in the list AT ALL +# - OmniOS 5.11 needs diffie-hellman-group1-sha1 +# - Old Cisco IOS (such as v12.2) only supports diffie-hellman-group1-sha1 +# - Ancient Debians (Sarge) and RedHats (7) only support diffie-hellman-group-exchange-sha1,diffie-hellman-group1-sha1 +KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256 + +# === AUTHENTICATION === + +# we allow only public key authentication ... +PubkeyAuthentication yes +# ... not password nor keyboard-interactive +# ... (set to yes if sshpass is to be used) +PasswordAuthentication no +# ChallengeResponseAuthentication=yes forces KbdInteractiveAuthentication=yes in the openssh code! +ChallengeResponseAuthentication yes +KbdInteractiveAuthentication yes +# ... not host-based +HostbasedAuthentication no +# now we specify the auth methods order we want for manual ssh calls. +# NOTE1: as per the ssh source code, an auth method omitted hereafter +# will not be used, even if set to "yes" above. +# NOTE2: the bastion code (namely, ttyrec), will always set the proper +# value explicitly on command-line (pubkey OR sshpass), so the value +# specified hereafter will be ignored. if you want to force-disable +# a method, set it to "no" in the list above, as those will never be +# overridden by the code. +PreferredAuthentications publickey,keyboard-interactive + +# === LOGIN ### + +# disable escape character use +EscapeChar none + +# detect if a hostkey changed due to DNS spoofing +CheckHostIP yes + +# ignore ssh-agent, only use specified keys (-i) +IdentitiesOnly yes +# disable auto-lookup of ~/.ssh/id_rsa ~/.ssh/id_ecdsa etc. +IdentityFile /dev/non/existent/file + +# carry those vars to the other side (includes LC_BASTION) +SendEnv LANG LC_* + +# allow usage of SSHFP DNS records +VerifyHostKeyDNS ask + +# yell if remote hostkey changed +StrictHostKeyChecking ask + +# === SYSTEM === + +# don't hash the users known_hosts files, in the context of a bastion, this adds no security +HashKnownHosts no + +# send an ssh ping each 57 seconds to the client and disconnect after 5 no-replies +ServerAliveInterval 57 +ServerAliveCountMax 5 diff --git a/etc/ssh/sshd_config.freebsd b/etc/ssh/sshd_config.freebsd new file mode 100644 index 0000000..d7de401 --- /dev/null +++ b/etc/ssh/sshd_config.freebsd @@ -0,0 +1,136 @@ +# Hardened SSHD bastion config -- modify wisely! +# Based on https://wiki.mozilla.org/Security/Guidelines/OpenSSH +# With additional restrictions where applicable + +# -lo and -rt users only have local console login +DenyUsers *-rt +DenyUsers *-lo + +# hardened params follow. every non-needed feature is disabled by default, +# following the principle of least rights and least features (more enabled +# features mean a more important attack surface). + +# === FEATURES === + +# disable non-needed sshd features +AllowAgentForwarding no +AllowTcpForwarding no +AllowStreamLocalForwarding no +X11Forwarding no +PermitTunnel no +PermitUserEnvironment no +PermitUserRC no +GatewayPorts no + +# === INFORMATION DISCLOSURE === + +# however, display a legal notice for each connection +Banner /etc/ssh/banner + +# don't print the bastion MOTD on connection +PrintMotd no + +# === CRYPTOGRAPHY === + +# enforce the use of ssh version 2 protocol, version 1 is disabled. +# all sshd_config options regarding protocol 1 are therefore omitted. +Protocol 2 + +# only use hostkeys with secure algorithms, and omit the ones using NIST curves +HostKey /etc/ssh/ssh_host_ed25519_key +HostKey /etc/ssh/ssh_host_rsa_key + +# list of allowed ciphers. +# chacha20-poly1305 is a modern cipher, considered very secure +# aes is still the standard, we prefer gcm cipher mode, but also +# allow ctr cipher mode for compatibility (ctr is still considered secure) +# we deny arcfour(rc4), 3des, blowfish and cast +Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr + +# list of allowed message authentication code algorithms. +# etm (encrypt-then-mac) are considered the more secure, we +# prefer umac (has been proven secure) then sha2. +# for older ssh client, fallback to the non-etm version of +# the algorithms. +# we deny md5 and sha1 +MACs umac-128-etm@openssh.com,umac-64-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128@openssh.com,umac-64@openssh.com,hmac-sha2-512,hmac-sha2-256 + +# List of allowed key exchange algorithms. +# we prefer curve25519-sha256 which is considered the most modern/secure, +# and still allow diffie hellman with group exchange using sha256 which is +# the most secure dh-based kex. +# we avoid algorithms based on the disputed NIST curves, and anything based +# on sha1. +KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256 + +# force rekey every 512M of data or 6 hours of connection, whichever comes first +RekeyLimit 512M 6h + +# === AUTHENTICATION === + +# we allow only public key authentication ... +PubkeyAuthentication yes +# ... not password +PasswordAuthentication no +# ... keyboard interactive (needed for MFA through PAM) +KbdInteractiveAuthentication yes +# ... not kerberos +KerberosAuthentication no +# ... challenge-response (needed for MFA through PAM) +ChallengeResponseAuthentication yes +# ... not host-based +HostbasedAuthentication no + +# just in case, we also explicitly deny empty passwords +PermitEmptyPasswords no + +# this needs to be set at "yes" to allow PAM keyboard-interactive authentication, +# which is not a security issue because the AuthenticationMethods below force the use of +# either publickey or publickey+keyboard-interactive, hence password-only login is never +# possible, for root or any other account for that matter +PermitRootLogin yes + +# === LOGIN === + +# disconnect after 30 seconds if user didn't log in successfully +LoginGraceTime 30 + +# not more than 1 session per network connection (connection sharing with ssh client's master/shared mode) +MaxSessions 1 + +# maximum concurrent unauth connections to the sshd daemon +MaxStartups 50:30:500 + +# accept LANG and LC_* vars (also includes LC_BASTION) +AcceptEnv LANG LC_* + +# === SYSTEM === + +# sshd log level at verbose in auth facility for auditing purposes +LogLevel VERBOSE +SyslogFacility AUTH + +# check sanity of user HOME dir before allowing user to login +StrictModes yes + +# never use dns (slows down connections) +UseDNS no + +# use PAM facility +UsePAM yes + +# === AuthenticationMethods vs potential root OTP vs potential user MFA === +# 2FA has been configured for root, so we force pubkey+PAM for it +#Match User root +# AuthenticationMethods publickey,keyboard-interactive:pam +# Unconditionally skip PAM auth for members of the bastion-nopam group +Match Group bastion-nopam + AuthenticationMethods publickey +# if in one of the mfa groups, use pam +Match Group mfa-totp-configd + AuthenticationMethods publickey,keyboard-interactive:pam +Match Group mfa-password-configd + AuthenticationMethods publickey,keyboard-interactive:pam +# by default, always ask the publickey (no PAM) +Match All + AuthenticationMethods publickey diff --git a/etc/sudoers.account.template.d/600-pamtester.freebsd.sudoers b/etc/sudoers.account.template.d/600-pamtester.freebsd.sudoers new file mode 100644 index 0000000..e493bbd --- /dev/null +++ b/etc/sudoers.account.template.d/600-pamtester.freebsd.sudoers @@ -0,0 +1,2 @@ +# under FreeBSD, non-root accounts can't read /etc/spwd.db and there's no helper for pam_unix.so to authenticate users +%ACCOUNT% ALL=(root) NOPASSWD:/usr/bin/env pamtester sshd %ACCOUNT% authenticate diff --git a/lib/perl/OVH/Bastion.pm b/lib/perl/OVH/Bastion.pm index 85d6696..1240fb2 100644 --- a/lib/perl/OVH/Bastion.pm +++ b/lib/perl/OVH/Bastion.pm @@ -920,4 +920,49 @@ sub build_ttyrec_cmdline { return R('OK', value => {saveFile => $saveFile, cmd => \@ttyrec}); } +sub do_pamtester { + my %params = @_; + my $sysself = $params{'sysself'}; + my $self = $params{'self'}; + my $fnret; + + if (!$sysself || !$self) { + return R('ERR_MISSING_PARAMETER', msg => "Missing mandatory arguments 'sysself' or 'self'"); + } + + # use system() instead of OVH::Bastion::execute() because we need it to grab the term + my $pamtries = 3; + while (1) { + my $pamsysret; + if (OVH::Bastion::is_freebsd()) { + $pamsysret = system('sudo', '-n', '-u', 'root', '--', '/usr/bin/env', 'pamtester', 'sshd', $sysself, 'authenticate'); + } + else { + $pamsysret = system('pamtester', 'sshd', $sysself, 'authenticate'); + } + if ($pamsysret < 0) { + return R('KO_MFA_FAILED', msg => "MFA is required for this host, but this bastion is missing the `pamtester' tool, aborting"); + } + elsif ($pamsysret != 0) { + if (--$pamtries <= 0) { + return R('KO_MFA_FAILED', msg => "Sorry, but Multi-Factor Authentication failed, I can't connect you to this host"); + } + next; + } + + # success, if we are configured to launch a external command on pamtester success, do it. + # see the bastion.conf.dist file for usage example. + my $MFAPostCommand = OVH::Bastion::config('MFAPostCommand')->value; + if (ref $MFAPostCommand eq 'ARRAY' && @$MFAPostCommand) { + s/%ACCOUNT%/$self/g for @$MFAPostCommand; + $fnret = OVH::Bastion::execute(cmd => $MFAPostCommand, must_succeed => 1); + if (!$fnret) { + warn_syslog("MFAPostCommand returned a non-zero value: " . $fnret->msg); + } + } + last; + } + return R('OK_MFA_SUCCESS'); +} + 1; diff --git a/lib/shell/functions.inc b/lib/shell/functions.inc index dc392be..436db66 100644 --- a/lib/shell/functions.inc +++ b/lib/shell/functions.inc @@ -67,6 +67,26 @@ if [ ! -e "$SSH_DIR" ]; then SSH_DIR=/etc/ssh fi +# set PAM_DIR +PAM_DIR=$ETC_DIR/pam.d +if [ ! -e "$PAM_DIR" ]; then + PAM_DIR=/etc/pam.d +fi + +# set PAM_SSHD +# under FreeBSD, both /usr/local/etc/pam.d and /etc/pam.d can exist +PAM_SSHD="/etc/pam.d/sshd" +if [ -e "/usr/local/etc/pam.d/sshd" ]; then + # shellcheck disable=SC2034 + PAM_SSHD="/usr/local/etc/pam.d/sshd" +fi + +# set CRON_DIR +CRON_DIR=$ETC_DIR/cron.d +if [ ! -e "$CRON_DIR" ]; then + CRON_DIR=/etc/cron.d +fi + action_doing() { printf '\r*** %b\n' "$*" @@ -108,6 +128,15 @@ sed_compat() fi } +md5sum_compat() +{ + if command -v gmd5sum >/dev/null; then + gmd5sum "$@"; return $? + else + md5sum "$@"; return $? + fi +} + useradd_compat() { local _user="$1" _uid="" _home="" _shell="" _gid="" _extra="" diff --git a/tests/functional/docker/target_role.sh b/tests/functional/docker/target_role.sh index 1cdf148..e81ed35 100755 --- a/tests/functional/docker/target_role.sh +++ b/tests/functional/docker/target_role.sh @@ -3,15 +3,18 @@ # This entrypoint is ONLY for instances to run functional tests on # DO NOT USE IN PROD (check docker/ under main dir for that) set -e -set -u basedir=$(readlink -f "$(dirname "$0")"/../../..) # shellcheck source=lib/shell/functions.inc . "$basedir"/lib/shell/functions.inc # do we have a key? -[ -n "$USER_PUBKEY_B64" ] && user_pubkey=$(base64 -d <<< "$USER_PUBKEY_B64") -[ -n "$ROOT_PUBKEY_B64" ] && root_pubkey=$(base64 -d <<< "$ROOT_PUBKEY_B64") +if [ -n "$USER_PUBKEY_B64" ]; then + user_pubkey=$(base64 -d <<< "$USER_PUBKEY_B64") +fi +if [ -n "$ROOT_PUBKEY_B64" ]; then + root_pubkey=$(base64 -d <<< "$ROOT_PUBKEY_B64") +fi if [ -z "$user_pubkey" ] ; then echo "Missing ENV user_pubkey (or USER_PUBKEY_B64), aborting" >&2 exit 1 @@ -37,7 +40,9 @@ echo "Port 226" >> /etc/ssh/sshd_config [ -d "$UID0HOME/.ssh" ] || mkdir "$UID0HOME/.ssh" echo "$root_pubkey" >> "$UID0HOME/.ssh/authorized_keys" # also unlock the root account, which can sometimes prevent us connecting through SSH (CentOS 8) -usermod -U "$UID0" +if [ "$OS_FAMILY" = Linux ]; then + usermod -U "$UID0" +fi HOME="$UID0HOME" USER="$UID0" "$basedir"/bin/plugin/restricted/accountCreate '' '' '' '' --uid 5000 --account "$TARGET_USER" --public-key "$user_pubkey FOR_TESTS_ONLY" HOME="$UID0HOME" USER="$UID0" "$basedir"/bin/plugin/restricted/accountGrantCommand '' '' '' '' --account "$TARGET_USER" --command accountGrantCommand @@ -51,7 +56,7 @@ cat /home/"$TARGET_USER"/.ssh/id_*.pub > ~test-shell_/.ssh/authorized_keys add_user_to_group_compat test-shell_ bastion-nopam # install a fake ttyrec just so that our connection tests work -if [ ! -e /usr/bin/ttyrec ] ; then +if ! command -v ttyrec >/dev/null; then "$basedir"/bin/admin/install --nothing --no-wait --install-fake-ttyrec fi @@ -70,7 +75,7 @@ fi # now OS-specific things -if [ "$(uname -s)" = Linux ] ; then +if [ "$OS_FAMILY" = Linux ] ; then test -x /etc/init.d/ssh && /etc/init.d/ssh start test -x /etc/init.d/syslog-ng && /etc/init.d/syslog-ng start @@ -89,7 +94,7 @@ if [ "$(uname -s)" = Linux ] ; then /usr/sbin/syslog-ng fi -elif [ "$(uname -s)" = OpenBSD ] || [ "$(uname -s)" = FreeBSD ] || [ "$(uname -s)" = NetBSD ] ; then +elif [ "$OS_FAMILY" = OpenBSD ] || [ "$OS_FAMILY" = FreeBSD ] || [ "$OS_FAMILY" = NetBSD ] ; then # setup some 127.0.0.x IPs (needed for our tests) # this automatically works under Linux on lo @@ -110,6 +115,10 @@ elif [ "$(uname -s)" = OpenBSD ] || [ "$(uname -s)" = FreeBSD ] || [ "$(uname - set -e fi +if [ -n "$NO_SLEEP" ]; then + exit 0 +fi + echo "Now sleeping forever (docker mode)" while : ; do sleep 3600 diff --git a/tests/functional/launch_tests_on_instance.sh b/tests/functional/launch_tests_on_instance.sh index 0ce7f5a..02cec8c 100755 --- a/tests/functional/launch_tests_on_instance.sh +++ b/tests/functional/launch_tests_on_instance.sh @@ -15,16 +15,19 @@ account0="$3" user_ssh_key_path="$4" root_ssh_key_path="$5" osh_etc="$6" +remote_basedir="$7" [ -n "$osh_etc" ] || osh_etc=/etc/bastion +[ -n "$remote_basedir" ] || remote_basedir="$basedir" -[ -z "$HAS_ED25519" ] && HAS_ED25519=1 -[ -z "$HAS_BLACKLIST" ] && HAS_BLACKLIST=0 -[ -z "$HAS_MFA" ] && HAS_MFA=1 -[ -z "$HAS_PAMTESTER" ] && HAS_PAMTESTER=1 -[ -z "$nocc" ] && nocc=0 -[ -z "$nowait" ] && nowait=0 -[ -z "$TARGET" ] && TARGET='' -[ -z "$TEST_SCRIPT" ] && TEST_SCRIPT='' +[ -z "$HAS_ED25519" ] && HAS_ED25519=1 +[ -z "$HAS_BLACKLIST" ] && HAS_BLACKLIST=0 +[ -z "$HAS_MFA" ] && HAS_MFA=1 +[ -z "$HAS_MFA_PASSWORD" ] && HAS_MFA_PASSWORD=0 +[ -z "$HAS_PAMTESTER" ] && HAS_PAMTESTER=1 +[ -z "$nocc" ] && nocc=0 +[ -z "$nowait" ] && nowait=0 +[ -z "$TARGET" ] && TARGET='' +[ -z "$TEST_SCRIPT" ] && TEST_SCRIPT='' # die if using an unset var set -u @@ -103,7 +106,7 @@ cat >"$mytmpdir/ssh_config" <>"$mytmpdir/ssh_config" < "$tmpscript" echo "$*" >> "$tmpscript" chmod 755 "$tmpscript" @@ -399,7 +402,7 @@ runtests() COUNTONLY=0 echo === running unit tests === -if ! $r0 perl "$basedir/tests/unit/run.pl"; then +if ! $r0 perl "$remote_basedir/tests/unit/run.pl"; then printf "%b%b%b\\n" "$WHITE_ON_RED" "Unit tests failed :(" "$NOC" exit 1 fi diff --git a/tests/functional/tests.d/350-groups.sh b/tests/functional/tests.d/350-groups.sh index 190448b..d30bfe7 100644 --- a/tests/functional/tests.d/350-groups.sh +++ b/tests/functional/tests.d/350-groups.sh @@ -690,11 +690,11 @@ EOS success selfListAccesses a3_list_own_accesses $a3 --osh selfListAccesses json .command selfListAccesses .error_code OK - contain REGEX '77\.66\.55\.0/24\s+\(any\)\s+\(any\)\s+personal\s+'$account0'\s' - contain REGEX '1\.2\.3\.4\s+\(any\)\s+\(any\)\s+personal\s+'$account0'\s' - contain REGEX '77\.66\.55\.4\s+\(any\)\s+\(any\)\s+personal\s+'$account0'\s' - contain REGEX '127\.0\.0\.1\s+22\s+g1\s+'$group1'\(group-guest\)\s+'$account2'\s' - contain REGEX '10\.20\.0\.0/17\s+\(any\)\s+\(any\)\s+'$group3'\(group-member\)\s+'$account3'\s' + contain REGEX '77\.66\.55\.0/24[[:space:]]+\(any\)[[:space:]]+\(any\)[[:space:]]+personal[[:space:]]+'$account0'[[:space:]]' + contain REGEX '1\.2\.3\.4[[:space:]]+\(any\)[[:space:]]+\(any\)[[:space:]]+personal[[:space:]]+'$account0'[[:space:]]' + contain REGEX '77\.66\.55\.4[[:space:]]+\(any\)[[:space:]]+\(any\)[[:space:]]+personal[[:space:]]+'$account0'[[:space:]]' + contain REGEX '127\.0\.0\.1[[:space:]]+22[[:space:]]+g1[[:space:]]+'$group1'\(group-guest\)[[:space:]]+'$account2'[[:space:]]' + contain REGEX '10\.20\.0\.0/17[[:space:]]+\(any\)[[:space:]]+\(any\)[[:space:]]+'$group3'\(group-member\)[[:space:]]+'$account3'[[:space:]]' contain "5 accesses listed" run accountDelete notingroup $a1 --osh accountDelete --account $account2 @@ -826,11 +826,11 @@ EOS # group1: a1(owner,aclkeeper,gatekeeper,member) a2() servers(127.0.0.10,127.0.0.11,127.0.0.12-TTL) success groupListServers list $a1 --osh groupListServers --group $group1 json .command groupListServers .error_code OK - contain REGEX '127\.0\.0\.1\s+22\s+g1\s+'$group1'\(group\)\s+'$account2'\s' - contain REGEX '127\.0\.0\.2\s+22\s+g2\s+'$group1'\(group\)\s+'$account2'\s' - contain REGEX '127\.0\.0\.10\s+\(any\)\s+\(any\)\s+'$group1'\(group\)\s+'$account1'\s' - contain REGEX '127\.0\.0\.11\s+\(any\)\s+\(any\)\s+'$group1'\(group\)\s+'$account1'\s' - contain REGEX '127\.0\.0\.12\s+\(any\)\s+\(any\)\s+'$group1'\(group\)\s+'$account1'\s+\S+\s+00:00:[01][0123456789]' + contain REGEX '127\.0\.0\.1[[:space:]]+22[[:space:]]+g1[[:space:]]+'$group1'\(group\)[[:space:]]+'$account2'[[:space:]]' + contain REGEX '127\.0\.0\.2[[:space:]]+22[[:space:]]+g2[[:space:]]+'$group1'\(group\)[[:space:]]+'$account2'[[:space:]]' + contain REGEX '127\.0\.0\.10[[:space:]]+\(any\)[[:space:]]+\(any\)[[:space:]]+'$group1'\(group\)[[:space:]]+'$account1'[[:space:]]' + contain REGEX '127\.0\.0\.11[[:space:]]+\(any\)[[:space:]]+\(any\)[[:space:]]+'$group1'\(group\)[[:space:]]+'$account1'[[:space:]]' + contain REGEX '127\.0\.0\.12[[:space:]]+\(any\)[[:space:]]+\(any\)[[:space:]]+'$group1'\(group\)[[:space:]]+'$account1'[[:space:]]+\S+[[:space:]]+00:00:[01][0123456789]' contain '5 accesses listed' # wait for the access to expire @@ -839,11 +839,11 @@ EOS # group1: a1(owner,aclkeeper,gatekeeper,member) a2() servers(127.0.0.10,127.0.0.11) success groupListServers listttlexpired $a1 --osh groupListServers --group $group1 json .command groupListServers .error_code OK - contain REGEX '127\.0\.0\.1\s+22\s+g1\s+'$group1'\(group\)\s+'$account2'\s' - contain REGEX '127\.0\.0\.2\s+22\s+g2\s+'$group1'\(group\)\s+'$account2'\s' - contain REGEX '127\.0\.0\.10\s+\(any\)\s+\(any\)\s+'$group1'\(group\)\s+'$account1'\s' - contain REGEX '127\.0\.0\.11\s+\(any\)\s+\(any\)\s+'$group1'\(group\)\s+'$account1'\s' - nocontain REGEX '127\.0\.0\.12\s+\(any\)\s+\(any\)\s+'$group1'\(group\)\s+'$account1'\s' + contain REGEX '127\.0\.0\.1[[:space:]]+22[[:space:]]+g1[[:space:]]+'$group1'\(group\)[[:space:]]+'$account2'[[:space:]]' + contain REGEX '127\.0\.0\.2[[:space:]]+22[[:space:]]+g2[[:space:]]+'$group1'\(group\)[[:space:]]+'$account2'[[:space:]]' + contain REGEX '127\.0\.0\.10[[:space:]]+\(any\)[[:space:]]+\(any\)[[:space:]]+'$group1'\(group\)[[:space:]]+'$account1'[[:space:]]' + contain REGEX '127\.0\.0\.11[[:space:]]+\(any\)[[:space:]]+\(any\)[[:space:]]+'$group1'\(group\)[[:space:]]+'$account1'[[:space:]]' + nocontain REGEX '127\.0\.0\.12[[:space:]]+\(any\)[[:space:]]+\(any\)[[:space:]]+'$group1'\(group\)[[:space:]]+'$account1'[[:space:]]' contain '4 accesses listed' # group1: a1(owner,aclkeeper,gatekeeper,member) a2() servers(127.0.0.10,127.0.0.11) @@ -883,6 +883,9 @@ EOS success groupModify guest_ttl_limit $a1 --osh groupModify --group $group1 --guest-ttl-limit 0 json .command groupModify .error_code OK + # if we're just counting the number of tests, don't sleep + [ "$COUNTONLY" != 1 ] && sleep 1 + # group1: a1(owner,aclkeeper,gatekeeper,member) a2() servers(127.0.0.10,127.0.0.11) success groupAddGuestAccess works $a1 --osh groupAddGuestAccess --group $group1 --account $account2 --port-any --user-any --host 127.0.0.10 contain "has now access" diff --git a/tests/functional/tests.d/370-mfa.sh b/tests/functional/tests.d/370-mfa.sh index 6ae8f49..4d01eff 100644 --- a/tests/functional/tests.d/370-mfa.sh +++ b/tests/functional/tests.d/370-mfa.sh @@ -37,9 +37,9 @@ testsuite_mfa() a4_password=']BkL>3x#T)g~~B#rLv^!T2&N' script mfa a4_setup_pass_step2of2 "echo 'set timeout 30; \ spawn $a4 --osh selfMFASetupPassword --yes; \ - expect \":\" { send \"$a4_password_tmp\\n\"; }; \ - expect \":\" { send \"$a4_password\\n\"; }; \ - expect \":\" { send \"$a4_password\\n\"; }; \ + expect \":\" { sleep 0.2; send \"$a4_password_tmp\\n\"; }; \ + expect \":\" { sleep 0.2; send \"$a4_password\\n\"; }; \ + expect \":\" { sleep 0.2; send \"$a4_password\\n\"; }; \ expect eof; \ lassign [wait] pid spawnid value value; \ exit \$value' | expect -f -" @@ -52,17 +52,17 @@ testsuite_mfa() # now try to connect after we have a pass run mfa a4_connect_after_pass $a4f --osh groupList - if [ "$HAS_MFA" = 1 ]; then + if [ "$HAS_MFA" = 1 ] || [ "$HAS_MFA_PASSWORD" = 1 ]; then # now we need a password, we don't enter it so it'll timeout (124) retvalshouldbe 124 contain 'Multi-Factor Authentication enabled, an additional authentication factor is required (password).' - contain 'Password:' + contain REGEX 'Password:|Password for' nocontain 'JSON_OUTPUT' else # our system doesn't support MFA so it still works without asking for a password retvalshouldbe 0 nocontain 'Multi-Factor Authentication enabled' - nocontain 'Password:' + nocontain REGEX 'Password:|Password for' json .command groupList .error_code OK_EMPTY fi @@ -76,66 +76,71 @@ testsuite_mfa() # setup group to force JIT egress MFA script mfa a4_modify_g3_egress_mfa "echo 'set timeout 30; \ spawn $a4 --osh groupModify --group $group3 --mfa-required any; \ - expect \":\" { send \"$a4_password\\n\"; }; \ + expect \":\" { sleep 0.2; send \"$a4_password\\n\"; }; \ expect eof; \ lassign [wait] pid spawnid value value; \ exit \$value' | expect -f -" retvalshouldbe 0 contain 'Multi-Factor Authentication enabled, an additional authentication factor is required (password).' - contain 'Password:' + contain REGEX 'Password:|Password for' json .command groupModify .error_code OK # check that the MFA is set for the group script mfa a4_verify_g3_egress_mfa "echo 'set timeout 30; \ spawn $a4 --osh groupInfo --group $group3; \ - expect \":\" { send \"$a4_password\\n\"; }; \ + expect \":\" { sleep 0.2; send \"$a4_password\\n\"; }; \ expect eof; \ lassign [wait] pid spawnid value value; \ exit \$value' | expect -f -" retvalshouldbe 0 contain 'Multi-Factor Authentication enabled, an additional authentication factor is required (password).' - contain 'Password:' + contain REGEX 'Password:|Password for' json .command groupInfo .error_code OK json .value.mfa_required any # add 127.7.7.7 to this group script mfa a4_add_g3_server "echo 'set timeout 30; \ spawn $a4 --osh groupAddServer --group $group3 --host 127.7.7.7 --user-any --port-any --force; \ - expect \":\" { send \"$a4_password\\n\"; }; \ + expect \":\" { sleep 0.2; send \"$a4_password\\n\"; }; \ expect eof; \ lassign [wait] pid spawnid value value; \ exit \$value' | expect -f -" retvalshouldbe 0 contain 'Multi-Factor Authentication enabled, an additional authentication factor is required (password).' - contain 'Password:' + contain REGEX 'Password:|Password for' # connect to 127.7.7.7 with MFA JIT, bad password script mfa a4_connect_g3_server_badpass "echo 'set timeout 45; \ spawn $a4 root@127.7.7.7; \ - expect \"word:\" { send \"$a4_password\\n\"; }; \ - expect \"word:\" { send \"BADPASSWORD\\n\"; }; \ - expect \"word:\" { send \"BADPASSWORD\\n\"; }; \ - expect \"word:\" { send \"BADPASSWORD\\n\\n\"; }; \ + expect \"is required (password)\" { sleep 0.1; }; \ + expect \":\" { sleep 0.2; send \"$a4_password\\n\"; }; \ + expect \"is required (password)\" { sleep 0.1; }; \ + expect \":\" { sleep 0.2; send \"BADPASSWORD\\n\"; }; \ + expect \"is required (password)\" { sleep 0.1; }; \ + expect \":\" { sleep 0.2; send \"BADPASSWORD\\n\"; }; \ + expect \"is required (password)\" { sleep 0.1; }; \ + expect \":\" { sleep 0.2; send \"BADPASSWORD\\n\\n\"; }; \ expect eof; \ lassign [wait] pid spawnid value value; \ exit \$value' | expect -f -" retvalshouldbe 125 contain 'Multi-Factor Authentication enabled, an additional authentication factor is required (password).' - contain 'Password:' + contain REGEX 'Password:|Password for' contain 'pamtester: ' nocontain 'Permission denied' # connect to 127.7.7.7 with MFA JIT, good password script mfa a4_connect_g3_server_goodpass "echo 'set timeout 30; \ spawn $a4 root@127.7.7.7; \ - expect \"word:\" { send \"$a4_password\\n\"; }; \ - expect \"word:\" { send \"$a4_password\\n\"; }; \ + expect \":\" { sleep 0.2; send \"$a4_password\\n\"; }; \ + expect \"is required (password)\" { sleep 0.1; }; \ + expect \":\" { sleep 0.2; send \"$a4_password\\n\"; }; \ expect eof; \ lassign [wait] pid spawnid value value; \ exit \$value' | expect -f -" retvalshouldbe 255 contain 'Multi-Factor Authentication enabled, an additional authentication factor is required (password).' - contain 'Password:' + contain REGEX 'Password:|Password for' contain 'pamtester: successfully authenticated' contain 'Permission denied' @@ -150,7 +155,7 @@ testsuite_mfa() # add to JIT MFA group script mfa a0_add_a3_as_member "echo 'set timeout 30; \ spawn $a4 --osh groupAddMember --group $group3 --account $account3; \ - expect \"word:\" { send \"$a4_password\\n\"; }; \ + expect \":\" { sleep 0.2; send \"$a4_password\\n\"; }; \ expect eof; \ lassign [wait] pid spawnid value value; \ exit \$value' | expect -f -" @@ -187,25 +192,25 @@ testsuite_mfa() # change our password a4_password_new="rkw=*Ffyqs23" - if [ "$HAS_MFA" = 1 ]; then + if [ "$HAS_MFA" = 1 ] || [ "$HAS_MFA_PASSWORD" = 1 ]; then script mfa a4_change_pass "echo 'set timeout 30; \ spawn $a4 --osh selfMFASetupPassword --yes; \ - expect \":\" { send \"$a4_password\\n\"; }; \ - expect \":\" { send \"$a4_password\\n\"; }; \ - expect \":\" { send \"$a4_password_new\\n\"; }; \ - expect \":\" { send \"$a4_password_new\\n\"; }; \ + expect \":\" { sleep 0.2; send \"$a4_password\\n\"; }; \ + expect \":\" { sleep 0.2; send \"$a4_password\\n\"; }; \ + expect \":\" { sleep 0.2; send \"$a4_password_new\\n\"; }; \ + expect \":\" { sleep 0.2; send \"$a4_password_new\\n\"; }; \ expect eof; \ lassign [wait] pid spawnid value value; \ exit \$value' | expect -f -" retvalshouldbe 0 contain 'Multi-Factor Authentication enabled, an additional authentication factor is required (password).' - contain 'Password:' + contain REGEX 'Password:|Password for' else script mfa a4_change_pass "echo 'set timeout 30; \ spawn $a4 --osh selfMFASetupPassword --yes; \ - expect \":\" { send \"$a4_password\\n\"; }; \ - expect \":\" { send \"$a4_password_new\\n\"; }; \ - expect \":\" { send \"$a4_password_new\\n\"; }; \ + expect \":\" { sleep 0.2; send \"$a4_password\\n\"; }; \ + expect \":\" { sleep 0.2; send \"$a4_password_new\\n\"; }; \ + expect \":\" { sleep 0.2; send \"$a4_password_new\\n\"; }; \ expect eof; \ lassign [wait] pid spawnid value value; \ exit \$value' | expect -f -" @@ -219,16 +224,16 @@ testsuite_mfa() a4_password="$a4_password_new" unset a4_password_new - if [ "$HAS_MFA" = 1 ]; then + if [ "$HAS_MFA" = 1 ] || [ "$HAS_MFA_PASSWORD" = 1 ]; then script mfa a4_connect_with_pass "echo 'set timeout 30; \ spawn $a4 --osh groupList; \ - expect \":\" { send \"$a4_password\\n\"; }; \ + expect \":\" { sleep 0.2; send \"$a4_password\\n\"; }; \ expect eof; \ lassign [wait] pid spawnid value value; \ exit \$value' | expect -f -" retvalshouldbe 0 contain 'Multi-Factor Authentication enabled, an additional authentication factor is required (password).' - contain 'Password:' + contain REGEX 'Password:|Password for' json .command groupList .error_code OK_EMPTY fi @@ -241,10 +246,10 @@ testsuite_mfa() json .error_code OK .command accountModify .value.mfa_totp_required.error_code OK_NO_CHANGE # now try to connect with account4 - if [ "$HAS_MFA" = 1 ]; then + if [ "$HAS_MFA" = 1 ] || [ "$HAS_MFA_PASSWORD" = 1 ]; then script mfa a4_connect_with_totpreq "echo 'set timeout 30; \ spawn $a4 --osh groupList; \ - expect \":\" { send \"$a4_password\\n\"; }; \ + expect \":\" { sleep 0.2; send \"$a4_password\\n\"; }; \ expect eof; \ lassign [wait] pid spawnid value value; \ exit \$value' | expect -f -" @@ -258,14 +263,14 @@ testsuite_mfa() # setup totp script mfa a4_setup_totp "echo 'set timeout 30; \ spawn $a4 --osh selfMFASetupTOTP --no-confirm; \ - expect \"word:\" { send \"$a4_password\\n\"; }; \ - expect \"word:\" { send \"$a4_password\\n\"; }; \ + expect \"word:\" { sleep 0.2; send \"$a4_password\\n\"; }; \ + expect \"word:\" { sleep 0.2; send \"$a4_password\\n\"; }; \ expect eof; \ lassign [wait] pid spawnid value value; \ exit \$value' | expect -f -" retvalshouldbe 0 contain 'Multi-Factor Authentication enabled, an additional authentication factor is required (password).' - contain 'Password:' + contain REGEX 'Password:|Password for' a4_totp_code_1=$(get_stdout | grep -A1 'Your emergency scratch codes are:' | tail -n1 | tr -d '[:space:]') #a4_totp_code_2=$(get_stdout | grep -A2 'Your emergency scratch codes are:' | tail -n1 | tr -d '[:space:]') @@ -275,7 +280,7 @@ testsuite_mfa() # login and fail without totp (timeout) script mfa a4_connect_after_totp_fail "echo 'set timeout 30; \ spawn $a4 --osh groupList; \ - expect \"word:\" { send \"$a4_password\\n\"; }; \ + expect \"word:\" { sleep 0.2; send \"$a4_password\\n\"; }; \ expect eof; \ lassign [wait] pid spawnid value value; \ exit \$value' | expect -f -" @@ -284,30 +289,30 @@ testsuite_mfa() contain 'Multi-Factor Authentication enabled, an additional authentication factor is required (OTP).' contain 'Your password expires on' contain 'in 14 days' - contain 'Password:' + contain REGEX 'Password:|Password for' contain 'Verification code:' nocontain 'JSON_OUTPUT' # success with password + totp script mfa a4_connect_after_totp_ok "echo 'set timeout 30; \ spawn $a4 --osh groupList; \ - expect \"word:\" { send \"$a4_password\\n\"; }; \ - expect \"code:\" { send \"$a4_totp_code_1\\n\"; }; \ + expect \"word:\" { sleep 0.2; send \"$a4_password\\n\"; }; \ + expect \"code:\" { sleep 0.2; send \"$a4_totp_code_1\\n\"; }; \ expect eof; \ lassign [wait] pid spawnid value value; \ exit \$value' | expect -f -" retvalshouldbe 0 contain 'Multi-Factor Authentication enabled, an additional authentication factor is required (password).' contain 'Multi-Factor Authentication enabled, an additional authentication factor is required (OTP).' - contain 'Password:' + contain REGEX 'Password:|Password for' contain 'Verification code:' json .command groupList .error_code OK_EMPTY # totp scratch codes don't work twice script mfa a4_connect_after_totp_dupe "echo 'set timeout 30; \ spawn $a4 --osh groupList; \ - expect \"word:\" { send \"$a4_password\\n\"; }; \ - expect \"code:\" { send \"$a4_totp_code_1\\n\"; }; \ + expect \"word:\" { sleep 0.2; send \"$a4_password\\n\"; }; \ + expect \"code:\" { sleep 0.2; send \"$a4_totp_code_1\\n\"; }; \ expect \"word:\" { exit 222; }; \ expect eof; \ lassign [wait] pid spawnid value value; \ @@ -315,7 +320,7 @@ testsuite_mfa() retvalshouldbe 222 contain 'Multi-Factor Authentication enabled, an additional authentication factor is required (password).' contain 'Multi-Factor Authentication enabled, an additional authentication factor is required (OTP).' - contain 'Password:' + contain REGEX 'Password:|Password for' contain 'Verification code:' nocontain 'JSON_OUTPUT'