# TLS

> Serve over HTTPS and, optionally, require a client certificate (mutual TLS).

TLS (the successor of SSL) encrypts the connection between client and server using a **certificate** and its **private key**. Serving over HTTPS in srvx is a matter of providing them. **Mutual TLS (mTLS)** goes one step further and also asks the *client* to present a certificate, which the server verifies against a trusted CA — a common building block for service-to-service and zero-trust setups.

## Enabling HTTPS

Provide `tls.cert` and `tls.key`. srvx switches the [`protocol`](/guide/options#protocol) to `https` automatically.

```js
import { serve } from "srvx";

serve({
  tls: { cert: "./server.crt", key: "./server.key" },
  fetch: () => new Response("👋 Secure hello!"),
});
```

**tls options:**

- `cert`: Certificate in PEM format — file path or inline content (required).
- `key`: Private key in PEM format — file path or inline content (required).
- `passphrase`: Passphrase for the private key (optional).

Server TLS works on **Node.js**, **Deno** and **Bun**.

<tip>

`cert`/`key`/`ca` values are treated as inline PEM when they start with `-----BEGIN `, otherwise as file paths.

</tip>

<important>

- Never commit private keys; load them from environment variables or a secret manager.
- Consider automatic certificate management (e.g. Let's Encrypt) for production.

</important>

## Mutual TLS (mTLS)

Mutual TLS is provided by the opt-in `mtls()` plugin from `srvx/mtls`. It requests a client certificate during the handshake and exposes it — with the negotiated protocol and cipher — on [`request.tls`](#requesttls).

There are two places an unauthenticated client can be turned away, depending on `rejectUnauthorized`:

- **TLS handshake (default, rejectUnauthorized: true)** — a client without a certificate signed by a trusted `ca` never completes the handshake. The connection is dropped before any request reaches your `fetch` handler, so there is nothing to check for in application code.
- **Application layer (rejectUnauthorized: false)** — every handshake is allowed to complete, and `request.tls.authorized` tells you whether the presented certificate (if any) was trusted. This is useful when you want to respond with your own error (e.g. a JSON `401`) instead of a raw connection reset, or when unauthenticated clients should still reach some routes.

```js
import { serve } from "srvx/node";
import { mtls } from "srvx/mtls";

serve({
  tls: { cert: "./server.crt", key: "./server.key" },
  plugins: [
    mtls({
      ca: "./ca.crt",
      requestCert: true,
      // Accept the handshake even for untrusted/missing certs so the
      // handler can decide how to respond, instead of a TLS-level reset.
      rejectUnauthorized: false,
    }),
  ],
  fetch: (request) => {
    if (!request.tls?.authorized) {
      return new Response("client certificate required", { status: 401 });
    }
    return new Response(`Hello, ${request.tls.peerCertificate?.subject?.CN}`);
  },
});
```

<important>

With the default `rejectUnauthorized: true`, the `if (!request.tls?.authorized)` check above is unreachable — unauthenticated clients are already rejected at the TLS layer. Only set `rejectUnauthorized: false` if you intend to enforce authorization yourself in the handler, as shown here.

</important>

**mtls() options:**

- `ca`: Trusted CA certificate(s) in PEM format — file path(s) or inline content (optional). When set, replaces the well-known Mozilla CAs.
- `requestCert`: Request a certificate from connecting clients (default `true`).
- `rejectUnauthorized`: Reject the TLS handshake itself when the client's certificate is not signed by a trusted `ca` (default `true`). When `false`, the handshake always completes and an unverified certificate is instead exposed via [`request.tls`](#requesttls) with `authorized: false`, leaving enforcement to your handler.

### `request.tls`

While the plugin is active, [`request.tls`](/guide/handler#requesttls) provides:

- `peerCertificate` — the client certificate ([node:tls `PeerCertificate`](https://nodejs.org/api/tls.html#certificate-object)). An empty object (`{}`) when none was presented.
- `authorized` — `true` if the certificate was signed by a trusted `ca`.
- `authorizationError` — why verification failed, when `authorized` is `false`.
- `protocol` — negotiated TLS protocol, e.g. `"TLSv1.3"`.
- `cipher` — negotiated cipher suite.

### Runtime support

<note>

`mtls()` requires an HTTPS server (`tls.cert` + `tls.key`) and srvx's **Node.js adapter** (`import { serve } from "srvx/node"`); it throws otherwise.

- **Node.js** — works natively.
- **Deno** (2.8+) — works through the Node adapter, which runs on Deno via `node:https`. (Native `Deno.serve` cannot request client certificates.)
- **Bun** — not currently supported: neither native `Bun.serve` nor Bun's `node:http(s)` server exposes the peer certificate to the handler, so the plugin throws. Tracked in [oven-sh/bun#16254](https://github.com/oven-sh/bun/issues/16254).

</note>
