Skip to content

Commit 7270a43

Browse files
committed
Support constrained ACME mount smoke fallback
1 parent 14bd333 commit 7270a43

6 files changed

Lines changed: 102 additions & 16 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ jobs:
113113
run: cargo test
114114

115115
- name: Run isolated ACME mount-boundary regression
116-
run: scripts/smoke_acme_mount_boundary.sh
116+
run: FLUXHEIM_ACME_MOUNT_TEST_MODE=container FLUXHEIM_ACME_MOUNT_TEST_RUNTIME=docker scripts/smoke_acme_mount_boundary.sh
117117

118118
- name: Validate OWASP Top 10 2025 baseline
119119
run: scripts/validate-owasp-top10-2025.sh check

docs/certificate-renewal.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,15 @@ The Linux bind-mount regression is deliberately marked ignored in ordinary
8686
Rust test runs because it requires mount capability and an isolated mount
8787
namespace. CI, the deep release gate, and `scripts/test_starter.py` execute
8888
`scripts/smoke_acme_mount_boundary.sh`, which runs that exact ignored test in a
89-
root-mapped user namespace and a private mount namespace. The test receives no
90-
host-root or privileged-container capability. Hosts that prohibit unprivileged
91-
user namespaces fail the dedicated smoke explicitly instead of silently
92-
skipping it or falling back to broader privilege. A normal test inventory
93-
therefore reports the Rust test as ignored rather than as a false pass.
89+
root-mapped user namespace and a private mount namespace. When a host prohibits
90+
unprivileged user namespaces, including GitHub-hosted runners, the script uses
91+
a digest-pinned disposable container with networking disabled, a read-only root
92+
filesystem, all capabilities dropped except `SYS_ADMIN`, and
93+
`no-new-privileges`. Docker's AppArmor profile is disabled for this one-shot
94+
container because that profile denies `mount(2)` independently of capabilities;
95+
Docker's seccomp profile and the other restrictions remain active. The script
96+
never uses unrestricted `--privileged` access. A normal test inventory therefore
97+
reports the Rust test as ignored rather than as a false pass.
9498

9599
```bash
96100
fluxheim --config path/to/fluxheim.toml --check-tls-storage

release-notes/RELEASE_NOTES_1.7.9.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ products.
5353
mount points. The bind-mount regression is explicitly ignored in
5454
ordinary Rust runs and executed by CI and the deep release gate through a
5555
dedicated smoke using root-mapped user and private mount namespaces, without
56-
a privileged container.
56+
a privileged container. Hosts that disable user namespaces use a
57+
digest-pinned, network-isolated, read-only container with every capability
58+
dropped except the mount operation's required `SYS_ADMIN` capability.
5759
- Managed ACME account generation now creates P-256 material in zeroizing
5860
RustCrypto secret/document types before importing it into Ring and retaining
5961
the durable copy in `sanitization::SecretVec`, removing the transient

scripts/smoke_acme_mount_boundary.sh

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,6 @@ if [ "$(uname -s)" != "Linux" ]; then
99
exit 1
1010
fi
1111

12-
if ! command -v unshare >/dev/null 2>&1; then
13-
echo "ACME mount-boundary smoke requires util-linux unshare" >&2
14-
exit 1
15-
fi
16-
1712
test_name=acme_directory::tests::owned_directory_reconciliation_rejects_bind_mount
1813
build_output=$(cargo test --locked -p fluxheim-acme --features acme-client \
1914
--no-run --message-format=json)
@@ -25,8 +20,83 @@ if [ -z "$test_binary" ] || [ ! -x "$test_binary" ]; then
2520
exit 1
2621
fi
2722

28-
unshare --user --map-root-user --mount --propagation private --fork \
29-
"$test_binary" \
30-
--exact "$test_name" --ignored --nocapture
23+
run_in_user_namespace() {
24+
unshare --user --map-root-user --mount --propagation private --fork \
25+
"$test_binary" \
26+
--exact "$test_name" --ignored --nocapture
27+
}
28+
29+
run_in_constrained_container() {
30+
runtime=${FLUXHEIM_ACME_MOUNT_TEST_RUNTIME:-}
31+
if [ -z "$runtime" ]; then
32+
if command -v podman >/dev/null 2>&1; then
33+
runtime=podman
34+
elif command -v docker >/dev/null 2>&1; then
35+
runtime=docker
36+
else
37+
echo "ACME mount-boundary smoke requires user namespaces, Podman, or Docker" >&2
38+
exit 1
39+
fi
40+
fi
41+
case "$runtime" in
42+
podman | docker) ;;
43+
*)
44+
echo "unsupported ACME mount-boundary container runtime: $runtime" >&2
45+
exit 1
46+
;;
47+
esac
48+
49+
image=${FLUXHEIM_ACME_MOUNT_TEST_IMAGE:-docker.io/library/debian:13.3-slim@sha256:1d3c811171a08a5adaa4a163fbafd96b61b87aa871bbc7aa15431ac275d3d430}
50+
apparmor_option=
51+
if [ "$runtime" = docker ]; then
52+
# docker-default denies mount(2) independently of CAP_SYS_ADMIN.
53+
apparmor_option=--security-opt=apparmor=unconfined
54+
fi
55+
# apparmor_option is either empty or one fixed argument selected above.
56+
# shellcheck disable=SC2086
57+
"$runtime" run --rm \
58+
--network none \
59+
--read-only \
60+
--cap-drop ALL \
61+
--cap-add SYS_ADMIN \
62+
--security-opt no-new-privileges \
63+
$apparmor_option \
64+
--pids-limit 64 \
65+
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=64m \
66+
--user 0:0 \
67+
--workdir /tmp \
68+
--entrypoint /fluxheim-acme-test \
69+
-v "$test_binary:/fluxheim-acme-test:ro,Z" \
70+
"$image" \
71+
--exact "$test_name" --ignored --nocapture
72+
}
73+
74+
mode=${FLUXHEIM_ACME_MOUNT_TEST_MODE:-auto}
75+
case "$mode" in
76+
auto)
77+
if command -v unshare >/dev/null 2>&1 \
78+
&& unshare --user --map-root-user --mount --propagation private --fork true \
79+
>/dev/null 2>&1; then
80+
run_in_user_namespace
81+
else
82+
echo "ACME mount-boundary smoke: user namespaces unavailable; using constrained container" >&2
83+
run_in_constrained_container
84+
fi
85+
;;
86+
userns)
87+
if ! command -v unshare >/dev/null 2>&1; then
88+
echo "ACME mount-boundary smoke requires util-linux unshare in userns mode" >&2
89+
exit 1
90+
fi
91+
run_in_user_namespace
92+
;;
93+
container)
94+
run_in_constrained_container
95+
;;
96+
*)
97+
echo "unsupported ACME mount-boundary smoke mode: $mode" >&2
98+
exit 1
99+
;;
100+
esac
31101

32102
echo "ACME mount-boundary smoke passed"

scripts/test_starter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ class TestEntry:
106106
"ACME mount-boundary security smoke",
107107
"security",
108108
("scripts/smoke_acme_mount_boundary.sh",),
109-
"Runs the ignored ACME bind-mount regression in isolated user and mount namespaces.",
109+
"Runs the ignored ACME bind-mount regression in a user namespace or constrained container.",
110110
),
111111
TestEntry(
112112
"peer-fill",

scripts/validate-acme-mount-boundary-plan.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,18 @@ require_text() {
2020

2121
require_text "$test_source" '#[ignore = "requires an isolated privileged mount namespace"]'
2222
require_text "$smoke" 'unshare --user --map-root-user --mount --propagation private --fork'
23+
require_text "$smoke" '--cap-drop ALL'
24+
require_text "$smoke" '--cap-add SYS_ADMIN'
25+
require_text "$smoke" '--security-opt no-new-privileges'
26+
require_text "$smoke" '--security-opt=apparmor=unconfined'
27+
require_text "$smoke" '--network none'
28+
require_text "$smoke" '--read-only'
29+
require_text "$smoke" '--pids-limit 64'
30+
require_text "$smoke" '--tmpfs /tmp:rw,nosuid,nodev,noexec,size=64m'
31+
require_text "$smoke" '--workdir /tmp'
2332
require_text "$smoke" '--exact "$test_name" --ignored --nocapture'
2433
require_text "$ci" 'scripts/smoke_acme_mount_boundary.sh'
34+
require_text "$ci" 'FLUXHEIM_ACME_MOUNT_TEST_MODE=container'
2535
require_text "$stable_gate" 'scripts/smoke_acme_mount_boundary.sh'
2636
require_text "$deep_gate" 'FLUXHEIM_GATE_ACME_MOUNT_BOUNDARY'
2737
require_text "$starter" '"acme-mount-boundary"'

0 commit comments

Comments
 (0)