juanfont/headscale · critical
saving private key to disk at path %q: %w
Error message
saving private key to disk at path %q: %w
What it means
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.
Source
Thrown at hscontrol/app.go:979
}
privateKey, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
log.Info().Str("path", path).Msg("no private key file at path, creating...")
machineKey := key.NewMachine()
machineKeyStr, err := machineKey.MarshalText()
if err != nil {
return nil, fmt.Errorf(
"converting private key to string for saving: %w",
err,
)
}
err = os.WriteFile(path, machineKeyStr, privateKeyFileMode)
if err != nil {
return nil, fmt.Errorf(
"saving private key to disk at path %q: %w",
path,
err,
)
}
return &machineKey, nil
} else if err != nil {
return nil, fmt.Errorf("reading private key file: %w", err)
}
trimmedPrivateKey := strings.TrimSpace(string(privateKey))
var machineKey key.MachinePrivate
if err = machineKey.UnmarshalText([]byte(trimmedPrivateKey)); err != nil { //nolint:noinlineerr
return nil, fmt.Errorf("parsing private key: %w", err)
}
View on GitHub (pinned to 565fd254d0)
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.
Example fix
# before: /var/lib/headscale owned by root, running as headscale -> EACCES # after chown headscale:headscale /var/lib/headscale chmod 750 /var/lib/headscale systemctl restart headscale
Defensive patterns
Strategy: validation
Validate before calling
// Pre-flight: key directory writable and disk not full.
func canPersistKey(keyPath string) error {
var stat syscall.Statfs_t
if err := syscall.Statfs(filepath.Dir(keyPath), &stat); err == nil {
if stat.Bavail*uint64(stat.Bsize) < 1<<20 {
return fmt.Errorf("less than 1MiB free for key storage")
}
}
return keyDirWritable(keyPath)
} Try / catch
if err := h.Serve(); err != nil {
if errors.Is(err, syscall.ENOSPC) || errors.Is(err, syscall.EROFS) || errors.Is(err, syscall.EACCES) {
// storage issue: fix space/permissions, then restart; key was not rotated because write failed
}
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- reading or creating Noise protocol private key: %w
- reading or creating DERP server private key: %w
- ensuring private key directory: %w
- reading private key file: %w
- creating directory failed with permission error
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/6528dadc232277a5.
Report an issue: GitHub.