juanfont/headscale · critical
DERP server private key and noise private key are the same:
Error message
DERP server private key and noise private key are the same: %w
What it means
Startup guard rejecting the configuration where the embedded DERP server key and the Noise protocol key are literally the same key (derpServerKey.Equal(*noisePrivateKey)). Reusing one machine key for both protocols is unsafe because key compromise or usage contexts collide. Note a code defect in the source: this fmt.Errorf wraps `err`, which is nil at this point (the read succeeded), so the rendered message ends with '%!w(<nil>)' instead of a cause — the check itself is still a hard startup failure.
Source
Thrown at hscontrol/app.go:235
// handler to clobber /etc/resolv.conf on every tunnel-IP rebind
// — the handler reapplies a Clone of lastDNSConfig and the magic
// DNS routes vanish, taking the resolver with them for ~6 min
// until the next route-changing netmap. Empty slice survives
// Clone and carries the same "resolve locally" semantics
// (tailscale.com/ipn/ipnlocal/node_backend.go:869 documents the
// empty-resolver Routes form for Issue 2706).
app.cfg.TailcfgDNSConfig.Routes[d.WithoutTrailingDot()] = []*dnstype.Resolver{}
}
}
if cfg.DERP.ServerEnabled {
derpServerKey, err := readOrCreatePrivateKey(cfg.DERP.ServerPrivateKeyPath)
if err != nil {
return nil, fmt.Errorf("reading or creating DERP server private key: %w", err)
}
if derpServerKey.Equal(*noisePrivateKey) {
return nil, fmt.Errorf(
"DERP server private key and noise private key are the same: %w",
err,
)
}
if cfg.DERP.ServerVerifyClients {
t := http.DefaultTransport.(*http.Transport) //nolint:forcetypeassert
t.RegisterProtocol(
derpServer.DerpVerifyScheme,
derpServer.NewDERPVerifyTransport(app.handleVerifyRequest),
)
}
embeddedDERPServer, err := derpServer.NewDERPServer(
cfg.ServerURL,
key.NodePrivate(*derpServerKey),
&cfg.DERP,
)View on GitHub (pinned to 565fd254d0)
Solutions
- Point derp.server_private_key_path at a different, non-existent path — headscale will generate a fresh DERP key on startup
- Or pre-generate a second, independent key file for the DERP server
- Remove any manual copy of the noise key at the DERP path and restart
- Code-level: replace the nil-err wrap with a sentinel error so the message renders correctly
Example fix
# before (config.yaml) noise_private_key_path: /var/lib/headscale/noise_private_key derp: server_enabled: true server_private_key_path: /var/lib/headscale/noise_private_key # same file -> error # after noise_private_key_path: /var/lib/headscale/noise_private_key derp: server_enabled: true server_private_key_path: /var/lib/headscale/derp_private_key # auto-generated
Defensive patterns
Strategy: validation
Validate before calling
if cfg.DERP.ServerPrivateKeyPath == cfg.NoisePrivateKeyPath {
return errors.New("derp.server_private_key_path must differ from noise_private_key_path")
}
// stronger: compare file contents, since identical copies also trigger the guard
if b1, e1 := os.ReadFile(cfg.DERP.ServerPrivateKeyPath); e1 == nil {
if b2, e2 := os.ReadFile(cfg.NoisePrivateKeyPath); e2 == nil && bytes.Equal(b1, b2) {
return errors.New("DERP key and noise key are identical; generate a separate DERP key")
}
} Prevention
- Mount/generate a distinct secret for each key purpose in containerized deploys
- Config-review the two key paths together whenever the DERP server is enabled
- If the error appears, delete the DERP key file and restart — a fresh independent key is created
When it happens
Trigger: Setting derp.server_private_key_path to the same file as noise_private_key_path in the config; the DERP key file being a copy of the noise key file; both paths defaulting to one shared mounted secret.
Common situations: Ops configuring a single generic 'headscale key' secret mounted at both paths; copying an existing config and changing only one section; Docker/K8s setups mounting one key volume at both locations.
Related errors
- reading or creating Noise protocol private key: %w
- reading or creating DERP server private key: %w
- getting DERPMap: %w
- parsing private key: %w
- STUN address not set
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/e4feb2326d401ddd.
Report an issue: GitHub.