---
title: "TLS and mTLS"
url: "/docs/security/tls-and-mtls/"
kind: "feature"
summary: "Terminate TLS on listeners, require client certificates, originate TLS to upstreams, and select certificate chains by SNI."
---

> Arion site index: [/llms.txt](/llms.txt)

# TLS and mTLS

Terminate TLS on listeners, require client certificates, originate TLS to upstreams, and select certificate chains by SNI.

TLS in Arion is configured through transport sockets: a `DownstreamTlsContext` on a filter chain terminates TLS for incoming connections, and an `UpstreamTlsContext` on a cluster originates TLS to your backends. Use it whenever traffic crosses a network you do not trust — and add client-certificate verification (mTLS) when you also need to know who is connecting.

## Terminate TLS on a listener

Attach a transport socket to a filter chain and give it a certificate chain and private key. Both accept a file path (`filename`), `inline_string`, `inline_bytes`, or `environment_variable`.

```yaml,name=arion.yaml,arion=fragment
filter_chains:
  - name: https
    filters:
      # http_connection_manager, router, ...
    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: certs/example.com.cert.pem }
              private_key: { filename: certs/example.com.key.pem }
```

Instead of inlining the certificate you can reference a named secret with `tls_certificate_sds_secret_configs` — see [secrets and rotation](@/docs/security/secrets-and-rotation.md).

> [!NOTE]
> Server certificates must carry a DNS name in their Subject Alternative Name extension. Arion reads the serving name from the SAN to pick a certificate during the SNI handshake; a certificate without a DNS SAN is rejected with `secret doesn't contain server name`. See [troubleshooting](@/docs/operate/troubleshooting.md).

## Require client certificates (mTLS)

Add a `validation_context` with the CA that signs your clients, and set `require_client_certificate`. Without that flag the client certificate is verified only if one is presented.

```yaml,name=arion.yaml,arion=fragment
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: certs/example.com.cert.pem }
        private_key: { filename: certs/example.com.key.pem }
    validation_context:
      trusted_ca: { filename: certs/ca.cert.pem }
```

## Pin TLS versions

Arion negotiates TLS 1.2 or 1.3. The default minimum is 1.2; configurations asking for 1.0 or 1.1 are refused at startup.

```yaml,arion=fragment
common_tls_context:
  tls_params:
    tls_minimum_protocol_version: TLSv1_3
```

`tls_maximum_protocol_version` works the same way and defaults to the newest supported version.

## Originate TLS to an upstream

Put an `UpstreamTlsContext` on the cluster. `sni` is required — Arion always sends it and uses it to verify the upstream certificate against the `trusted_ca`.

```yaml,name=arion.yaml,arion=fragment
clusters:
  - name: api
    transport_socket:
      name: envoy.transport_sockets.tls
      typed_config:
        "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
        sni: api.example.com
        common_tls_context:
          validation_context:
            trusted_ca: { filename: /etc/ssl/certs/ca-certificates.crt }
```

For development against self-signed backends, `trust_chain_verification: ACCEPT_UNTRUSTED` disables verification (Arion logs a warning). For upstream mTLS, add one client certificate under `tls_certificates` — a cluster accepts at most one.

## Select a chain by SNI

Different hostnames on one port can get different certificates and filters. Add the TLS inspector so the chain is chosen from the ClientHello, then match with `server_names`:

```yaml,name=arion.yaml,arion=fragment
listener_filters:
  - name: tls_inspector
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector
filter_chains:
  - name: api
    filter_chain_match:
      server_names: [api.example.com]
    # filters + transport_socket with the api.example.com certificate
  - name: fallback
    # no server_names: matches everything else
```

The most specific match wins: `api.example.com` beats `*.example.com`, which beats a chain with no `server_names`. Wildcards may only appear as a leading `*.` label; a bare `"*"` entry is refused. A single chain can also hold several certificates, and Arion picks one by matching SNI against each certificate's SAN.

## Verify

```bash
curl -s http://127.0.0.1:9901/certs
```

The admin endpoint lists each loaded certificate with its SANs, validity window and days until expiration. To exercise the handshake end to end:

```bash
curl --cacert certs/ca.cert.pem --resolve example.com:8443:127.0.0.1 https://example.com:8443/
```

## Limits

- TLS 1.2 is the floor; 1.0 and 1.1 are rejected.
- Cipher suites, ECDH curves, signature algorithms and ALPN lists are not configurable; configs that set them are refused.
- The validation context checks the trust chain only: SAN matching, SPKI/hash pinning, CRLs and expired-certificate allowances are not supported.
- `CombinedValidationContext` and certificate providers are not supported; use inline validation or an SDS reference.
- `full_scan_certs_on_sni_mismatch`, session ticket keys and stateful session resumption options are not supported.
