fix(supervisor): fail fast with diagnostic error when CAP_SETPCAP is unavailable#2075
fix(supervisor): fail fast with diagnostic error when CAP_SETPCAP is unavailable#2075waynesun09 wants to merge 1 commit into
Conversation
|
All contributors have signed the DCO ✍️ ✅ |
|
I have read the DCO document and I hereby sign the DCO. |
|
recheck |
|
Few points of note:
|
7320552 to
1dc253d
Compare
|
/ok to test 1dc253d |
ad6106c to
3f95d51
Compare
|
/ok to test 3f95d51 |
|
The new OCSF degraded-mode alert may not fire. The parent-side probe returns early if Could we make the readiness probe test the actual bounding-set clear behavior ? |
adf21ac to
b3a0e2a
Compare
|
/ok to test b3a0e2a |
|
@waynesun09 look to me. Just fix the format issue and we're good to merge |
|
@alangou cool, I'm on it now. |
b3a0e2a to
9ab2fcb
Compare
|
@alangou it's updated, please check, thanks |
|
/ok to test 9ab2fcb |
9ab2fcb to
4d5f5ad
Compare
|
@alangou the new ci clippy failure on backticked AppArmor is fixed, sorry I missed that in the local test |
| toolchain: "1.95.0" | ||
| cache: false | ||
|
|
||
| - name: Run supervisor capability tests without CAP_SETPCAP |
There was a problem hiding this comment.
This test does not align with how we would ideally be testing this feature. We should add an e2e test suite that uses rootless podman, and not just run the unit tests as a regular user.
There was a problem hiding this comment.
Ok, I'm on checking the current e2e test which is with nested container, with the out layer started with --privilege and it might have the AppArmor disabled and could not cover the bug, I'm still testing on it, will let you know the findings.
If we could add the e2e test, do you want me to keep current drop privilege test or just keep it?
There was a problem hiding this comment.
I started #2119 to move the rootless tests out of the container, but I have not yet been able to reproduce the failure you're seeing.
Feel free to comment on the PR if you something obvious sticks out.
@alangou don't take my comments as blocking. If this fixes the regression, we can merge and then add better testing ... Although being able to reproduce the behaviour would have been a win.
There was a problem hiding this comment.
This is triggered by the supervisor image version pinning bug (#2068). The Podman driver defaults supervisor_image to :latest, so an older gateway (pre-#2001, SETPCAP in cap_drop) pulls a newer supervisor (v0.0.73 with bounding::clear()). The gateway's container spec never grants CAP_SETPCAP, but the supervisor now needs it — prctl(PR_CAPBST_DROP) returns EPERM and the supervisor crashes.
I verified locally via podman inspect on a gateway-created container: SETPCAP was in CapDrop, confirming the pre-#2001 container spec.
With a matched gateway+supervisor both post-#2001 (SETPCAP in cap_add), the prctl succeeds because drop_capability_bounding_set() runs before setuid() while CapEff is still full. I need to verify that locally first — will update here once confirmed.
To reproduce: use a pre-#2001 gateway binary with the latest supervisor image (the :latest default makes this happen naturally until #2068 is fixed).
Either way the fix is still valid as defensive code: the supervisor shouldn't crash when CAP_SETPCAP is absent regardless of the reason (version skew, custom container specs, non-podman runtimes).
There was a problem hiding this comment.
Confirmed locally: matched v0.0.73 gateway + supervisor does not crash.
$ openshell status
Version: 0.0.73
$ openshell sandbox create --no-keep -- echo "ok"
Created sandbox: lawful-soldierfish
ok
✓ Deleted sandbox lawful-soldierfish
$ openshell sandbox create --name verify-caps -- bash -c "cat /proc/self/status | grep -i cap; id"
CapBnd: 0000000000000000
uid=998(sandbox) gid=998(sandbox)
Bounding set cleared successfully — drop_capability_bounding_set() runs before setuid() while CapEff still has CAP_SETPCAP, so prctl(PR_CAPBSET_DROP) succeeds.
The crash only reproduces under version skew: pre-#2001 gateway (SETPCAP in cap_drop) + post-#2001 supervisor. The :latest pinning bug (#2068) creates this naturally when the registry publishes a new supervisor ahead of a gateway upgrade.
The fix remains valid as defensive hardening — the supervisor shouldn't crash when CAP_SETPCAP is absent regardless of the cause.
There was a problem hiding this comment.
Reproduced locally with v0.0.72 gateway + v0.0.73 supervisor:
$ /tmp/openshell-072/openshell status
Version: 0.0.72
$ /tmp/openshell-072/openshell sandbox create --no-keep -- echo "ok"
Error: sandbox is not ready
Container exited with code 1
$ podman inspect openshell-sandbox-repro-crash --format '{{json .HostConfig.CapDrop}}'
["CAP_DAC_OVERRIDE","CAP_FSETID","CAP_KILL","CAP_NET_BIND_SERVICE","CAP_SETFCAP","CAP_SETPCAP","CAP_SYS_CHROOT"]
v0.0.72 gateway has CAP_SETPCAP in cap_drop. v0.0.73 gateway removed it (via #2001). The version skew is the trigger.
0fb17ba to
a7db2b7
Compare
|
Rebased on main (includes #2119). Dropped the old Why The e2e builds the gateway from the same checkout ( Pinning a v0.0.72 gateway in e2e isn't good either, coupling a regression test to a specific old release is fragile. Why the unit test job works: The |
|
I am struggling with this change because it degrades the security boundary and I prefer the fail-closed behavior that exists today. I'd be interested in what @drew, @TaylorMutch, and/or @cgwalters have to say about it. |
|
Maybe there should be a config option that allows the degraded behavior 🤔 |
|
Since this is caused by a version mismatch between the gateway and the supervisor, I don't think we should change the handling of the bounding set at this stage. What we should do is:
|
I think we should avoid degrading any boundary and always prefer to fail closed. If there is a capability that is unavailable, then we should find a way to explicitly configure a topology (#2048) to support that configuration. |
…unavailable When the supervisor lacks CAP_SETPCAP in its effective set (for example, a container runtime that does not grant it), the child's prctl(PR_CAPBSET_DROP) returns EPERM during privilege drop. The clear runs inside pre_exec, which cannot emit structured logs, so the failure surfaced only as an opaque child exit with code 1. Keep the bounding-set clear fail-closed and make the failure diagnosable: - Probe PR_CAPBSET_DROP availability in the parent before fork() with a non-destructive bounding::drop() on a capability that is already absent from the bounding set. When the probe detects EPERM with a non-empty bounding set, emit an OCSF DetectionFinding alert and refuse the spawn with an actionable error. - Extend the child-side EPERM error in validate_capability_bounding_set_clear() with the CAP_SETPCAP diagnosis and remediation hint as a backstop for environments the parent probe cannot predict. Related to NVIDIA#2069 Signed-off-by: Wayne Sun <gsun@redhat.com>
2dc67dd to
3bf196a
Compare
|
@maxamillion @elezar @drew Thanks for the feedback — I've reworked the PR to keep the bounding-set clear fail-closed. The What remains is diagnosability: the clear runs inside Supporting genuinely capability-limited runtimes as an explicit opt-in seems like a better fit for the topology work in #2048. Verified in rust:1.95 containers, including a |
Summary
When the supervisor lacks
CAP_SETPCAPin its effective set (for example, a container runtime that does not grant it), the child'sprctl(PR_CAPBSET_DROP)returnsEPERMduring privilege drop. The clear runs insidepre_exec, which cannot emit structured logs, so the failure surfaced only as an opaque child exit with code 1.Following the review discussion, this PR keeps the bounding-set clear fail-closed — no tolerance path, no degraded mode — and instead makes the failure diagnosable:
PR_CAPBSET_DROPavailability beforefork(). When the clear would fail with a non-empty bounding set, it emits an OCSFDetectionFindingalert and refuses the spawn with an actionable error, instead of letting the child die insidepre_execwhere structured logging is unavailable.EPERMerror invalidate_capability_bounding_set_clear()now carries the sameCAP_SETPCAPdiagnosis and remediation hint, as a backstop for environments the parent probe cannot predict.The version-skew trigger that originally surfaced this (older gateway pulling a newer
:latestsupervisor image) was fixed separately by #2070, which closed #2068. This PR addresses the remaining gap from #2069: a supervisor that cannot clear the bounding set should fail fast with a clear diagnosis rather than an undiagnosable crash.Related Issue
Related to #2069
Changes
crates/openshell-supervisor-process/src/process.rs:check_capability_bounding_set_readiness(), called beforefork()underuses_privileged_process_setup(). It probes with a non-destructivebounding::drop()on a capability already absent from the bounding set (the sameprctl(PR_CAPBSET_DROP)syscallbounding::clear()uses), and onEPERMwith a non-empty set emits a High-severity OCSFDetectionFindingand fails the spawn with a diagnostic error.EPERM+ non-empty arm ofvalidate_capability_bounding_set_clear()to return a diagnostic error namingCAP_SETPCAPand the remediation, keeping the existing fail-closed behavior.capability_bounding_set_readiness_matches_clear_availabilitytest; strengthencapability_bounding_set_clear_rejects_nonempty_epermto assert theCAP_SETPCAPdiagnosis.architecture/sandbox.md: Keep the fail-closed invariant text and document the parent-side preflight (diagnostic error + OCSF finding whenCAP_SETPCAPis unavailable).Compared to the previous revision of this PR: the
EPERM-tolerance branch, the degraded-mode documentation, and therootless-capsCI job are all dropped per review feedback.Testing
cargo test -p openshell-supervisor-process --lib -- capability_bounding drop_privileges— 15 passed (rust:1.95 Linux container)--cap-drop SETPCAP(root, non-empty bounding set, deniedPR_CAPBSET_DROP) — 15 passed;drop_privileges_succeeds_for_current_groupexercises the fail-closed error path and the new readiness test hits its error branch with theCAP_SETPCAPdiagnosiscargo clippy -p openshell-supervisor-process --all-targets -- -D warningscleancargo fmt --checkcleanChecklist