Split One Port With Two Certificates
Goal
Put several filter chains on one listener and split what they accept by SNI, transport protocol and ALPN. You also check for yourself which one wins when conditions overlap.
Why it matters
In a setup where one ingress gateway accepts dozens of domains, "which chain accepts this connection" is "which certificate gets presented". If you misunderstand this rule as an order, you end up spending a day moving chains up and down. The actual decision is made by comparing specificity through eight steps in a fixed order, and if no chain matches, you get not an error response but a connection that is simply cut — if you go through these two things by hand, you can close an "only this domain does not work" report in a few minutes.
Steps
- In
/root/envd-listener/listener.yaml, put two listeners —edge-areturnslistener=aat127.0.0.1:10021andedge-breturnslistener=bat127.0.0.1:10022, both withdirect_response(the admin port is9921). After you start it, save/listeners?format=jsonto/root/envd-listener/01-listeners.json. - Create two self-signed certificates in
/root/envd-listener/tls/—a.crt/a.keyhave CN and SANa.envd.test, andb.crt/b.keyhaveb.envd.test. Write the subject and SAN of the two certificates to/root/envd-listener/02-certs.txt. - Add a listener
mixed(127.0.0.1:10443) to/root/envd-listener/listener.yaml. Turn on thetls_inspectorlistener filter and split two TLS chains byserver_names—a.envd.testreturnschain=sni-aandb.envd.testreturnschain=sni-b. Write the results of requesting with each of the two names to/root/envd-listener/03-sni.txtas two lines,a=andb=. - Connect to
10443twice withopenssl s_client, changing only-servername, and write which certificate comes out each time to/root/envd-listener/04-cert.txtas two lines,a=andb=(the value is the certificate's CN). - Add a chain
plainthat matchestransport_protocol: "raw_buffer"to themixedlistener, at the very front, and make it returnchain=plain. Send a plain HTTP request and a TLS request to the same port10443and write the results to/root/envd-listener/05-raw.txtas two lines,plain=andtls=. - Add a fourth chain
alpn-h2tomixed— it matches onlytransport_protocol: "tls"andapplication_protocols: ["h2"], and returnschain=alpn-h2over HTTP/2. Then request with the SNI set toa.envd.test, over HTTP/2 and check which chain accepts it, and request with the SNI set toz.envd.test, over HTTP/2 and check which chain accepts it, and write them to/root/envd-listener/06-alpn.txtas two lines,sni_and_h2=andh2_only=. - Send a HTTP/1.1 TLS request to
z.envd.test— it matches no chain. In/root/envd-listener/07-nomatch.txt, write two lines:nomatch_rc=(the curl exit code of that request) andmatch_rc=(the exit code of the same request sent toa.envd.test). - In
/root/envd-listener/08-report.md, write four lineschains=(the number of chains of the mixed listener),same_port_plain_and_tls=(yes or no),sni_beats_alpn=(the name of the chain that won when SNI and ALPN both matched in step 6) andnomatch_rc=, and below them write what you learned in at least four lines.
Notes
- When you start Envoy, use
setsid --fork nohup envoy -c <파일> --log-level warn > <로그> 2>&1 </dev/null(the placeholders are the file and the log), and before you start it again, clean up withpkill -x envoy. - When you wait for startup, use a loop that runs until
/readyreturns LIVE instead of a fixedsleep. - The certificates are self-signed, so
curlneeds-k. To send with a name, use--resolve 이름:포트:127.0.0.1together (the placeholders are the name and the port). - Common mistake — if you do not turn on
tls_inspector, theserver_namesandapplication_protocolsconditions match no one. The configuration passes and only the behavior fails. - Common mistake — if you leave out
-addext "subjectAltName=DNS:...", you get a certificate with only a CN.
One process listens on two ports
In /root/envd-listener/listener.yaml, put two listeners — edge-a returns listener=a at 127.0.0.1:10021 and edge-b returns listener=b at 127.0.0.1:10022, both with direct_response (the admin port is 9921). After you start it, save /listeners?format=json to /root/envd-listener/01-listeners.json.
A listener is a unit of "one address and port". One process can hold several, and each listener has its own list of filter chains. If you use direct_response, responses go out without an upstream, so you do not need to start a backend in this lab. /listeners on the admin port is text by default, so you need to add ?format=json to get the structure.
Bake two certificates with different names
Create two self-signed certificates in /root/envd-listener/tls/ — a.crt/a.key have CN and SAN a.envd.test, and b.crt/b.key have b.envd.test. Write the subject and SAN of the two certificates to /root/envd-listener/02-certs.txt.
To split chains by SNI you need at least two certificates with different names. One line of openssl req -x509 -newkey rsa:2048 -nodes gives you the key and the certificate together. Do not leave out -addext "subjectAltName=DNS:..." — today's clients do not look at the CN, only at the SAN. Check with openssl x509 -noout -subject -ext subjectAltName.
Same port, but a different chain accepts depending on the name
Add a listener mixed (127.0.0.1:10443) to /root/envd-listener/listener.yaml. Turn on the tls_inspector listener filter and split two TLS chains by server_names — a.envd.test returns chain=sni-a and b.envd.test returns chain=sni-b. Write the results of requesting with each of the two names to /root/envd-listener/03-sni.txt as two lines, a= and b=.
The SNI is in plain text in the first message of the TLS handshake. So it can be read even before decryption, and what does that job is the tls_inspector listener filter. If you do not turn this filter on, the server_names condition matches no connection at all. Send the request with curl -k --resolve a.envd.test:포트:127.0.0.1 https://... (the placeholder is the port).
When the chains split, the certificates presented split too
Connect to 10443 twice with openssl s_client, changing only -servername, and write which certificate comes out each time to /root/envd-listener/04-cert.txt as two lines, a= and b= (the value is the certificate's CN).
Each chain has its own transport_socket, so which chain is picked is which certificate is presented. This is why one ingress gateway can hold the certificates of dozens of domains. Check with echo | openssl s_client -connect 127.0.0.1:포트 -servername 이름 2>/dev/null | openssl x509 -noout -subject (the placeholders are the port and the name).
Accept plain text and TLS together on one port
Add a chain plain that matches transport_protocol: "raw_buffer" to the mixed listener, at the very front, and make it return chain=plain. Send a plain HTTP request and a TLS request to the same port 10443 and write the results to /root/envd-listener/05-raw.txt as two lines, plain= and tls=.
tls_inspector looks at the first bytes, decides whether this connection is TLS or not, and sets transport_protocol to tls or raw_buffer. That is why the same port can accept both. A plain-text request is curl http://127.0.0.1:포트/ (the placeholder is the port), and the TLS request is done the same way as in the previous step. The chain order does not affect matching — the specificity of the conditions decides.
When both conditions match, which one wins
Add a fourth chain alpn-h2 to mixed — it matches only transport_protocol: "tls" and application_protocols: ["h2"], and returns chain=alpn-h2 over HTTP/2. Then request with the SNI set to a.envd.test, over HTTP/2 and check which chain accepts it, and request with the SNI set to z.envd.test, over HTTP/2 and check which chain accepts it, and write them to /root/envd-listener/06-alpn.txt as two lines, sni_and_h2= and h2_only=.
Filter chain matching narrows through eight steps in a fixed order — destination port, destination IP, server name (SNI), transport protocol, application protocol (ALPN), then the source conditions. The key point of this step is that SNI comes before ALPN. See for yourself what remains when both conditions match at the same time. Use curl --http2 -k --resolve ... to make the ALPN h2.
If no chain matches, you get silence, not a response
Send a HTTP/1.1 TLS request to z.envd.test — it matches no chain. In /root/envd-listener/07-nomatch.txt, write two lines: nomatch_rc= (the curl exit code of that request) and match_rc= (the exit code of the same request sent to a.envd.test).
If no chain matches, Envoy does not give a 404 — because it never got as far as the HTTP layer. It just closes the connection. On the client side it looks like a TLS handshake failure, and curl returns a nonzero exit code. A big share of the reports that come in as "I added the certificate but the connection is just cut" in production is this. Take the exit code of curl from $?.
Leave a chain design memo
In /root/envd-listener/08-report.md, write four lines chains= (the number of chains of the mixed listener), same_port_plain_and_tls= (yes or no), sni_beats_alpn= (the name of the chain that won when SNI and ALPN both matched in step 6) and nomatch_rc=, and below them write what you learned in at least four lines.
This memo is something you will read yourself the next time you design a gateway. Do not write only values; write "so what I will be careful about". For the number of chains, do not count by eye; it is more accurate to pull out the length of filter_chains with yq.