feat: Add inboundInterception=transparent (drops port stealing) - #511
Draft
huang195 wants to merge 2 commits into
Draft
feat: Add inboundInterception=transparent (drops port stealing)#511huang195 wants to merge 2 commits into
huang195 wants to merge 2 commits into
Conversation
…ing)
Adds an opt-in inbound shape for proxy-sidecar / lite that stops stealing the
agent's port. When inboundInterception is "transparent", proxy-init installs a
PREROUTING REDIRECT and AuthBridge recovers each connection's real destination
via SO_ORIGINAL_DST, so the agent keeps the port it already binds.
That removes the three problems with port stealing, all of which the existing
code acknowledges:
- The relocated port (originalPort+1) is declared in the pod spec and directly
reachable, so any pod could reach the agent without JWT validation. The pod
is the granularity Kubernetes NetworkPolicy and ztunnel both enforce at, so
this was the boundary that mattered and it was open.
- Relocation depends on the agent honoring PORT. pod_mutator.go concedes that
agents which hardcode their listen port "won't be affected" — in practice
they collide with AuthBridge on the stolen port and the pod never starts.
- Only Ports[0] of the first container with ports was relocated, so a
second declared port was never proxied.
Default is "reverse-proxy" (port stealing). Transparent costs a privileged
proxy-init container, so it must be chosen deliberately; with the field unset
every existing path is byte-identical.
The switch is the namespace authbridge-runtime-config ConfigMap
(inboundInterception), resolved namespace > cluster default, with a
proxy.allowedInboundInterception allowlist so a platform admin can forbid it (no
NET_ADMIN) or mandate it. Two fallbacks are deliberately biased toward the
unprivileged shape:
- An unrecognized value falls back to reverse-proxy, so a typo cannot silently
grant a NET_ADMIN init container.
- transparent + egressEnforcement=none falls back to reverse-proxy. The two
features share one proxy-init container; without it nothing would REDIRECT
to the inbound listener and inbound would be silently unenforced — worse
than port stealing, which at least validates Service-routed traffic.
Deliberately NOT adding an AgentRuntime spec field. The obvious move was to
mirror spec.egressEnforcement, but that field is dead surface: the pod mutator
has no access to the CR and resolves everything from the namespace ConfigMap, so
nothing reads spec.egressEnforcement (spec.mtlsMode reaches the pod only as a
rollout-triggering annotation, not as mutator input). An enum-validated spec
field that silently does nothing is worse than no field, and plumbing CR ->
mutator properly is a separate change that would have to fix mtlsMode and
egressEnforcement too.
Wiring details:
- The per-agent ConfigMap gets inbound_interception + transparent_inbound_addr
and must NOT get reverse_proxy_addr / reverse_proxy_backend: authbridge's
config validation rejects that combination, so emitting both would
crash-loop the pod.
- proxy-init gets INBOUND_TRANSPARENT_PORT and POD_IP (downward API). POD_IP is
not optional — the init script uses it as the DNAT target for the Istio
ambient inbound path, which arrives through OUTPUT rather than PREROUTING,
and refuses to start without it rather than install PREROUTING-only rules
that wave all mesh traffic through.
- SIDECAR_PORTS_EXCLUDE carries the RESOLVED forward-proxy port, not the
script's 8081 default: findFreePort may have moved it, and an unexempted
forward-proxy port would be swallowed by the inbound REDIRECT.
- The sidecar declares its inbound port as "transparent-in" rather than
"reverse-proxy", so which shape is running is visible in the pod spec.
- Config validation rejects transparentInboundPort == transparentPort. Both are
listeners in one container, so a shared value fails the second bind at pod
start, long after admission succeeded.
Drive-by lint cleanups in touched code: extracted the duplicated
resolution-source strings ("cluster-default", "default-invalid-fallback") into
constants alongside the existing sourceNamespaceConfigMap, and dropped an unused
parameter from the new private sidecar builder.
make test: 21 packages green. No CRD changes (none needed).
Refs: rossoctl/cortex#330
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
proxy-init's ambient DNAT target must match the address family of the traffic. POD_IP is the pod's PRIMARY address (usually v4), so on a dual-stack pod the other family's HBONE delivery passed unvalidated while that family's PREROUTING rules were installed — half-enforcement, which the POD_IP guard refuses to ship in the equivalent case. Adds POD_IPS from the Downward API (status.podIPs) alongside POD_IP. proxy-init falls back to POD_IP when absent, so an older init image still works for its own family. Refs: rossoctl/cortex#330 Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
Member
Author
|
Adds Review found that proxy-init's ambient DNAT target was keyed off
Assisted-By: Claude Code |
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
Adds an opt-in inbound shape for
proxy-sidecar/litethat stops stealing the agent's port. Companion to rossoctl/cortex#776, which implements the listener and iptables rules; this is the switch that turns them on.With
inboundInterception: transparent, proxy-init installs a PREROUTING REDIRECT and AuthBridge recovers each connection's real destination viaSO_ORIGINAL_DST, so the agent keeps the port it already binds.Why
Port stealing has three problems, all of which the existing code acknowledges:
originalPort+1is declared in the pod spec and directly reachable, so any pod can reach the agent without JWT validation. Pods are the granularity NetworkPolicy and ztunnel enforce at, so this was the boundary that mattered.PORT.pod_mutator.goconcedes that agents which hardcode their listen port "won't be affected" — in practice they collide with AuthBridge on the stolen port and the pod never starts. No config can fix that.Ports[0]of the first container with ports is relocated, so a second declared port was never proxied. Undeclared ports aren't even inusedPorts, sofindFreePortcan hand the agent a port it already uses.Transparent interception removes all three: no relocation, no
PORTenv var, no second port to discover, and every port the agent listens on is covered.Default is unchanged
Default stays
reverse-proxy. Transparent costs a privileged proxy-init container, so it is chosen deliberately; with the field unset every existing path is byte-identical. Two fallbacks are deliberately biased toward the unprivileged shape:reverse-proxy, so a typo cannot silently grant aNET_ADMINinit container.transparent+egressEnforcement: nonefalls back toreverse-proxy. The two features share one proxy-init container; without it nothing would REDIRECT to the inbound listener and inbound would be silently unenforced — worse than port stealing, which at least validates Service-routed traffic.proxy.allowedInboundInterceptionlets a platform admin forbid it (["reverse-proxy"]) or mandate it (["transparent"]).Deliberately NOT adding an AgentRuntime spec field
The obvious move was to mirror
spec.egressEnforcement— but that field is dead surface. The pod mutator has no access to the CR and resolves everything from the namespace ConfigMap, so nothing readsspec.egressEnforcement(spec.mtlsModereaches the pod only as a rollout-triggering annotation, not as mutator input). An enum-validated spec field that silently does nothing is worse than no field, and plumbing CR → mutator properly is a separate change that would have to fixmtlsModeandegressEnforcementtoo.Worth deciding separately:
spec.egressEnforcementshould probably be either wired up or removed.Details
inbound_interception+transparent_inbound_addrand must not getreverse_proxy_addr/reverse_proxy_backend— authbridge's config validation rejects that combination, so emitting both would crash-loop the pod.SIDECAR_PORTS_EXCLUDEcarries the resolved forward-proxy port, not the script's8081default:findFreePortmay have moved it, and an unexempted forward-proxy port would be swallowed by the inbound REDIRECT. Gating9091would put kubelet probes behind JWT validation.transparent-in, so which shape is running is visible in the pod spec.transparentInboundPort == transparentPort: both are listeners in one container, so a shared value fails the second bind at pod start, long after admission succeeded.Upgrade-safe: the config loader overlays YAML onto compiled defaults, so an existing ConfigMap without the new keys keeps
8083/ both-allowed. Verified by running this operator against an unmodified 22-day-old cluster ConfigMap.Verification
make test— 21 packages green, including 10 new injector tests and 8 new config-validation cases.make manifests generateproduces no drift.:8000with noPORToverride, sidecar declarestransparent-in=8083+forward-proxy=8081, proxy-init receivesINBOUND_TRANSPARENT_PORT=8083/POD_IP(downward API) /SIDECAR_PORTS_EXCLUDE=8081,9091,9093,9094, and the per-agent ConfigMap containsinbound_interception: transparent+transparent_inbound_addr: :8083with noreverse_proxy_*keys.Draft: outstanding gate
Live traffic assertions (pod-to-pod → 401) are not yet run — the cluster used lacks the Keycloak CRD, so per-agent credential Secrets are never created and any new agent stays
PendingonFailedMountindependent of this change. Gated by the e2e suite in the rossoctl companion PR.Refs rossoctl/cortex#330
Assisted-By: Claude Code