Octopus
1.x
Docs/Octopus/Enable mutual TLS
Open

Reading4 min
Updated31 Jul 2026
Sourcev1/guides/enable-mtls.mdx

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:

  1. The TLS listener verifies the client certificate cryptographically (chain validation against a CA). This is gateway.tls.

  2. The mtls auth provider reads the verified certificate's Common Name (CN) and maps it to a principal and roles. It does not re-verify the certificate.

Note

Without TLS-level mTLS configured there is no verified client CN for the auth provider to read, so configure both layers together.

1

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.

Warning

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 CN
Warning

The 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-of

Note 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.yaml

The 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/status

A 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