From 5dbf2c1983df3b1bc003542ff24f7b12678dfcc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lesimple?= Date: Mon, 8 Jun 2026 15:31:26 +0000 Subject: [PATCH 01/12] enh: tests: replace 'screen' by 'script' for a small run speedup --- docker/Dockerfile.tester | 2 +- tests/functional/launch_tests_on_instance.sh | 35 ++++++-------------- tests/functional/tests.d/390-mfa-realm.sh | 2 +- 3 files changed, 13 insertions(+), 26 deletions(-) diff --git a/docker/Dockerfile.tester b/docker/Dockerfile.tester index a35b48d..a7a1c80 100644 --- a/docker/Dockerfile.tester +++ b/docker/Dockerfile.tester @@ -2,7 +2,7 @@ FROM debian:bookworm LABEL maintainer="stephane.lesimple+bastion@ovhcloud.com" # install prerequisites -RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y jq netcat-traditional openssh-client procps bsdutils screen expect shellcheck libperl-critic-perl fping curl rsync +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y jq netcat-traditional openssh-client procps bsdutils screen expect shellcheck libperl-critic-perl fping curl rsync moreutils # add our code COPY . /opt/bastion diff --git a/tests/functional/launch_tests_on_instance.sh b/tests/functional/launch_tests_on_instance.sh index 9ca3b71..b0238b9 100755 --- a/tests/functional/launch_tests_on_instance.sh +++ b/tests/functional/launch_tests_on_instance.sh @@ -369,13 +369,6 @@ outdir="$mytmpdir/out" mkdir -p $outdir || exit 1 touch "$outdir/.basename" -# checking which screen syntax works on this OS -screen="screen -L" -if screen -h 2>&1 | grep -q -- -Logfile; then - screen="screen -L -Logfile" -fi -# /checking - testno=0 testcount=0 basename="" @@ -459,8 +452,12 @@ run() # put an invalid value in this file, should be overwritten. we also use it as a lock file. echo -1 > $outdir/$basename.retval # run the test - flock "$outdir/$basename.retval" $screen "$outdir/$basename.log" -D -m -fn -ln bash -c "set -f; $* ; echo \$? > $outdir/$basename.retval ; sleep $sleepafter" - flock "$outdir/$basename.retval" true + if [ "$OS_FAMILY" = FreeBSD ]; then + command script -q "$outdir/$basename.log" bash -c "set -f; $*; echo \$? > $outdir/$basename.retval ; sleep $sleepafter" >/dev/null 2>&1 || true + else + command script -q -c "set -f; $* ; echo \$? > $outdir/$basename.retval ; sleep $sleepafter" "$outdir/$basename.log" >/dev/null 2>&1 || true + grep -Eva '^Script (started|done) on ' "$outdir/$basename.log" | sponge "$outdir/$basename.log" || true + fi unset sleepafter # look for generally bad strings in the output @@ -476,21 +473,11 @@ run() if [ "$opt_consistency_check" = 1 ]; then # sleep 1s if sshd has been reloaded [ "$case" = "sshd_reload" ] && sleep 1 - flock "$outdir/$basename.retval" $screen "$outdir/$basename.cc" -D -m -fn -ln $r0 ' - /opt/bastion/bin/admin/check-consistency.pl ; echo _RETVAL_CC=$?= ; - grep -Fw -e warn -e die -e code-warning /var/log/bastion/bastion.log - | grep -Fv - -e "'"${code_warn_exclude:-__none__}"'" - -e "System does not support IPv6" - -e "Defaulting to E[UG]ID" - -e "starting!" - -e "Binding to SSL port" - | sed "s/^/_SYSLOG=/" ; - : > /var/log/bastion/bastion.log - ' - flock "$outdir/$basename.retval" true - ccret=$( grep _RETVAL_CC= "$outdir/$basename.cc" | cut -d= -f2) - syslogline=$(grep _SYSLOG= "$outdir/$basename.cc" | cut -d= -f2-) + if ! $r0 bash -c '/opt/bastion/bin/admin/check-consistency.pl ; echo _RETVAL_CC=$?= ; grep -Fw -e warn -e die -e code-warning /var/log/bastion/bastion.log | sed s/^/_SYSLOG=/ ; : > /var/log/bastion/bastion.log' >"$outdir/$basename.cc" 2>&1; then + fail "CONSISTENCY CHECK CALL FAILED" + fi + ccret=$( grep _RETVAL_CC= "$outdir/$basename.cc" | cut -d= -f2 || true) + syslogline=$(grep _SYSLOG= "$outdir/$basename.cc" | cut -d= -f2- | grep -Fv -e "${code_warn_exclude:-__none__}" -e "System does not support IPv6" -e "Defaulting to EUID" -e "Defaulting to EGID" -e "starting!" -e "Binding to SSL port" || true) if [ "$ccret" != 0 ]; then nbfailedcon=$(( nbfailedcon + 1 )) fail "CONSISTENCY CHECK" diff --git a/tests/functional/tests.d/390-mfa-realm.sh b/tests/functional/tests.d/390-mfa-realm.sh index 2ebeb66..15ee193 100644 --- a/tests/functional/tests.d/390-mfa-realm.sh +++ b/tests/functional/tests.d/390-mfa-realm.sh @@ -107,7 +107,7 @@ testsuite_mfa_realm() contain "Permission denied (publickey)" # cleanup - success realmDelete $a0 --osh realmDelete --realm $realm_shared_account "<<< \"Yes, do as I say and delete $realm_shared_account, kthxbye\"" + script realmDelete $a0 --osh realmDelete --realm $realm_shared_account "<<< \"Yes, do as I say and delete $realm_shared_account, kthxbye\"" script a0_delete_a4 $a0 --osh accountDelete --account $account4 "<<< \"Yes, do as I say and delete $account4, kthxbye\"" retvalshouldbe 0 From 6a77244c84f07d1c75e959e712b8a9ec887a0e62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lesimple?= Date: Mon, 8 Jun 2026 15:32:11 +0000 Subject: [PATCH 02/12] chore: tests: bump tester from bookworm to trixie --- docker/Dockerfile.tester | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile.tester b/docker/Dockerfile.tester index a7a1c80..ec6b354 100644 --- a/docker/Dockerfile.tester +++ b/docker/Dockerfile.tester @@ -1,4 +1,4 @@ -FROM debian:bookworm +FROM debian:trixie LABEL maintainer="stephane.lesimple+bastion@ovhcloud.com" # install prerequisites From 5b2a14916e9113becdc81c31523f7ad27993d0e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lesimple?= Date: Mon, 8 Jun 2026 15:32:47 +0000 Subject: [PATCH 03/12] chore: target_role.sh: replace 1h loop by sleep infinity --- tests/functional/docker/target_role.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/functional/docker/target_role.sh b/tests/functional/docker/target_role.sh index cb16e84..86ff93c 100755 --- a/tests/functional/docker/target_role.sh +++ b/tests/functional/docker/target_role.sh @@ -186,7 +186,5 @@ if [ "$WANT_HTTP_PROXY" = 1 ]; then done else echo "Now sleeping forever (docker mode)" - while : ; do - sleep 3600 - done + sleep infinity fi From 9e9d514f0089997a88a5d08f752b67583b116ab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lesimple?= Date: Mon, 8 Jun 2026 15:36:14 +0000 Subject: [PATCH 04/12] chore: 900-strict-checking.sh: remove stray debug line --- tests/functional/tests.d/900-strict-checking.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/functional/tests.d/900-strict-checking.sh b/tests/functional/tests.d/900-strict-checking.sh index 2db14c1..83c4c35 100644 --- a/tests/functional/tests.d/900-strict-checking.sh +++ b/tests/functional/tests.d/900-strict-checking.sh @@ -25,7 +25,6 @@ testsuite_strict_checking() # change the remote hostkeys, and get the proper sshd PID so that we can force it to reload success change_host_keys $r0 "\"find /etc/ssh/ -type f -name 'ssh_host_*key*' -print -delete; ssh-keygen -A; ps faxu; printf %s SSHD_PIDS=; ps ax -o pid,args | grep -E '^ *[0-9]+ +(sshd: .+listener|/usr/sbin/sshd)' | awk '{print \\\$1}' | tr '\\\n' ' '\"" contain 'generating new host keys' - get_stdout local sshd_pids sshd_pids=$(get_stdout | grep SSHD_PIDS= | cut -d= -f2-) From 7017f4a5cf4abe1f9daa094be90bf5009863da9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lesimple?= Date: Mon, 8 Jun 2026 15:43:17 +0000 Subject: [PATCH 05/12] enh: functional tests: use 'waitfor' that depends on default_timeout when sleeping is needed in tests Also optimize 325-accountinfo.sh tests ordering to lower the amount of time we wait for the TTL to expire --- tests/functional/launch_tests_on_instance.sh | 13 +++++++ tests/functional/tests.d/325-accountinfo.sh | 39 +++++++++---------- .../341-selfaccesses-force-password.sh | 2 +- .../tests.d/346-testagentforward.sh | 2 +- tests/functional/tests.d/350-groups.sh | 18 ++++----- tests/functional/tests.d/370-mfa.sh | 6 +-- tests/functional/tests.d/400-piv.sh | 7 +--- tests/functional/tests.d/500-http-proxy.sh | 10 +---- 8 files changed, 47 insertions(+), 50 deletions(-) diff --git a/tests/functional/launch_tests_on_instance.sh b/tests/functional/launch_tests_on_instance.sh index b0238b9..4640ca2 100755 --- a/tests/functional/launch_tests_on_instance.sh +++ b/tests/functional/launch_tests_on_instance.sh @@ -403,6 +403,19 @@ prefix() fi } +infomsg() +{ + [ "$COUNTONLY" = 1 ] && return + printf '%b %b%b%b\n' "$(prefix)" "$DARKGRAY" "$*" "$NOC" +} + +waitfor() +{ + [ "$COUNTONLY" = 1 ] && return + infomsg "sleeping for $1 seconds ${2:-...}" + sleep $1 +} + run() { # display verbose output about the previous test if it was bad diff --git a/tests/functional/tests.d/325-accountinfo.sh b/tests/functional/tests.d/325-accountinfo.sh index fe1dfbb..4c051ac 100644 --- a/tests/functional/tests.d/325-accountinfo.sh +++ b/tests/functional/tests.d/325-accountinfo.sh @@ -16,23 +16,35 @@ testsuite_accountinfo() json .error_code OK .command accountCreate .value null # create a third account with a ttl - local ttl_account_seconds - ttl_account_seconds=55 + local ttl_account_seconds=$default_timeout success a0_create_a3 $a0 --osh accountCreate --always-active --account $account3 --uid $uid3 --public-key "\"$(cat $account3key1file.pub)\"" --ttl ${ttl_account_seconds}s json .error_code OK .command accountCreate .value null - local ttl_account_created_at - ttl_account_created_at=$(date +%s) # check that account3 can connect during their TTL success a3_ttl_connect $a3 --osh info json .error_code OK + # check that account3 info has the ttl in it + success a0_info_a3_ttl $a0 --osh accountInfo --account $account3 + json .error_code OK .value.is_ttl_expired 0 .value.is_ttl_set 1 + + # sleep to ensure TTL has expired + waitfor $(( ttl_account_seconds + 1 )) "waiting for account to expire" + + # check that account3 can no longer connect due to their TTL + run a3_ttl_connect_no $a3 --osh info + retvalshouldbe 121 + contain 'TTL has expired' + + success a0_info_a3_ttl_no $a0 --osh accountInfo --account $account3 + json .error_code OK .value.is_ttl_expired 1 + # grant accountInfo to a1 success a0_grant_a1_accountinfo $a0 --osh accountGrantCommand --command accountInfo --account $account1 - # check that account3 info has the ttl in it + # check that account3 info has the ttl in it and that it's expired success a0_info_a3_ttl $a0 --osh accountInfo --account $account3 - json .error_code OK .value.is_ttl_expired 0 + json .error_code OK .value.is_ttl_expired 1 .value.is_ttl_set 1 # a1 should see basic info about a2 success a1_accountinfo_a2_basic $a1 --osh accountInfo --account $account2 @@ -134,21 +146,6 @@ EOS plgfail a1_accountinfo_all_no_auditor $a1 --osh accountInfo --all json .command accountInfo .error_code ERR_ACCESS_DENIED .value null - # sleep to ensure TTL has expired. add 2 seconds to be extra-sure and avoid int-rounding errors - local sleep_for - sleep_for=$(( ttl_account_seconds - ( $(date +%s) - ttl_account_created_at ) + 2 )) - if [ "$COUNTONLY" != 1 ] && [ $sleep_for -gt 0 ]; then - sleep $sleep_for - fi - - # check that account3 can no longer connect due to their TTL - run a3_ttl_connect_no $a3 --osh info - retvalshouldbe 121 - contain 'TTL has expired' - - success a0_info_a3_ttl_no $a0 --osh accountInfo --account $account3 - json .error_code OK .value.is_ttl_expired 1 - # lock account2 success a0_freeze_a2 $a0 --osh accountFreeze --account $account2 --reason "\"'cest la vie'\"" json .command accountFreeze .error_code OK .value.account $account2 .value.reason "cest la vie" diff --git a/tests/functional/tests.d/341-selfaccesses-force-password.sh b/tests/functional/tests.d/341-selfaccesses-force-password.sh index dbf52e5..909f465 100644 --- a/tests/functional/tests.d/341-selfaccesses-force-password.sh +++ b/tests/functional/tests.d/341-selfaccesses-force-password.sh @@ -48,7 +48,7 @@ testsuite_selfaccesses_force_password() success sshd_config_patch $r0 "\"echo -e 'Match User ${account4}\n KbdInteractiveAuthentication yes\n AuthenticationMethods keyboard-interactive' >> /etc/ssh/sshd_config\"" success sshd_reload $r0 "\"pkill -SIGHUP -f '^(/usr/sbin/sshd\\\$|sshd.+listener)'\"" # during tests, under some OSes it takes some time for sshd to accept new connections again after the SIGHUP - [ "$COUNTONLY" != 1 ] && sleep 1 + waitfor 1 "waiting for sshd to reload its configuration" fi diff --git a/tests/functional/tests.d/346-testagentforward.sh b/tests/functional/tests.d/346-testagentforward.sh index ff564b4..57821a7 100644 --- a/tests/functional/tests.d/346-testagentforward.sh +++ b/tests/functional/tests.d/346-testagentforward.sh @@ -22,7 +22,7 @@ testsuite_agent_forwarding() # pkill doesn't work well under FreeBSD, so do it ourselves for all OSes success sshd_reload $r0 "\"ps -U 0 -o pid,command | grep -E '/usr/sbin/sshd\\\$|sshd:.+liste[n]er' | awk '{print \\\$1}' | xargs -r kill -SIGHUP\"" # during tests, under some OSes it takes some time for sshd to accept new connections again after the SIGHUP - [ "$COUNTONLY" != 1 ] && sleep 1 + waitfor 1 "waiting for sshd to reload its configuratnio" # Test if ssh-agent is spawned without requesting it; it shouldn't run shellaccount_noagent $a0 $shellaccount@$remote_ip --kbd-interactive -- ssh-add -L diff --git a/tests/functional/tests.d/350-groups.sh b/tests/functional/tests.d/350-groups.sh index fb2ca16..6b4f409 100644 --- a/tests/functional/tests.d/350-groups.sh +++ b/tests/functional/tests.d/350-groups.sh @@ -159,7 +159,6 @@ EOS run a1_ssh_wait $a1 --wait 192.0.2.0 retvalshouldbe 124 # timeout contain "Waiting for port 22 to be open on 192.0.2.0" - contain REGEX "Still trying to connect to 192.0.2.0:22 after 1[0-9] seconds" success a1_remove_non_routable_ip $a1 --osh groupDelServer --host 192.0.2.0 --user-any --port-any --group $group1 @@ -955,13 +954,13 @@ EOS json .command groupAddServer .error_code OK .value.group $group1 .value.ip 127.0.0.11 .value.port null .value.user null # group1: a1(owner,aclkeeper,gatekeeper,member) a2() servers(127.0.0.10,127.0.0.11) - success thirdaddttl $a1 --osh groupAddServer --group $group1 --host 127.0.0.12 --port-any --user-any --force --ttl 0w19s0d + success thirdaddttl $a1 --osh groupAddServer --group $group1 --host 127.0.0.12 --port-any --user-any --force --ttl 0w${default_timeout}s0d contain "was added to group" - contain "expires in 00:00:" - json .command groupAddServer .error_code OK .value.group $group1 .value.ip 127.0.0.12 .value.port null .value.user null .value.ttl 19 + contain "expires in 00:0" + json .command groupAddServer .error_code OK .value.group $group1 .value.ip 127.0.0.12 .value.port null .value.user null '.value.ttl > 1' true # group1: a1(owner,aclkeeper,gatekeeper,member) a2() servers(127.0.0.10,127.0.0.11,127.0.0.12-TTL) - success list $a1 --osh groupListServers --group $group1 + success list_before_ttl_expiry $a1 --osh groupListServers --group $group1 json .command groupListServers .error_code OK 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:]]' @@ -971,10 +970,7 @@ EOS contain '5 accesses listed' # wait for the access to expire - if [ "$COUNTONLY" != 1 ]; then - echo "Waiting 20 seconds for the access to expire..." - sleep 20 - fi + waitfor $((default_timeout + 1)) "waiting for the access to expire" # group1: a1(owner,aclkeeper,gatekeeper,member) a2() servers(127.0.0.10,127.0.0.11) success listttlexpired $a1 --osh groupListServers --group $group1 @@ -1088,8 +1084,8 @@ EOS success 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 + # wait for the 1s-ttl guest access added above to expire, so that the re-add below is an actual change (and not OK_NO_CHANGE) + waitfor 2 "waiting for the 1s-ttl guest access to expire" # group1: a1(owner,aclkeeper,gatekeeper,member) a2() servers(127.0.0.10,127.0.0.11) success works $a1 --osh groupAddGuestAccess --group $group1 --account $account2 --port-any --user-any --host 127.0.0.10 diff --git a/tests/functional/tests.d/370-mfa.sh b/tests/functional/tests.d/370-mfa.sh index 7482def..3e04d64 100644 --- a/tests/functional/tests.d/370-mfa.sh +++ b/tests/functional/tests.d/370-mfa.sh @@ -404,7 +404,7 @@ testsuite_mfa() # login and fail without totp (timeout) script a4_connect_after_totp_fail "echo 'set timeout $default_timeout; - spawn $a4 --osh groupList; + spawn $a4f --osh groupList; expect \"word:\" { sleep 0.2; send \"$a4_password\\n\"; }; expect eof; lassign [wait] pid spawnid value value; @@ -488,7 +488,7 @@ testsuite_mfa() json .command groupList .error_code OK_EMPTY # pubkey-auth-optional disabled: fail with pubkey but no password (timeout) - script a4_no_pubkeyauthoptional_login_pubkey_nopam $a4 --osh groupList + script a4_no_pubkeyauthoptional_login_pubkey_nopam $a4f --osh groupList retvalshouldbe 124 contain 'Multi-Factor Authentication enabled, an additional authentication factor is required (password).' contain 'Your password expires on' @@ -536,7 +536,7 @@ testsuite_mfa() json .command groupList .error_code OK_EMPTY # pubkey-auth-optional enabled: fail with pubkey only - script a4_pubkeyauthoptional_login_pubkey_nopam $a4 --osh groupList + script a4_pubkeyauthoptional_login_pubkey_nopam $a4f --osh groupList retvalshouldbe 124 contain 'Multi-Factor Authentication enabled, an additional authentication factor is required (password).' contain 'Your password expires on' diff --git a/tests/functional/tests.d/400-piv.sh b/tests/functional/tests.d/400-piv.sh index 0e84508..f61c9e0 100644 --- a/tests/functional/tests.d/400-piv.sh +++ b/tests/functional/tests.d/400-piv.sh @@ -124,7 +124,7 @@ EOF contain "Permission denied" # set PIV grace on account1 - success a0_piv_grace_a1 $a0 --osh accountPIV --policy grace --ttl 10 --account $account1 + success a0_piv_grace_a1 $a0 --osh accountPIV --policy grace --ttl $default_timeout --account $account1 json .command accountPIV .error_code OK # account1 should be able to connect now @@ -132,10 +132,7 @@ EOF json .command selfListIngressKeys .error_code OK '.value.keys|length' 2 # sleep to ensure grace expires - if [ "$COUNTONLY" != 1 ]; then - echo "sleeping 10 seconds to wait for grace expiration" - sleep 10 - fi + waitfor $((default_timeout + 1)) "to wait for grace expiration" # manually launch the grace reaper (normally done by cron) success grace_reaper $r0 $opt_remote_basedir/bin/cron/osh-piv-grace-reaper.pl diff --git a/tests/functional/tests.d/500-http-proxy.sh b/tests/functional/tests.d/500-http-proxy.sh index 9ecda9a..99f5b7d 100644 --- a/tests/functional/tests.d/500-http-proxy.sh +++ b/tests/functional/tests.d/500-http-proxy.sh @@ -213,10 +213,7 @@ testsuite_proxy() ignorecodewarn 'osh-http-proxy-daemon' # pkill doesn't work well under FreeBSD, so do it ourselves for all OSes success force_restart $r0 "\"ps -U proxyhttp -o pid,command | grep -v PID | awk '{print \\\$1}' | xargs -r kill; true\"" - if [ "$COUNTONLY" != 1 ]; then - # wait for target_role.sh to restart the daemon - sleep 4 - fi + waitfor 4 "waiting for daemon restart" # post some data script post_data "curl -ski -X POST -d somedata -u '$account0@test@127.0.0.1%9443:$proxy_password' https://$remote_ip:$remote_proxy_port/test | cat; exit \${PIPESTATUS[0]}" @@ -293,10 +290,7 @@ testsuite_proxy() ignorecodewarn 'osh-http-proxy-daemon' # pkill doesn't work well under FreeBSD, so do it ourselves for all OSes success force_restart $r0 "\"ps -U proxyhttp -o pid,command | grep -v PID | awk '{print \\\$1}' | xargs -r kill; true\"" - if [ "$COUNTONLY" != 1 ]; then - # wait for target_role.sh to restart the daemon - sleep 4 - fi + waitfor 4 "waiting for daemon restart" # when daemon will restart, it'll log stuff, ignore it ignorecodewarn 'osh-http-proxy-daemon' From 593e368d65760122527657e6a403d0d2fdecdcf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lesimple?= Date: Mon, 8 Jun 2026 15:43:48 +0000 Subject: [PATCH 06/12] enh: functional tests: lower default_timeout, print elapsed_ms for each test --- docker/Dockerfile.tester | 2 +- tests/functional/launch_tests_on_instance.sh | 15 +++++++++++++-- tests/functional/tests.d/370-mfa.sh | 12 ++++++------ 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/docker/Dockerfile.tester b/docker/Dockerfile.tester index ec6b354..f98cafc 100644 --- a/docker/Dockerfile.tester +++ b/docker/Dockerfile.tester @@ -2,7 +2,7 @@ FROM debian:trixie LABEL maintainer="stephane.lesimple+bastion@ovhcloud.com" # install prerequisites -RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y jq netcat-traditional openssh-client procps bsdutils screen expect shellcheck libperl-critic-perl fping curl rsync moreutils +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y jq netcat-traditional openssh-client procps bsdutils screen expect shellcheck libperl-critic-perl fping curl rsync moreutils bc # add our code COPY . /opt/bastion diff --git a/tests/functional/launch_tests_on_instance.sh b/tests/functional/launch_tests_on_instance.sh index 4640ca2..efaf281 100755 --- a/tests/functional/launch_tests_on_instance.sh +++ b/tests/functional/launch_tests_on_instance.sh @@ -227,9 +227,10 @@ check_sourced_module_output() jq="jq --raw-output --compact-output --sort-keys" js="--json-greppable" - default_timeout=$((30 * opt_slowness_factor)) + default_timeout=$((8 * opt_slowness_factor)) t="timeout --foreground $default_timeout" tf="timeout --foreground $((default_timeout / 2))" + td="timeout --foreground $((default_timeout * 2))" a0=" $t ssh -F $mytmpdir/ssh_config -i $account0key1file $account0@$remote_ip -p $remote_port -- $js " a0f="$tf ssh -F $mytmpdir/ssh_config -i $account0key1file $account0@$remote_ip -p $remote_port -- $js " a1=" $t ssh -F $mytmpdir/ssh_config -i $account1key1file $account1@$remote_ip -p $remote_port -- $js " @@ -238,6 +239,7 @@ check_sourced_module_output() a3=" $t ssh -F $mytmpdir/ssh_config -i $account3key1file $account3@$remote_ip -p $remote_port -- $js " a4=" $t ssh -F $mytmpdir/ssh_config -i $account4key1file $account4@$remote_ip -p $remote_port -- $js " a4f="$tf ssh -F $mytmpdir/ssh_config -i $account4key1file $account4@$remote_ip -p $remote_port -- $js " + a4d="$td ssh -F $mytmpdir/ssh_config -i $account4key1file $account4@$remote_ip -p $remote_port -- $js " a4np="$t ssh -F $mytmpdir/ssh_config -o PubkeyAuthentication=no $account4@$remote_ip -p $remote_port -- $js " r0=" $t ssh -F $mytmpdir/ssh_config -i $rootkeyfile root@$remote_ip -p $remote_port -- " @@ -450,6 +452,15 @@ run() fi case="$1" shift + + # before replacing $basename with the current test's basename, print elapsed_ms for the previous test + if [ -n "${previous_test_t0:-}" ]; then + local elapsed_ms + elapsed_ms=$(perl -MTime::HiRes=time -e 'printf "%.0f\n", ((time() * 1000) - '"$previous_test_t0"')') + infomsg "elapsed_ms $elapsed_ms $basename" + fi + previous_test_t0=$(perl -MTime::HiRes=time -e 'printf "%.0f\n", time() * 1000') + basename=$(printf '%04d-%s-%s' $testno $name $case | sed -re "s=/=_=g") # if we're about to run a script, keep a copy there @@ -700,7 +711,7 @@ dump_vars_and_funcs() { set | grep -v -E '^('\ 'testno|section|code_warn_exclude|COPROC_PID|LINES|COLUMNS|PIPESTATUS|_|'\ -'BASH_LINENO|basename|case|json|name|tmpscript|grepit|got|isbad|'\ +'BASH_LINENO|basename|case|json|name|tmpscript|grepit|got|isbad|previous_test_t0|'\ 'nbfailedgrep|nbfailedcon|nbfailedgeneric|nbfailedlog|nbfailedret|shouldbe|modulename)=' } diff --git a/tests/functional/tests.d/370-mfa.sh b/tests/functional/tests.d/370-mfa.sh index 3e04d64..3d8f02e 100644 --- a/tests/functional/tests.d/370-mfa.sh +++ b/tests/functional/tests.d/370-mfa.sh @@ -147,8 +147,8 @@ testsuite_mfa() contain REGEX 'Password:|Password for' # connect to 127.7.7.7 with MFA JIT, bad password - script a4_connect_g3_server_badpass "echo 'set timeout 45; \ - spawn $a4 root@127.7.7.7; \ + script a4_connect_g3_server_badpass "echo 'set timeout $((default_timeout * 2)); \ + spawn $a4d root@127.7.7.7; \ expect \"is required (password)\" { sleep 0.1; }; \ expect \":\" { sleep 0.2; send \"$a4_password\\n\"; }; \ expect \"is required (password)\" { sleep 0.1; }; \ @@ -184,8 +184,8 @@ testsuite_mfa() success a4_gen_self_egress_pass $a0 --osh accountGeneratePassword --account $account4 --do-it json .command accountGeneratePassword .error_code OK - script a4_connect_g3_server_selfpass_jitmfa "echo 'set timeout $default_timeout; \ - spawn $a4 root@127.7.7.7 -P; \ + script a4_connect_g3_server_selfpass_jitmfa "echo 'set timeout $((default_timeout * 2)); \ + spawn $a4d root@127.7.7.7 -P; \ expect \"is required (password)\" { sleep 0.1; }; \ expect \":\" { sleep 0.2; send \"$a4_password\\n\"; }; \ expect \"is required (password)\" { sleep 0.1; }; \ @@ -217,8 +217,8 @@ testsuite_mfa() contain REGEX 'Password:|Password for' json .command groupGeneratePassword .error_code OK - script a4_connect_g3_server_grouppass_jitmfa "echo 'set timeout $default_timeout; \ - spawn $a4 root@127.7.7.7 --password $group3; \ + script a4_connect_g3_server_grouppass_jitmfa "echo 'set timeout $((default_timeout * 2)); \ + spawn $a4d root@127.7.7.7 --password $group3; \ expect \"is required (password)\" { sleep 0.1; }; \ expect \":\" { sleep 0.2; send \"$a4_password\\n\"; }; \ expect \"is required (password)\" { sleep 0.1; }; \ From 133d5f0c03dfe3487a164b4e3705d7f9a23f840d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lesimple?= Date: Mon, 8 Jun 2026 15:45:08 +0000 Subject: [PATCH 07/12] fix: defensive: when using --bind, refuse to proceed shall get_bastion_ips() fail --- bin/shell/osh.pl | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/bin/shell/osh.pl b/bin/shell/osh.pl index 59df8fa..7edaebf 100755 --- a/bin/shell/osh.pl +++ b/bin/shell/osh.pl @@ -456,10 +456,15 @@ if ($longHelp) { if ($bind) { $fnret = OVH::Bastion::get_bastion_ips(); - if ($fnret) { - if (not grep { $bind eq $_ } @{$fnret->value}) { - main_exit OVH::Bastion::EXIT_CONFLICTING_OPTIONS, "invalid_bind", "Invalid binding IP specified ($bind)"; - } + + # if we can't get the list of our own IPs, we can't validate the + # user-supplied bind IP, so refuse rather than letting an arbitrary value through + if (!$fnret) { + main_exit OVH::Bastion::EXIT_CONFLICTING_OPTIONS, "invalid_bind", + "Couldn't verify the binding IP specified ($bind): " . $fnret->msg; + } + if (not grep { $bind eq $_ } @{$fnret->value}) { + main_exit OVH::Bastion::EXIT_CONFLICTING_OPTIONS, "invalid_bind", "Invalid binding IP specified ($bind)"; } } From fcba07b130a115f8e5cf706b76a0d8e63201d201 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lesimple?= Date: Mon, 8 Jun 2026 15:46:05 +0000 Subject: [PATCH 08/12] fix: re-validate remote user when passed as a magic ssh-password-line --- bin/shell/osh.pl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bin/shell/osh.pl b/bin/shell/osh.pl index 7edaebf..cd66444 100755 --- a/bin/shell/osh.pl +++ b/bin/shell/osh.pl @@ -675,6 +675,12 @@ if (defined $user and $user =~ /^(telnet|ssh)-passw(or)?d-([^-]+)(-([^-]+))?$/) # update user $user = $3; + # $user was just rewritten from the password-login prefix syntax, so we re-validate it + if (!OVH::Bastion::is_valid_remote_user(user => $user, allowWildcards => ($osh_command ? 1 : 0))) { + main_exit OVH::Bastion::EXIT_INVALID_REMOTE_USER, 'invalid_remote_user', + "Remote user name '$user' seems invalid"; + } + if ($4) { $userPasswordClue = $5; } From 58f42bbb2925ff2e1efa24e1016df0e95ef6f593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lesimple?= Date: Mon, 8 Jun 2026 15:49:27 +0000 Subject: [PATCH 09/12] fix: tests: 900-strict-checking.sh: more resilient version of the change_host_keys step --- tests/functional/tests.d/900-strict-checking.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/functional/tests.d/900-strict-checking.sh b/tests/functional/tests.d/900-strict-checking.sh index 83c4c35..572d545 100644 --- a/tests/functional/tests.d/900-strict-checking.sh +++ b/tests/functional/tests.d/900-strict-checking.sh @@ -23,10 +23,12 @@ testsuite_strict_checking() contain "Permanently added" # change the remote hostkeys, and get the proper sshd PID so that we can force it to reload - success change_host_keys $r0 "\"find /etc/ssh/ -type f -name 'ssh_host_*key*' -print -delete; ssh-keygen -A; ps faxu; printf %s SSHD_PIDS=; ps ax -o pid,args | grep -E '^ *[0-9]+ +(sshd: .+listener|/usr/sbin/sshd)' | awk '{print \\\$1}' | tr '\\\n' ' '\"" + success change_host_keys $r0 "\"find /etc/ssh/ -type f -name 'ssh_host_*key*' -print -delete; ssh-keygen -A; ps faxu; printf %s SSHD_PIDS=; ps ax -o pid,args | grep -E '^ *[0-9]+ +(sshd: .+listener|/usr/sbin/sshd)' | awk '{print \\\$1}' | tr '\\\n' ' '; echo\"" contain 'generating new host keys' + contain REGEX 'SSHD_PIDS=[0-9]' local sshd_pids - sshd_pids=$(get_stdout | grep SSHD_PIDS= | cut -d= -f2-) + sshd_pids=$(get_stdout | grep -Eo 'SSHD_PIDS=[0-9 ]+' | cut -d= -f2) + infomsg "detected sshd PIDs are: $sshd_pids" success reload_target_sshd $r0 "\"kill -HUP $sshd_pids\"" From 7e5af2a26fa7f6f35ecb5511c7bb736cceddbb74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lesimple?= Date: Mon, 8 Jun 2026 20:54:10 +0200 Subject: [PATCH 10/12] enh: tests: docker_build_and_run_tests_all: keep logs at the end --- .../docker/docker_build_and_run_tests_all.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/functional/docker/docker_build_and_run_tests_all.sh b/tests/functional/docker/docker_build_and_run_tests_all.sh index 7d0093f..c1b27c9 100755 --- a/tests/functional/docker/docker_build_and_run_tests_all.sh +++ b/tests/functional/docker/docker_build_and_run_tests_all.sh @@ -24,8 +24,8 @@ echo "GO!" tempdir=$(mktemp -d) # shellcheck disable=SC2317 cleanup() { - test -d "$tempdir" && rm -rf "$tempdir" docker ps | grep -Eo 'bastion_.*_(target|tester)$' | xargs -r docker kill + printf '\n%b%b%b\n\n' "$WHITE_ON_BLUE" "Individual logs are under $tempdir/, when you're done, you can \`rm -rf $tempdir'" "$NOC" } trap 'cleanup' EXIT @@ -34,8 +34,8 @@ do [ "$t" = "-" ] && continue ( friendlyname=$(echo "$t" | sed -re 's/@[^:]+//') - DOCKER_TTY=false ./docker_build_and_run_tests.sh "$t" "--log-prefix=$friendlyname $*" - echo $? > "$tempdir/$friendlyname" + DOCKER_TTY=false ./docker_build_and_run_tests.sh "$t" "--log-prefix=$friendlyname $*" |& tee "$tempdir/$friendlyname.log" + echo "${PIPESTATUS[0]}" > "$tempdir/$friendlyname" ) & done wait @@ -51,18 +51,18 @@ do err=$(cat "$tempdir/$friendlyname" 2>/dev/null) rm -f "$tempdir/$friendlyname" if [ -z "$err" ]; then - printf "%b%16s: tests couldn't run properly%b\\n" "$BLACK_ON_RED" "$friendlyname" "$NOC" + printf "%b%16s: tests couldn't run properly%b\\n" "$WHITE_ON_RED" "$friendlyname" "$NOC" nberrors=$(( nberrors + 1 )) elif [ "$err" = 0 ]; then printf "%b%16s: no errors :)%b\\n" "$BLACK_ON_GREEN" "$friendlyname" "$NOC" elif [ "$err" = 143 ]; then - printf "%b%16s: tests interrupted%b\\n" "$BLACK_ON_RED" "$friendlyname" "$NOC" + printf "%b%16s: tests interrupted%b\\n" "$WHITE_ON_RED" "$friendlyname" "$NOC" nberrors=$(( nberrors + 1 )) elif [ "$err" -lt 254 ]; then - printf "%b%16s: $err tests failed%b\\n" "$BLACK_ON_RED" "$friendlyname" "$NOC" + printf "%b%16s: $err tests failed%b log: %b\\n" "$WHITE_ON_RED" "$friendlyname" "$NOC" "less -SR $tempdir/$friendlyname.log" nberrors=$(( nberrors + 1 )) else - printf "%b%16s: $err errors%b\\n" "$BLACK_ON_RED" "$friendlyname" "$NOC" + printf "%b%16s: $err errors%b log: %b\\n" "$WHITE_ON_RED" "$friendlyname" "$NOC" "less -SR $tempdir/$friendlyname.log" nberrors=$(( nberrors + 1 )) fi done From 39eac3c92d0776618802cd090c8b99c36682d8e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lesimple?= Date: Mon, 8 Jun 2026 21:18:01 +0200 Subject: [PATCH 11/12] fix: tests: target_role: don't rely on deprecated 'start-stop-daemon' --- tests/functional/docker/target_role.sh | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/functional/docker/target_role.sh b/tests/functional/docker/target_role.sh index 86ff93c..aec07b5 100755 --- a/tests/functional/docker/target_role.sh +++ b/tests/functional/docker/target_role.sh @@ -171,12 +171,8 @@ if [ "$WANT_HTTP_PROXY" = 1 ]; then echo "Starting HTTP Proxy and fake remote server" while : ; do if ! pgrep -f /osh-http-proxy-daemon >/dev/null; then - if [ -x /etc/init.d/osh-http-proxy ] && [ -f /lib/init/vars.sh ] && command -v start-stop-daemon >/dev/null 2>&1; then - /etc/init.d/osh-http-proxy start - else - sudo -n -u proxyhttp -- /opt/bastion/bin/proxy/osh-http-proxy-daemon & - disown - fi + sudo -n -u proxyhttp -- /opt/bastion/bin/proxy/osh-http-proxy-daemon & + disown fi if ! pgrep -f /remote-daemon >/dev/null; then "$basedir"/tests/functional/proxy/remote-daemon & From 2391671f82516dc3225dfa2a6c45d6c74beaf08e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lesimple?= Date: Mon, 8 Jun 2026 21:37:52 +0200 Subject: [PATCH 12/12] fix: osh-encrypt-rsync: tame misbehaving lsofs (added timeout 5) --- bin/cron/osh-encrypt-rsync.pl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/cron/osh-encrypt-rsync.pl b/bin/cron/osh-encrypt-rsync.pl index 010d172..a95ed89 100755 --- a/bin/cron/osh-encrypt-rsync.pl +++ b/bin/cron/osh-encrypt-rsync.pl @@ -499,7 +499,8 @@ sub potentially_work_on_this_file { state $openedFiles; if (ref $openedFiles ne 'HASH') { $openedFiles = {}; - if (open(my $fh_lsof, '-|', "lsof -a -n -c ttyrec -- /home/")) { + # in some circumstances, lsof may misbehave, ensure it doesn't eat up too much CPU by setting a timeout + if (open(my $fh_lsof, '-|', "timeout 5 lsof -a -n -c ttyrec -- /home/")) { while (<$fh_lsof>) { chomp; m{\s(/home/[^/]+/ttyrec/\S+)$} and $openedFiles->{$1} = 1;