Ceryx logo
Menu
View as Markdown

API protection with mTLS

Let only clients with a certificate you trust reach an API. No VPN required.

With mutual TLS (mTLS), clients prove who they are with a certificate, the way servers do. Turn it on for a route, and Ceryx only lets through requests from clients with a certificate signed by a certificate authority (CA) you choose. Everything else gets 403 Forbidden before it reaches your service.

1. Create a CA and a client certificate

If you don't have a CA for clients yet, create one with OpenSSL, then a certificate for your first client:

mkdir -p /opt/ceryx/mtls && cd /opt/ceryx/mtls

# The CA. Keep ca.key secret: it can sign certificates for new clients.
openssl req -x509 -newkey rsa:4096 -nodes -days 3650 \
  -subj '/CN=Example API clients' -keyout ca.key -out ca.pem

# A certificate for one client, valid for 90 days.
openssl req -newkey rsa:2048 -nodes -subj '/CN=billing-service' \
  -keyout client.key -out client.csr
openssl x509 -req -in client.csr -CA ca.pem -CAkey ca.key -CAcreateserial \
  -days 90 -out client.pem

Give client.pem and client.key to the client.

2. Make the CA available to Ceryx

Add this volume mount to the compose.yml file you use to run Ceryx, so the CA certificate is available inside the container:

services:
  ceryx:
    volumes:
      - ./mtls/ca.pem:/etc/ceryx/mtls/ca.pem:ro

Then apply it:

cd /opt/ceryx
docker compose up -d

3. Require client certificates on the route

docker compose -p ceryx exec ceryx curl -X POST localhost:8080/api/routes \
  -H 'Content-Type: application/json' \
  -d '{
    "source": "api.example.com",
    "target": "http://api:8000",
    "settings": {
      "enforce_https": true,
      "mtls_require_client_certificate": true,
      "mtls_trusted_ca": "/etc/ceryx/mtls/ca.pem"
    }
  }'

Routes that require client certificates must also enforce HTTPS, since certificates are only exchanged over TLS. Ceryx checks that it can read the CA file and answers 400 if it can't.

4. Test it

Without a certificate, Ceryx refuses the request:

curl -i https://api.example.com
HTTP/2 403
...
client certificate required: no client certificate

With the client's certificate, the request goes through:

curl --cert client.pem --key client.key https://api.example.com

What clients see

Request Response
Plain HTTP 301 redirect to HTTPS.
HTTPS without a certificate, or with one your CA didn't sign 403 Forbidden.
HTTPS on a connection opened for another hostname 421 Misdirected Request. The client retries on a new connection.
HTTPS with a valid certificate Proxied to your service.

Protect many routes with one CA

Set a default CA for every route that requires client certificates and doesn't name its own by setting this environment variable when running docker compose up -d:

CERYX_MTLS_DEFAULT_TRUSTED_CA=/etc/ceryx/mtls/ca.pem

To require client certificates on every new route, also set CERYX_ROUTE_SETTINGS_DEFAULT_MTLS_REQUIRE_CLIENT_CERTIFICATE=true, together with CERYX_ROUTE_SETTINGS_DEFAULT_ENFORCE_HTTPS=true.

Revoking access

Ceryx doesn't check certificate revocation lists. Keep client certificates short-lived, and to cut off every client at once, replace the CA.

Esc

Type to search the docs.