{"record":{"id":"0dfee573fb10b79e","repo":"juanfont/headscale","slug":"reading-or-creating-derp-server-private-key-w","errorCode":null,"errorMessage":"reading or creating DERP server private key: %w","messagePattern":"reading or creating DERP server private key: %w","errorType":"console","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"hscontrol/app.go","lineNumber":231,"sourceCode":"\t\t\t// and dns.Config.Clone in tailscale drop map entries whose\n\t\t\t// value is nil (see tailscale.com/tailcfg/tailcfg_clone.go and\n\t\t\t// tailscale.com/net/dns/dns_clone.go: `if sv == nil { continue }`).\n\t\t\t// Sending nil here caused the client's wgengine LinkChange:major\n\t\t\t// handler to clobber /etc/resolv.conf on every tunnel-IP rebind\n\t\t\t// — the handler reapplies a Clone of lastDNSConfig and the magic\n\t\t\t// DNS routes vanish, taking the resolver with them for ~6 min\n\t\t\t// until the next route-changing netmap. Empty slice survives\n\t\t\t// Clone and carries the same \"resolve locally\" semantics\n\t\t\t// (tailscale.com/ipn/ipnlocal/node_backend.go:869 documents the\n\t\t\t// empty-resolver Routes form for Issue 2706).\n\t\t\tapp.cfg.TailcfgDNSConfig.Routes[d.WithoutTrailingDot()] = []*dnstype.Resolver{}\n\t\t}\n\t}\n\n\tif cfg.DERP.ServerEnabled {\n\t\tderpServerKey, err := readOrCreatePrivateKey(cfg.DERP.ServerPrivateKeyPath)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"reading or creating DERP server private key: %w\", err)\n\t\t}\n\n\t\tif derpServerKey.Equal(*noisePrivateKey) {\n\t\t\treturn nil, fmt.Errorf(\n\t\t\t\t\"DERP server private key and noise private key are the same: %w\",\n\t\t\t\terr,\n\t\t\t)\n\t\t}\n\n\t\tif cfg.DERP.ServerVerifyClients {\n\t\t\tt := http.DefaultTransport.(*http.Transport) //nolint:forcetypeassert\n\t\t\tt.RegisterProtocol(\n\t\t\t\tderpServer.DerpVerifyScheme,\n\t\t\t\tderpServer.NewDERPVerifyTransport(app.handleVerifyRequest),\n\t\t\t)\n\t\t}\n\n\t\tembeddedDERPServer, err := derpServer.NewDERPServer(","sourceCodeStart":213,"sourceCodeEnd":249,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/app.go#L213-L249","documentation":"readOrCreatePrivateKey failed for the embedded DERP server key (same function as the Noise key, hscontrol/app.go:955, invoked when derp.server_enabled is true). It ensures the key directory, reads server_private_key_path — generating and writing a fresh key if absent — and parses existing content. Failure means unwritable directory, unreadable file, write failure for a new key, or invalid key content at that path.","triggerScenarios":"derp.server_private_key_path pointing at a read-only volume, a directory the headscale user cannot create, or an existing file that is not a valid machine private key (corrupt, empty, hand-edited).","commonSituations":"Enabling the embedded DERP server for the first time on a locked-down filesystem; sharing/mounting a config volume that already contains a placeholder or truncated key file; SELinux/AppArmor denying writes to the DERP key location.","solutions":["Check the wrapped error stage: directory creation, read, write, or parse","Give the headscale user write access to the key's parent directory, or pre-place a valid key with correct ownership","If the existing file is corrupt, remove it and restart — a new DERP key is generated (clients must re-learn the region key)","Never reuse the noise key file path for the DERP key (see the separate identical-keys error)"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"if cfg.DERP.ServerEnabled {\n    p := cfg.DERP.ServerPrivateKeyPath\n    if p == cfg.NoisePrivateKeyPath {\n        return errors.New(\"DERP key path must differ from noise key path\")\n    }\n    if err := util.EnsureDir(filepath.Dir(p)); err != nil { return err }\n}","typeGuard":null,"tryCatchPattern":"if _, err := readOrCreatePrivateKey(cfg.DERP.ServerPrivateKeyPath); err != nil {\n    if errors.Is(err, os.ErrPermission) {\n        // grant write access to the DERP key directory, then restart\n    }\n    // corrupt existing file: remove to regenerate (clients re-learn region key)\n}","preventionTips":["Give derp.server_private_key_path its own file location from day one","Pre-create writable directories for both keys on locked-down hosts","Never copy the noise key file to the DERP path when cloning configs"],"tags":["derp","crypto-keys","filesystem","permissions","startup"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}