Skip to content

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_PTRACE to trace, or a file the workload uid cannot already read for DAC_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_PTRACE writes process memory, so read-only does not apply. It would fit DAC_READ_SEARCH alone, which is not the pricing member
  • Derived: Second flaw × Single container = MEDIUM
  • Shipped: MEDIUM
  • Scoping assumptions: DAC_READ_SEARCH's open_by_handle_at host read, and SYS_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; and pid: host, flagged by CL-0010). That is what the clause is for. PERFMON and SYS_TIME were 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 narrows SYS_PTRACE's same-uid reach. It does not move the cell: the rule is priced on the cross-uid grant, which no ptrace_scope value affects
  • Evidence: _cl0011 in scripts/validate_rule_premises.py proves only that cap_add deposits 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_ATTACH to a different-uid process in the same container is refused (Operation not permitted), and adding SYS_PTRACE attaches. The probe crosses a uid boundary deliberately — that test is ptrace_may_access's capability check and does not depend on kernel.yama.ptrace_scope, whereas the same-uid case does and is not the grant (it succeeds with every capability dropped). _t_dac_read_search covers the second member: a mode-000, other-owned file is unreadable at --cap-drop ALL and readable with DAC_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 with pid: "service:<name>" rather than pid: 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

See also

  • CL-0024 — host-code-execution capabilities (CRITICAL)
  • CL-0011 — strong host-adjacent capabilities (HIGH)
  • CL-0028 — host-reaching capabilities (HIGH), which PERFMON and SYS_TIME moved to
  • CL-0010pid: host, which removes SYS_PTRACE's namespace confinement