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

  1. Verify both files exist and are readable by the headscale user: ls -l <tls_cert_path> <tls_key_path>.
  2. 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).
  3. Fix acme_challenge_type: it must be exactly HTTP-01 or TLS-ALPN-01 when letsencrypt.hostname is set.
  4. 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

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

Related errors


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/a2467e014c0c967e. Report an issue: GitHub.