CL-0027: Bounded-grant capabilities (SYS_PTRACE, DAC_READ_SEARCH)¶
Severity: MEDIUM
Derivation (see severity model):
- Baseline: A — the attacker already has code execution in this container, as the workload uid
- Precondition: Second flaw — each member's grant converts into impact only where the image supplies something this file cannot see: a process running as a different uid for
SYS_PTRACEto trace, or a file the workload uid cannot already read forDAC_READ_SEARCH. Same-uid tracing and same-uid reads need no capability at all. Parallel to CL-0003, whose named primitive is likewise a property of the image - Impact: Single container — priced at
SYS_PTRACE, whose grant is read and write access to another process's memory inside this container.DAC_READ_SEARCH's host leg reaches further but only where a host bind mount is already present — a sibling the model scores in isolation and CL-0013/CL-0025 flag separately - Qualifier/modifier: none —
SYS_PTRACEwrites process memory, soread-onlydoes not apply. It would fitDAC_READ_SEARCHalone, which is not the pricing member - Derived: Second flaw × Single container = MEDIUM
- Shipped: MEDIUM
- Scoping assumptions:
DAC_READ_SEARCH'sopen_by_handle_athost read, andSYS_PTRACE's reach into host processes, are both excluded — each is conditional on a sibling key the model scores in isolation (a host bind mount, flagged by CL-0013/CL-0025; andpid: host, flagged by CL-0010). That is what the clause is for.PERFMONandSYS_TIMEwere once excluded here too, which was wrong — neither depends on a sibling key, so there was nothing to hold at a secure default. They are CL-0028's - Daemon assumptions: grounded at the upstream kernel default
kernel.yama.ptrace_scope = 0(ADR-020). Arch and Ubuntu ship 1, which narrowsSYS_PTRACE's same-uid reach. It does not move the cell: the rule is priced on the cross-uid grant, which noptrace_scopevalue affects - Evidence:
_cl0011inscripts/validate_rule_premises.pyproves only thatcap_adddeposits the capability into the effective set — not what any member grants. The pricing member is checked by_t_sys_ptrace: with--cap-drop ALL --cap-add SETUID,SETGID,PTRACE_ATTACHto a different-uid process in the same container is refused (Operation not permitted), and addingSYS_PTRACEattaches. The probe crosses a uid boundary deliberately — that test isptrace_may_access's capability check and does not depend onkernel.yama.ptrace_scope, whereas the same-uid case does and is not the grant (it succeeds with every capability dropped)._t_dac_read_searchcovers the second member: a mode-000, other-owned file is unreadable at--cap-drop ALLand readable withDAC_READ_SEARCH. Both run against a live daemon on every CI run
What it detects¶
cap_add entries whose grant is real but bounded:
| Capability | Grants | Why it is not higher |
|---|---|---|
SYS_PTRACE |
trace and read other processes' memory | confined to this container's own PID namespace unless pid: host is also set, which CL-0010 flags |
DAC_READ_SEARCH |
bypass file-read checks; read host files via open_by_handle_at |
the host-read leg needs a host bind mount to be present, and that mount is flagged by CL-0013 or CL-0025 |
Why it matters¶
These weaken isolation without opening an escape, and without leaving the container. That distinction is the whole point of the tier: before the split they were graded HIGH alongside kernel-module loading, which made the higher tiers less believable and put a debugger sidecar in the same bucket as a container escape.
Both members are also conditional on something the compose file cannot see. On the overwhelmingly common single-process, single-uid container, neither grants anything at all: same-uid tracing and same-uid reads need no capability. What they add is reach over a neighbour the image happens to supply — which is why the precondition is Second flaw rather than Direct.
If you are looking for PERFMON or SYS_TIME, they are CL-0028.
Their reach is host-wide and needs nothing from the image, so they do not belong
in this cell.
Fix¶
Remove the capability unless the workload demonstrably needs it:
# Before
services:
profiler:
image: myprofiler:1.0
cap_add:
- SYS_PTRACE
# After — if the sidecar genuinely profiles a neighbour, scope it explicitly
services:
profiler:
image: myprofiler:1.0
cap_drop:
- ALL
cap_add:
- SYS_PTRACE
pid: "service:app" # not pid: host — see CL-0010
When to suppress¶
These are the capabilities with the most legitimate uses in the set, so suppression here is ordinary rather than exceptional:
SYS_PTRACE— debugger and APM sidecars. Scope them to the container they inspect withpid: "service:<name>"rather thanpid: host.DAC_READ_SEARCH— backup and indexing agents that must read files the workload uid does not own. Prefer fixing the ownership.
Suppress per service with a reason: naming the workload.
ATT&CK coverage¶
Remediating this finding contributes to mitigating the following MITRE ATT&CK techniques (pinned to ATT&CK v18). compose-lint is a static analyser, so this is mitigation coverage — it detects nothing at runtime.
| Technique | Tactic |
|---|---|
| T1055 Process Injection | Privilege Escalation |
| T1003 OS Credential Dumping | Credential Access |
| T1552.001 Unsecured Credentials: Credentials In Files | Credential Access |
| T1611 Escape to Host | Privilege Escalation |