fix(ci): isolate agent smoke in namespace-capable helper
quality / changes (push) Successful in 6s
quality / commitlint (push) Skipped
quality / openapi (push) Successful in 27s
quality / web (push) Successful in 49s
quality / api (push) Successful in 1m13s
quality / linux-agent-smoke (push) Failing after 1m31s
quality / docker-check (push) Failing after 4m11s
CD / quality (push) Failing after 8m19s
CD / publish (push) Skipped
quality / changes (push) Successful in 6s
quality / commitlint (push) Skipped
quality / openapi (push) Successful in 27s
quality / web (push) Successful in 49s
quality / api (push) Successful in 1m13s
quality / linux-agent-smoke (push) Failing after 1m31s
quality / docker-check (push) Failing after 4m11s
CD / quality (push) Failing after 8m19s
CD / publish (push) Skipped
This commit is contained in:
1 parent
9fa6269174
commit
a37ffdf061
7 files changed
+133
-6
No files matched your search
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
# Fail before any firewall work if the daemon/kernel still forbids namespaces.
|
||||
if ! unshare --net --mount --pid --fork --mount-proc true; then
|
||||
echo 'Linux agent helper cannot create required namespaces. A rootful Docker daemon with namespace/netfilter support and the documented helper capabilities is required; smoke was not run.' >&2
|
||||
exit 1
|
||||
fi
|
||||
python3 -B scripts/ci/linux-agent-fixture.test.py
|
||||
export EVOFW_TEST_ETC_DATA=/work/helper-etc
|
||||
exec bash scripts/ci/linux-netns-smoke.sh
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env sh
|
||||
# Run from the checkout root. Build context is sent to Docker, never bind-mounted.
|
||||
set -eu
|
||||
command -v docker >/dev/null
|
||||
[ "$(docker info --format '{{.OSType}}')" = linux ] || { echo 'A Linux Docker daemon is required for the agent namespace smoke' >&2; exit 1; }
|
||||
tmp="$(mktemp -d)"
|
||||
name="evofw-linux-smoke-${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}-$$"
|
||||
image=''
|
||||
cleanup() {
|
||||
result=$?
|
||||
trap - EXIT HUP INT TERM
|
||||
docker rm -f "$name" >/dev/null 2>&1 || true
|
||||
if [ -n "$image" ]; then docker image rm "$image" >/dev/null 2>&1 || true; fi
|
||||
rm -rf "$tmp"
|
||||
exit "$result"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 129' HUP
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
docker build --file scripts/ci/linux-agent-smoke.Dockerfile --iidfile "$tmp/image-id" .
|
||||
image="$(cat "$tmp/image-id")"
|
||||
printf '%s\n' "$image" | grep -Eq '^sha256:[0-9a-f]{64}$' || { echo 'Missing immutable helper image ID' >&2; exit 1; }
|
||||
set -- docker run --rm --name "$name" --network none \
|
||||
--cap-drop ALL --cap-add SYS_ADMIN --cap-add NET_ADMIN \
|
||||
--security-opt no-new-privileges:true \
|
||||
--read-only --tmpfs /tmp:rw,nosuid,nodev,noexec,size=64m \
|
||||
--pids-limit 256 --cpus 2 --memory 512m
|
||||
# docker-default AppArmor denies the mount operations used by unshare/ip-netns.
|
||||
# Only this disposable, networkless helper gets the mount-capable exception.
|
||||
# Default seccomp remains enabled and adapts to the two granted capabilities.
|
||||
security="$(docker info --format '{{json .SecurityOptions}}')"
|
||||
if printf '%s' "$security" | grep -q apparmor; then
|
||||
set -- "$@" --security-opt apparmor=unconfined
|
||||
fi
|
||||
set -- "$@" "$image"
|
||||
"$@"
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env sh
|
||||
# Exercise the wrapper without a Docker daemon or any firewall operation.
|
||||
set -eu
|
||||
wrapper="$(pwd)/scripts/ci/linux-agent-container-smoke.sh"
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
|
||||
mkdir "$tmp/bin"
|
||||
cat >"$tmp/bin/docker" <<'DOCKER'
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
printf '%s\n' "$*" >>"$FAKE_LOG"
|
||||
case "$1" in
|
||||
info)
|
||||
case "$*" in
|
||||
*SecurityOptions*) if [ "$FAKE_APPARMOR" = true ]; then printf '["name=apparmor","name=seccomp,profile=default"]\n'; else printf '["name=seccomp,profile=default"]\n'; fi ;;
|
||||
*) printf 'linux\n' ;;
|
||||
esac ;;
|
||||
build)
|
||||
while [ "$#" -gt 0 ]; do
|
||||
if [ "$1" = --iidfile ]; then shift; printf 'sha256:%064d\n' 0 >"$1"; break; fi
|
||||
shift
|
||||
done ;;
|
||||
run) exit "$FAKE_RUN_EXIT" ;;
|
||||
rm|image) exit 0 ;;
|
||||
*) exit 98 ;;
|
||||
esac
|
||||
DOCKER
|
||||
chmod +x "$tmp/bin/docker"
|
||||
for expected in 0 23; do
|
||||
log="$tmp/docker-$expected.log"
|
||||
apparmor=false; [ "$expected" = 0 ] || apparmor=true
|
||||
if PATH="$tmp/bin:$PATH" FAKE_LOG="$log" FAKE_APPARMOR="$apparmor" FAKE_RUN_EXIT="$expected" sh "$wrapper"; then actual=0; else actual=$?; fi
|
||||
[ "$actual" = "$expected" ] || { echo 'Wrapper did not preserve the container exit code' >&2; exit 1; }
|
||||
grep -F 'build --file scripts/ci/linux-agent-smoke.Dockerfile --iidfile ' "$log" >/dev/null
|
||||
grep -F ' --network none --cap-drop ALL --cap-add SYS_ADMIN --cap-add NET_ADMIN ' "$log" >/dev/null
|
||||
grep -F ' --security-opt no-new-privileges:true --read-only --tmpfs /tmp:' "$log" >/dev/null
|
||||
grep -F 'image rm sha256:' "$log" >/dev/null
|
||||
grep -E '^rm -f evofw-linux-smoke-' "$log" >/dev/null
|
||||
if grep -E -- '(--privileged|--network host|--pid host|--mount|--volume|docker.sock|seccomp=)' "$log" >/dev/null; then echo 'Unsafe helper isolation flags' >&2; exit 1; fi
|
||||
if [ "$apparmor" = true ]; then grep -F ' --security-opt apparmor=unconfined ' "$log" >/dev/null
|
||||
elif grep -F 'apparmor=unconfined' "$log" >/dev/null; then echo 'Unneeded AppArmor exception' >&2; exit 1
|
||||
fi
|
||||
done
|
||||
echo 'Helper wrapper regression passed (flags, remote context, exit status, cleanup)'
|
||||
@@ -0,0 +1,23 @@
|
||||
FROM debian:bookworm-slim
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
bash ca-certificates coreutils curl iproute2 ipset iptables netbase nftables python3 util-linux \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
# The harness masks /etc. Debian's public xtables links normally traverse
|
||||
# /etc/alternatives, so resolve them while that directory is still visible.
|
||||
RUN set -eu; for tool in iptables iptables-save iptables-restore ip6tables ip6tables-save ip6tables-restore; do \
|
||||
binary="$(readlink -f "$(command -v "$tool")")"; \
|
||||
test -x "$binary"; \
|
||||
case "$binary" in /etc/*) exit 1;; esac; \
|
||||
ln -sf "$binary" "/usr/sbin/$tool"; \
|
||||
test "$(readlink -f "/usr/sbin/$tool")" = "$binary"; \
|
||||
done
|
||||
WORKDIR /work
|
||||
# Public protocol/service name databases only; never copy the runner's /etc.
|
||||
RUN mkdir helper-etc && cp /etc/protocols /etc/services helper-etc/
|
||||
# COPY makes this work with a remote Docker daemon; no runner path is mounted.
|
||||
COPY apps/api/src/agent-scripts/evofw-firewall.sh apps/api/src/agent-scripts/evofw-firewall.sh
|
||||
COPY scripts/ci/linux-netns-smoke.sh scripts/ci/linux-agent-fixture.py scripts/ci/linux-agent-fixture.test.py scripts/ci/linux-agent-container-entry.sh scripts/ci/
|
||||
RUN bash -n scripts/ci/linux-netns-smoke.sh scripts/ci/linux-agent-container-entry.sh \
|
||||
&& python3 -B scripts/ci/linux-agent-fixture.test.py
|
||||
ENTRYPOINT ["bash", "scripts/ci/linux-agent-container-entry.sh"]
|
||||
@@ -24,6 +24,10 @@ export NO_PROXY='*'
|
||||
mount --make-rprivate /
|
||||
# Agent hardcoded config/state/log paths are private mounts, never host writes.
|
||||
for directory in /etc /var/lib /var/log /run; do mount -t tmpfs tmpfs "$directory"; done
|
||||
if [[ -n "${EVOFW_TEST_ETC_DATA:-}" ]]; then
|
||||
[[ "$EVOFW_TEST_ETC_DATA" == /work/helper-etc ]]
|
||||
cp "$EVOFW_TEST_ETC_DATA/protocols" "$EVOFW_TEST_ETC_DATA/services" /etc/
|
||||
fi
|
||||
mkdir -p /etc/evofw /var/lib/evofw
|
||||
ip link set lo up
|
||||
ip netns add client
|
||||
|
||||
Reference in new issue
Block a user