{"record":{"id":"6528dadc232277a5","repo":"juanfont/headscale","slug":"saving-private-key-to-disk-at-path-q-w","errorCode":null,"errorMessage":"saving private key to disk at path %q: %w","messagePattern":"saving private key to disk at path %q: %w","errorType":"console","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"hscontrol/app.go","lineNumber":979,"sourceCode":"\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 {\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","sourceCodeStart":961,"sourceCodeEnd":997,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/app.go#L961-L997","documentation":"Returned by readOrCreatePrivateKey when os.WriteFile fails to persist a newly generated machine/noise private key at the configured path (hscontrol/app.go:976-982). The key was generated in memory but never written, so on next start a different key will be created and all nodes registered against the old identity would break — the error is fatal to startup to prevent silently rotating the server identity.","triggerScenarios":"Disk full (ENOSPC); directory not writable by the headscale user (EACCES); read-only filesystem; path is a directory (EISDIR); quota exceeded; container with a read-only volume mounted at the key path.","commonSituations":"First start on a fresh install before permissions are fixed; long-running servers whose disk filled; Docker deployments missing the writable data volume; moving the config dir without moving ownership.","solutions":["Check space: df -h <key_dir> and free space or enlarge the volume.","Fix write permissions: chown headscale:headscale <key_dir> && chmod 750 <key_dir>.","Ensure the path is a file location, not a directory.","After fixing, restart headscale; since the write failed, the key was never persisted, so no identity rotation occurred."],"exampleFix":"# before: /var/lib/headscale owned by root, running as headscale -> EACCES\n\n# after\nchown headscale:headscale /var/lib/headscale\nchmod 750 /var/lib/headscale\nsystemctl restart headscale","handlingStrategy":"validation","validationCode":"// Pre-flight: key directory writable and disk not full.\nfunc canPersistKey(keyPath string) error {\n    var stat syscall.Statfs_t\n    if err := syscall.Statfs(filepath.Dir(keyPath), &stat); err == nil {\n        if stat.Bavail*uint64(stat.Bsize) < 1<<20 {\n            return fmt.Errorf(\"less than 1MiB free for key storage\")\n        }\n    }\n    return keyDirWritable(keyPath)\n}","typeGuard":null,"tryCatchPattern":"if err := h.Serve(); err != nil {\n    if errors.Is(err, syscall.ENOSPC) || errors.Is(err, syscall.EROFS) || errors.Is(err, syscall.EACCES) {\n        // storage issue: fix space/permissions, then restart; key was not rotated because write failed\n    }\n}","preventionTips":["Monitor free space on the data volume with alerts well above the key file size.","Keep the state directory on a writable persistent volume, never read-only.","Back up the noise private key alongside the database — losing it invalidates all nodes."],"tags":["crypto-keys","filesystem","permissions","disk-space","startup"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}