Enable mutual TLS
Mutual TLS (mTLS) lets only clients holding a certificate signed by a CA you trust reach the gateway, and turns the certificate's identity into a principal with roles. There are two distinct layers, and you need both:
The TLS listener verifies the client certificate cryptographically (chain validation against a CA). This is
gateway.tls.The
mtlsauth provider reads the verified certificate's Common Name (CN) and maps it to a principal and roles. It does not re-verify the certificate.
Without TLS-level mTLS configured there is no verified client CN for the auth provider to read, so configure both layers together.
Have a server cert, key, and client CA ready01
You need three PEM files on the gateway host:
a server certificate and private key (to terminate TLS), and
the CA certificate that signed the client certificates you will accept.
For a quick local test you can self-sign them with openssl; in production these
come from your PKI. Point the gateway at their paths in the next step.
Configure the TLS listener for client certs02
Under gateway.tls, set the server cert_file/key_file, then add
client_ca_file and require_client_cert: true to require and verify client
certificates during the handshake.
gateway:
listen: "0.0.0.0:8443"
tls:
cert_file: /etc/octopus/tls/server-cert.pem
key_file: /etc/octopus/tls/server-key.pem
client_ca_file: /etc/octopus/tls/client-ca.pem
require_client_cert: true
min_tls_version: "1.2"With require_client_cert: true, a client that does not present a certificate
chaining to client_ca_file fails the handshake. Set it to false to make the
certificate optional (verified if presented). See TLS for
the full listener behavior.
min_tls_version is validated and logged but the file-based acceptor does not
yet pin the protocol version, so it does not disable TLS 1.2 at the
protocol level today — treat it as advisory. File-based certificate hot-reload
is also not swapped into the live acceptor; plan a restart to pick up rotated
file certs. (On Kubernetes the operator path does hot-swap — see
TLS with cert-manager.)
Add the mTLS auth provider03
The mtls provider consumes the verified CN. extract_cn_as_principal: true
makes the CN the principal id and derives roles from cn_to_roles. A pattern
matches a CN when it is * (any CN), equals the CN, or is a suffix of the CN
— so all matching entries contribute their roles.
auth_providers:
client-certs:
type: mtls
client_ca_file: /etc/octopus/tls/client-ca.pem
require_client_cert: true
extract_cn_as_principal: true
cn_to_roles:
"*": ["client"] # every verified client
".internal.example.com": ["internal-service"] # CN suffix match
"admin.client.example.com": ["admin"] # exact CNThe provider's own client_ca_file field is required by the schema but not
used by the provider — the actual CA verification is the
gateway.tls.client_ca_file from the previous step. Set both to the same file
to avoid confusion.
Enforce auth and authorize routes04
Make the provider the default, enforce globally, and gate sensitive routes by the roles you derived from the CN.
auth:
default_provider: client-certs
global_enforce: true
routes:
- path: /internal/*
upstream: internal-api
require_roles: ["internal-service", "admin"] # any-ofNote there is no WWW-Authenticate challenge for mTLS — the identity comes from
the handshake, not a header. See Authorization.
Validate and run05
octopus validate -c gateway.yaml
octopus serve -c gateway.yamlThe complete, validated configuration:
gateway:
listen: "0.0.0.0:8443"
tls:
cert_file: /etc/octopus/tls/server-cert.pem
key_file: /etc/octopus/tls/server-key.pem
client_ca_file: /etc/octopus/tls/client-ca.pem
require_client_cert: true
min_tls_version: "1.2"
upstreams:
- name: internal-api
instances:
- id: internal-1
host: 10.0.0.31
port: 8080
auth_providers:
client-certs:
type: mtls
client_ca_file: /etc/octopus/tls/client-ca.pem
require_client_cert: true
extract_cn_as_principal: true
cn_to_roles:
"*": ["client"]
".internal.example.com": ["internal-service"]
"admin.client.example.com": ["admin"]
auth:
default_provider: client-certs
global_enforce: true
routes:
- path: /internal/*
upstream: internal-api
require_roles: ["internal-service", "admin"]Try it06
# No client cert -> TLS handshake fails
curl -i https://localhost:8443/internal/status
# With a client cert signed by client-ca.pem
curl -i --cert client.pem --key client-key.pem \
https://localhost:8443/internal/statusA client whose CN ends with .internal.example.com is authenticated with roles
["client", "internal-service"] and is allowed; an unrelated client gets
403 forbidden.
See also07
TLS and TLS configuration — the listener layer.
mTLS provider — CN-to-roles matching and principal mapping.
TLS with cert-manager — operator-managed, hot-reloaded certs on Kubernetes.