Skip to content

feat(config): collapse the operator surface to two settings — mode + enforceScope (JEF-274)#147

Merged
thejefflarson merged 3 commits into
mainfrom
feat/config-collapse-mode-enforcescope
Jul 3, 2026
Merged

feat(config): collapse the operator surface to two settings — mode + enforceScope (JEF-274)#147
thejefflarson merged 3 commits into
mainfrom
feat/config-collapse-mode-enforcescope

Conversation

@thejefflarson

Copy link
Copy Markdown
Owner

What & why

Collapses protector's ~four-dozen operator settings toward ~a dozen by replacing the
drift-prone per-surface enforcement knobs with two settings (ADR-0021):

  • mode: audit (DEFAULT) — everything observes & proposes; nothing blocks or acts (shadow).
  • mode: enforce + enforceScope {namespaces, labels} — the ONE scope. Arms all three
    enforcement surfaces together, each confined to exactly that scope: signature-webhook
    deny + mesh-webhook deny + the engine's reversible network cut. The fail-closed
    enforcing-webhook selector
    and the actuation RBAC grant are DERIVED from the same
    enforceScope, so they can no longer drift. No per-surface toggle, no
    enforce-everywhere wildcard
    (empty enforce is refused by both the engine at startup
    and helm at render).

ADR-0021 records the model and the load-bearing decision that one enforceScope now arms
the engine's live cut (a mutation, unlike the view-only ADR-0016 surface) — the widened
blast radius of flipping enforce, guarded by audit-being-default + no-wildcard + the
cut keeping all its own gates (corroboration, blast-radius, adjudicator veto, self-revert).

Actuator-count finding

PROTECTOR_ENGINE_ACTUATOR is kept (not made a constant): more than one live actuator
is real per ADR-0007/0010 — networkpolicy (IsolationActuator, flannel/kube-router / any
NetworkPolicy-enforcing CNI) and adminnetworkpolicy (KubeActuator, surgical ANP edge-cut
for Cilium/Calico), plus dryrun. It's a per-cluster CNI choice (real infra), defaulted
to the portable networkpolicy.

Old → new migration (all legacy defaults were audit ⇒ safe default unchanged)

Removed knob Now derived from
PROTECTOR_ENFORCE_NAMESPACES / _LABELS mode + enforceScope
PROTECTOR_MESH_ENFORCE_NAMESPACES / _LABELS mode + enforceScope
PROTECTOR_ENGINE_ENABLE mode: enforce ⇒ network cut armed
PROTECTOR_ENGINE_ENFORCE_NAMESPACES enforceScope (namespaces + labels)
PROTECTOR_ENGINE (on/off master) engine always runs (shadow by default)
chart engine.actuationRBAC mode: enforce
chart webhook.enforcedNamespaceSelector enforceScope
PROTECTOR_ENGINE_HYPOTHESIS=model a model endpoint being configured
PROTECTOR_INGEST_TOKEN (inline) standardized on PROTECTOR_INGEST_TOKEN_FILE
chart KEV/EPSS file paths fixed mount default in code (JEF-273 owns feeds)
chart verify/cache/max-image/model/notify tuning code-default env escape hatches
— added — opt-in rekor lane (rekor.enabled / url, ADR-0020 §4)

Kept unchanged: PROTECTOR_ALLOW_EXTERNAL_MODEL / _NOTIFY (separate egress escape
hatches) and all real infra (ADDR, TLS, DASHBOARD_ADDR, JOURNAL_PATH, CLUSTER_LABEL,
TUF_CACHE, FALCO_ADDR, AUDIT_ADDR, REGISTRY_*, model endpoint/name, notify url/verbose).

Operator-surface count

Chart env knobs before ≈ 48 → after ≈ a dozen operator-facing values (mode,
enforceScope, engine.actuator, engine.model.*, engine.notify.*, rekor.*,
signature.gatedPrefixes/identityRegexp/oidcIssuer, ingest/cert/feed infra).

Manual security review

  • mode: audit is byte-identical to today's safe default: empty EnforceScopes
    (audit everywhere) + EnabledActions::none()build_actuator returns the dry-run
    actuator before reading the actuator value; webhooks render audit-everywhere +
    match-nothing sentinel; RBAC grants no NetworkPolicy write; the buried tuning falls to
    the same code defaults (5 / 300 / 32) the old chart set.
  • enforce arms EXACTLY the three surfaces in EXACTLY enforceScope, never wider:
    the gates share one EnforceScope; the engine ActuationScope matches namespace or
    Pod label and requires every cut endpoint in scope (a scope leak can't widen it);
    the webhook selector maps namespaces→namespaceSelector In and labels→objectSelector
    In (AND-ing a mixed scope narrows to the intersection — never wider); no wildcard
    (empty enforce refused twice). Model promotion (judgement) is no longer
    operator-enabled — a conservative removal of a widening path.

Tests / gates (from engine/)

  • cargo fmt, cargo build, cargo build --example dashboard_preview — clean
  • cargo clippy --all-targets — no warnings
  • cargo test — 499 + 6 + 9 + 1 pass, 1 ignored (added Posture derivation tests,
    ActuationScope label-scoping test, re-pointed the ingest-token test)
  • helm lint charts/protector — passes; helm template with audit-default AND
    mode=enforce (namespaces + labels + adminnetworkpolicy) verified; all chart CI
    invariants (fail-closed webhook, ingest authn, feed sidecar) pass locally, re-pointed
    at the new scope

Decisions made (autonomous)

  • Audit webhook no longer excludes enforced namespaces (relies on the fail-closed
    webhook dominating on outage — safe double-validation) to avoid the impossible
    OR-exclusion across namespace/object selectors; documented in ADR-0021.
  • Mixed enforceScope (namespaces AND labels): the fail-closed webhook AND-s the two
    selectors (intersection for outage-blocking); the union is still fully enforced when
    protector is up. Prefer a single axis per scope. Flagged in ADR-0021.
  • Model-timeout code default raised 30 → 120s so burying the chart knob doesn't
    regress model deploys.
  • KEV/EPSS: added fixed-path code defaults (env now a true escape hatch); left the
    feed-sidecar wiring to JEF-273. ANP actuation RBAC added when
    actuator: adminnetworkpolicy (was a pre-existing gap).
  • e2e.sh re-pointed at mode/enforceScope (in-repo test that exercises the engine).

🤖 Generated with Claude Code

thejefflarson and others added 3 commits July 2, 2026 23:33
…enforceScope (JEF-274)

Replace the drift-prone per-surface enforcement knobs with one honest switch.
`mode: audit` (default) observes & proposes everywhere; `mode: enforce` +
`enforceScope {namespaces,labels}` arms all three enforcement surfaces together —
signature-webhook deny, mesh-webhook deny, and the engine's reversible network cut —
each confined to exactly that scope, with the fail-closed enforcing-webhook selector
and the actuation RBAC grant DERIVED from the same source (ADR-0021). No per-surface
toggle, no enforce-everywhere wildcard (empty enforce is refused by both the engine and
helm).

Engine (main.rs): parse mode + enforceScope, derive the signature/mesh EnforceScope,
the engine EnabledActions, and the ActuationScope from one Posture. ActuationScope now
matches Pod labels as well as namespaces (labels behave like namespaces), and a cut is
in scope only when every workload endpoint is — so a scope leak can never widen
actuation. Subsume PROTECTOR_ENGINE (engine always runs, shadow by default),
PROTECTOR_ENGINE_ENABLE, and PROTECTOR_ENGINE_ENFORCE_NAMESPACES. Derive
model-hypothesis from a configured model endpoint (drop the opt-in). Standardize the
ingest token on the mounted-file form (drop the inline env variant). Default the
KEV/EPSS feed paths to the fixed mount (JEF-273 owns the feed mechanism). Bury the
verify/cache/max-image/model/notify/rekor tuning as code-defaulted env escape hatches
(raise the model-timeout code default to 120 so burying the chart knob doesn't regress
model deploys).

PROTECTOR_ENGINE_ACTUATOR is KEPT: more than one live actuator is real per ADR-0007/0010
(networkpolicy for flannel/kube-router, adminnetworkpolicy for Cilium/Calico) — a
per-cluster CNI choice, not a posture toggle. Defaulted to the portable networkpolicy.

Chart: expose `mode` + `enforceScope`; render the deployment env, the enforcing-webhook
namespaceSelector/objectSelector, and the actuation RBAC from them; drop the removed
values/env; add the opt-in `rekor` lane. The chart's fail-closed-webhook and ingest-authn
CI invariants still hold (re-pointed at the new scope). ADR-0021 records the two-setting
model and the load-bearing decision that one enforceScope arms the live cut. The e2e
harness is re-pointed at mode/enforceScope.

Closes JEF-274

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP
…(review nits)

- scripts/e2e.sh: reject YAML-unsafe chars in the operator-set PROTECTOR_E2E_MODEL /
  _MODEL_NAME before they're interpolated into the Deployment heredoc. A test-harness
  input (not an untrusted boundary), but a stray quote/newline could corrupt the
  manifest — fail fast instead. (soundcheck Stop-hook nit; severity overstated for a
  test script, but the guard is cheap and removes the latent break.)
- runtime.rs / audit.rs: the unauthenticated-ingest warning named both
  PROTECTOR_INGEST_TOKEN and _FILE; only _FILE is supported after the token
  standardization in this PR — update the message. (security-review cosmetic nit.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP
… the ADR-0021 invariant gate)

The new "Assert mode: enforce derives the pod-label objectSelector and actuation
RBAC" step name is an unquoted YAML scalar containing ': ', which the workflow
parser reads as a nested mapping — the whole Chart workflow failed to load at 0s
("workflow file issue"), so the fail-closed-webhook / RBAC-derivation / sentinel-scope
invariant assertions this PR adds never actually ran. Quote the name; workflow parses
and all chart assertions pass locally (helm lint clean).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP
@thejefflarson
thejefflarson merged commit fd06789 into main Jul 3, 2026
4 checks passed
@thejefflarson
thejefflarson deleted the feat/config-collapse-mode-enforcescope branch July 3, 2026 07:31
thejefflarson added a commit that referenced this pull request Jul 3, 2026
…lid (JEF-276) (#148)

The signing observer (JEF-261) was keyless-only: it read posture from the
Fulcio certificate, so any correctly-signed image that wasn't
keyless-Fulcio-on-a-.sig-tag fell into "signature present but doesn't verify"
and rendered as the loud INVALID posture. Real images signed with other valid
schemes were falsely alarmed:

  - quay.io/jetstack/cert-manager-cainjector (key-based `cosign sign --key`:
    no Fulcio cert, valid Rekor bundle) → was mislabeled invalid.
  - docker.io/curlimages/curl (OCI-referrer sig on a multi-arch index +
    a Rekor/TUF trust-root variance) → was mislabeled invalid.

Split SigningPosture honestly (ADR-0020): keyless-verified `Signed` (unchanged,
the only trusted identity), calm `SignedKeyBased` (verified Rekor bundle, no
Fulcio identity — opaque signer), calm `UnverifiableHere` (couldn't verify
against our trust root — not "forged"), and reserve the loud `InvalidSignature`
for a genuine failure. Classification is refactored behind a testable
`LayerFacts` projection over the fetched layers. Discovery already covers both
the legacy `.sig` tag and OCI 1.1 referrers via sigstore-rs 0.14's
`trusted_signature_layers`, so the bug was purely classification — no new egress.

Drift (JEF-264) treats the two calm states as continuous (a genuine signature
REMOVAL still regresses loudly); the dashboard renders them calm (distinct
colour/glyph/word), with only genuine invalid on the loud/breach rail.

Kept the footprint out of policies/signature/mod.rs to minimize conflict with
JEF-274 (#147).


Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant