{"record":{"id":"f93e8be935b0c923","repo":"juanfont/headscale","slug":"reading-or-creating-noise-protocol-private-key-w","errorCode":null,"errorMessage":"reading or creating Noise protocol private key: %w","messagePattern":"reading or creating Noise protocol private key: %w","errorType":"console","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"hscontrol/app.go","lineNumber":120,"sourceCode":"var (\n\tprofilingEnabled = envknob.Bool(\"HEADSCALE_DEBUG_PROFILING_ENABLED\")\n\tprofilingPath    = envknob.String(\"HEADSCALE_DEBUG_PROFILING_PATH\")\n\ttailsqlEnabled   = envknob.Bool(\"HEADSCALE_DEBUG_TAILSQL_ENABLED\")\n\ttailsqlStateDir  = envknob.String(\"HEADSCALE_DEBUG_TAILSQL_STATE_DIR\")\n\ttailsqlTSKey     = envknob.String(\"TS_AUTHKEY\")\n\tdumpConfig       = envknob.Bool(\"HEADSCALE_DEBUG_DUMP_CONFIG\")\n)\n\nfunc NewHeadscale(cfg *types.Config) (*Headscale, error) {\n\tvar err error\n\n\tif profilingEnabled {\n\t\truntime.SetBlockProfileRate(1)\n\t}\n\n\tnoisePrivateKey, err := readOrCreatePrivateKey(cfg.NoisePrivateKeyPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"reading or creating Noise protocol private key: %w\", err)\n\t}\n\n\ts, err := state.NewState(cfg)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"init state: %w\", err)\n\t}\n\n\tapp := Headscale{\n\t\tcfg:               cfg,\n\t\tnoisePrivateKey:   noisePrivateKey,\n\t\tclientStreamsOpen: sync.WaitGroup{},\n\t\tstate:             s,\n\t}\n\n\tif len(cfg.TrustedProxies) > 0 {\n\t\tapp.realIPMiddleware, err = trustedProxyRealIP(cfg.TrustedProxies)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"building trusted_proxies middleware: %w\", err)","sourceCodeStart":102,"sourceCodeEnd":138,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/app.go#L102-L138","documentation":"readOrCreatePrivateKey failed for the Noise protocol key (hscontrol/app.go:955). The function ensures the key directory exists, reads the key file — creating and persisting a fresh key.MachinePrivate if it does not exist — and otherwise parses the existing content. Failure means: the directory could not be created, the file could not be read (permissions), a new key could not be written (read-only filesystem, wrong ownership), or the existing file content is not a valid machine private key.","triggerScenarios":"noise_private_key_path pointing to an unwritable directory (read-only container fs, root-owned dir with headscale running as non-root), an existing key file with corrupt/truncated/hand-edited content, or a file permissions error (mode is deliberately strict on read).","commonSituations":"Kubernetes/Docker deployments mounting a read-only secret at the key path without the key present; ops staff editing the key file and introducing whitespace/newlines that break parsing; volume permission mismatches after switching the headscale user; SELinux denying reads.","solutions":["Check the wrapped message — it distinguishes 'ensuring private key directory', 'reading private key file', and 'saving private key to disk at path %q'","Fix permissions: the headscale process user must be able to create the parent dir and read/write the key file","If the existing key file is corrupt, back it up, delete it, and restart headscale — a new key is generated automatically (nodes must then re-register)","On read-only mounts, pre-generate the key out-of-band and mount it with correct ownership"],"exampleFix":"# before: key file owned by root, headscale runs as non-root\nls -l /var/lib/headscale/noise_private_key\n# -rw------- 1 root root\n\n# after\nchown headscale:headscale /var/lib/headscale/noise_private_key\nchmod 600 /var/lib/headscale/noise_private_key","handlingStrategy":"validation","validationCode":"// Validate key path expectations before startup\np := cfg.NoisePrivateKeyPath\nif err := util.EnsureDir(filepath.Dir(p)); err != nil { return err }\nif b, err := os.ReadFile(p); err == nil {\n    if _, perr := key.ParseMachinePrivate(string(b)); perr != nil { // or equivalent parse\n        return fmt.Errorf(\"noise key file corrupt: %w\", perr)\n    }\n}","typeGuard":null,"tryCatchPattern":"if _, err := readOrCreatePrivateKey(cfg.NoisePrivateKeyPath); err != nil {\n    if errors.Is(err, os.ErrPermission) {\n        // fix ownership/mode of the key file and its directory, then restart\n    }\n    // parse errors on an existing file: rotate the key (nodes must re-register)\n}","preventionTips":["Pre-create the key directory with correct ownership before first start","Never hand-edit key files; if corrupt, delete and let headscale regenerate (accept re-registration)","On read-only mounts, generate keys out-of-band and mount them read-only with strict modes"],"tags":["noise","crypto-keys","filesystem","permissions","startup"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}