Merge pull request #48 from speed47/freebsd

feat: add FreeBSD in the automated tests
This commit is contained in:
Stéphane Lesimple 2020-11-17 20:11:58 +01:00 committed by GitHub
commit e1d7ef9d26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 618 additions and 234 deletions

37
.github/workflows/freebsd.yml vendored Normal file
View file

@ -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

View file

@ -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

View file

@ -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'}) {

View file

@ -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"

View file

@ -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

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -3,5 +3,6 @@
"selfMFASetupPassword" , {"pr" : ["<enter>"]}
],
"master_only": true,
"execution_mode_on_freebsd": "system",
"terminal_mode": "noecho"
}

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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"

13
etc/pam.d/sshd.freebsd Normal file
View file

@ -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

114
etc/ssh/ssh_config.freebsd Normal file
View file

@ -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

136
etc/ssh/sshd_config.freebsd Normal file
View file

@ -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

View file

@ -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

View file

@ -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;

View file

@ -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=""

View file

@ -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

View file

@ -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" <<EOF
PasswordAuthentication no
RequestTTY yes
EOF
if [ "$HAS_MFA" = 1 ]; then
if [ "$HAS_MFA" = 1 ] || [ "$HAS_MFA_PASSWORD" = 1 ]; then
cat >>"$mytmpdir/ssh_config" <<EOF
ChallengeResponseAuthentication yes
KbdInteractiveAuthentication yes
@ -230,7 +233,7 @@ script() {
return
fi
tmpscript=$(mktemp -p $outdir)
tmpscript=$(mktemp)
echo "#! /usr/bin/env bash" > "$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

View file

@ -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"

View file

@ -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'