Skip to content

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_TIME writes the host's wall clock because Docker does not namespace CLOCK_REALTIME; PERFMON reads 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. PERFMON reaches 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 demands CAP_SYS_ADMIN instead and leaves PERFMON inert; that is hardening and reads the severity down, as rootless mode does. It does not move the cell: SYS_TIME is the pricing member and no sysctl affects it
  • Evidence: _t_perfmon in scripts/validate_rule_premises.py asserts the grounded behaviour against a live daemon: at --cap-drop ALL a system-wide perf_event_open (pid=-1, cpu=0, kernel samples included) is refused, and --cap-drop ALL --cap-add PERFMON opens one. Measured on Arch at perf_event_paranoid=2 (granted) and Debian 13 at 3 (refused, where --cap-add SYS_ADMIN grants 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_time covers the pricing member: a clock_gettimeclock_settime round-trip of the same timespec is refused without the capability and succeeds with it. That the affected clock is the host's follows from CLOCK_REALTIME not 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

See also

  • CL-0024 — host-code-execution capabilities (CRITICAL)
  • CL-0011 — strong host-adjacent capabilities (HIGH)
  • CL-0027 — bounded, container-scoped capabilities (MEDIUM)