juanfont/headscale · critical
ensuring private key directory: %w
Error message
ensuring private key directory: %w
What it means
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.
Source
Thrown at hscontrol/app.go:960
MinVersion: tls.VersionTLS12,
}
cert, err := tls.LoadX509KeyPair(h.cfg.TLS.CertPath, h.cfg.TLS.KeyPath)
if err != nil {
return nil, err
}
tlsConfig.Certificates[0] = cert
return tlsConfig, nil
}
func readOrCreatePrivateKey(path string) (*key.MachinePrivate, error) {
dir := filepath.Dir(path)
err := util.EnsureDir(dir)
if err != nil {
return nil, fmt.Errorf("ensuring private key directory: %w", err)
}
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 {View on GitHub (pinned to 565fd254d0)
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.
Example fix
# before: dir created by root package install, service runs as headscale ls -ld /var/lib/headscale # drwx------ root root # after chown -R headscale:headscale /var/lib/headscale systemctl restart headscale
Defensive patterns
Strategy: validation
Validate before calling
// Pre-flight for the private key directory.
func keyDirWritable(keyPath string) error {
dir := filepath.Dir(keyPath)
if err := os.MkdirAll(dir, 0o700); err != nil { return err }
probe := filepath.Join(dir, ".probe")
if err := os.WriteFile(probe, nil, 0o600); err != nil { return err }
return os.Remove(probe)
} Try / catch
if err := h.Serve(); err != nil && strings.Contains(err.Error(), "ensuring private key directory") {
log.Fatalf("state dir unwritable: check ownership of %s", filepath.Dir(cfg.NoisePrivateKeyPath))
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- reading or creating Noise protocol private key: %w
- reading or creating DERP server private key: %w
- saving private key to disk at path %q: %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/eb36c9db83cc9c02.
Report an issue: GitHub.