CL-0028: Host-reaching capabilities (PERFMON, SYS_TIME)¶
Severity: HIGH
Derivation (see severity model):
- Baseline: A — the attacker already has code execution in this container, as the workload uid
- Precondition: Direct — one supported syscall realises the impact.
clock_settime(2)sets the clock;perf_event_open(2)opens the host-wide event. Neither needs a published technique, a defect outside this file, or anything from the image - Impact: Host — both members reach past the container onto the host itself, with no sibling key required.
SYS_TIMEwrites the host's wall clock because Docker does not namespaceCLOCK_REALTIME;PERFMONreads host processes and kernel samples. This is the axis on which the rule was split from CL-0027, whose members stay inside the container and depend on what the image supplies - Qualifier/modifier: integrity-only — priced at
SYS_TIME, whose grant is a write that corrupts host state without disclosing anything or granting any control over the host.PERFMONreaches the same tier by the other route,read-only: it is pure observation. Both members land at HIGH, so the rule is homogeneous at the tier even though the two take different qualifiers to get there - Derived: Direct × Host + integrity-only = HIGH
- Shipped: HIGH
- Scoping assumptions: none. That is the point of the split — neither member's reach is conditional on another key in the file, so there is nothing to hold at a secure default and nothing to scope away
- Daemon assumptions: grounded at the upstream kernel default
kernel.perf_event_paranoid = 2(ADR-020). Debian and Ubuntu ship 3, a downstream hardening patch that demandsCAP_SYS_ADMINinstead and leavesPERFMONinert; that is hardening and reads the severity down, as rootless mode does. It does not move the cell:SYS_TIMEis the pricing member and no sysctl affects it - Evidence:
_t_perfmoninscripts/validate_rule_premises.pyasserts the grounded behaviour against a live daemon: at--cap-drop ALLa system-wideperf_event_open(pid=-1, cpu=0, kernel samples included) is refused, and--cap-drop ALL --cap-add PERFMONopens one. Measured on Arch atperf_event_paranoid=2(granted) and Debian 13 at 3 (refused, where--cap-add SYS_ADMINgrants it instead — the check asserts that fallback rather than assuming it, and reports the host as hardened rather than the premise as wrong)._t_sys_timecovers the pricing member: aclock_gettime→clock_settimeround-trip of the same timespec is refused without the capability and succeeds with it. That the affected clock is the host's follows fromCLOCK_REALTIMEnot being namespaced; it is reasoned, not measured — the round-trip deliberately changes nothing
What it detects¶
cap_add entries naming a capability whose reach lands on the host:
| Capability | Grants | Why it is not CRITICAL |
|---|---|---|
SYS_TIME |
set the system clock, host-global — Docker does not isolate CLOCK_REALTIME |
it corrupts host state; it does not execute code on the host or hand over control of it |
PERFMON |
perf_event_open across the host — kernel samples, every process on the machine |
a read primitive: it discloses, it does not write or execute |
Why it matters¶
SYS_TIME is the one most often underestimated. The clock is host-global — a
container holding this capability moves the host's wall clock, and every workload
on the machine sees it. Certificate validation, time-based one-time passwords and
Kerberos all break together, and the failure looks like an outage rather than an
attack. Rolling the clock backwards is worse than forwards: it can bring
expired certificates back inside their validity window and reopen TOTP windows
that have already been used.
PERFMON is a host-wide read that needs no other key in the compose file. On a
kernel at the upstream default it samples across every process on the machine,
which is a credible route to material from workloads that have nothing to do
with this container.
Neither hands over the host, which is why they sit below CL-0024 — but both leave this container, which is why they sit above CL-0027.
Fix¶
Remove the capability unless the workload demonstrably needs it:
# Before
services:
ntp:
image: chrony:4.5
cap_add:
- SYS_TIME
# After — sync the clock on the host and let containers inherit it
services:
ntp:
image: chrony:4.5
cap_drop:
- ALL
When to suppress¶
These have real legitimate uses, so suppression here is ordinary rather than exceptional — but both have a better answer than the capability:
SYS_TIME— an NTP client (chrony, ntpd) in a container. Prefer syncing the clock on the host; a container that sets the host clock is a design smell even when it works.PERFMON— profilers and APM agents. Prefer profiling from the host, where the same access needs no container capability.
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 |
|---|---|
| T1070 Indicator Removal | Stealth |
| T1499 Endpoint Denial of Service | Impact |
| T1003 OS Credential Dumping | Credential Access |