# 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:

```bash
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:

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

Then apply it:

```bash
cd /opt/ceryx
docker compose up -d
```

## 3. Require client certificates on the route

```bash
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:

```bash
curl -i https://api.example.com
```

```text
HTTP/2 403
...
client certificate required: no client certificate
```

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

```bash
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`:

```bash
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.
