{"record":{"id":"a3fdb243f5fef4cb","repo":"juanfont/headscale","slug":"reading-private-key-file-w","errorCode":null,"errorMessage":"reading private key file: %w","messagePattern":"reading private key file: %w","errorType":"console","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"hscontrol/app.go","lineNumber":988,"sourceCode":"\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\n\t\t\t\t\"converting private key to string for saving: %w\",\n\t\t\t\terr,\n\t\t\t)\n\t\t}\n\n\t\terr = os.WriteFile(path, machineKeyStr, privateKeyFileMode)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\n\t\t\t\t\"saving private key to disk at path %q: %w\",\n\t\t\t\tpath,\n\t\t\t\terr,\n\t\t\t)\n\t\t}\n\n\t\treturn &machineKey, nil\n\t} else if err != nil {\n\t\treturn nil, fmt.Errorf(\"reading private key file: %w\", err)\n\t}\n\n\ttrimmedPrivateKey := strings.TrimSpace(string(privateKey))\n\n\tvar machineKey key.MachinePrivate\n\tif err = machineKey.UnmarshalText([]byte(trimmedPrivateKey)); err != nil { //nolint:noinlineerr\n\t\treturn nil, fmt.Errorf(\"parsing private key: %w\", err)\n\t}\n\n\treturn &machineKey, nil\n}\n\n// Change is used to send changes to nodes.\n// All change should be enqueued here and empty will be automatically\n// ignored.\nfunc (h *Headscale) Change(cs ...change.Change) {\n\th.mapBatcher.AddWork(cs...)\n}","sourceCodeStart":970,"sourceCodeEnd":1006,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/app.go#L970-L1006","documentation":"Returned by readOrCreatePrivateKey when os.ReadFile on the private key path fails with an error other than os.ErrNotExist (hscontrol/app.go:988). The ErrNotExist branch creates a new key; any other read error — permission denied, path is a directory, I/O error — is surfaced here. It means a key file is present (or the path is broken) but cannot be read.","triggerScenarios":"The key file exists but is mode 0600 owned by root while headscale runs as another user (EACCES); the configured path points at a directory (EISDIR); the file is on a failing disk or an unavailable mount at boot time (systemd starting before the mount).","commonSituations":"Running the binary manually as root once (key written as root), then via systemd as headscale; mounts not ready before the service starts (missing Requires/After on the mount unit); restoring backups with wrong ownership.","solutions":["Fix ownership/permissions: chown headscale:headscale <key_file> && chmod 600 <key_file>.","If systemd races a mount, add Requires= and After= for the mount unit to headscale.service.","Confirm the path is a regular file: ls -l <path>.","If ownership cannot be fixed, run headscale as the user that owns the key file (matching the packaging defaults)."],"exampleFix":"# before: key file owned by root, service runs as headscale\n-rw------- root root /var/lib/headscale/noise_private.key\n\n# after\nchown headscale:headscale /var/lib/headscale/noise_private.key\nchmod 600 /var/lib/headscale/noise_private.key","handlingStrategy":"validation","validationCode":"// Pre-flight: key file (if present) must be readable.\nfunc keyFileReadable(path string) error {\n    f, err := os.Open(path)\n    if err != nil { return err }\n    return f.Close()\n}","typeGuard":null,"tryCatchPattern":"if err := h.Serve(); err != nil {\n    var pe *os.PathError\n    if errors.As(err, &pe) && errors.Is(pe.Err, syscall.EACCES) {\n        // ownership mismatch: chown the key file to the service user and restart\n    }\n}","preventionTips":["Never run headscale under two different users against the same data dir.","After restores or secrets-manager deployments, verify ownership and mode 0600 on the key file.","Order systemd units after mounts holding the key file (Requires=/After=)."],"tags":["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"}