feat(config): collapse the operator surface to two settings — mode + enforceScope (JEF-274)#147
Merged
Merged
Conversation
…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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 threeenforcement 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, noenforce-everywhere wildcard (empty
enforceis refused by both the engine at startupand helm at render).
ADR-0021 records the model and the load-bearing decision that one
enforceScopenow armsthe 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 + thecut keeping all its own gates (corroboration, blast-radius, adjudicator veto, self-revert).
Actuator-count finding
PROTECTOR_ENGINE_ACTUATORis kept (not made a constant): more than one live actuatoris real per ADR-0007/0010 —
networkpolicy(IsolationActuator, flannel/kube-router / anyNetworkPolicy-enforcing CNI) and
adminnetworkpolicy(KubeActuator, surgical ANP edge-cutfor Cilium/Calico), plus
dryrun. It's a per-cluster CNI choice (real infra), defaultedto the portable
networkpolicy.Old → new migration (all legacy defaults were audit ⇒ safe default unchanged)
PROTECTOR_ENFORCE_NAMESPACES/_LABELSmode+enforceScopePROTECTOR_MESH_ENFORCE_NAMESPACES/_LABELSmode+enforceScopePROTECTOR_ENGINE_ENABLEmode: enforce⇒ network cut armedPROTECTOR_ENGINE_ENFORCE_NAMESPACESenforceScope(namespaces + labels)PROTECTOR_ENGINE(on/off master)engine.actuationRBACmode: enforcewebhook.enforcedNamespaceSelectorenforceScopePROTECTOR_ENGINE_HYPOTHESIS=modelPROTECTOR_INGEST_TOKEN(inline)PROTECTOR_INGEST_TOKEN_FILErekorlane (rekor.enabled/url, ADR-0020 §4)Kept unchanged:
PROTECTOR_ALLOW_EXTERNAL_MODEL/_NOTIFY(separate egress escapehatches) 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: auditis byte-identical to today's safe default: emptyEnforceScopes(audit everywhere) +
EnabledActions::none()⇒build_actuatorreturns the dry-runactuator 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.
enforcearms EXACTLY the three surfaces in EXACTLYenforceScope, never wider:the gates share one
EnforceScope; the engineActuationScopematches namespace orPod label and requires every cut endpoint in scope (a scope leak can't widen it);
the webhook selector maps namespaces→
namespaceSelectorIn and labels→objectSelectorIn (AND-ing a mixed scope narrows to the intersection — never wider); no wildcard
(empty
enforcerefused twice). Model promotion (judgement) is no longeroperator-enabled — a conservative removal of a widening path.
Tests / gates (from
engine/)cargo fmt,cargo build,cargo build --example dashboard_preview— cleancargo clippy --all-targets— no warningscargo 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 templatewith audit-default ANDmode=enforce(namespaces + labels + adminnetworkpolicy) verified; all chart CIinvariants (fail-closed webhook, ingest authn, feed sidecar) pass locally, re-pointed
at the new scope
Decisions made (autonomous)
webhook dominating on outage — safe double-validation) to avoid the impossible
OR-exclusion across namespace/object selectors; documented in ADR-0021.
enforceScope(namespaces AND labels): the fail-closed webhook AND-s the twoselectors (intersection for outage-blocking); the union is still fully enforced when
protector is up. Prefer a single axis per scope. Flagged in ADR-0021.
regress model deploys.
feed-sidecar wiring to JEF-273. ANP actuation RBAC added when
actuator: adminnetworkpolicy(was a pre-existing gap).mode/enforceScope(in-repo test that exercises the engine).🤖 Generated with Claude Code