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 to https automatically.
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.
cert/key/ca values are treated as inline PEM when they start with -----BEGIN , otherwise as file paths.- Never commit private keys; load them from environment variables or a secret manager.
- Consider automatic certificate management (e.g. Let's Encrypt) for production.
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.
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 trustedcanever completes the handshake. The connection is dropped before any request reaches yourfetchhandler, so there is nothing to check for in application code. - Application layer (
rejectUnauthorized: false) β every handshake is allowed to complete, andrequest.tls.authorizedtells you whether the presented certificate (if any) was trusted. This is useful when you want to respond with your own error (e.g. a JSON401) instead of a raw connection reset, or when unauthenticated clients should still reach some routes.
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}`);
},
});
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.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 (defaulttrue).rejectUnauthorized: Reject the TLS handshake itself when the client's certificate is not signed by a trustedca(defaulttrue). Whenfalse, the handshake always completes and an unverified certificate is instead exposed viarequest.tlswithauthorized: false, leaving enforcement to your handler.
request.tls
While the plugin is active, request.tls provides:
peerCertificateβ the client certificate (node:tlsPeerCertificate). An empty object ({}) when none was presented.authorizedβtrueif the certificate was signed by a trustedca.authorizationErrorβ why verification failed, whenauthorizedisfalse.protocolβ negotiated TLS protocol, e.g."TLSv1.3".cipherβ negotiated cipher suite.
Runtime support
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. (NativeDeno.servecannot request client certificates.) - Bun β not currently supported: neither native
Bun.servenor Bun'snode:http(s)server exposes the peer certificate to the handler, so the plugin throws. Tracked in oven-sh/bun#16254.