docs: use tls block keys and describe CertMagic

Updates #2527
This commit is contained in:
Kristoffer Dalby committed 2026-10-18 20:53:18 +02:00
1 parent ed854650f0
commit 2e8611c159
2 files changed
+24 -37

No files matched your search

+6 -4
View File
@@ -38,8 +38,9 @@ values to your Headscale [configuration file](../configuration.md):
```yaml title="config.yaml" hl_lines="1"
server_url: https://<SERVER_NAME>
tls_cert_path: ""
tls_key_path: ""
tls:
cert_path: ""
key_path: ""
```
Headscale logs `WRN listening without TLS but ServerURL does not start with http://` during startup. This is expected
@@ -92,8 +93,9 @@ metrics_listen_addr: 127.0.0.1:9090
trusted_proxies:
- 127.0.0.1/32
- ::1/128
tls_cert_path: ""
tls_key_path: ""
tls:
cert_path: ""
key_path: ""
```
### Apache
+18 -33
View File
@@ -2,29 +2,32 @@
## Bring your own certificate
Headscale can be configured to expose its web service via TLS. To configure the certificate and key file manually, set the `tls_cert_path` and `tls_key_path` configuration parameters. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.
Headscale can be configured to expose its web service via TLS. To configure the certificate and key file manually, set the `tls.cert_path` and `tls.key_path` configuration parameters. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.
```yaml title="config.yaml"
tls_cert_path: ""
tls_key_path: ""
tls:
cert_path: ""
key_path: ""
```
The certificate should contain the full chain, else some clients, like the Tailscale Android client, will reject it.
## Let's Encrypt / ACME
To get a certificate automatically via [Let's Encrypt](https://letsencrypt.org/), set `tls_letsencrypt_hostname` to the desired certificate hostname. This name must resolve to the IP address(es) headscale is reachable on (i.e., it must correspond to the `server_url` configuration parameter). The certificate and Let's Encrypt account credentials will be stored in the directory configured in `tls_letsencrypt_cache_dir`. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.
To get a certificate automatically via [Let's Encrypt](https://letsencrypt.org/), set `tls.acme.hostname` to the desired certificate hostname. This name must resolve to the IP address(es) headscale is reachable on (i.e., it must correspond to the `server_url` configuration parameter). The certificate and Let's Encrypt account credentials will be stored in the directory configured in `tls.acme.cache_dir`. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.
```yaml title="config.yaml"
tls_letsencrypt_hostname: ""
tls_letsencrypt_listen: ":http"
tls_letsencrypt_cache_dir: ".cache"
tls_letsencrypt_challenge_type: HTTP-01
tls:
acme:
hostname: ""
http_listen: ":http"
cache_dir: /var/lib/headscale/cache
challenge_type: HTTP-01
```
### Challenge types
Headscale only supports two values for `tls_letsencrypt_challenge_type`: `HTTP-01` (default) and `TLS-ALPN-01`.
Headscale supports three values for `tls.acme.challenge_type`: `HTTP-01` (default), `TLS-ALPN-01` and `DNS-01`. `DNS-01` publishes the challenge through the DNS provider in `dns.challenge.provider`.
#### HTTP-01
@@ -32,9 +35,9 @@ For `HTTP-01`, headscale must be reachable on port 80 for the Let's Encrypt auto
!!! warning "`listen_addr` cannot also be port 80"
`listen_addr` and `tls_letsencrypt_listen` must not bind the same TCP socket. A common mistake is setting `listen_addr: 0.0.0.0:80` together with `tls_letsencrypt_hostname` — both endpoints try to claim port 80, and the second bind fails with `address already in use` even though no other process is involved. Headscale validates this at startup and refuses to launch with a `Fatal config error: listen_addr and tls_letsencrypt_listen would bind the same TCP socket` message. Use `listen_addr: 0.0.0.0:443` (or any non-80 port) when HTTP-01 is enabled.
`listen_addr` and `tls.acme.http_listen` must not bind the same TCP socket. A common mistake is setting `listen_addr: 0.0.0.0:80` together with `tls.acme.hostname` — both endpoints try to claim port 80, and the second bind fails with `address already in use` even though no other process is involved. Headscale validates this at startup and refuses to launch with a `Fatal config error: listen_addr and tls.acme.http_listen would bind the same TCP socket` message. Use `listen_addr: 0.0.0.0:443` (or any non-80 port) when HTTP-01 is enabled.
If you need to change the ip and/or port used by headscale for the Let's Encrypt validation process, set `tls_letsencrypt_listen` to the appropriate value. This can be handy if you are running headscale as a non-root user (or can't run `setcap`). Keep in mind, however, that Let's Encrypt will _only_ connect to port 80 for the validation callback, so if you change `tls_letsencrypt_listen` you will also need to configure something else (e.g. a firewall rule) to forward the traffic from port 80 to the ip:port combination specified in `tls_letsencrypt_listen`.
If you need to change the ip and/or port used by headscale for the Let's Encrypt validation process, set `tls.acme.http_listen` to the appropriate value. This can be handy if you are running headscale as a non-root user (or can't run `setcap`). Keep in mind, however, that Let's Encrypt will _only_ connect to port 80 for the validation callback, so if you change `tls.acme.http_listen` you will also need to configure something else (e.g. a firewall rule) to forward the traffic from port 80 to the ip:port combination specified in `tls.acme.http_listen`.
#### TLS-ALPN-01
@@ -42,12 +45,12 @@ For `TLS-ALPN-01`, headscale listens on the ip:port combination defined in `list
### Technical description
Headscale uses [autocert](https://pkg.go.dev/golang.org/x/crypto/acme/autocert), a Golang library providing [ACME protocol](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment) verification, to facilitate certificate renewals via [Let's Encrypt](https://letsencrypt.org/about/). Certificates will be renewed automatically, and the following can be expected:
Headscale uses [CertMagic](https://github.com/caddyserver/certmagic), a Golang library providing [ACME protocol](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment) verification, to facilitate certificate renewals via [Let's Encrypt](https://letsencrypt.org/about/). Certificates will be renewed automatically, and the following can be expected:
- Certificates provided from Let's Encrypt have a validity of 3 months from date issued.
- Renewals are only attempted by headscale when 30 days or less remains until certificate expiry.
- Renewal attempts by autocert are triggered at a random interval of 30-60 minutes.
- No log output is generated when renewals are skipped, or successful.
- Renewals start when a third of the validity remains.
- Renewal checks run every 10 minutes.
- Log lines from the library are logged with `logger=acme`.
#### Checking certificate expiry
@@ -62,21 +65,3 @@ $ openssl s_client -servername [hostname] -connect [hostname]:443 | openssl x509
notBefore=Feb 8 09:48:26 2024 GMT
notAfter=May 8 09:48:25 2024 GMT
```
#### Log output from the autocert library
As these log lines are from the autocert library, they are not strictly generated by headscale itself.
```plaintext
acme/autocert: missing server name
```
Likely caused by an incoming connection that does not specify a hostname, for example a `curl` request directly against the IP of the server, or an unexpected hostname.
```plaintext
acme/autocert: host "[foo]" not configured in HostWhitelist
```
Similarly to the above, this likely indicates an invalid incoming request for an incorrect hostname, commonly just the IP itself.
The source code for autocert can be found [here](https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.19.0:acme/autocert/autocert.go)