{"record":{"id":"eb36c9db83cc9c02","repo":"juanfont/headscale","slug":"ensuring-private-key-directory-w","errorCode":null,"errorMessage":"ensuring private key directory: %w","messagePattern":"ensuring private key directory: %w","errorType":"console","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"hscontrol/app.go","lineNumber":960,"sourceCode":"\t\tMinVersion:   tls.VersionTLS12,\n\t}\n\n\tcert, err := tls.LoadX509KeyPair(h.cfg.TLS.CertPath, h.cfg.TLS.KeyPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttlsConfig.Certificates[0] = cert\n\n\treturn tlsConfig, nil\n}\n\nfunc readOrCreatePrivateKey(path string) (*key.MachinePrivate, error) {\n\tdir := filepath.Dir(path)\n\n\terr := util.EnsureDir(dir)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ensuring private key directory: %w\", err)\n\t}\n\n\tprivateKey, err := os.ReadFile(path)\n\tif errors.Is(err, os.ErrNotExist) {\n\t\tlog.Info().Str(\"path\", path).Msg(\"no private key file at path, creating...\")\n\n\t\tmachineKey := key.NewMachine()\n\n\t\tmachineKeyStr, err := machineKey.MarshalText()\n\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 {","sourceCodeStart":942,"sourceCodeEnd":978,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/app.go#L942-L978","documentation":"Thrown by readOrCreatePrivateKey (hscontrol/app.go:958) when util.EnsureDir cannot create the directory that holds the server's noise private key file (the parent of noise.private_key_path / the private key path passed in). This runs during startup before the server accepts connections; a failure means the key directory is unwritable or uncreatable.","triggerScenarios":"The private key path's parent is root-owned and headscale runs unprivileged (EACCES on MkdirAll); a path component exists as a regular file (ENOTDIR); read-only root filesystem without a mounted data volume; SELinux/AppArmor denial on the state directory.","commonSituations":"Fresh installs where /var/lib/headscale was created by root during packaging but never chowned; running the binary manually as a user while systemd config expects the headscale user; containers without a persistent writable /var/lib/headscale volume.","solutions":["Create and own the directory: install -d -o headscale -g headscale /var/lib/headscale (match whatever path noise.private_key_path uses).","If a file blocks a directory component, inspect each level: namei -l <path> and fix the offending entry.","In containers, mount a writable volume at the state directory: -v headscale-data:/var/lib/headscale.","Relax or correct SELinux contexts: restorecon -Rv /var/lib/headscale."],"exampleFix":"# before: dir created by root package install, service runs as headscale\nls -ld /var/lib/headscale   # drwx------ root root\n\n# after\nchown -R headscale:headscale /var/lib/headscale\nsystemctl restart headscale","handlingStrategy":"validation","validationCode":"// Pre-flight for the private key directory.\nfunc keyDirWritable(keyPath string) error {\n    dir := filepath.Dir(keyPath)\n    if err := os.MkdirAll(dir, 0o700); err != nil { return err }\n    probe := filepath.Join(dir, \".probe\")\n    if err := os.WriteFile(probe, nil, 0o600); err != nil { return err }\n    return os.Remove(probe)\n}","typeGuard":null,"tryCatchPattern":"if err := h.Serve(); err != nil && strings.Contains(err.Error(), \"ensuring private key directory\") {\n    log.Fatalf(\"state dir unwritable: check ownership of %s\", filepath.Dir(cfg.NoisePrivateKeyPath))\n}","preventionTips":["chown the state directory to the service user at install time and after restores.","Always mount a writable persistent volume at the data dir in containers.","Include a directory-permission assertion in deployment playbooks."],"tags":["filesystem","permissions","crypto-keys","startup"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}