Overview :
This article explains how TLS negotiation and cipher suite selection occurs when using Envoy as a data plane proxy in Consul service mesh. It covers both North-South traffic (user to API Gateway) and East-West traffic (API Gateway to services).
Background :
For TLS versions TLSv1.2 and TLSv1.3, Envoy negotiates the following default cipher suites.
Default Supported Cipher Suites
TLSv1.2
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
TLSv1.3
TLS_AES_128_GCM_SHA256
TLS_AES_256_GCM_SHA384
TLS_CHACHA20_POLY1305_SHA256
1. TLS Flow: User → API Gateway (North-South traffic)
When a client (user) accesses the API Gateway, the TLS negotiation occurs between the client and Envoy (acting as the server).
Example Gateway configuration
apiVersion: gateway.networking.k8s.io/v1beta1
kind: Gateway
metadata:
name: api-gateway
namespace: consul
spec:
gatewayClassName: consul
listeners:
- protocol: HTTPS
port: 443
name: https
allowedRoutes:
namespaces:
from: All
tls:
certificateRefs:
- name: generic-test-cert
namespace: consul
mode: Terminate
options:
# api-gateway.consul.hashicorp.com/tls_min_version: "TLSv1_3"
api-gateway.consul.hashicorp.com/tls_max_version: "TLSv1_2"
Here, the tls:
block defines TLS behavior for incoming requests (users → API Gateway).
Cipher Suite Behavior :
-
Case 1:
tls_max_version = TLSv1.2
Negotiation will use one of the default TLSv1.2 cipher suites.
You may further restrict this using the
tls_cipher_suites
option if you have to specifically choose any one of the supported default cipher suite for TLS negotiation.-
Example:
api-gateway.consul.hashicorp.com/tls_cipher_suites: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"
- Validation of the Cipher suite used in TLS negotiation ( Note: https://localhost:32338 is my api gateway ) :
% nmap --script ssl-enum-ciphers -p 32338 localhost Starting Nmap 7.97 ( https://nmap.org ) at 2025-09-09 13:07 +0530 Nmap scan report for localhost (127.0.0.1) Host is up (0.00021s latency). Other addresses for localhost (not scanned): ::1 PORT STATE SERVICE 32338/tcp open unknown | ssl-enum-ciphers: | TLSv1.2: | ciphers: | TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (ecdh_x25519) - A | compressors: | NULL | cipher preference: indeterminate | cipher preference error: Too few ciphers supported |_ least strength: A Nmap done: 1 IP address (1 host up) scanned in 0.11 seconds
openssl s_client -connect localhost:32338 -crlf Connecting to ::1 CONNECTED(00000005) Can't use SSL_get_servername depth=1 CN=pri-133j554.vault.ca.a11bae7c.consul verify error:num=20:unable to get local issuer certificate verify return:1 depth=0 verify return:1 --- Certificate chain 0 s: i:CN=pri-133j554.vault.ca.a11bae7c.consul a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA256 v:NotBefore: Sep 9 07:09:25 2025 GMT; NotAfter: Sep 12 07:09:55 2025 GMT 1 s:CN=pri-133j554.vault.ca.a11bae7c.consul i:CN=pri-sq4897h3.vault.ca.a11bae7c.consul a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA256 v:NotBefore: Sep 5 06:14:17 2025 GMT; NotAfter: Oct 7 06:14:47 2025 GMT --- Server certificate -----BEGIN CERTIFICATE----- xxxxxxxxx -----END CERTIFICATE----- subject= issuer=CN=pri-133j554.vault.ca.a11bae7c.consul --- Acceptable client certificate CA names CN=pri-sq4897h3.vault.ca.a11bae7c.consul Client Certificate Types: RSA sign, ECDSA sign Requested Signature Algorithms: xxxxxxxxxxxxxxxxxx Peer signing digest: SHA256 Peer signature type: ecdsa_secp256r1_sha256 Peer Temp Key: X25519, 253 bits --- SSL handshake has read 1645 bytes and written 1629 bytes Verification error: unable to get local issuer certificate --- New, TLSv1.2, Cipher is ECDHE-ECDSA-AES128-GCM-SHA256 Protocol: TLSv1.2 Server public key is 256 bit Secure Renegotiation IS supported Compression: NONE Expansion: NONE No ALPN negotiated SSL-Session: Protocol : TLSv1.2 Cipher : ECDHE-ECDSA-AES128-GCM-SHA256 Session-ID: 1396AD7E563DD3XXXXXXXXXXXXXXXXXXXXXXX31B61548E0CE Session-ID-ctx: Master-Key: C50158xxxxxxxxxxxxxxxxxxxxxxxxxxxx0DBDD33 PSK identity: None PSK identity hint: None SRP username: None TLS session ticket lifetime hint: 7200 (seconds) TLS session ticket:
Result: TLSv1.2, Cipher is ECDHE-ECDSA-AES128-GCM-SHA256
is negotiated here.
-
Case 2:
tls_min_version = TLSv1.3
Negotiation will always use one of the default TLSv1.3 cipher suites.
tls_cipher_suites
cannot be applied here.Validation of the Cipher suite used in TLS negotiation ( Note: https://localhost:32338 is my api gateway ):
% nmap --script ssl-enum-ciphers -p 32339 localhost Starting Nmap 7.97 ( https://nmap.org ) at 2025-09-03 13:39 +0530 Nmap scan report for localhost (127.0.0.1) Host is up (0.00018s latency). Other addresses for localhost (not scanned): ::1 PORT STATE SERVICE 32339/tcp open unknown | ssl-enum-ciphers: | TLSv1.3: | ciphers: | TLS_AKE_WITH_AES_128_GCM_SHA256 (ecdh_x25519) - A | TLS_AKE_WITH_AES_256_GCM_SHA384 (ecdh_x25519) - A | TLS_AKE_WITH_CHACHA20_POLY1305_SHA256 (ecdh_x25519) - A | cipher preference: client |_ least strength: A Nmap done: 1 IP address (1 host up) scanned in 0.10 seconds
% openssl s_client -connect localhost:32339 -crlf Connecting to ::1 CONNECTED(00000005) Can't use SSL_get_servername depth=1 CN=pri-1wrfudr.vault.ca.77c176ec.consul verify error:num=20:unable to get local issuer certificate verify return:1 depth=0 verify return:1 --- Certificate chain 0 s: i:CN=pri-1wrfudr.vault.ca.77c176ec.consul a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA256 v:NotBefore: Sep 2 14:42:17 2025 GMT; NotAfter: Sep 5 14:42:47 2025 GMT 1 s:CN=pri-1wrfudr.vault.ca.77c176ec.consul i:CN=pri-ke47f8wu.vault.ca.77c176ec.consul a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA256 v:NotBefore: Sep 2 14:18:05 2025 GMT; NotAfter: Oct 4 14:18:35 2025 GMT --- Server certificate -----BEGIN CERTIFICATE----- XXXXXXXXX -----END CERTIFICATE----- subject= issuer=CN=pri-1wrfudr.vault.ca.77c176ec.consul --- Acceptable client certificate CA names CN=pri-ke47f8wu.vault.ca.77c176ec.consul Requested Signature Algorithms: ECDSA+SHA256:XXXXXXXXXXXXXX:RSA+SHA1 Shared Requested Signature Algorithms: ECDSA+SXXXXXXXXXXXXXXPSS+SHXXXXXXXXXXXA512 Peer signing digest: SHA256 Peer signature type: ecdsa_XXXXXXXXXXXXXX_sha256 Peer Temp Key: X25519, 253 bits --- SSL handshake has read 1485 bytes and written 1634 bytes Verification error: unable to get local issuer certificate --- New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384 Protocol: TLSv1.3 Server public key is 256 bit This TLS version forbids renegotiation. Compression: NONE Expansion: NONE No ALPN negotiated Early data was not sent Verify return code: 20 (unable to get local issuer certificate)
Result: TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
is negotiated here.
-
Case 3:
tls_min_version = TLSv1.2
andtls_max_version = TLSv1.3
Negotiation defaults to TLSv1.3.
tls_cipher_suites
for TLSv1.2 will be ignored.Validation of the Cipher suite used in TLS negotiation ( Note: https://localhost:32338 is my api gateway ):
% nmap --script ssl-enum-ciphers -p 32339 localhost Starting Nmap 7.97 ( https://nmap.org ) at 2025-09-03 14:10 +0530 Nmap scan report for localhost (127.0.0.1) Host is up (0.00017s latency). Other addresses for localhost (not scanned): ::1 PORT STATE SERVICE 32339/tcp open unknown | ssl-enum-ciphers: | TLSv1.2: | ciphers: | TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (ecdh_x25519) - A | TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 (ecdh_x25519) - A | TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 (ecdh_x25519) - A | compressors: | NULL | cipher preference: client | TLSv1.3: | ciphers: | TLS_AKE_WITH_AES_128_GCM_SHA256 (ecdh_x25519) - A | TLS_AKE_WITH_AES_256_GCM_SHA384 (ecdh_x25519) - A | TLS_AKE_WITH_CHACHA20_POLY1305_SHA256 (ecdh_x25519) - A | cipher preference: client |_ least strength: A Nmap done: 1 IP address (1 host up) scanned in 0.12 seconds
% openssl s_client -connect localhost:32339 -crlf Connecting to ::1 CONNECTED(00000005) Can't use SSL_get_servername depth=1 CN=pri-1wrfudr.vault.ca.77c176ec.consul verify error:num=20:unable to get local issuer certificate verify return:1 depth=0 verify return:1 --- Certificate chain 0 s: i:CN=pri-1wrfudr.vault.ca.77c176ec.consul a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA256 v:NotBefore: Sep 2 14:42:17 2025 GMT; NotAfter: Sep 5 14:42:47 2025 GMT 1 s:CN=pri-1wrfudr.vault.ca.77c176ec.consul i:CN=pri-ke47f8wu.vault.ca.77c176ec.consul a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA256 v:NotBefore: Sep 2 14:18:05 2025 GMT; NotAfter: Oct 4 14:18:35 2025 GMT --- Server certificate -----BEGIN CERTIFICATE----- XXXXXXX -----END CERTIFICATE----- subject= issuer=CN=pri-1wrfudr.vault.ca.77c176ec.consul --- Acceptable client certificate CA names CN=pri-ke47f8wu.vault.ca.77c176ec.consul Requested Signature Algorithms: ECDSA+XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX:RSA+SHA512:RSA+SHA1 Shared Requested Signature Algorithms: ECDSA+XXXXXXXXXXXXXXXXXXXXXXXXXXXSHA512 Peer signing digest: SHA256 Peer signature type: ecdsa_secp256r1_sha256 Peer Temp Key: X25519, 253 bits --- SSL handshake has read 1485 bytes and written 1634 bytes Verification error: unable to get local issuer certificate --- New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384 Protocol: TLSv1.3 Server public key is 256 bit This TLS version forbids renegotiation. Compression: NONE Expansion: NONE No ALPN negotiated Early data was not sent Verify return code: 20 (unable to get local issuer certificate) ---
Result: TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
is negotiated here.
Key Point: When the tls_cipher_suites
list is specified, the TLS listener will restrict negotiation to only the defined ciphers for TLS version 1.2. This setting does not apply to TLS 1.3, as cipher suite selection is predefined by the protocol.
2. TLS Flow: API Gateway → End Services (East-West traffic)
When the API Gateway forwards requests to the end services, the TLS negotiation follows the same rules as above, using Envoy’s default supported ciphers.
-
Default negotiation (no Mesh CRD defined):
TLSv1.3 is selected with one of the default TLSv1.3 cipher suites.
Validation of the Cipher suite used in TLS negotiation ( Note: I have executed the below command from my api gateway pod to the end service (frontend.default) listening on port 3000):
# nmap --script ssl-enum-ciphers -p 3000 frontend.default Starting Nmap 7.97 ( https://nmap.org ) at 2025-09-03 08:19 +0000 Nmap scan report for frontend.default (10.96.144.181) Host is up (0.0010s latency). rDNS record for 10.96.144.181: frontend.default.svc.cluster.local PORT STATE SERVICE 3000/tcp open ppp | ssl-enum-ciphers: | TLSv1.2: | ciphers: | TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (ecdh_x25519) - A | TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 (ecdh_x25519) - A | TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 (ecdh_x25519) - A | compressors: | NULL | cipher preference: client | TLSv1.3: | ciphers: | TLS_AKE_WITH_AES_128_GCM_SHA256 (ecdh_x25519) - A | TLS_AKE_WITH_AES_256_GCM_SHA384 (ecdh_x25519) - A | TLS_AKE_WITH_CHACHA20_POLY1305_SHA256 (ecdh_x25519) - A | cipher preference: client |_ least strength: A Nmap done: 1 IP address (1 host up) scanned in 2.70 seconds
~ # openssl s_client -connect frontend.default:3000 -crlf Connecting to 10.96.144.181 CONNECTED(00000003) depth=1 CN=pri-1wrfudr.vault.ca.77c176ec.consul verify error:num=20:unable to get local issuer certificate verify return:1 depth=0 verify return:1 --- Certificate chain 0 s: i:CN=pri-1wrfudr.vault.ca.77c176ec.consul a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA256 v:NotBefore: Sep 2 14:47:21 2025 GMT; NotAfter: Sep 5 14:47:51 2025 GMT 1 s:CN=pri-1wrfudr.vault.ca.77c176ec.consul i:CN=pri-ke47f8wu.vault.ca.77c176ec.consul a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA256 v:NotBefore: Sep 2 14:18:05 2025 GMT; NotAfter: Oct 4 14:18:35 2025 GMT --- Server certificate -----BEGIN CERTIFICATE----- XXXXXXXXXXXXX -----END CERTIFICATE----- subject= issuer=CN=pri-1wrfudr.vault.ca.77c176ec.consul --- Acceptable client certificate CA names CN=pri-ke47f8wu.vault.ca.77c176ec.consul Requested Signature Algorithms: ECDSA+XXXXXXXXXXXXXXXXXXX+SHA1 Shared Requested Signature Algorithms: ECDSA+XXXXXXXXXXXXXXXSHA512 Peer signing digest: SHA256 Peer signature type: ecdsa_secp256r1_sha256 Peer Temp Key: X25519, 253 bits --- SSL handshake has read 1483 bytes and written 1659 bytes Verification error: unable to get local issuer certificate --- New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384 Protocol: TLSv1.3 Server public key is 256 bit
-
Result:
TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
is negotiated here.
-
Custom negotiation (using Mesh CRD):
You can enforce TLSv1.2 by creating a
Mesh
CRD and aligning theincoming.tls
block in Mesh configuration with thetls:
block in the Gateway configuration.Sample Mesh configuration where
TLSv1_2
is negotiated withTLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
cipher is given below.:apiVersion: consul.hashicorp.com/v1alpha1 kind: Mesh metadata: name: mesh spec: tls: incoming: tlsMaxVersion: "TLSv1_2" cipherSuites: ["TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"] outgoing: tlsMaxVersion: "TLSv1_2" cipherSuites: ["TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"]
- Validation of the Cipher suite used in TLS negotiation ( Note: I have executed the below command from my api gateway pod to the end service (frontend.default) listening on port 3000):
~ # nmap --script ssl-enum-ciphers -p 3000 frontend.default Starting Nmap 7.97 ( https://nmap.org ) at 2025-09-09 07:46 +0000 Nmap scan report for frontend.default (10.96.171.22) Host is up (0.00039s latency). rDNS record for 10.96.171.22: frontend.default.svc.cluster.local PORT STATE SERVICE 3000/tcp open ppp | ssl-enum-ciphers: | TLSv1.2: | ciphers: | TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (ecdh_x25519) - A | compressors: | NULL | cipher preference: indeterminate | cipher preference error: Too few ciphers supported |_ least strength: A Nmap done: 1 IP address (1 host up) scanned in 1.83 seconds
~ # openssl s_client -connect frontend.default:3000 -crlf Connecting to 10.96.171.22 CONNECTED(00000003) depth=1 CN=pri-133j554.vault.ca.a11bae7c.consul verify error:num=20:unable to get local issuer certificate verify return:1 Certificate chain 0 s: i:CN=pri-133j554.vault.ca.a11bae7c.consul a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA256 v:NotBefore: Sep 8 01:58:13 2025 GMT; NotAfter: Sep 11 01:58:43 2025 GMT 1 s:CN=pri-133j554.vault.ca.a11bae7c.consul i:CN=pri-sq4897h3.vault.ca.a11bae7c.consul a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA256 v:NotBefore: Sep 5 06:14:17 2025 GMT; NotAfter: Oct 7 06:14:47 2025 GMT --- Server certificate -----BEGIN CERTIFICATE----- XXXXXXX -----END CERTIFICATE----- subject= issuer=CN=pri-133j554.vault.ca.a11bae7c.consul --- Acceptable client certificate CA names CN=pri-sq4897h3.vault.ca.a11bae7c.consul Client Certificate Types: RSA sign, ECDSA sign Requested Signature Algorithms: XXXXXXXX Shared Requested Signature Algorithms: XXXXXXXXXXX Peer signing digest: SHA256 Peer signature type: ecdsa_secp256r1_sha256 Peer Temp Key: X25519, 253 bits --- SSL handshake has read 1393 bytes and written 1654 bytes Verification error: unable to get local issuer certificate --- New, TLSv1.2, Cipher is ECDHE-ECDSA-AES128-GCM-SHA256 Protocol: TLSv1.2 Server public key is 256 bit Secure Renegotiation IS supported No ALPN negotiated SSL-Session: Protocol : TLSv1.2 Cipher : ECDHE-ECDSA-AES128-GCM-SHA256 Session-ID: Session-ID-ctx: Master-Key: XXXXXXXXXXXX PSK identity: None PSK identity hint: None SRP username: None Start Time: 1757404060
-
Result:
TLSv1.2, Cipher is ECDHE-ECDSA-AES128-GCM-SHA256
is negotiated here.
Regarding Certificate Authority Used:
Testing was performed with both Consul’s internal CA and Vault as the connectCA.
The results remained the same in both cases.
When Vault acts as the connectCA, it generates certificates through the PKI secrets engine.
For example when Vault is used as the connectCA :
% openssl s_client -connect localhost:32339 -crlf Connecting to ::1 CONNECTED(00000005) Can't use SSL_get_servername depth=1 CN=pri-1wrfudr.vault.ca.77c176ec.consul verify error:num=20:unable to get local issuer certificate verify return:1 depth=0 verify return:1 --- Certificate chain 0 s: i:CN=pri-1wrfudr.vault.ca.77c176ec.consul a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA256 v:NotBefore: Sep 2 14:42:17 2025 GMT; NotAfter: Sep 5 14:42:47 2025 GMT 1 s:CN=pri-1wrfudr.vault.ca.77c176ec.consul i:CN=pri-ke47f8wu.vault.ca.77c176ec.consul a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA256 v:NotBefore: Sep 2 14:18:05 2025 GMT; NotAfter: Oct 4 14:18:35 2025 GMT --- Server certificate -----BEGIN CERTIFICATE----- XXXXXXXXX -----END CERTIFICATE----- subject= issuer=CN=pri-1wrfudr.vault.ca.77c176ec.consul --- Acceptable client certificate CA names CN=pri-ke47f8wu.vault.ca.77c176ec.consul Requested Signature Algorithms: ECDSA+SHA256:XXXXXXXXXXXXXX:RSA+SHA1 Shared Requested Signature Algorithms: ECDSA+SXXXXXXXXXXXXXXPSS+SHXXXXXXXXXXXA512 Peer signing digest: SHA256 Peer signature type: ecdsa_XXXXXXXXXXXXXX_sha256 Peer Temp Key: X25519, 253 bits --- SSL handshake has read 1485 bytes and written 1634 bytes Verification error: unable to get local issuer certificate --- New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384 Protocol: TLSv1.3 Server public key is 256 bit This TLS version forbids renegotiation. Compression: NONE Expansion: NONE No ALPN negotiated Early data was not sent Verify return code: 20 (unable to get local issuer certificate)
- In the issuer section you can clearly see the issuer as Vault as given here
issuer=CN=pri-1wrfudr.vault.ca.77c176ec.consul
Limitation
The Vault PKI secrets engine does not provide an option to configure custom cipher suites (as confirmed in official documentation here).
-
Therefore, TLS negotiation relies solely on the default cipher suites supported by Envoy, regardless of whether:
Consul internal CA is used, or
Vault is used as the connectCA.
Outcome
In practice, the cipher suites negotiated will always be limited to Envoy’s default supported list.
Customization of cipher suites at the certificate issuance stage is not possible with Vault PKI secrets engine.
TLSv1.2: | ciphers: | TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 | TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 | TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 | | TLSv1.3: | ciphers: | TLS_AKE_WITH_AES_128_GCM_SHA256 | TLS_AKE_WITH_AES_256_GCM_SHA384 | TLS_AKE_WITH_CHACHA20_POLY1305_SHA256
Note: Refer to the KB article given here in order to use Vault as connectCA in Consul.
Conclusion :
Envoy restricts TLS negotiations to its default cipher suites for TLSv1.2 and TLSv1.3.
tls_cipher_suites
customization is supported only for TLSv1.2 and the only case where you can usetls_cipher_suites
is when you settls_max_version
asTLSv1.2
For TLSv1.3, customization is not possible; negotiation will always pick from Envoy’s default cipher list.
When no Mesh CRD is defined, service-to-service (East-West) TLS defaults to TLSv1.3.
Recommendation :
Use tls_max_version=TLSv1.2
if you need tighter control over which cipher suite is used. Otherwise, allow TLSv1.3 for stronger security, noting that its ciphers cannot be customized.
References :
https://developer.hashicorp.com/consul/docs/reference/k8s/api-gateway/gateway
https://go.dev/blog/tls-cipher-suites
https://developer.hashicorp.com/consul/docs/reference/config-entry/mesh#mesh-wide-tls-min-version
https://developer.hashicorp.com/consul/docs/reference/config-entry/mesh#tls