# Trusted Candidate Builder — CAP_SYS_PTRACE security contract Status: formal (V2R2C2) · Applies to: `deploy/systemd/aios-trusted-candidate-builder.service` (the live baseline unit) ## 0. Live baseline (rea truth, not nominal) The authoritative deployed unit is the live systemd unit on the `hw` control plane host: - `User=root` / `Group=root` (the builder runs as root) - socket: `aios-trusted-candidate-builder.socket` `ListenStream=/run/aios-trusted-candidate-builder.sock`, `SocketUser=root`, `SocketGroup=gitops`, `SocketMode=0660` - credentials: `LoadCredentialEncrypted` for `signing_private_key` / `builder_hmac` (under `/etc/credstore.encrypted/aios-trusted-candidate-builder/`) and `source_admission_hmac` (under `/etc/credstore.encrypted/aios-release-source-admission/`) - hardening: `NoNewPrivileges`, `PrivateTmp`, `PrivateNetwork`, `ProtectSystem=strict`, `ProtectHome`, `ProtectControlGroups`, `ProtectKernelTunables/Modules/Logs`, `RestrictAddressFamilies=AF_UNIX`, `LockPersonality`, `MemoryDenyWriteExecute`, `RestrictSUIDSGID`, `SystemCallArchitectures=native` - capability: `AmbientCapabilities=CAP_SYS_PTRACE` + `CapabilityBoundingSet=CAP_SYS_PTRACE` (live current state) The tracked `deploy/systemd/aios-trusted-candidate-builder.service` is kept byte-identical (LF-normalized) to the live unit so sha256 drift is detectable: live sha256 = `9d194c23ec981528d29d55f601d1be858635ac6229c7e25c10736df4e44042d3`. > This contract deliberately does NOT introduce a separate non-root > `-ptrace` unit. §2 of the V2R2C2 instruction forbids inventing a distinct > service uid/group, `ProtectProc`/`ProcSubset`, credential paths or socket > ownership that diverge from the live baseline. The live baseline is root + > the single minimal capability, and that is the only variant tracked. ## 1. Why a capability is declared at all `TrustedCandidateBuilder._verify_peer` validates the connecting peer before signing any candidate. It reads three `/proc/` views of the peer: | view | function | failure → error | |---|---|---| | `/proc//cgroup` | `_resolve_cgroups` | `PEER_CGROUP_UNAVAILABLE` | | `/proc//exe` | `_resolve_process_identity` | `PEER_PROCESS_UNAVAILABLE` | | `/proc//cmdline` | `_resolve_process_identity` | `PEER_PROCESS_UNAVAILABLE` | A cross-uid `readlink("/proc//exe")` for a peer of a **different uid** is subject to the Yama `ptrace_scope` sysctl and returns `EACCES` for a process without the minimal `CAP_SYS_PTRACE`. Result: the builder fails closed (`PEER_PROCESS_UNAVAILABLE`), which is safe but loses availability for legitimate cross-uid peers. The minimal `CAP_SYS_PTRACE` restores the ability to *inspect* the peer's `/proc` view across uids. ## 2. Capability sensitivity — MANDATORY reading `CAP_SYS_PTRACE` is a **high-sensitivity capability**. It is not a read-only helper: it broadens the process's observation and debugging authority across uids, including `ptrace`, reading `/proc//mem`, and inspecting another user's process state. It is therefore: - limited to the **single capability** `CAP_SYS_PTRACE` (both `AmbientCapabilities` and `CapabilityBoundingSet`), and - scoped to the dedicated builder service unit only. It **must never** be used to imply "read-only" or "no control authority." It carries real inspection/debug power and is declared for one concrete purpose: restoring cross-uid `/proc//exe` identity resolution for the builder's peer validation. Nothing more. ### Three-state verification contract - **A. No CAP_SYS_PTRACE (fail-closed):** cross-uid `/proc//exe` raises `PermissionError(EACCES)` → resolvers fail closed → `PEER_PROCESS_UNAVAILABLE` (and `PEER_CGROUP_UNAVAILABLE` for cgroup). Never degrade to allow. - **B. Minimal CAP_SYS_PTRACE + legitimate peer:** peer uid in `allowed_peer_uid`, cgroup in `allowed_peer_cgroups`, executable matches `allowed_peer_executable_prefix`, argv matches the runtime argument contract → readiness `READY`. - **C. CAP present but peer illegitimate:** any identity check fails → `PEER_IDENTITY_DENIED` / `PEER_CGROUP_DENIED` / `PEER_PROCESS_DENIED`. The capability **must never bypass the identity policy** — it restores the ability to perform identity validation, not the authorization itself. ## 3. Operational rules - Keep `CapabilityBoundingSet` and `AmbientCapabilities` pinned to exactly `CAP_SYS_PTRACE`. Never use capability wildcards (e.g. `CAP_*` applying to every capability). - Keep `NoNewPrivileges=true`; ambient capabilities are the only inheritance path into the exec'd Python process. - Do not re-introduce a separate non-root unit with a different uid/group/`ProtectProc`/`ProcSubset`/credential paths/socket ownership (see §0). The root + single-capability unit is the live baseline. - Runtime verification (three-state) MUST be executed on the real Linux host that runs systemd; the adversarial states require real uids/cgroups/argv. ## 4. Testing - Local (no `/proc` peer needed): the resolvers are exercised with monkeypatched `PermissionError(EACCES)` — mirroring the exact bytes a missing `CAP_SYS_PTRACE` / cross-uid read produces — covering `cgroup → PEER_CGROUP_UNAVAILABLE`, `exe → PEER_PROCESS_UNAVAILABLE`, `cmdline → PEER_PROCESS_UNAVAILABLE` (see `tests/test_trusted_candidate_builder_captrace.py`). - Host (Linux, required for final acceptance): run `systemd-analyze verify` on the tracked unit; confirm the unit matches the live sha256; launch with/without the capability; confirm A → `PEER_PROCESS_UNAVAILABLE`, B → `READY`, C → `*_DENIED`.