Envoy 1.40: HTTP/3 Goes Stable, Post-Quantum TLS Goes Default, and Your MCP Gateway Just Lost Its Authorization Passthrough

Sources

Envoy Proxy 1.40.0 shipped October 13, 2026 — the quarterly minor in which the CNCF graduate stops treating agentic traffic as "just HTTP with weird payloads": its MCP router now enforces audience-bound token discipline by default, HTTP/3 is stable, and post-quantum key agreement is on by default. We downloaded the release binary, ran it on a live host, validated configs against it with --mode validate, and hand-shook its TLS stack. Three findings below came out of the binary itself, not the changelog.

First, the release posture. Envoy majors are cut quarterly (per RELEASES.md: 1.37 January → 1.38 April → 1.39 July 14 → 1.40 October 13), with a 12-month extended maintenance window; 1.39's EOL is 2027-07-14. The release was published by publish-envoy[bot] and the GitHub notes are signed off by Jonh Wendell (Red Hat) and Ryan Northey (synca.io). The release binary is distributed with a signed checksums.txt.asc — the SHA-256 of envoy-1.40.0-linux-x86_64 we downloaded and executed matched it exactly. That is the supply-chain hygiene level you should demand from any component in your request path.

ChangeClassPrimary evidenceBlast radius
MCP router stops forwarding authorization to backends by defaultBreaking (security-correct)mcp_router.proto HeaderForwarding; PR #46818 (merged 2026-10-05)Any MCP gateway that relied on passthrough auth
HTTP/3 + QUIC promoted from alpha to stable (incl. upstream H3, QUIC mTLS listeners, P-384/P-521 curves)Stability milestone1.40.0 version historyMesh edges, client-facing gateways
X25519MLKEM768 hybrid added to default ECDH curves (upstream + downstream, incl. FIPS builds); revert via envoy.reloadable_features.pqc_default_ecdh_curvesSecurity defaultSame notes; our live-handshake test belowEvery TLS listener on upgrade — CPU + ClientHello size
Container-aware CPU detection finally on by default (workers = min(threads, affinity, cgroup limit) when --concurrency unset)Ops fixIssue #45410 (filed by @tsaarni); fix documented in 1.40.0 notesEvery Envoy in a CPU-limited pod — the good kind of surprise
Bazel WORKSPACE removed; bzlmod is mandatory, Envoy's own registry required (raw.githubusercontent.com/envoyproxy/bazel-registry)Breaking (build)Commit b6e7e12ed / PR #47217 (2026-09-04)Anyone building Envoy or WASM filters from source
Hermetic Clang toolchain 18 → 22; --config=clang/clang-local removed; AWS-LC no longer selectable SSL libBreaking (build)1.40.0 version historyFIPS consumers on ppc64le must switch to --config=openssl
New route_specifiers post-match extension point; priority_group cluster specifier shipsArchitectureVerified in the release binary's extension registry (our run log)Route-policing / traffic-shaping use cases, no forklift
AI Protocol Manager gains response-side LLM token-usage extraction (OpenAI/Anthropic/Gemini dialects → envoy.data.ai.v3.TokenUsage)New capability (alpha)Same notes; filter present in release binaryLLM cost attribution at the data plane

The headline: MCP token passthrough is dead, and that is correct

Historically, an MCP router filter in Envoy forwarded all downstream headers to every aggregated backend — except a hardcoded skip-list. That is how every naive LLM gateway works: take the client's authorization: Bearer … header and relay it to whichever tool backend the agent picked. The 1.40 change in PR #46818 (author @kkukadia, merged 2026-10-05) flips the default: no downstream-controlled headers are forwarded unless you explicitly allow them. The proto's comment is unusually blunt for an API doc — unset policy forwards nothing, "because MCP requires audience-bound tokens and prohibits implicit token passthrough between a client and a backend it did not authenticate to."

Why this is right, in one scenario: a single mcp_router aggregates a sales-CRM MCP server and an internal-dev-tools MCP server. The client holds one bearer token scoped to the CRM. Under forward-everything, an agent that calls the dev-tools backend ships the CRM token straight into that backend's logs and request stores. The failure mode is not exotic — it is the default behavior of every passthrough gateway, and the reason "MCP gateway" shows up in agent authZ patterns as a token-sprawl liability rather than a control.

The evaluation order the filter implements (verified against the v1.40.0 proto): (1) disallowed_headers match → never forwarded, overrides everything; (2) forward_all: true → forwarded; (3) allowed_headers match → forwarded; (4) otherwise → dropped. Router-owned, framing, session, and hop-by-hop headers are handled separately and can never be forwarded through this mechanism. And note the contrast with ext_proc embedded in the same proto: ext_proc's unset policy forwards everything; mcp_router's unset policy forwards nothing. The MCP filter chose the secure default; the ext_proc heritage did not.

Migration is a one-line per-backend decision. If you genuinely have a same-trust backend and want the old behavior, set forward_all: true — but you are re-arming the footgun, and you should be injecting backend-specific credentials (via credential injector) instead. The config below is the strict posture, and it validated clean against the real 1.40.0 binary via envoy --mode validate (more on that below):

http_filters:
  - name: envoy.filters.http.mcp_router
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.http.mcp_router.v3.McpRouter
      servers:
        - name: tools_backend
          mcp_cluster:
            cluster: mcp_backend_a
            path: /mcp
          header_forwarding:
            forward_all: false
            allowed_headers:
              patterns:
                - exact: x-request-id
                - prefix: x-trust-
            disallowed_headers:
              patterns:
                - exact: authorization

Two hard-won facts from that validation run. First: mcp_router is a terminal filter — our first config had the router filter after envoy.filters.http.router, and the binary refused it with terminal filter named envoy.filters.http.mcp_router of type envoy.filters.http.mcp_router must be the last filter in a http filter chain. It replaces the router, it does not precede it. Second: the binary logs, for every mcp_router message, that the proto is "marked as work-in-progress. API features marked as work-in-progress are not considered stable, are not covered by the threat model, are not supported by the security team, and are subject to breaking changes." Read that twice before you put an MCP router in front of anything with real blast radius. The Envoy threat model explicitly carves out WIP APIs; the 1.40 notes call the AI Protocol Manager filter alpha. This lane is where Istio was in 2018 — powerful, fast-moving, and not yet for your regulated path.

Data-plane diagram: where the token stops.

MCP client                  +--------------------------+ MCP backends (aggregated) 
(agent runtime)             |route match -> backend    | +----------------------+  
                            |aggregate capabilities    | | crm_tools            |  
initialize + tool calls     |                          | | audience: crm-scope  |  
Authorization: XxX (bearer) |header_forwarding (1.40): | +----------^-----------+  
                            |  unset  = forward NOTHING|            |              
                            |  authorization  DROPPED  | +----------+----------+   
                            |  x-request-id   allowed  | | dev_tools            |  
                            |  x-trust-*      allowed  | | audience: dev-scope |   
                            +--------------------------+ +----------------------+  

XxX = client bearer token. Under the 1.40 default it is dropped at the
router; backends receive synthesized framing headers plus only what the
per-backend header_forwarding policy explicitly allows. Credentials for
each audience belong in a credential injector, not in passthrough.

HTTP/3 graduates, post-quantum becomes the default curve

The 1.40.0 version history marks the full QUIC/HTTP/3 stack — downstream and upstream H3, QUIC listeners with client-certificate (mTLS) support, all QUIC extensions — as moving from alpha to stable. On the wire this matters most where TCP handshakes are the long pole: mobile clients, cross-region backend hops. The mTLS-on-QUIC-listeners addition removes the last common excuse for pinning a front door to TCP-only.

The post-quantum move is the sleeper. Envoy 1.40 adds X25519MLKEM768 — the hybrid of classical X25519 and the NIST ML-KEM-768 key encapsulation from BoringSSL — to the default ECDH curves for both upstream and downstream TLS, including FIPS builds. The revert knob is envoy.reloadable_features.pqc_default_ecdh_curves. Concretely:

# 1) Verify the binary against the release's signed checksum manifest
$ curl -sL -o envoy-1.40.0 \
    https://github.com/envoyproxy/envoy/releases/download/v1.40.0/envoy-1.40.0-linux-x86_64
$ grep linux-x86_64 checksums.txt.asc
  8aa137a7d00994ce43f613556f5a120709229f63bedb86eedde5ccbc402c2b80  envoy-1.40.0-linux-x86_64
$ sha256sum envoy-1.40.0
  8aa137a7d00994ce43f613556f5a120709229f63bedb86eedde5ccbc402c2b80  envoy-1.40.0

# 2) Bootstrap with a TLS listener (self-signed cert, TLSv1_3 min)
$ ./envoy-1.40.0 -c /tmp/envoy-tls.yaml --base-id 3

# 3) Handshake from a Python ssl client (OpenSSL 3.0.13 under the hood)
$ python3 -c 'import ssl, socket
  ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
  ctx.load_verify_locations("/tmp/tls.crt")
  ... TLSv1.3 / TLS_AES_256_GCM_SHA384 / body: pqc-handshake-ok'

# 4) Admin interface — which curve actually served the handshake
$ curl -s http://127.0.0.1:9901/stats | grep ssl.curves
  listener.127.0.0.1_8443.ssl.curves.X25519: 1

That last line is the honest interop story: our client (OpenSSL 3.0.13) cannot offer the hybrid group, so the handshake fell back to plain X25519 — PQC-by-default is server-side capability, not a guarantee. Clients need OpenSSL 3.5+ (April 2025, per the 3.5 release notes) or a BoringSSL build with ML-KEM to actually negotiate it. Deploy 1.40 and you gain PQC readiness the day your fleet upgrades its clients; until then, handshake CPU cost may tick up slightly on connections that exchange the larger ClientHello, and the hybrid only engages where both sides speak it. Monitor listener.*.ssl.curves.* — if you do not see X25519MLKEM768 counters appear once modern clients arrive, your "PQC deployment" is theater.

Container-aware CPU detection: the fix that snuck in

Issue #45410 (filed by @tsaarni) is the release's best production-pain story, and the fix quietly landed inside 1.40. Background: Envoy sizes worker threads from --concurrency, defaulting to hardware thread count. Inside a Kubernetes pod with resources.limits.cpu: 4 on a 20-thread node, a pre-1.40 Envoy without --cpuset-threads spawned 20 workers for a 4-CPU quota — wasted memory (per-worker connection pools, buffers), scheduler thrash, and a stats line lying to you. The cgroup detection code existed since PR #40997 (October 2025, "Use cgroup CPU limit to set concurrency"), but it was only wired to --cpuset-threads — a flag documented for cpuset sizing, not cgroup limits. The 1.40 release notes' own admission: the v1.37 behavior "was only applied when --cpuset-threads was set. It is now applied whenever --concurrency is not set."

The v1.40.0 source confirms the wiring: OptionsImplPlatform::getCpuCount() (min of hardware threads, CPU affinity, cgroup CPU limit on Linux) is now the default path in options_impl.cc, with --cpuset-threads demoted to a no-op that logs "--cpuset-threads is now the default behavior and no longer required". If you have a Helm chart or sidecar injector that still sets --concurrency from a Node's CPU count, it is still lying to you — the fix only applies when the flag is unset. Our own run log shows the detection in action on a cgroupv2 host: [warning][misc] [source/server/cgroup_cpu_util.cc:313] Expected cgroup v2 file not accessible — meaning on hosts where the cgroup hierarchy is not readable, Envoy logs and falls back to hardware count rather than crashing. That is the right failure mode; grep your logs for it after upgrading.

Post-fix, the pod math is finally honest. A 4-CPU-limited pod gets 4 workers:

# Before 1.40 (20-worker Envoy in a 4-CPU pod — silent oversubscription):
#   $ curl -s localhost:9901/server_info | jq .command_line_options.concurrency
#   "concurrency": 20          # hardware thread count of the NODE
#   memory: 20 × (per-worker pool + buffer arenas) ≈ wasted GiB
#   throttling: 20 runnable workers → 4 CFS quota → latency jitter under load
#
# 1.40 default on the same pod (no flags):
#   concurrency = min(20 hardware, affinity, cgroup limit 4) = 4
#
# The pod manifest that now does the right thing WITHOUT --cpuset-threads:
resources:
  limits:
    cpu: 4
    memory: 2Gi
env: []            # ENVOY_CGROUP_CPU_DETECTION defaults on; set false to opt out
args: []           # no --concurrency, no --cpuset-threads — let the detector work

The bzlmod migration tax: read this before you build anything

If you only consume the official Docker images, skip this section. If you build Envoy, WASM filters, or anything downstream of its API from source, 1.40 is a forced migration. The WORKSPACE file is gone (commit b6e7e12ed, PR #47217, landed 2026-09-04); builds require MODULE.bazel and must add Envoy's custom registry (envoyproxy/bazel-registry) alongside the Bazel Central Registry — many patched dependencies exist only there. The hermetic Clang toolchain jumps 18 → 22, with --config=clang/clang-local removed (Clang/libc++ is now default; GCC survives via --config=gcc), and AWS-LC is removed as a selectable SSL library — ppc64le FIPS consumers must move to --config=openssl. Wasm engine selection moves to --@proxy-wasm-cpp-host//bazel:engine=<engine>; the old --define wasm= flags now fail the build. None of this is optional; the removed flags do not warn — they error.

Everything else worth your fifteen minutes

Routing becomes extensible after match. A new route_specifiers extension point post-processes the matched route — refine, drop, or synthesize — configured at route-config, virtual-host, and route level, evaluated in that order. A priority_group cluster specifier plugin ships in the release binary (we verified it in the extension registry dump from our run). This is Envoy internalizing what service meshes previously built in control planes: per-request traffic shaping without regenerating route tables.

AI Protocol Manager (alpha) learns to count tokens. The filter now extracts response-side LLM token usage for OpenAI, Anthropic, and Gemini dialects (streaming SSE and JSON) — synchronously on encode callbacks against a bounded side copy, without stopping filter-chain iteration — and publishes it as typed dynamic metadata (envoy.data.ai.v3.TokenUsage, namespace envoy.ai.token_usage). Consumable via ext_proc typed-metadata forwarding or any filter reading dynamic metadata. If you are building LLM cost attribution or per-tenant model metering at the gateway, this is a config line where it used to be a sidecar or a log-scraping job. Same WIP caveat as mcp_router applies.

Load balancing gets fleet-shaped. New per_worker_subset policy: each worker keeps its own subset of hosts (EQUAL_PARTITIONS or RANDOM_PARTITIONS) so connection-pool sizes decouple from cluster size — aimed at very large upstreams with short server-side keepalive. New load_aware_locality picks localities by ORCA-derived headroom, consuming in-band, out-of-band, or both. Coalesced LB rebuilds on EDS batch updates are now on by default. None of these are hype features; they are the "our cluster has 10,000 endpoints" class of fix.

Drain semantics got fixed, not just tuned. A non-graceful /drain_listeners previously stopped listeners without notifying their connections — no connection-level drain logic ran at all. Now drain is event-driven: decisions are pushed to connections when drain starts, which also fixes inbound-only drain for the Mongo/Redis/Thrift/generic proxies. Plus hot restart now covers UDP: the parent forwards unknown-session datagrams to the child so connectionless listeners do not go dark during restart (with a fix for the parent forwarding packets of QUIC connections it had just closed, which the child answered with stateless resets). If you run Envoy behind a load balancer that drains by health check, retest your drain times after upgrading — behavior changed, not just internals.

Security carryover. 1.40.0 inherits the 1.39.x fixes: URL normalization of path parameters (CVE-2026-73511/73551), HTTP/2 trailers-without-END_STREAM accounting (CVE-2026-73513/73550), an HTTP/3 use-after-free (CVE-2026-73512), ext_authz pathless-request crash (CVE-2026-73547), generic-upgrade body pipelining (CVE-2026-73548), safe_regex Latin1 mode (CVE-2026-73552), RBAC ignore_path_parameters (CVE-2026-73553), admin stat-name sanitization (CVE-2026-73546), and the oauth2/api_key_auth CONNECT crashes (GHSA-8vc2-jrm4-835w, GHSA-47vj-9r25-wv5j). If you are on 1.39.x, this is the release that carries them plus a family of RDS race fixes (route-level AWS signing, composite, filter chain, local ratelimit, dynamic modules, SDS secrets configs could be destroyed on a worker thread after an RDS update). Also new: HTTP/1 accepts the RFC 10008 QUERY method — and rejects any QUERY without a Content-Type with a 400 and response-code detail query_missing_content_type, per the RFC's Section 2. Redis proxy gains RESP3 via protocol_version (default stays RESP2; pre-HELLO data commands get -NOPROTO and a new downstream_rq_noproto counter).

Hidden costs, deprecation schedule, migration vectors

CostWho paysMitigation
MCP auth-header drop on upgrade (1.39 → 1.40) breaks backends that relied on passthroughMCP gateway operatorsAudit per backend; either allowlist explicitly, or fix the real bug with credential injection per audience
PQC ClientHello/curve CPU + size overhead on every TLS listenerHigh-RPS edgesRevert via envoy.reloadable_features.pqc_default_ecdh_curves=false; watch ssl.curves for real negotiation rates
bzlmod toolchain migration, registry wiring, removed build flagsSource builders, WASM filter devsMove to MODULE.bazel + Envoy registry; budget a day, not an afternoon
mcp_router / AI Protocol Manager are WIP API — no threat-model coverage, no security-team supportAnyone routing production agent traffic todayKeep it out of regulated paths until it stabilizes; re-read the binary's own warning each upgrade
Terminal-filter constraint on mcp_router (must be last, replaces router filter)Config reviewersValidate configs with envoy --mode validate in CI — catches exactly this class of mistake
1.40.x is what you will run for the next quarter; 1.39.x leaves extended maintenance 2027-07-14Ops planningPer RELEASES.md, minors hold ~12 months of extended maintenance; the quarterly cadence means the 1.41 planning conversation starts now

Deprecations with dates none of them give you: config-validator factories keyed by type URL, dynamic-module log/timing-info callbacks, legacy HTTP filter factory base classes, reverse-tunnel auto_close_connections, and Wasm plugin-level capability restriction all move to "deprecated" in 1.40 — the removal cycle will catch them in a future minor. Nothing here breaks next Tuesday; all of it breaks if you upgrade yearly.

Who should skip

Skip if you are on managed meshes: Istio, Envoy Gateway, and the cloud provider meshes pin their own Envoy versions on their own schedule; nothing in 1.40 reaches you until your control plane ships it — and the WIP-gated MCP/AI filters are not something to expect in a managed distribution's supported surface soon. Skip if you build WASM filters: the bzlmod migration plus Wasm engine flag changes make 1.40 a project, not an upgrade. Skip if your TLS is terminated elsewhere: the PQC and H3 wins only matter on listeners Envoy actually terminates. Everyone else on self-managed Envoy: take it — the CPU-detection fix alone is worth the ride-along risk of a quarterly minor, and the CVE carryover makes 1.39.x a liability.

Commercial paths, for completeness: Envoy's ecosystem monetizes through support and managed control planes — Tetrate (enterprise Istio/Envoy support) and Solo.io (Gloo Gateway, built on Envoy) are the two names to know when you need an SLA behind the CNCF logo. The upstream project itself stays vendor-neutral OSS.

Verdict

Envoy 1.40 is the release where the data plane starts treating agentic traffic as a first-class protocol concern — audience-bound tokens, per-dialect token accounting, MCP aggregation — rather than as "just HTTP with weird payloads." The MCP default flip is the rare breaking change that is unambiguously correct: it converts a class of token-sprawl vulnerabilities from misconfiguration to non-configuration. HTTP/3 stability and PQC-by-default are the boring kind of milestone, which is to say the kind that matters five years from now. The honest caveats: the AI/MCP surface is alpha and uncovered by the threat model, the bzlmod migration is a real tax on anyone building from source, and PQC negotiation remains client-gated. Grade: upgrade for the CPU fix and CVE carryover; prototype the MCP router now, do not put it in a regulated path yet.

References & Further Reading