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.