Skip to content
Jörg Thalheim edited this page May 10, 2026 · 5 revisions

mTLS Client Certificates

If the niks3 server sits behind a reverse proxy that verifies client certificates, the certificate is the credential — no bearer token required. This is a good fit for fleets of machines that already manage X.509 certs (internal CA, step-ca, vault PKI, SPIFFE).

Client

Pass the cert and skip the auth flags entirely:

niks3 push --server-url https://niks3.example.com \
  --client-cert /etc/niks3/client.pem \
  --client-key  /etc/niks3/client.key \
  --ca-cert     /etc/niks3/ca.pem \
  ./result

--ca-cert is optional — without it the system CA pool is used to verify the server. The niks3-hook serve daemon takes the same flags.

For Nix to read from the cache through a vhost that requires a client cert (require = true below), set the cert in nix.conf:

ssl-cert-file = /etc/niks3/client.pem
ssl-key-file = /etc/niks3/client.key

Server

There are two ways for the server to learn the verified client cert:

Native TLS termination

The server can terminate TLS itself and verify client certs directly:

niks3-server \
  --tls-cert /etc/niks3/server.pem \
  --tls-key /etc/niks3/server.key \
  --tls-client-ca /etc/niks3/client-ca.pem \
  --mtls-bound-subject 'CN=ci-runner-*' \
  ...

This is simpler and safer (no header trust) but you give up nginx's ACME, HTTP/2, and rate-limiting features. Best for internal deployments behind a firewall.

Behind a reverse proxy

When a reverse proxy terminates TLS, it verifies the client cert and signals the result via headers. niks3 trusts those headers because --mtls-proxy-header opts in.

niks3-server \
  --mtls-proxy-header X-SSL-Client-Verify \
  --mtls-bound-subject 'CN=ci-runner-*' \
  ...

Security: --mtls-proxy-header must only be enabled behind a proxy that overrides the header on every request — including requests where verification failed or no cert was presented. Otherwise any client can fake X-SSL-Client-Verify: SUCCESS and bypass auth entirely.

Flags

Flag Default Description
--tls-cert / --tls-key (off) Terminate TLS in the server instead of a proxy
--tls-client-ca (off) CA bundle for native mTLS client verification
--mtls-proxy-header (off) Header set to SUCCESS by the proxy after verifying the client cert
--mtls-subject-header X-SSL-Client-Dn Header carrying the verified cert's subject DN
--mtls-bound-subject (any) Restrict write auth to certs whose subject DN matches this glob; repeatable
--mtls-bound-subject-read (public) Gate the read proxy behind mTLS for matching cert DNs; repeatable. Empty = public reads

--tls-client-ca and --mtls-proxy-header are mutually exclusive.

Subject DN format: native TLS and nginx (≥1.11.6) both emit RFC 4514 DNs, e.g. CN=ci-runner-1,O=Acme. Older nginx or the _legacy variable uses OpenSSL's slash format. Run with --debug to see the exact string the server matches against, or use a glob like *CN=ci-runner*.

NixOS module

services.niks3.nginx = {
  enable = true;
  domain = "niks3.example.com";
  mtls = {
    enable = true;
    clientCAFile = "/etc/niks3/client-ca.pem";
    boundSubjects = [ "CN=ci-runner-*" ];
    # boundSubjectsRead = [ "CN=*,O=Acme" ];  # gate reads too (private cache)
    # require = true;  # `optional` (default) lets bearer-token auth coexist
  };
};

This configures nginx with ssl_verify_client, always sets X-SSL-Client-Verify and X-SSL-Client-Dn on proxied requests (overriding any inbound values), and passes the matching --mtls-* flags to the server.

With require = false (the default, nginx ssl_verify_client optional), clients without a cert can still authenticate with a bearer token. With require = true, every TLS connection — including Nix substituter reads — must present a cert.

Private read proxy

By default the read proxy is public: Nix substituters present no credentials and the cache contents are integrity-signed. Set boundSubjectsRead to gate reads behind mTLS — useful for caches that must not leak which derivations exist.

The write and read allowlists are independent: a CI runner cert can be allowed to push but not browse, or vice versa. Setting boundSubjectsRead implies require = true since every read must present a cert.

When to use mTLS vs OIDC

  • mTLS: long-lived machines with managed certs, no external identity provider needed, both sides verify each other.
  • OIDC: short-lived CI jobs (GitHub Actions, GitLab CI), no certs to provision, identity comes from the CI platform.
  • API token: simplest, but a shared secret. Good for bootstrapping or a single trusted operator.

Clone this wiki locally