A single Envoy listener on port 443 carries several filter_chains with different filter_chain_match blocks. How does Envoy decide which chain handles an incoming connection, and what has to happen before it can match on the TLS server name?
answer
- most specific wins, not first match
- fixed criteria priority order
- absent criterion means wildcard
- SNI is inside the ClientHello
- listener filters run before selection
basics
~20 sEnvoy picks the single most specific matching filter chain, comparing criteria in a fixed priority order rather than taking the first that matches. Matching on SNI requires a listener filter to read the TLS ClientHello before selection happens.
solid answer
~50 sFilter-chain selection is **most-specific-wins**, not first-match. Envoy evaluates the `filter_chain_match` criteria of every chain in a fixed priority order — destination port, then destination IP, then `server_names` (SNI), then `transport_protocol`, then `application_protocols`, then source type, source IP and source port — narrowing the candidate set at each step. A chain that omits a criterion acts as a wildcard for it and loses to a chain that specifies it. If exactly one chain survives it handles the connection; if none does, Envoy uses the `default_filter_chain` when configured and otherwise closes the connection. Overlapping chains that could both win for the same connection are a config error, so you cannot express ambiguity. The SNI part is the catch: SNI is inside the TLS ClientHello, which the listener has not read yet at selection time. That is the job of `envoy.filters.listener.tls_inspector`, which peeks at the handshake and publishes the server name and ALPN so the match can use them.
go deeper
Know that one Envoy listener can hold several filter chains and that a filter_chain_match decides which one a connection gets — for example a different chain per TLS hostname.
Explain that selection is most-specific-wins over a fixed criteria order rather than first-match, and that SNI and ALPN criteria depend on a listener filter inspecting the connection before selection happens.
Diagnose the wrong-certificate and mixed-plaintext cases by reasoning about which chain won and why, and account for the pre-selection window that listener filters and their timeout introduce.
Decide how much per-hostname and per-protocol differentiation belongs on a single shared listener versus separate listeners or separate proxy fleets, weighing config blast radius, certificate ownership and the rejection semantics of overlapping chains.
## Why one listener has many chains A listener owns a socket; a filter chain owns what to *do* with a connection on it. Splitting them lets one port serve several different treatments: distinct certificates per hostname, TLS on the same port as plaintext, or an L4 `tcp_proxy` chain alongside an L7 `http_connection_manager` chain. Each chain carries its own `filters` and its own `transport_socket`. ## The selection algorithm This is where intuition misleads people who have written nginx `server` blocks or Envoy routes, because chain selection is **not** ordered evaluation. Envoy compares criteria in a fixed priority sequence and keeps the most specific match at each stage: 1. destination port 2. destination IP address (`prefix_ranges`) 3. `server_names` — the TLS SNI value 4. `transport_protocol` — typically `raw_buffer` or `tls` 5. `application_protocols` — ALPN values such as `h2` or `http/1.1` 6. source type (any, local, external) 7. source IP address 8. source port An absent criterion is a wildcard, and a chain specifying a criterion beats one that leaves it open. So a chain matching `server_names: ["api.example.com"]` wins over a catch-all chain for connections carrying that SNI, regardless of the order they appear in the YAML. Reordering your config changes nothing — a genuinely useful property once a control plane is generating the list. Two consequences follow. First, **overlapping chains are rejected**: if two chains could both be the most specific match for the same connection, Envoy refuses the config rather than picking arbitrarily. Second, an unmatched connection has a defined fate — the listener's `default_filter_chain`, if configured, and otherwise the connection is closed. ## The chicken-and-egg problem with SNI `server_names` matches on the TLS Server Name Indication extension. But SNI lives inside the ClientHello, which is TLS data — and TLS is terminated by the chain's `transport_socket`, which has not been chosen yet. Selection needs a fact that only decryption-time processing would normally produce. `listener_filters` exist for exactly this. They run **before** filter-chain selection, on the raw accepted socket, and their job is to inspect the opening bytes and publish metadata: - `envoy.filters.listener.tls_inspector` peeks at the ClientHello and extracts the SNI server name and the ALPN list, feeding the `server_names`, `transport_protocol` and `application_protocols` criteria. - `envoy.filters.listener.http_inspector` detects the HTTP protocol version so `application_protocols` can be matched on plaintext connections. - `envoy.filters.listener.original_dst` recovers the original destination address of a redirected connection, feeding destination-IP matching. ```yaml listener_filters: - name: envoy.filters.listener.tls_inspector filter_chains: - filter_chain_match: { server_names: ["api.example.com"] } transport_socket: { name: envoy.transport_sockets.tls } - filter_chain_match: {} # catch-all, loses whenever SNI matches above ``` Modern Envoy releases insert the TLS inspector implicitly when a chain match requires SNI or ALPN, so the explicit entry is belt-and-braces on current versions and load-bearing on older ones. Writing it out costs nothing and removes the version question from your config review. ## What this costs at connection time Listener filters delay handling until they have enough bytes. `listener_filters_timeout` bounds that wait (one second by default), and `continue_on_listener_filters_timeout` decides whether a connection that never produced enough data is dropped or proceeds without the inspected metadata. A client that opens a connection and sends nothing therefore occupies a slot for that window — worth knowing when a scanner or a broken health checker starts hammering the port. ## Diagnosing it When the wrong certificate is being served, or plaintext clients get a handshake error on a port that also serves TLS, the question is always which chain won. Envoy's admin config dump shows the chains as loaded, and per-chain naming lets you attribute traffic. The mental model to hold: *listener filters produce facts, chain matching consumes them, and the most specific chain wins.*
- Does moving a filter chain to the top of the filter_chains list make it win more often?No. Unlike routes, filter chains are not evaluated in order — Envoy compares the match criteria by a fixed priority and selects the most specific one. Order in the YAML is irrelevant, which is deliberate: a control plane can emit chains in any order without changing behaviour. If two chains could both be most specific for the same connection, Envoy rejects the configuration.
- What happens to a connection that matches no filter chain at all?If the listener defines a `default_filter_chain`, that chain handles it. Otherwise Envoy closes the connection immediately, which the client sees as a connection reset rather than an HTTP error — no filter chain means no codec, so there is nothing that could produce a status code. Configuring a default chain is the usual way to serve a deliberate rejection instead.
- Why does a listener filter need a timeout at all?Because it must wait for bytes the client may never send. The TLS inspector cannot publish an SNI value until the ClientHello arrives, so the connection is held in a pre-selection state. `listener_filters_timeout` bounds that wait, and `continue_on_listener_filters_timeout` decides whether a stalled connection is dropped or proceeds without the inspected metadata.
saying these in an interview costs you the question
- Thinks filter chains are evaluated top to bottom
- Expects SNI matching to work without inspecting the handshake
- Assumes an unmatched connection gets an HTTP error
- Believes overlapping chains are resolved by order
- Confuses filter chain matching with route matching