feat(engine): ingress exposure observer — route-transitive Exposure::Internet through declared L7 routes (ADR-0038) - #313
Merged
thejefflarson merged 2 commits intoAug 1, 2026
Conversation
…gress routes (ADR-0038)
Add IngressExposureAdapter: a backend Service that a live, internet-exposed
Ingress controller routes to is promoted to Exposure::Internet, so it becomes
a normal entry and flows through the existing edge-CVE promotion lane with
zero change to proof, prompt, guards, or menu. Entry derivation already keys
solely on Workload.exposure (reason/proof/chain.rs), so setting that field is
the entire integration surface.
Controller-anchoring (D1) matches a live controller's Ingress.status.loadBalancer
address against its own fronting Service's status.loadBalancer address — the
one deterministic, controller-agnostic K8s signal available, since IngressClass
carries no object reference to the workload implementing spec.controller. An
Ingress whose controller never claimed it with a live address doesn't
propagate (under-promote fail direction). Promotion runs to a bounded fixpoint
(D2, bounded by node count) so a promoted backend can anchor further routes it
serves ("chains compose").
Plumbs Snapshot.ingresses/ingress_classes through the poll observer and the
watch reflectors, and grants read-only networking.k8s.io ingresses/
ingressclasses RBAC. Flips ADR-0038 to Accepted with a D1/D2 addendum.
Closes JEF-697.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP
thejefflarson
enabled auto-merge (squash)
August 1, 2026 20:24
… absent The Ingress/IngressClass watch is the first ADR-0038 grant that can legitimately be missing (an older render, or the forked cluster chart before its RBAC hand-port lands) — every other watched type is always granted. A 403/404 hit kube-runtime's default retry-forever watch loop, which spammed "watch error ... forbidden" and starved the rest of the engine loop, taking the whole proof loop down with it (e2e: "structural chain web->session-key never proved within 300s"). Add observe::ingress_availability: a shared classifier for the Forbidden/ absent-API case and a log-once guard. Both the watch reflector (a one-time preflight LIST decides whether to start the persistent watch at all) and Snapshot::observe's initial LIST now degrade to an empty ingresses/ ingress_classes pair instead of retrying indefinitely or failing the whole call — IngressExposureAdapter already no-ops on an empty list (the existing under-promote fail direction), so this never touches the proof loop's correctness, only its resilience to a missing grant. Any other error (a transient blip) still retries exactly like every other watched type. Also grants the same networking.k8s.io ingresses/ingressclasses RBAC in scripts/e2e.sh (a hand-maintained mirror of the chart's ClusterRole, separate from charts/protector/templates/clusterrole.yaml which the prior commit already updated) so the e2e scenario exercises route-transitive exposure end-to-end rather than just degrading. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP
thejefflarson
deleted the
thejefflarson/jef-697-ingress-exposure-observer-route-transitive-exposureinternet
branch
August 1, 2026 20:50
5 tasks
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.
Summary
Implements ADR-0038: makes internet-exposure follow declared L7 routes so a
route-forwarded backend of a live internet-exposed Ingress controller becomes
a normal
Exposure::Internetentry, reusing the entire existing edge-CVEpromotion lane (proof, prompt, guards, menu, ledger, bench) with zero new
evidence class, model tag, or graph relation.
IngressExposureAdapter(engine/src/engine/observe/adapter/ingress_exposure.rs),registered right after
ExposureAdapterindefault_adapters().IngressClassto the workload implementing itsspec.controllerstring.Rather than guess via a naming/label convention (reopening the
over-promotion hazard the ADR calls out), the adapter matches a live
controller's
Ingress.status.loadBalanceraddress against its own frontingService's
status.loadBalanceraddress — the one deterministic,controller-agnostic signal the K8s API actually provides. An Ingress whose
controller never claimed it with a live address doesn't propagate
(under-promote fail direction, per ADR-0012's precedent).
makes no new promotion, bounded by
graph.node_count(), so a promotedbackend can anchor further routes it serves ("chains compose") with no
risk of an unbounded loop.
Snapshot.ingresses/Snapshot.ingress_classesthrough both thepoll observer (
Snapshot::observe) and the watch reflectors(
run_loop.rs), and grants read-onlynetworking.k8s.ioingresses/ingressclassesRBAC in the chart.docs/adr/0038-transitive-internet-exposure-l7-routes.mdtoAccepted, with a D1/D2 addendum recording the decisions above.
Both decisions are recorded in the ADR addendum since ADR-0038 explicitly
left the controller-anchoring mechanism to the implementation.
Required human follow-up
The forked cluster chart (
../cluster/charts/protector, a separate infrarepo) needs the same
ingresses/ingressclassesRBAC + watch additionshand-ported — that fork only auto-bumps the image tag; chart template
changes here do not propagate to it. This PR does not touch that repo.
Test plan
engine/src/engine/observe/adapter/ingress_exposure/tests.rs:route_forwarded_backend_of_live_controller_becomes_a_proven_entry—drives the full
reason::proof::provewalk and asserts anEXPLOIT_PUBLIC_FACINGfoothold on the route-forwarded backend.orphan_ingress_does_not_promote— both orphan shapes (unresolved class;resolvable class with no live address match) stay below
Internet.chains_compose_one_further_hop— a backend promoted in round 1 anchorsa second Ingress in round 2, proving the fixpoint re-derives against
current graph state.
cloudflared_annotation_still_wins_with_no_in_cluster_ingress— theADR-0012 declared annotation is untouched when there's no Ingress at all.
cargo fmt --all -- --checkcleancargo clippy --all-targets -- -D warningscleancargo nextest run --workspace— 1162 passed, 2 skipped (includes thefile-size guard)
reason/proof/, the adjudication prompt, guards, ormenu (
git diff origin/main -- engine/src/engine/reason/is empty) —existing suites pass unedited, confirming the seam
(
Node::Workload+w.exposure == Exposure::Internet) is the entireintegration surface.
/soundcheck:pr-review— no Critical/High findings (RBAC additionsare read-only and narrowly scoped; new logic operates only on
already-trusted, RBAC-gated cluster API data, same trust boundary as
every other observe-layer adapter).
/simplifypass — one efficiency fix applied (hoisted a per-selector-keylabel-map clone in
selected_podsto once per pod); no other findings.Closes JEF-697
🤖 Generated with Claude Code
https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP