Skip to content

feat(webhook): Wire envoy-sidecar mTLS through pod mutator + fix svid-output mount#381

Merged
huang195 merged 1 commit into
rossoctl:mainfrom
huang195:feat/envoy-sidecar-mtls
May 27, 2026
Merged

feat(webhook): Wire envoy-sidecar mTLS through pod mutator + fix svid-output mount#381
huang195 merged 1 commit into
rossoctl:mainfrom
huang195:feat/envoy-sidecar-mtls

Conversation

@huang195

@huang195 huang195 commented May 27, 2026

Copy link
Copy Markdown
Member

Summary

  • Wires Spec.MTLSMode on AgentRuntime CRs through to a per-agent rendered Envoy data-plane TLS configuration when Spec.AuthBridgeMode: envoy-sidecar. Closes the operator-side gap that the kagenti-extensions PR Bug: SharedTrust and MLflow controllers block all reconciliation when RBAC is missing #440 demo had to work around with hand-crafted manifests.
  • Fixes a bundled bug: the /opt mount on envoy-sidecar's authbridge container was set readOnly: true (asymmetric with proxy-sidecar's RW), blocking the in-process spiffe Provider mirror from writing /opt/svid*.pem and Envoy from booting.
  • Relaxes the admission webhook's envoy-sidecar + non-disabled mtls rejection. CRD enum validation already pins mtlsMode; no remaining incompatible combinations today.

Design

Mode semantics match what the kagenti-extensions PR #440 demo proved at the data-plane level:

mtlsMode Inbound (peer-facing :15124) Outbound (app-egress :15123)
disabled (default) plaintext plaintext
permissive tls_inspector + two filter chains: TLS chain terminates mTLS, raw_buffer chain accepts plaintext plaintext (no TLS-wrap attempt)
strict tls_inspector + single TLS chain; plaintext rejected at chain match UpstreamTlsContext on original_destination_tls cluster: TLS-or-fail

Inbound is byte-identical to proxy-sidecar's tlssniff.Listener semantics, expressed as Envoy filter chains — matches Istio's PERMISSIVE/STRICT inbound exactly.

Outbound is Istio-shaped: no per-connection try-then-fallback (Envoy has no native primitive for it, and Istio itself doesn't do it). Blanket TLS-or-fail in strict mode is practical for the same reason proxy-sidecar's strict mode is: outbound calls that need plaintext (Keycloak / JWKS / external HTTPS) bypass the listener (plugin-side via Go net/http, external HTTPS via iptables exclusion).

Wiring

  • envoy.yaml.tmpl — adds conditional TLS blocks gated on MTLSEnabled (permissive: tls_inspector + TLS chain + raw_buffer chain; strict: tls_inspector + TLS chain only) plus a strict-only original_destination_tls cluster with UpstreamTlsContext. SVID files at /opt/svid*.pem.
  • envoy_template.goenvoyTemplateData carries MTLSEnabled bool + MTLSMode string; RenderEnvoyConfig populates from ResolvedConfig.MTLSMode.
  • resolved_config.go — adds MTLSMode + AuthBridgeMode fields, populated via the same CR > namespace ConfigMap > "" chain pod_mutator already implements (ExtractMode / ExtractMTLSMode helpers).
  • pod_mutator.go — envoy-sidecar branch now passes the resolved mtlsMode to ensurePerAgentConfigMap (was hardcoded "" under the old webhook gate). When mtls is non-disabled, calls a new ensurePerAgentEnvoyConfigMap that SSAs an envoy-config-<crName> ConfigMap with the rendered envoy.yaml. SSA + OwnerReference machinery mirrors the existing ensurePerAgentConfigMap.
  • volume_builder.go — adds overrideEnvoyConfigMapInVolumes (mirroring the existing overrideAuthBridgeConfigMapInVolumes) so the envoy-config volume reference points at the per-agent CM.
  • agentruntime_webhook.gocheckMTLSCompatibleWithMode no longer rejects envoy-sidecar + non-disabled mtls. The function stays as a hook for future incompatible combinations.

Bug fix

  • container_builder.go:117 — removed ReadOnly: true on the svid-output volumeMount in BuildEnvoyProxyContainerWithSpireOption. Asymmetric with BuildProxySidecarContainerWithPorts (correctly RW). The in-process spiffe Provider mirror inside the authbridge-envoy combined image needs to write SVID files there; under the RO mount it logged atomicWrite: ... read-only file system and Envoy refused to boot with Invalid path: /opt/svid_bundle.pem. Historical context: the RO was a copy-paste from the days of an external spiffe-helper writer container that no longer exists.
  • container_builder_test.go — flipped the wrong test assertion. The test currently checks if !vm.ReadOnly { t.Error("svid-output mount should be read-only") } — i.e., it was asserting the bug as the desired behavior. Replaced with the opposite check.

Tests

  • envoy_template_test.go — three new cases lock in the rendered shape per mode (disabled = no TLS bits; permissive = both chains, no UpstreamTls; strict = TLS-only chain + UpstreamTlsContext + TLS cluster).
  • agentruntime_webhook_test.goenvoy-sidecar + permissive and envoy-sidecar + strict cases flipped from wantErr: true to wantErr: false. Drops the strings.Contains("envoy-sidecar mTLS is tracked as a follow-up") check that's no longer applicable.
  • container_builder_test.go — assertion flipped (svid-output mount must be RW).

Test plan

  • go test -race -count=1 ./internal/webhook/... passes (config + injector + v1alpha1).
  • go test -race -count=1 ./... passes everything except test/e2e which wants a kind cluster named "kind" and is infra-dependent.
  • go build ./... clean.
  • End-to-end on a SPIRE-enabled local kind cluster: built the operator image with this PR, loaded into kind, restarted the controller, applied an AgentRuntime CR with Spec.AuthBridgeMode: envoy-sidecar + Spec.MTLSMode: strict plus its target Deployment. Verified:
    • Operator created envoy-config-<name> with tls_inspector + DownstreamTlsContext + UpstreamTlsContext + original_destination_tls cluster (strict-mode shape).
    • Operator created authbridge-config-<name> with mtls.mode: strict propagated through the resolution chain.
    • Pod reached 2/2 Running. The in-process spiffe Provider mirror successfully wrote /opt/svid.pem, /opt/svid_key.pem, /opt/svid_bundle.pem — confirming the bug 1 fix is what unblocks Envoy.
    • Envoy logs: starting workers cleanly, no Invalid path: /opt/svid_bundle.pem failures.

Follow-up (separate, small PRs, not blocking):

  • kagenti-extensions: update authbridge/demos/mtls/scripts/patch-mtls-config.sh to patch the namespace authbridge-runtime-config CM instead of the per-agent CM (operator-owned). Fixes the existing make demo-mtls (proxy-sidecar) verification step.
  • kagenti: relax the envoy-sidecar + non-disabled mtls rejection in backend/app/routers/agents.py and remove the matching UI lock in ImportAgentPage.tsx.

Assisted-By: Claude (Anthropic AI) noreply@anthropic.com

@pdettori pdettori left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean PR — correct Envoy TLS wiring (matches Istio PERMISSIVE/STRICT semantics), well-justified ReadOnly bug fix, and good template-level test coverage. All 16 CI checks pass including E2E.

Two minor suggestions inline, nothing blocking.

OutboundPort: cfg.Platform.Proxy.Port,
InboundPort: cfg.Platform.Proxy.InboundProxyPort,
ExtProcPort: defaultExtProcPort,
MTLSEnabled: cfg.MTLSMode != "" && cfg.MTLSMode != MTLSModeDisabled,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: The double-check cfg.MTLSMode != "" && cfg.MTLSMode != MTLSModeDisabled is defensive — worth a one-line comment noting that "" is treated as disabled (since ResolvedConfig defaults it to "" when unset). Prevents a future reader from wondering why both conditions are needed.

}
}
return result
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: overrideEnvoyConfigMapInVolumes has no unit test. A small table-driven test (volume found → CM name swapped, not found → list unchanged) would lock in correctness cheaply.

// + mtls we want template-rendered output, so we deliberately
// clear EnvoyYAML before rendering. Setting it on the namespace
// CM was a way to bypass templating; with mtls it's incompatible.
resolvedForEnvoy := ResolveConfig(currentConfig, nsConfig, arOverrides)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: The 6-line comment block (lines 595-600) is helpful context but dense — consider condensing to ~2 lines, e.g. "ResolveConfig is cheap/idempotent; we clear EnvoyYAML so RenderEnvoyConfig uses the template path instead of short-circuiting."

…-output mount

Adds end-to-end support for Spec.MTLSMode on AgentRuntime CRs targeting
authBridgeMode=envoy-sidecar. The kagenti-extensions PR rossoctl#440 demo proved
the Envoy data-plane TLS design (tls_inspector + filter_chain_match for
inbound, UpstreamTlsContext on a TLS-originating cluster for strict
outbound). This wires that into the operator: per-agent envoy-config
ConfigMaps render those TLS blocks based on the resolved mtlsMode, and
the admission webhook stops rejecting envoy-sidecar + non-disabled mtls.

Wiring (the bulk of the diff):

* envoy.yaml.tmpl gains conditional TLS blocks gated on MTLSEnabled
  (permissive: tls_inspector + TLS chain + raw_buffer chain; strict:
  tls_inspector + TLS chain only) plus a strict-only original_destination_tls
  cluster with UpstreamTlsContext. SVID files at /opt/svid*.pem.
* envoyTemplateData carries MTLSEnabled bool + MTLSMode string;
  RenderEnvoyConfig populates them from ResolvedConfig.
* ResolvedConfig gains MTLSMode + AuthBridgeMode fields, populated via
  the same CR > namespace ConfigMap > "" chain pod_mutator already
  implements (so RenderEnvoyConfig reads the resolved value directly).
* pod_mutator's envoy-sidecar branch now passes the real mtlsMode to
  ensurePerAgentConfigMap (was hardcoded "" defensively under the old
  webhook gate) and, when mtls is on, calls a new
  ensurePerAgentEnvoyConfigMap that SSAs an envoy-config-<crName>
  ConfigMap with the rendered envoy.yaml. overrideEnvoyConfigMapInVolumes
  in volume_builder.go redirects the envoy-config volume to the
  per-agent CM (mirroring the existing authbridge-runtime override).
* checkMTLSCompatibleWithMode no longer rejects envoy-sidecar +
  non-disabled mtls. The function stays as a hook for future
  incompatible combinations.

Bug fix bundled in:

* container_builder.go's BuildEnvoyProxyContainerWithSpireOption was
  mounting svid-output (/opt) read-only on the envoy-proxy container
  while BuildProxySidecarContainerWithPorts correctly mounted it RW.
  The in-process spiffe Provider mirror inside the authbridge-envoy
  combined image needs to write /opt/svid.pem, /opt/svid_key.pem,
  /opt/svid_bundle.pem on every SPIRE rotation; under the RO mount it
  logged "atomicWrite: read-only file system" and Envoy refused to
  boot with "Invalid path: /opt/svid_bundle.pem". container_builder_test
  was asserting the bug as the desired behavior; flipped to require RW.

Tests:
* envoy_template_test.go: three new cases lock in the rendered shape
  per mode (disabled = no TLS bits; permissive = both chains, no
  UpstreamTls; strict = TLS-only chain + UpstreamTlsContext + TLS cluster).
* agentruntime_webhook_test.go: envoy-sidecar + permissive/strict
  flipped from rejected to allowed.
* container_builder_test.go: assertion flipped (svid-output must be RW).

E2E spot-check: operator built and loaded into local kind, AgentRuntime
CR with authBridgeMode=envoy-sidecar + mtlsMode=strict applied, the
operator created envoy-config-<name> with tls_inspector +
DownstreamTlsContext + UpstreamTlsContext + original_destination_tls
cluster, and authbridge-config-<name> propagated mtls.mode: strict
through the resolution chain. (Pod-level mTLS verification needs SPIRE
agent + CSI driver; the local cluster's spire-system namespace had been
deleted out-of-band, so end-to-end traffic test is deferred — the
kagenti-extensions PR rossoctl#440 demo already verified the same Envoy YAML
design works against a SPIRE-enabled cluster.)

Companion to kagenti-extensions PR rossoctl#440. Once this lands, that demo's
hand-crafted manifests can be replaced (or supplemented) with an
operator-injected variant that simply sets Spec.MTLSMode on the
AgentRuntime CR.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
@huang195
huang195 force-pushed the feat/envoy-sidecar-mtls branch from 64a9d29 to 7e448f8 Compare May 27, 2026 19:25
@huang195
huang195 merged commit d8350dd into rossoctl:main May 27, 2026
15 checks passed
@huang195
huang195 deleted the feat/envoy-sidecar-mtls branch May 27, 2026 19:49
@github-project-automation github-project-automation Bot moved this from New /:ToDo to Done in Rossoctl Issue Prioritization May 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants