support ambient capabilities - #96
Conversation
tgross
left a comment
There was a problem hiding this comment.
I've left some comments about some details. But before I get into the meat of this, can you do me a favor and review the unshare man page for --keep-caps:
When the --user option is given, ensure that capabilities granted in the user namespace are preserved in the child process.
This seems like the obvious approach to explore here. Maybe it's the wrong approach, but you didn't even comment on it in your PR description, which suggests there's some missing research.
I did explored different approaches before moving forward with the current approach, but missed those to add on the PR description.
Additionally These were the reasons why went with the current approach. |
Landlock is also disabled on RHEL, making that irrelevant. But for some reason I thought we always set user namespaces here and that doesn't seem to be the case, so nevermind on that. I'll re-review in that light. |
| // steps 4+5: restore caps in permitted/effective/inheritable then raise as ambient; | ||
| // for root the permitted set is already full so CAPSET still works without KEEPCAPS |
There was a problem hiding this comment.
If I've changed to a non-root UID/GID by this point, don't I need to also do PR_SET_NO_NEW_PRIVS so that the task we fork can't re-escalate via setuid/setgid binaries and set caps?
Otherwise, we'd also need to restrict the bounding set here after this call.
There was a problem hiding this comment.
Added PR_SET_NO_NEW_PRIVS in step 4 before capset.
tgross
left a comment
There was a problem hiding this comment.
Looking pretty good. I think it'd be nice to see if we can pull out pkg/capabilities if possible. It'll mean less importing the shim into the plugin. Don't forget that we'll need to update the website docs as well once this is ready to ship
079a89f to
bca1bd3
Compare
Adds two new configuration fields allow_caps (plugin level) and cap_add / cap_drop (task level) that let operators and job authors selectively grant Linux ambient capabilities to exec2 tasks. This is the capability model used by Nomad's built-in exec driver, now brought to exec2. The primary use case is letting a non-root dynamic workload user bind a privileged port (e.g. port 80) via CAP_NET_BIND_SERVICE without running as root — something that was structurally impossible before this change. Fixes: #79 Ref: https://hashicorp.atlassian.net/browse/NMD-1096 # modified: go.mod
28ef98c to
2614e02
Compare
| // capNames maps normalized capability names (lowercase, no "cap_" prefix) to | ||
| // their kernel integer values. Built at init time from the moby/sys/capability | ||
| // library's authoritative list — automatically covering every capability the | ||
| // library knows about, with no manual maintenance required. |
There was a problem hiding this comment.
| // capNames maps normalized capability names (lowercase, no "cap_" prefix) to | |
| // their kernel integer values. Built at init time from the moby/sys/capability | |
| // library's authoritative list — automatically covering every capability the | |
| // library knows about, with no manual maintenance required. | |
| // capNames maps normalized capability names (lowercase, no "cap_" prefix) to | |
| // their kernel integer values |
"built at init time... <emdash>... with no manual maintenance required"? Yeah, no kidding it's called an import. Write your own docstrings if needed instead of having the LLM pour slop onto it. What's extra frustrating here is that it's not even true; the list is code-generated and committed into the upstream package, not init-time.
Fixes: #79
Summary
Adds two new configuration fields
allow_capsandalloc_capsthat let operators and job authors selectively grant Linux ambient capabilities to exec2 tasks. This is the capability model used by Nomad's built-in exec driver, now brought to exec2.The primary use case is letting a non-root dynamic workload user bind a privileged port (e.g. port 80) via CAP_NET_BIND_SERVICE without running as root — something that was structurally impossible before this change.
Before This Change
The problem
exec2 ran every task through a three-level process chain, with unshare --setuid responsible for the uid/gid transition:
The kernel enforces a hard rule: any call to
setresuid()with a new non-zero real UID clears the entire ambient capability set. Because unshare --setuid calls setresuid() internally as part of its own uid transition, the ambient set was always wiped before the shim even started. There was no way to recover it.Root cause
Kernel rule from capabilities(7): A call to setresuid(nonzero) unconditionally clears the entire ambient set. The only way to preserve permitted capabilities across that transition is
prctl(PR_SET_KEEPCAPS, 1)set before the uid change whichunsharedoes not do.The Fix — Move the uid/gid Transition Into the Shim
unshare --setuidand--setgidare removed. The shim now receives the target uid, gid, and requested capability names as argv slots, and performs the uid/gid transition itself using the sequence prescribed bycapabilities(7):The dropPrivileges function — four-case matrix
Argv protocol change
New Configuration Fields
Capability names are case-insensitive and accept all four common formats:
Default allow_caps (13 caps — matches Nomad exec driver)
Testing
Details
1) verify alloc_caps works end-to-end:Result:
Result:
Result:
Changes to Security Controls
Are there any changes to security controls (access controls, encryption, logging) in this pull request? If so, explain.