juanfont/headscale · critical
configuring TLS settings: %w
Error message
configuring TLS settings: %w
What it means
Wraps failures of Headscale.getTLSSettings() (hscontrol/app.go:879) during startup. That function either builds an autocert (Let's Encrypt) config or loads a static certificate pair with tls.LoadX509KeyPair(h.cfg.TLS.CertPath, h.cfg.TLS.KeyPath). The error is the raw failure: an unsupported ACME challenge type (errUnsupportedLetsEncryptChallengeType) or a cert/key file that cannot be read or parsed.
Source
Thrown at hscontrol/app.go:680
socketHandler := http.NewServeMux()
socketHandler.Handle("/api/v2/", apiv2.WithLocalTrust(humaV2Mux))
socketHandler.Handle("/", apiv1.WithLocalTrust(humaMux))
socketServer := &http.Server{
Handler: socketHandler,
ReadTimeout: types.HTTPTimeout,
}
errorGroup.Go(func() error { return socketServer.Serve(socketListener) })
//
//
// Set up REMOTE listeners
//
tlsConfig, err := h.getTLSSettings()
if err != nil {
return fmt.Errorf("configuring TLS settings: %w", err)
}
//
//
// HTTP setup
//
// This is the regular router that we expose
// over our main Addr
router := h.createRouter(humaMux, humaV2Mux)
httpServer := &http.Server{
Addr: h.cfg.Addr,
Handler: router,
ReadTimeout: types.HTTPTimeout,
// Long polling should not have any timeout, this is overridden
// further down the chain
WriteTimeout: types.HTTPTimeout,View on GitHub (pinned to 565fd254d0)
Solutions
- Verify both files exist and are readable by the headscale user: ls -l <tls_cert_path> <tls_key_path>.
- Validate the pair matches and parses: openssl x509 -in cert.pem -noout -modulus | openssl md5 and openssl rsa -in key.pem -noout -modulus | openssl md5 (hashes must be equal).
- Fix acme_challenge_type: it must be exactly HTTP-01 or TLS-ALPN-01 when letsencrypt.hostname is set.
- Check the PEM files are unmodified (no appended logs/text) and use Unix line endings; re-copy from the source if in doubt.
Example fix
# before (config.yaml) tls_cert_path: /etc/headscale/tls/fullchain.pem tls_key_path: /etc/headscale/tls/privkey.pem # cert renewed via certbot to /etc/letsencrypt/live/... -> files missing # after tls_cert_path: /etc/letsencrypt/live/example.org/fullchain.pem tls_key_path: /etc/letsencrypt/live/example.org/privkey.pem
Defensive patterns
Strategy: validation
Validate before calling
// Pre-flight TLS pair check before handing config to Headscale.
func tlsPairLoadable(certPath, keyPath string) error {
_, err := tls.LoadX509KeyPair(certPath, keyPath)
return err
}
// And for ACME:
func challengeTypeValid(t string) bool {
return t == "HTTP-01" || t == "TLS-ALPN-01"
} Try / catch
if err := h.Serve(); err != nil {
if errors.Is(err, errUnsupportedLetsEncryptChallengeType) {
// fix acme_challenge_type in config
} else if _, ok := err.(*tls.CertificateVerificationError); ok || strings.Contains(err.Error(), "PEM") {
// fix cert/key files
}
} Prevention
- Validate cert/key pairs in CI for any config change (openssl x509/rsa modulus check).
- Automate cert renewal with a deploy hook that reloads headscale.
- Pin acme_challenge_type explicitly to HTTP-01 or TLS-ALPN-01 when using Let's Encrypt.
When it happens
Trigger: tls_cert_path/tls_key_path point to missing or unreadable files; the cert and key do not match (tls: private key does not match public key); the key file is malformed (tls: failed to find any PEM data); acme_challenge_type is set to anything other than HTTP-01 or TLS-ALPN-01 while letsencrypt.hostname is set.
Common situations: Renewed certificates deployed to a different path than configured; pasting a fullchain vs privkey in the wrong config keys (common with Let's Encrypt /certbot/letsencrypt/live paths); typos in the paths; PEM files with Windows line endings or extra text around the blocks; upgrading config and losing the acme_challenge_type setting so it no longer matches a valid value.
Understand the failure class
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- configuration error: %w
- init state: %w
- building trusted_proxies middleware: %w
- DERP server private key and noise private key are the same:
- getting DERPMap: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/a2467e014c0c967e.
Report an issue: GitHub.