From 775b81a7fa3aff1de02913956595b67d38a6aa45 Mon Sep 17 00:00:00 2001 From: Aarnav Tale Date: Sun, 31 May 2026 14:38:10 -0400 Subject: [PATCH] feat: support multiple CAs through documentation --- config.example.yaml | 5 +++ docs/.vitepress/config.ts | 1 + docs/configuration/index.md | 35 +++------------- docs/configuration/tls.md | 80 +++++++++++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 30 deletions(-) create mode 100644 docs/configuration/tls.md diff --git a/config.example.yaml b/config.example.yaml index eeb95cd..af73341 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -69,6 +69,11 @@ headscale: # If you use the TLS configuration in Headscale, and you are not using # Let's Encrypt for your certificate, pass in the path to the certificate. # (This has no effect if `url` does not start with `https://`) + # + # Note: this pins Headscale's connection to exactly this one cert. If you + # have a private CA you want trusted everywhere (Headscale, OIDC, Docker, + # etc.), set `NODE_EXTRA_CA_CERTS=/path/to/ca.pem` on the Headplane + # process instead — see https://headplane.net/configuration/tls#custom-certificate-authorities # tls_cert_path: "/var/lib/headplane/tls.crt" # Optional, public URL if its different from the `headscale.url` diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index fb36915..d04a606 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -38,6 +38,7 @@ export default defineConfig({ link: "/configuration", items: [ { text: "Common Issues", link: "/configuration/common-issues" }, + { text: "TLS & Certificates", link: "/configuration/tls" }, { text: "Sensitive Values", link: "/configuration#sensitive-values", diff --git a/docs/configuration/index.md b/docs/configuration/index.md index a94e22d..34d393d 100644 --- a/docs/configuration/index.md +++ b/docs/configuration/index.md @@ -86,37 +86,12 @@ To enable debug logging, set the **`HEADPLANE_DEBUG_LOG=true`** environment vari This will enable all debug logs for Headplane, which could fill up log space very quickly. This is not recommended in production environments. -## TLS Termination +## TLS & Certificates -Headplane can terminate TLS itself when both `server.tls_cert_path` and -`server.tls_key_path` are set in the configuration file. Both must point to -PEM-encoded files that Headplane can read. - -```yaml -server: - port: 443 - tls_cert_path: "/var/lib/headplane/tls/fullchain.pem" - tls_key_path: "/var/lib/headplane/tls/privkey.pem" -``` - -When TLS is configured Headplane serves HTTPS/1.1 on `server.port`. HTTP/2 -and HTTP/3 are intentionally not supported in-process — terminate those at a -reverse proxy (e.g. Caddy or Traefik) and forward to Headplane over HTTP/1.1 -if you need them today. - -`server.cookie_secure` is forced to `true` whenever TLS is enabled (browsers -refuse `Secure`-less cookies over HTTPS); a warning is logged if your config -had it set to `false`. - -For most deployments we still recommend terminating TLS at a reverse proxy -([see below](#reverse-proxying)) so you can share certificates with Headscale -and other services. Built-in TLS is meant for the simpler "Headplane on a -single box" scenarios. - -The bundled Docker healthcheck picks up the right scheme and port -automatically — Headplane writes its loopback URL to `/tmp/headplane-listen` -when it starts, and the healthcheck reads it from there. No extra environment -variables, no duplicated config. +Anything to do with TLS — terminating HTTPS in-process, trusting a private +CA for outbound connections to Headscale or your OIDC provider, the +interaction with `cookie_secure` and the Docker healthcheck — lives on its +own page: [TLS & Certificates](./tls). ## Reverse Proxying diff --git a/docs/configuration/tls.md b/docs/configuration/tls.md new file mode 100644 index 0000000..3348594 --- /dev/null +++ b/docs/configuration/tls.md @@ -0,0 +1,80 @@ +# TLS & Certificates + +Everything Headplane does with TLS lives on this page: terminating HTTPS +in-process, trusting private certificate authorities for outbound +connections, and the interactions with cookies and the bundled Docker +healthcheck. + +## Custom Certificate Authorities + +If you front any of the services Headplane talks to — your Headscale server, +your OIDC provider, an HTTPS Docker daemon, etc. — with a private or +self-signed certificate authority, Headplane needs to trust that CA. The +cleanest way to do that is the standard Node.js `NODE_EXTRA_CA_CERTS` +environment variable, which is honoured for **every** outbound TLS +connection in the process (OIDC discovery, Headscale API, Docker, anything +else that uses `fetch` or `https`). + +Provide it as a path to a PEM-encoded bundle of one or more CA certificates: + +```bash +NODE_EXTRA_CA_CERTS=/etc/headplane/extra-cas.pem +``` + +In Docker, bind-mount the bundle and pass the variable through: + +```yaml +services: + headplane: + image: ghcr.io/tale/headplane:latest + environment: + NODE_EXTRA_CA_CERTS: /etc/headplane/extra-cas.pem + volumes: + - "./internal-ca.pem:/etc/headplane/extra-cas.pem:ro" + - "./config.yaml:/etc/headplane/config.yaml" + - "./headplane-data:/var/lib/headplane" +``` + +The bundle is added **on top of** the system trust store, so public +certificates (Let's Encrypt, ZeroSSL, etc.) keep working. + +> `headscale.tls_cert_path` is a narrower knob that pins Headscale's API +> connection to exactly one certificate, bypassing the rest of the trust +> store. It still has its place if you want Headplane to refuse anything +> other than that specific cert for Headscale, but `NODE_EXTRA_CA_CERTS` is +> the right tool whenever you simply want to add a CA to the trusted set. + +## TLS Termination + +Headplane can terminate TLS itself when both `server.tls_cert_path` and +`server.tls_key_path` are set in the configuration file. Both must point to +PEM-encoded files that Headplane can read. + +```yaml +server: + port: 443 + tls_cert_path: "/var/lib/headplane/tls/fullchain.pem" + tls_key_path: "/var/lib/headplane/tls/privkey.pem" +``` + +When TLS is configured Headplane serves HTTPS/1.1 on `server.port`. HTTP/2 +and HTTP/3 are intentionally not supported in-process — terminate those at a +reverse proxy (e.g. Caddy or Traefik) and forward to Headplane over HTTP/1.1 +if you need them today. + +`server.cookie_secure` is forced to `true` whenever TLS is enabled (browsers +refuse `Secure`-less cookies over HTTPS); a warning is logged if your config +had it set to `false`. + +For most deployments we still recommend terminating TLS at a reverse proxy +(see [Reverse Proxying](./#reverse-proxying)) so you can share certificates +with Headscale and other services. Built-in TLS is meant for the simpler +"Headplane on a single box" scenarios. + +## Healthcheck + +The bundled Docker healthcheck picks up the right scheme and port +automatically — Headplane writes its loopback URL to `/tmp/headplane-listen` +when it starts, and the healthcheck reads it from there. So flipping TLS on +or off requires no healthcheck-specific configuration; everything just +works.