Envoy is a high-performance proxy used as a sidecar in service meshes (Istio, Consul Connect) and as a standalone edge proxy. TLS in Envoy is more explicitly configured than in nginx or HAProxy — every certificate, CA bundle, and TLS parameter is referenced through a structured YAML API. This article covers configuring Envoy as a TLS-terminating edge proxy, setting up mTLS to upstream services, using SDS (Secret Discovery Service) for dynamic certificate management, and common troubleshooting patterns.
Envoy TLS architecture
Envoy calls the client side of a TLS connection downstream TLS (clients connect to Envoy over TLS) and the server side upstream TLS (Envoy connects to backends over TLS).
DownstreamTlsContext— configured on listeners to accept TLS connectionsUpstreamTlsContext— configured on clusters to make TLS connections to backends
Each context contains a common_tls_context with the certificate, key, and CA bundle.
Static configuration file approach
Envoy reads a bootstrap YAML file at startup. All configuration that does not come from xDS (dynamic control plane) is in this file.
Minimal TLS termination example
Save as /etc/envoy/envoy.yaml:
static_resources:
listeners:
- name: listener_https
address:
socket_address:
address: 0.0.0.0
port_value: 8443
filter_chains:
- transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
common_tls_context:
tls_certificates:
- certificate_chain:
filename: /etc/envoy/certs/server.crt
private_key:
filename: /etc/envoy/certs/server.key
# Minimum TLS version
tls_params:
tls_minimum_protocol_version: TLSv1_2
tls_maximum_protocol_version: TLSv1_3
cipher_suites:
- ECDHE-RSA-AES256-GCM-SHA384
- ECDHE-RSA-CHACHA20-POLY1305
- TLS_AES_256_GCM_SHA384
filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
codec_type: AUTO
route_config:
name: local_route
virtual_hosts:
- name: backend
domains:
- "*"
routes:
- match:
prefix: "/"
route:
cluster: backend_service
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: backend_service
connect_timeout: 0.25s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: backend_service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: backend.example.com
port_value: 8080
admin:
address:
socket_address:
address: 127.0.0.1
port_value: 9901
HTTP to HTTPS redirect
Add a separate listener on port 80 that redirects to 443:
listeners:
- name: listener_http
address:
socket_address:
address: 0.0.0.0
port_value: 8080
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: redirect
codec_type: AUTO
route_config:
virtual_hosts:
- name: redirect_all
domains: ["*"]
routes:
- match:
prefix: "/"
redirect:
https_redirect: true
port_redirect: 443
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
Upstream TLS — connecting to HTTPS backends
When backends require TLS, configure the cluster with UpstreamTlsContext:
clusters:
- name: secure_backend
connect_timeout: 0.25s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
common_tls_context:
# CA bundle for verifying backend certificates
validation_context:
trusted_ca:
filename: /etc/envoy/certs/ca-bundle.crt
# Server name for SNI (use the backend's FQDN)
sni: backend.example.com
load_assignment:
cluster_name: secure_backend
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: backend.example.com
port_value: 443
Mutual TLS — presenting a client certificate
For mTLS to backend services, add tls_certificates to the UpstreamTlsContext:
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
common_tls_context:
# Client certificate for mTLS
tls_certificates:
- certificate_chain:
filename: /etc/envoy/certs/client.crt
private_key:
filename: /etc/envoy/certs/client.key
# CA to verify backend cert
validation_context:
trusted_ca:
filename: /etc/envoy/certs/ca.crt
sni: backend.example.com
Mutual TLS on the downstream listener (requiring client certificates)
When Envoy should reject connections from clients without a valid certificate:
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
require_client_certificate: true
common_tls_context:
tls_certificates:
- certificate_chain:
filename: /etc/envoy/certs/server.crt
private_key:
filename: /etc/envoy/certs/server.key
validation_context:
trusted_ca:
filename: /etc/envoy/certs/client-ca.crt
Clients must present a certificate signed by the CA in client-ca.crt.
SNI-based routing (multiple domains, multiple backends)
Envoy can route based on SNI without terminating TLS:
listeners:
- name: listener_sni
address:
socket_address:
address: 0.0.0.0
port_value: 443
filter_chains:
- filter_chain_match:
server_names:
- "app.example.com"
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
common_tls_context:
tls_certificates:
- certificate_chain:
filename: /etc/envoy/certs/app.example.com.crt
private_key:
filename: /etc/envoy/certs/app.example.com.key
filters:
- name: envoy.filters.network.http_connection_manager
# ... routes to app backend
- filter_chain_match:
server_names:
- "api.example.com"
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
common_tls_context:
tls_certificates:
- certificate_chain:
filename: /etc/envoy/certs/api.example.com.crt
private_key:
filename: /etc/envoy/certs/api.example.com.key
filters:
- name: envoy.filters.network.http_connection_manager
# ... routes to API backend
SDS (Secret Discovery Service) for dynamic certificate management
In production Envoy deployments, hardcoding file paths is fragile. SDS provides certificates to Envoy dynamically. This is how Istio manages mTLS certificates.
Simple file-based SDS (reads from a static JSON file, useful for automated renewal):
tls_certificates:
- certificate_chain:
filename: /etc/envoy/certs/server.crt
private_key:
filename: /etc/envoy/certs/server.key
When Envoy is configured with watched_directory or SDS API, it can pick up new certificates without restart:
common_tls_context:
tls_certificate_sds_secret_configs:
- name: server_cert
sds_config:
path_config_source:
path: /etc/envoy/sds/server_cert.yaml
watched_directory:
path: /etc/envoy/sds/
validation_context_sds_secret_config:
name: validation_context
sds_config:
path_config_source:
path: /etc/envoy/sds/validation_context.yaml
The SDS secret file:
resources:
- "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret
name: server_cert
tls_certificate:
certificate_chain:
inline_string: |
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
private_key:
inline_string: |
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
When the SDS file changes (e.g., after certificate renewal), Envoy reloads it automatically — without any restart or hot reload required.
Certificate renewal with SDS
For automated renewal (e.g., with cert-manager or a custom script):
#!/bin/bash
# After certificate renewal, update the SDS file
CERT=/etc/letsencrypt/live/example.com/fullchain.pem
KEY=/etc/letsencrypt/live/example.com/privkey.pem
SDS_FILE=/etc/envoy/sds/server_cert.yaml
CERT_CONTENT=$(cat ${CERT} | awk '{printf "%s\\n", $0}')
KEY_CONTENT=$(cat ${KEY} | awk '{printf "%s\\n", $0}')
cat > ${SDS_FILE} <<EOF
resources:
- "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret
name: server_cert
tls_certificate:
certificate_chain:
inline_string: "$(cat ${CERT} | python3 -c "import sys; print(sys.stdin.read().replace('\n', '\\\\n'))")"
private_key:
inline_string: "$(cat ${KEY} | python3 -c "import sys; print(sys.stdin.read().replace('\n', '\\\\n'))")"
EOF
echo "SDS file updated. Envoy will reload automatically."
Running Envoy as a systemd service
sudo nano /etc/systemd/system/envoy.service
[Unit]
Description=Envoy Proxy
After=network.target
[Service]
Type=simple
User=envoy
Group=envoy
ExecStart=/usr/local/bin/envoy -c /etc/envoy/envoy.yaml --log-level info
Restart=on-failure
RestartSec=5s
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now envoy
Verify TLS configuration
# Check the certificate being served
openssl s_client -connect example.com:8443 -servername example.com </dev/null 2>&1 \
| openssl x509 -noout -subject -dates
# Check TLS version and cipher
openssl s_client -connect example.com:8443 -tls1_2 </dev/null 2>&1 | grep "Protocol"
openssl s_client -connect example.com:8443 -tls1_3 </dev/null 2>&1 | grep "Protocol"
# Check Envoy admin for TLS certificate info
curl http://localhost:9901/certs | python3 -m json.tool | head -40
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
TLS error: CERTIFICATE_VERIFY_FAILED (upstream) | Backend CA not in trusted_ca | Add the backend’s CA certificate |
TLS error: no suitable certificates found | Certificate file path wrong or permissions issue | Check path and chown envoy |
| SNI routing not matching | server_names in filter_chain_match wrong | Verify client SNI matches the filter exactly |
| Old certificate served after renewal | File-based certs require hot restart | Use SDS with watched_directory for hot reload |
HANDSHAKE_ERROR with mTLS | Client cert not signed by the expected CA | Verify client CA in validation_context |
Summary
Envoy TLS is configured through DownstreamTlsContext on listeners (for accepting TLS connections) and UpstreamTlsContext on clusters (for making TLS connections to backends). All certificate references use PEM files or inline strings inside common_tls_context. For zero-downtime certificate rotation, use SDS with a watched_directory — Envoy automatically detects file changes and reloads the certificate without restart. For dynamic environments (Kubernetes, service mesh), the SDS API provides certificates from a central secrets store.