|
| 1 | +# Enforcement (design) — opt-in, externally-implemented intervention |
| 2 | + |
| 3 | +a3s-observer **observes**. This is the design for *optional* intervention (block / redirect), |
| 4 | +kept strictly separate so the observer core stays passive and safe. |
| 5 | + |
| 6 | +## Why separate (first principles) |
| 7 | + |
| 8 | +Observation is passive and read-only — tracepoints → ring buffers **cannot block**. |
| 9 | +Enforcement is security-critical: a bug blocks legitimate traffic or breaks the agent, and it |
| 10 | +needs fail-open/closed semantics + a policy engine. Per "minimal core + external extensions", |
| 11 | +enforcement is an **opt-in extension**, never baked into the observer's blast radius. |
| 12 | + |
| 13 | +## Mechanism (what the core extension would provide) |
| 14 | + |
| 15 | +eBPF *can* enforce — but via different hooks than the observer's tracepoints: |
| 16 | + |
| 17 | +| Intervention | eBPF hook | Effect | |
| 18 | +|---|---|---| |
| 19 | +| Block file open/write | **LSM-BPF** `lsm/file_open`, `lsm/path_*` | return `-EPERM` | |
| 20 | +| Block exec | **LSM-BPF** `lsm/bprm_check_security` | return `-EPERM` | |
| 21 | +| Block / redirect egress | **TC** egress or **cgroup/connect4** | drop / RST / redirect by SNI or peer | |
| 22 | +| Drop at the NIC | **XDP** | drop before the stack | |
| 23 | + |
| 24 | +Honest caveat: you can **drop / RST / redirect** a connection, but you **cannot modify |
| 25 | +encrypted (TLS) payload** — it's encrypted. Only plaintext (pre-TLS / non-TLS) is rewritable |
| 26 | +via TC, which is rarely useful. LSM hooks need `CONFIG_BPF_LSM` (kernel ≥ 5.7). |
| 27 | + |
| 28 | +## External policy (the pluggable part) |
| 29 | + |
| 30 | +eBPF can't do a userspace round-trip per syscall (too slow to block inline). So the split is: |
| 31 | + |
| 32 | +``` |
| 33 | +external policy ──writes──▶ BPF policy maps ──inline lookup──▶ enforcement eBPF |
| 34 | +(allow/deny rules) (keyed by cgroup / SNI / path) (LSM / TC) → allow | deny |
| 35 | +``` |
| 36 | + |
| 37 | +The **policy lives outside the core** — two ways to implement it, both first-class: |
| 38 | + |
| 39 | +1. **In-process** — a Rust `Policy` trait impl (mirrors `IdentityResolver` / `Exporter`): |
| 40 | + ```rust |
| 41 | + pub enum Verdict { Allow, Deny } |
| 42 | + pub trait Policy: Send + Sync { |
| 43 | + fn egress(&self, id: &Identity, sni: Option<&str>, peer: IpAddr) -> Verdict; |
| 44 | + fn file_write(&self, id: &Identity, path: &str) -> Verdict; |
| 45 | + fn exec(&self, id: &Identity, argv: &[String]) -> Verdict; |
| 46 | + } |
| 47 | + ``` |
| 48 | + The enforcer compiles verdicts into policy-map entries the eBPF reads inline. |
| 49 | + |
| 50 | +2. **Out-of-process (fully external / language-agnostic)** — a separate controller consumes |
| 51 | + the observer's existing event stream (NDJSON / OTel) and pushes verdicts through a |
| 52 | + **control API** (CLI / unix-socket / gRPC that updates the policy maps). The policy engine |
| 53 | + (OPA/Rego, a service, your own code in any language) lives entirely outside the binary; the |
| 54 | + core only enforces what the maps say. **This is the "外部实现" path.** |
| 55 | + |
| 56 | +Default policy = `AllowAll` (fail-open — never break an agent unless a rule opts in). |
| 57 | + |
| 58 | +## Fail-safe |
| 59 | + |
| 60 | +- **fail-open** (default): unknown → allow. Observability-first; never break the agent. |
| 61 | +- **fail-closed**: unknown → deny (e.g., an egress allowlist). Security-first; opt-in per rule. |
| 62 | + |
| 63 | +Every deny is *also* emitted as an observed event, so enforcement is auditable. |
| 64 | + |
| 65 | +## Staged plan (same shape the observer was built) |
| 66 | + |
| 67 | +1. Contract + this design + `Policy` / `Verdict` (the seam). |
| 68 | +2. PoC: TC-egress **SNI allowlist** — drop the ClientHello to non-approved providers, driven |
| 69 | + by a policy map. Validate on a **non-prod box**. |
| 70 | +3. PoC: LSM `file_open` deny for a path policy. |
| 71 | +4. Control API for the out-of-process policy path (the language-agnostic external engine). |
| 72 | +5. Harden: fail-safe semantics, per-cgroup scoping, audit-every-deny. |
| 73 | + |
| 74 | +> Enforcement must be validated on a **non-prod box** — blocking real syscalls/egress on a |
| 75 | +> shared prod node is unacceptable. |
0 commit comments