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

niks3 itself doesn't terminate TLS — a reverse proxy does, verifies the client cert, and signals the result via headers. niks3 trusts those headers because --mtls-proxy-header opts in.

Flag Default Description
--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 to certs whose subject DN matches this glob; repeatable
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.

NixOS module

services.niks3.nginx = {
  enable = true;
  domain = "niks3.example.com";
  mtls = {
    enable = true;
    clientCAFile = "/etc/niks3/client-ca.pem";
    boundSubjects = [ "CN=ci-runner-*" ];
    # 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.

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