Short answer

Use mTLS on the hop between API gateway and backend, so the backend only accepts connections from the gateway — and only because the gateway can present a valid client certificate. The gateway authenticates users to the outside world; mTLS ensures the traffic to the backend actually comes from the gateway and not via a detour. This prevents a compromised neighbour service or an internal attacker from hitting the backend directly.

The threat mTLS closes

A typical setup: gateway at the edge, backend services on an internal network. The problem is that "internal network" is rarely a real security boundary. Any pod, container or server that can route to the backend can call it — bypassing the gateway's authentication entirely. mTLS makes the backend selective: it only opens up for clients with a certificate it trusts.

Step 1: issue a client certificate for the gateway

The backend must trust a CA, and the gateway receives a client certificate from that same CA:

# The backend's internal CA (can be your existing internal CA)
openssl req -x509 -newkey rsa:4096 -nodes -keyout backend-ca.key \
  -out backend-ca.crt -days 3650 -subj "/CN=Backend Internal CA"

# Client certificate for the gateway, with clientAuth EKU
openssl req -newkey rsa:2048 -nodes -keyout gateway.key -out gateway.csr \
  -subj "/CN=api-gateway"
openssl x509 -req -in gateway.csr -CA backend-ca.crt -CAkey backend-ca.key \
  -CAcreateserial -out gateway.crt -days 365 \
  -extfile <(printf "extendedKeyUsage=clientAuth")

Step 2: backend requires a client certificate (nginx)

The backend's TLS termination — here nginx in front of the application — is set to require and verify the client certificate:

server {
    listen 8443 ssl;
    server_name backend.internal;

    ssl_certificate     /etc/ssl/backend/fullchain.pem;
    ssl_certificate_key /etc/ssl/backend/privkey.pem;

    ssl_client_certificate /etc/ssl/backend/backend-ca.crt;
    ssl_verify_client      on;

    location / {
        # reject anything that is not a verified gateway identity
        if ($ssl_client_s_dn !~ "CN=api-gateway") { return 403; }
        proxy_pass http://127.0.0.1:8080;
    }
}

Here we use $ssl_client_s_dn both to require a valid certificate and to check that it is the gateway's. This separates authentication (a valid cert) from authorisation (the right cert).

Step 3: the gateway presents its client certificate (Envoy)

If you use Envoy as the gateway, configure an upstream TLS context with a client certificate and the CA that validates the backend:

transport_socket:
  name: envoy.transport_sockets.tls
  typed_config:
    "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
    common_tls_context:
      tls_certificates:
        - certificate_chain: { filename: "/etc/envoy/gateway.crt" }
          private_key:       { filename: "/etc/envoy/gateway.key" }
      validation_context:
        trusted_ca: { filename: "/etc/envoy/backend-ca.crt" }
        match_typed_subject_alt_names:
          - san_type: DNS
            matcher: { exact: "backend.internal" }

With nginx as the gateway the equivalent is proxy_ssl_certificate, proxy_ssl_certificate_key and proxy_ssl_trusted_certificate in a location block towards the upstream.

Verify the hop

Test that the backend rejects you without a certificate and accepts you with one:

# Must fail (no client cert):
curl https://backend.internal:8443/health

# Must succeed (gateway identity):
curl --cert gateway.crt --key gateway.key \
     --cacert backend-ca.crt \
     https://backend.internal:8443/health

If it fails even with a certificate, check the common mTLS pitfalls — wrong CA, missing EKU or an incomplete chain — in troubleshooting mTLS.

Watch out: the gateway certificate that expires in silence

Gateway-to-backend mTLS creates a certificate nobody sees in a browser. When the gateway's client certificate expires, all traffic to the backend stops working at once — with no warning and no friendly error page. CertControl also monitors this kind of internal client certificate from internal CAs, tracks expiry and warns in good time, so the hop between gateway and backend does not silently become a single point of failure. Understand the basics in our mTLS guide and see the wider picture in mTLS and Zero Trust.

Frequently asked questions

Why isn't TLS alone enough between gateway and backend?

Ordinary TLS encrypts and proves the backend's identity, but not the gateway's. Any client on the internal network can still connect. mTLS requires the client — the gateway — to also prove its identity, so the backend can reject everyone else.

Should I use the same CA for the gateway and for users?

No, and usually you should not. Users' TLS to the gateway typically uses a public certificate, while gateway-to-backend mTLS uses an internal CA you control completely.

Can I restrict which identity the backend accepts?

Yes. Requiring a valid certificate is authentication; checking that it is the right certificate (e.g. via $ssl_client_s_dn in nginx or SAN matchers in Envoy) is authorisation. Use both.

What happens if the gateway's client certificate expires?

The backend rejects all connections from the gateway, and your API stops responding — usually without a clear error. That is why monitoring expiry on internal client certificates is critical.

Does it work with multiple gateway instances?

Yes. Either issue the same client certificate to all instances, or give each instance its own certificate from the same CA. The backend trusts the CA, not a single instance.