Envoy Proxy TLS Configuration: Downstream TLS Termination and Upstream TLS Origination

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 connections
  • UpstreamTlsContext — 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

ProblemCauseFix
TLS error: CERTIFICATE_VERIFY_FAILED (upstream)Backend CA not in trusted_caAdd the backend’s CA certificate
TLS error: no suitable certificates foundCertificate file path wrong or permissions issueCheck path and chown envoy
SNI routing not matchingserver_names in filter_chain_match wrongVerify client SNI matches the filter exactly
Old certificate served after renewalFile-based certs require hot restartUse SDS with watched_directory for hot reload
HANDSHAKE_ERROR with mTLSClient cert not signed by the expected CAVerify 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.

Scroll to Top