mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-30 15:48:34 +00:00
Scope the deb-install and dns-resolver e2e image tags to the run.
Both suites bake the run's artifact into an image they build themselves:
deb-install copies the package in, and the dns-resolver e2e scenarios copy
the fips and fips-gateway binaries in. The tags, fips-deb-test:<distro> and
fips-dns-test:e2e-<distro>, were the same for every run while the container
names already carried the run's suffix, so two local CI runs on one Docker
daemon rebuilt each other's tags, and a run that built just before another
booted the other run's package or binaries. The RPM suite on master failed
this way on the internal builder.
The tags now end in ${FIPS_CI_NAME_SUFFIX}, the images are built with the
generic and run labels (a CI_LABELS array in testing/lib/image-build.sh,
whose build_inline now passes extra arguments to docker build), and each
scenario removes its image when it ends. A run killed before that leaves
its images to ci-local.sh's teardown, which already reaps every image
carrying the run label; the Debian upgrade image, already suffixed, gets
the labels too.
After boot each scenario checks that the container holds this run's
artifact by sha256, the package in deb-install and both binaries in
dns-resolver, and stops the scenario if not. Two runs of one commit build
packages of the same name, so only the hash tells them apart.
check-image-scoping.sh now also fails a fips-deb-test or fips-dns-test:e2e-
tag that does not carry the run suffix. dns-resolver's other fips-dns-test
tags hold nothing of the run and stay shared.
Give every suite container the CI run label, and bound the systemd waits.
A dns-resolver run cancelled mid-scenario left its container running on
the internal builder for three days. The suite's containers carried only
the generic com.corganlabs.fips-ci=1 label; ci-local.sh's teardown reaps
by the run label, so it passed the container over, and the builder's
orphan sweep, which is skipped while any generic-labelled container is
up, stopped for every run after it. Cancelling a run this way on a local
host reproduces it: the teardown runs and the container survives.
The install suites, native-api and mdns now take their labels from the
CI_LABELS array in testing/lib/image-build.sh, which all of them source,
so the run label cannot be left off one site; the NAT topology helper,
which does not source the library, adds the run label beside the generic
one as its sibling scripts do. dns-resolver also gets a TERM/INT/EXIT trap
that removes the scenario container and e2e image in use, since a SIGKILL
30 seconds after the TERM leaves ci-local.sh's teardown no time.
testing/check-ci-labels.py, run in local CI's guards and the CI parity
job, fails any docker run, create, build, or network or volume create
whose labels, with variables and arrays expanded, include the generic
label without the run label, and any argument array that does.
The scenario had stalled for about 36 minutes before it was cancelled,
and `systemctl is-system-running --wait` could have been where: it blocks
until boot finishes, so the wait loops in dns-resolver, deb-install and
tarball-install, which bounded only their attempt count, could block for
ever on one call. Each call now runs under `timeout` inside the container
(systemd_state in systemd-container.sh), as do dns-resolver's boot
diagnostics and its fips-dns-setup and fips-dns-teardown runs, so a stall
ends and names its step.
110 lines
5.1 KiB
Bash
110 lines
5.1 KiB
Bash
#!/bin/bash
|
|
# Shared flags and isolation check for test containers that boot systemd.
|
|
#
|
|
# Source this file from a harness one level under testing/:
|
|
# source "$SCRIPT_DIR/../lib/systemd-container.sh"
|
|
# docker run -d ... "${SYSTEMD_CAPS[@]}" ... "$image"
|
|
# check_isolation "$name"
|
|
# state=$(systemd_state "$name")
|
|
#
|
|
# check_isolation reports through the harness's own pass() and fail(), so
|
|
# those must be defined before it is called.
|
|
#
|
|
# Why these containers are not started with --privileged: a privileged
|
|
# container sees the host's real VT and serial devices (/dev/tty0, /dev/tty1,
|
|
# /dev/ttyS0, ...) and a writable /proc/sys and /sys. The systemd inside the
|
|
# image treats them as its own, so it starts a getty on the host's console,
|
|
# where each side's hangup kills the other's getty until the host's unit hits
|
|
# its start limit and the machine is left with no console login; logind holds
|
|
# a host VT open; and systemd-sysctl applies the image's sysctl.d to the host's
|
|
# kernel parameters. Some images also set up the host's virtual consoles and
|
|
# run a udev coldplug against the host's /sys.
|
|
#
|
|
# Without --privileged, docker gives the container only its default /dev and
|
|
# mounts /proc/sys and /sys read-only, so those units skip on their own
|
|
# conditions. That removes the console and kernel-parameter reach; it does not
|
|
# seal the container off from the host. systemd still mounts the host's fusectl
|
|
# and a hugetlbfs, and SYS_ADMIN would let a process inside remount /proc/sys
|
|
# read-write, which nothing in these images does.
|
|
#
|
|
# What each flag is for:
|
|
# --cap-add SYS_ADMIN mount namespaces for unit sandboxing (PrivateTmp,
|
|
# ProtectHome, ProtectKernelModules in the fips units,
|
|
# and the sandboxing resolved and logind carry)
|
|
# --cap-add NET_ADMIN TUN and dummy fips0 links, and nftables and neighbour
|
|
# proxy entries in the container's own network namespace
|
|
# --security-opt apparmor=unconfined
|
|
# the default AppArmor profile denies the mounts systemd
|
|
# makes for that sandboxing; without it boot ends
|
|
# degraded with logind and resolved failed. Needed on
|
|
# AppArmor hosts, which include Ubuntu hosts and
|
|
# GitHub's ubuntu-latest runners.
|
|
# shellcheck disable=SC2034 # read by the sourcing harness, not within this file
|
|
SYSTEMD_CAPS=(
|
|
--cap-add SYS_ADMIN
|
|
--cap-add NET_ADMIN
|
|
--security-opt apparmor=unconfined
|
|
)
|
|
|
|
# Print the container's systemd state once boot has finished, or nothing when
|
|
# it has not finished within SYSTEMD_STATE_TIMEOUT seconds or the container
|
|
# cannot be asked. The callers' wait loops call this until it prints
|
|
# `running` or `degraded`.
|
|
#
|
|
# `systemctl is-system-running --wait` blocks until boot finishes, so a boot
|
|
# that never finishes never returns it, and the loop's attempt count bounds
|
|
# nothing: one blocked call holds the whole suite. The bound is inside the
|
|
# container, where `timeout` kills systemctl itself; a bound on the docker
|
|
# client would return and leave the call running there. It only has to stop
|
|
# one call hanging, since the caller loops: a boot slower than this is read
|
|
# again on the next attempt.
|
|
SYSTEMD_STATE_TIMEOUT=5
|
|
systemd_state() {
|
|
docker exec "$1" timeout "$SYSTEMD_STATE_TIMEOUT" systemctl is-system-running --wait 2>/dev/null || true
|
|
}
|
|
|
|
# Fail the suite if a running container can reach the host console or write
|
|
# the host's kernel parameters.
|
|
#
|
|
# Device nodes and mount modes are fixed when the container is created, so one
|
|
# look straight after `docker run` is enough; a remount made later from inside
|
|
# is out of its reach. The probe prints a marker first and exits 0, so the
|
|
# exec's status says only whether the probe ran: a container that could not be
|
|
# inspected is a failure, never a pass. On any failure the container is
|
|
# removed at once, so a privileged container does not live on to reach the
|
|
# host's console.
|
|
#
|
|
# Returns 0 when the container is isolated, 1 otherwise.
|
|
check_isolation() {
|
|
local name="$1" out findings
|
|
if ! out=$(docker exec "$name" sh -c '
|
|
echo ISOLATION-PROBE
|
|
for d in /dev/tty[0-9]* /dev/ttyS[0-9]* /dev/console; do
|
|
[ -e "$d" ] && echo "device $d"
|
|
done
|
|
[ -w /proc/sys/kernel/core_pattern ] && echo "writable /proc/sys"
|
|
[ -w /sys/kernel ] && echo "writable /sys"
|
|
exit 0' 2>&1); then
|
|
fail "isolation: could not inspect $name: ${out:-no output}"
|
|
docker rm -f "$name" >/dev/null 2>&1 || true
|
|
return 1
|
|
fi
|
|
case $'\n'"$out" in
|
|
*$'\n'ISOLATION-PROBE*) ;;
|
|
*)
|
|
fail "isolation: probe of $name printed no marker: ${out:-no output}"
|
|
docker rm -f "$name" >/dev/null 2>&1 || true
|
|
return 1
|
|
;;
|
|
esac
|
|
findings=${out#*ISOLATION-PROBE}
|
|
findings=${findings#$'\n'}
|
|
if [ -n "$findings" ]; then
|
|
fail "isolation: $name reaches the host: ${findings//$'\n'/, }"
|
|
docker rm -f "$name" >/dev/null 2>&1 || true
|
|
return 1
|
|
fi
|
|
pass "isolation: $name has no host console devices and read-only /proc/sys and /sys"
|
|
return 0
|
|
}
|