Self-hosted & ConnectorsMutual TLS

Mutual TLS

Require a client certificate before any request reaches a self-hosted API, either at the Kubernetes ingress or in the API process itself.

What mutual TLS adds

With mutual TLS (mTLS) the client proves its identity with a certificate during the TLS handshake, in addition to the server proving its own. A client without a certificate signed by your CA cannot open a connection at all.

mTLS is off by default and is defence in depth, not a replacement for credentials: every request still needs an API key or OAuth token, exactly as without mTLS. Certificate subjects are never logged.

There are two places to enforce it. Use one, not both:

PlacementUse when
At the ingress (Helm ingress.mtls.*)You run the Helm chart behind ingress-nginx. Recommended
In the API process (TRANSMUTE_TLS_*)You run Docker Compose or a plain host install with no ingress in front

At the ingress (Helm)

Store the CA that signs your client certificates in a Secret under the key ca.crt:

kubectl -n transmute create secret generic transmute-client-ca --from-file=ca.crt=./client-ca.pem

Enable the ingress and its mTLS block:

ingress:
  enabled: true
  className: nginx
  hosts:
    - host: transmute.example.internal
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: transmute-server-tls
      hosts: [transmute.example.internal]
  mtls:
    enabled: true
    caSecret: transmute/transmute-client-ca   # <namespace>/<secret> holding ca.crt
    verifyDepth: 1                            # raise if you issue through intermediate CAs
ValueDefaultMeaning
ingress.mtls.enabledfalseRequire and verify a client certificate at the ingress
ingress.mtls.caSecret(empty)<namespace>/<secret> holding ca.crt. Required when enabled: helm template and helm install fail without it
ingress.mtls.verifyDepth1Maximum certificate chain depth accepted

The chart renders the ingress-nginx annotations auth-tls-secret, auth-tls-verify-client: "on" and auth-tls-verify-depth. Other ingress controllers need their own equivalent annotations, which you can set through ingress.annotations. The API pods keep serving plain HTTP inside the cluster, so the chart's health probes are unaffected.

Verify from a client:

# without a client certificate: the handshake is refused
curl --cacert server-ca.pem https://transmute.example.internal/v1/health

# with one: 200 {"status":"ok"}
curl --cacert server-ca.pem --cert client.pem --key client.key \
  https://transmute.example.internal/v1/health

In the API process

Three variables turn the API's own listener (on PORT) into a TLS or mTLS listener. Each takes a path to a PEM file; mount the files read-only. The private key is a secret: deliver it as a file from your secret store, never inline in a config file.

VarMeaning
TRANSMUTE_TLS_CERTServer certificate (PEM). With TRANSMUTE_TLS_KEY, the listener serves TLS, minimum TLS 1.2
TRANSMUTE_TLS_KEYServer private key (PEM)
TRANSMUTE_TLS_CLIENT_CACA bundle (PEM). The listener then requires a client certificate signed by one of these CAs and verifies it on every connection

All three unset (the default) means plain HTTP.

The API refuses to start on any partial or broken configuration, so it never silently falls back to plain HTTP or to TLS without client verification:

  • a certificate without a key, or a key without a certificate;
  • a client CA without the certificate and key;
  • an unreadable or unparsable file, or a CA file containing no certificate.

The startup log line listening (TLS) includes client_cert_required: true when mTLS is active.

Docker Compose example:

services:
  api:
    image: ghcr.io/403ent/transmute:<release>
    ports:
      - "8443:8443"
    environment:
      PORT: "8443"
      TRANSMUTE_TLS_CERT: /etc/transmute/tls/server.crt
      TRANSMUTE_TLS_KEY: /etc/transmute/tls/server.key
      TRANSMUTE_TLS_CLIENT_CA: /etc/transmute/tls/client-ca.pem
    volumes:
      - /opt/transmute/tls:/etc/transmute/tls:ro

Once mTLS is on, everything that talks to the API needs a client certificate, including your health checks. The requirement applies to every path, /v1/health included, so probe over HTTPS and present a client certificate.

On Kubernetes, prefer the ingress placement: the chart's probes call GET /v1/health over plain HTTP and cannot present a client certificate.