Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 29 additions & 11 deletions charts/protector/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ reads or transmits any cluster data, and the **engine itself stays zero-egress**

| Property | Default | Why |
| ------------------- | -------------------------------------- | --------------------------------------------------------------- |
| Operating posture | **`mode: audit`** (`enforceScope` empty) | Everything observes & proposes; nothing blocks or acts (ADR-0021). Signature + mesh audit-only, engine shadow. Flip `mode: enforce` + `enforceScope` to arm all three surfaces at once. |
| Operating posture | **`mode: audit`** (`enforceScope` empty, `enforceRung: edge-cut`) | Everything observes & proposes; nothing blocks or acts (ADR-0021). Signature + mesh audit-only, engine shadow. Flip `mode: enforce` + `enforceScope` to arm the webhooks and the engine's cut up to `enforceRung` (ADR-0035, default `edge-cut` — the surgical cut alone). |
| Webhook scope | **audit every namespace** (`webhook.excludeNamespaces: []`) | The fail-open audit webhook observes Pod creates cluster-wide, including kube-system / cert-manager / linkerd / argocd / protector. List names in `excludeNamespaces` to opt some out. |
| Webhook failure | audit `failurePolicy: Ignore`; enforcing webhook `Fail` but **scoped to nothing** | The audit webhook never blocks API writes (so auditing every namespace is safe even for kube-system); the fail-closed enforcing webhook matches no namespace until `mode: enforce` + `enforceScope` opt one in. |
| Ingest auth | **on** (`ingestAuth.enabled: true`) | The :9999 runtime ingest requires a bearer token (mounted file only); engine + agent share a chart-provisioned Secret. |
Expand Down Expand Up @@ -73,11 +73,11 @@ Nothing below that **acts on or blocks** your workloads is enabled by default (t
default-on item is the read-only **feed-fetcher** egress, covered below). Arm in this order
and review the decision journal / audit log at each step.

### Enforce: one scope arms all three surfaces (ADR-0021)
### Enforce: one scope arms the webhooks + the engine's cut up to one rung (ADR-0021/0035)

Enforcement is **two settings**: `mode` + `enforceScope`. Flipping `mode: enforce`
arms all three enforcement surfaces together — signature-webhook deny, mesh-webhook
deny, and the engine's reversible network cut — each confined to *exactly*
Enforcement is `mode` + `enforceScope` + `enforceRung`. Flipping `mode: enforce`
arms the signature-webhook deny and the mesh-webhook deny together, and arms the
engine's reversible network cut **up to `enforceRung`** — each confined to *exactly*
`enforceScope`:

```sh
Expand All @@ -98,6 +98,19 @@ enforced in any namespace):
There is **no enforce-everywhere wildcard**: `mode: enforce` with an empty
`enforceScope` is refused (by both helm and the engine at startup).

**The engine's cut arms on an ORDERED LADDER, not all at once (ADR-0035).** The default
`enforceRung: edge-cut` arms *only* the surgical, most-reversible `DenyNetworkPath`
edge-cut — the broader entry/workload quarantines stay propose-only. Escalate to the
quarantines only as an explicit second opt-in, after baking confidence on the edge-cut
rung:

```sh
--set enforceRung=quarantine
```

A rung implies its narrower predecessor — `quarantine` still arms the edge-cut too —
so this is one ordered position to reason about, not independent per-cut toggles.

**The fail-closed webhook and the actuation RBAC are derived from the same
`enforceScope`** — they can no longer drift from what the gates enforce. By default the
audit webhook **fails open** (`failurePolicy: Ignore`, so a protector outage never
Expand Down Expand Up @@ -212,13 +225,17 @@ your own `kev.json` / `epss.csv` into the engine container at `/var/lib/protecto
(e.g. via a ConfigMap/Secret/PVC you manage) and set `PROTECTOR_KEV_FILE` /
`PROTECTOR_EPSS_FILE` accordingly.

### The engine's live cut is armed by `mode: enforce`
### The engine's live cut is armed by `mode: enforce` + `enforceRung`

The engine's reversible network cut is one of the three surfaces `mode: enforce` arms
(above) — there is no separate engine arming switch. In `mode: audit` the engine is
always dry-run; under `mode: enforce` it applies its cut on a corroborated attack path
The engine's reversible network cut is armed together with the webhook surfaces by
`mode: enforce` (above) — there is no separate engine arming switch — but *how much* of
it is armed is its own ORDERED ladder, `enforceRung` (ADR-0035): `edge-cut` (the
default) arms only the surgical edge-cut; `quarantine` also arms the broader
quarantines. In `mode: audit` the engine is always dry-run, regardless of `enforceRung`;
under `mode: enforce` it applies the rung's armed cut(s) on a corroborated attack path
whose endpoints are within `enforceScope`, and the NetworkPolicy write grant is derived
from the same `mode` (they arm together). Choose the CNI mechanism the cut renders with:
from `mode` (they arm together, independent of the rung — the RBAC grant covers the same
object kind at either rung). Choose the CNI mechanism the cut renders with:

```sh
--set engine.actuator=networkpolicy # default — any NetworkPolicy-enforcing CNI (ADR-0010)
Expand Down Expand Up @@ -255,9 +272,10 @@ Requires the `protector-agent` image and probes load-tested on your kernel (see

| Key | Default | Notes |
| ---------------------------- | ------------------------------------ | -------------------------------------------------- |
| `mode` | `audit` | **The posture switch** (ADR-0021). `enforce` arms all three surfaces in `enforceScope`. |
| `mode` | `audit` | **The posture switch** (ADR-0021). `enforce` arms the webhooks + the engine's cut up to `enforceRung`, all in `enforceScope`. |
| `enforceScope.namespaces` | `[]` | Namespace names to enforce (used only under `mode: enforce`). No wildcard. |
| `enforceScope.labels` | `{}` | Pod labels (`key: value`) to enforce anywhere; labels behave like namespaces. |
| `enforceRung` | `edge-cut` | **The engine's cut-severity ladder** (ADR-0035, used only under `mode: enforce`). `edge-cut` arms only the surgical `DenyNetworkPath` cut; `quarantine` also arms the broader entry/workload quarantines. Ordered — `quarantine` implies `edge-cut`. |
| `image.tag` | `""` → chart `appVersion` | Pin a cosign-signed semver tag. |
| `imagePullSecrets` | `[]` | protector publishes to a public ghcr repo. |
| `engine.enabled` | `true` | The mitigation engine (the product). |
Expand Down
8 changes: 5 additions & 3 deletions charts/protector/templates/NOTES.txt
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
protector is installed in namespace {{ .Release.Namespace }}.

Posture (safe-by-default — two settings, ADR-0021):
Posture (safe-by-default — mode + enforceScope + enforceRung, ADR-0021/0035):
{{- $enforce := eq .Values.mode "enforce" }}
{{- $scope := "" }}
{{- if .Values.enforceScope.namespaces }}{{ $scope = printf "namespaces=[%s]" (join "," .Values.enforceScope.namespaces) }}{{ end }}
{{- if .Values.enforceScope.labels }}{{ $scope = printf "%s labels=[%s]" $scope (include "protector.enforceScopeLabels" .) }}{{ end }}
Mode : {{ if $enforce }}ENFORCE in {{ $scope }} — signature+mesh DENY and the engine actuates (via '{{ .Values.engine.actuator }}'), confined to that scope{{ else }}AUDIT (everything observes & proposes; nothing blocks or acts — shadow){{ end }}
Webhook failurePolicy : {{ .Values.webhook.failurePolicy }} (audit) / {{ .Values.webhook.enforcedFailurePolicy }} (enforcing, scoped to enforceScope)
Engine : {{ if .Values.engine.enabled }}{{ if $enforce }}ARMED (reversible network cut inside enforceScope){{ else }}SHADOW (propose-only, touches nothing){{ end }}{{ else }}disabled (webhook only){{ end }}
Engine : {{ if .Values.engine.enabled }}{{ if $enforce }}ARMED at rung '{{ .Values.enforceRung }}' (inside enforceScope){{ else }}SHADOW (propose-only, touches nothing){{ end }}{{ else }}disabled (webhook only){{ end }}
Model : {{ if .Values.engine.model.endpoint }}{{ .Values.engine.model.endpoint }}{{ else }}none (deterministic enumerator only){{ end }}
Breach notifier : {{ if .Values.engine.notify.url }}configured (egress to your sink){{ else }}off (zero egress){{ end }}
Rekor lane : {{ if .Values.rekor.enabled }}enabled (opt-in transparency-log egress){{ else }}off (zero egress){{ end }}
Expand All @@ -31,4 +31,6 @@ Verify cert-manager injected the webhook caBundle:
{{- end }}

Arming is a single, deliberate flip: set `mode: enforce` + `enforceScope` (one scope
arms all three surfaces). There is no enforce-everywhere wildcard.
arms the signature/mesh webhooks and the engine's cut, up to `enforceRung` — an ORDERED
ladder, ADR-0035: `edge-cut` (default) arms only the surgical cut, `quarantine` is a
second explicit opt-in). There is no enforce-everywhere wildcard.
21 changes: 17 additions & 4 deletions charts/protector/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
{{- if and (eq .Values.mode "enforce") (not .Values.enforceScope.namespaces) (not .Values.enforceScope.labels) }}
{{- fail "mode: enforce requires a non-empty enforceScope (namespaces and/or labels) — there is no enforce-everywhere wildcard (ADR-0021)" }}
{{- end }}
{{- /* ADR-0035 arming-ladder validation (defense-in-depth; the engine also falls back to
the narrowest rung on an unrecognized value): enforceRung must be a rung on the ladder. */}}
{{- if not (or (eq .Values.enforceRung "edge-cut") (eq .Values.enforceRung "quarantine")) }}
{{- fail (printf "enforceRung must be 'edge-cut' or 'quarantine', got %q" .Values.enforceRung) }}
{{- end }}
{{- /*
Feed-fetcher sidecar wiring (JEF-238, JEF-243). When feedSync is ON (the default), a native
sidecar (an initContainer with restartPolicy: Always) fetches TWO public read-only feeds
Expand Down Expand Up @@ -238,6 +243,13 @@ spec:
value: {{ join "," .Values.enforceScope.namespaces | quote }}
- name: PROTECTOR_ENFORCE_SCOPE_LABELS
value: {{ include "protector.enforceScopeLabels" . | quote }}
# The ordered network-cut arming ladder (ADR-0035): how far `mode: enforce`
# is armed. `edge-cut` (the default) arms ONLY the surgical DenyNetworkPath
# cut; `quarantine` is an explicit second opt-in that adds the broader
# QuarantineEntry/QuarantineWorkload denies (implying the edge-cut rung too
# — one ordered position, not independent toggles). Ignored under mode: audit.
- name: PROTECTOR_ENFORCE_RUNG
value: {{ .Values.enforceRung | quote }}
{{- if .Values.rekor.enabled }}
# Rekor transparency-log lane (ADR-0020 §4): the one opt-in egress carve-out on
# the signing lane. Off unless enabled here; timeouts/caps use code defaults.
Expand Down Expand Up @@ -283,10 +295,11 @@ spec:
{{- end }}
{{- if .Values.engine.enabled }}
# --- mitigation engine ---
# Arming is derived from the top-level `mode`/`enforceScope` (ADR-0021), NOT a
# per-engine switch: `mode: enforce` arms the reversible network cut confined to
# `enforceScope`; `mode: audit` (default) is dry-run (shadow). `actuator` only
# selects the CNI mechanism used when it does actuate.
# Arming is derived from the top-level `mode`/`enforceScope`/`enforceRung`
# (ADR-0021/0035), NOT a per-engine switch: `mode: enforce` arms the reversible
# network cut(s) up to `enforceRung`, confined to `enforceScope`; `mode: audit`
# (default) is dry-run (shadow). `actuator` only selects the CNI mechanism used
# when it does actuate.
- name: PROTECTOR_ENGINE_ACTUATOR
value: {{ .Values.engine.actuator | quote }}
{{- if .Values.engine.falco.enabled }}
Expand Down
27 changes: 21 additions & 6 deletions charts/protector/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
# The signature + mesh webhooks audit everywhere (log +
# meter, never deny) and the engine runs in shadow
# (proposes cuts, applies none). This is the whole default.
# * mode: enforce + enforceScope {namespaces/labels}: arms ALL THREE
# enforcement surfaces together, confined to EXACTLY that
# * mode: enforce + enforceScope {namespaces/labels}: arms the signature +
# mesh webhook denies together, confined to EXACTLY that
# scope — unsigned/regressed images denied, unmeshed Pods
# denied, and the engine applies its reversible network cut.
# denied. The engine's network cut arms up to enforceRung
# below (ADR-0035): edge-cut (DEFAULT) arms ONLY the
# surgical DenyNetworkPath cut; quarantine is a SECOND
# explicit opt-in that also arms the broader quarantines.
# The fail-closed enforcing webhook + the actuation RBAC are
# DERIVED from `enforceScope` (they can't drift). There is no
# per-surface toggle and NO enforce-everywhere wildcard.
Expand Down Expand Up @@ -39,12 +42,13 @@

replicaCount: 1

# === OPERATING POSTURE (ADR-0021) — the two settings that govern enforcement ===
# === OPERATING POSTURE (ADR-0021 + ADR-0035) — mode + enforceScope + enforceRung ===
#
# mode: audit | enforce
# audit (DEFAULT) — observe & propose everywhere; nothing blocks or acts (shadow).
# enforce — arm all three enforcement surfaces (signature webhook deny, mesh webhook
# deny, engine live network-cut) confined to EXACTLY enforceScope below.
# enforce — arm the signature webhook deny, the mesh webhook deny, and — up to
# enforceRung below (ADR-0035) — the engine's live network-cut, confined
# to EXACTLY enforceScope below.
mode: audit
# The ONE enforced scope, honoured identically by all three surfaces and by the derived
# fail-closed webhook selector + actuation RBAC. Used only when mode: enforce.
Expand All @@ -62,6 +66,17 @@ enforceScope:
namespaces: []
labels: {}

# The ORDERED network-cut arming ladder (ADR-0035) — how far `mode: enforce` is armed.
# One position, not independent per-cut toggles: a higher rung always implies its
# narrower predecessor. Used only when mode: enforce; ignored under mode: audit.
# edge-cut (DEFAULT) — arms ONLY the surgical DenyNetworkPath edge-cut, the
# narrowest, most-reversible cut. The quarantines stay propose-only.
# quarantine — an explicit SECOND opt-in beyond edge-cut: also arms the default-deny
# entry quarantine and the compromised-workload quarantine (both still the
# same additive/reversible network-deny mechanism, ADR-0010). Escalate here
# only after baking confidence on edge-cut.
enforceRung: edge-cut

image:
repository: ghcr.io/thejefflarson/protector
pullPolicy: IfNotPresent
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0035-per-cut-class-arming-ladder.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 0035. Per-cut-class arming granularity: an ordered ladder under `enforce`

- Status: Proposed
- Status: Accepted
- Date: 2026-07-31

## Context
Expand Down
Loading