juanfont/headscale · critical

reading or creating Noise protocol private key: %w

Error message

reading or creating Noise protocol private key: %w

What it means

readOrCreatePrivateKey failed for the Noise protocol key (hscontrol/app.go:955). The function ensures the key directory exists, reads the key file — creating and persisting a fresh key.MachinePrivate if it does not exist — and otherwise parses the existing content. Failure means: the directory could not be created, the file could not be read (permissions), a new key could not be written (read-only filesystem, wrong ownership), or the existing file content is not a valid machine private key.

Source

Thrown at hscontrol/app.go:120

var (
	profilingEnabled = envknob.Bool("HEADSCALE_DEBUG_PROFILING_ENABLED")
	profilingPath    = envknob.String("HEADSCALE_DEBUG_PROFILING_PATH")
	tailsqlEnabled   = envknob.Bool("HEADSCALE_DEBUG_TAILSQL_ENABLED")
	tailsqlStateDir  = envknob.String("HEADSCALE_DEBUG_TAILSQL_STATE_DIR")
	tailsqlTSKey     = envknob.String("TS_AUTHKEY")
	dumpConfig       = envknob.Bool("HEADSCALE_DEBUG_DUMP_CONFIG")
)

func NewHeadscale(cfg *types.Config) (*Headscale, error) {
	var err error

	if profilingEnabled {
		runtime.SetBlockProfileRate(1)
	}

	noisePrivateKey, err := readOrCreatePrivateKey(cfg.NoisePrivateKeyPath)
	if err != nil {
		return nil, fmt.Errorf("reading or creating Noise protocol private key: %w", err)
	}

	s, err := state.NewState(cfg)
	if err != nil {
		return nil, fmt.Errorf("init state: %w", err)
	}

	app := Headscale{
		cfg:               cfg,
		noisePrivateKey:   noisePrivateKey,
		clientStreamsOpen: sync.WaitGroup{},
		state:             s,
	}

	if len(cfg.TrustedProxies) > 0 {
		app.realIPMiddleware, err = trustedProxyRealIP(cfg.TrustedProxies)
		if err != nil {
			return nil, fmt.Errorf("building trusted_proxies middleware: %w", err)

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Check the wrapped message — it distinguishes 'ensuring private key directory', 'reading private key file', and 'saving private key to disk at path %q'
  2. Fix permissions: the headscale process user must be able to create the parent dir and read/write the key file
  3. If the existing key file is corrupt, back it up, delete it, and restart headscale — a new key is generated automatically (nodes must then re-register)
  4. On read-only mounts, pre-generate the key out-of-band and mount it with correct ownership

Example fix

# before: key file owned by root, headscale runs as non-root
ls -l /var/lib/headscale/noise_private_key
# -rw------- 1 root root

# after
chown headscale:headscale /var/lib/headscale/noise_private_key
chmod 600 /var/lib/headscale/noise_private_key
Defensive patterns

Strategy: validation

Validate before calling

// Validate key path expectations before startup
p := cfg.NoisePrivateKeyPath
if err := util.EnsureDir(filepath.Dir(p)); err != nil { return err }
if b, err := os.ReadFile(p); err == nil {
    if _, perr := key.ParseMachinePrivate(string(b)); perr != nil { // or equivalent parse
        return fmt.Errorf("noise key file corrupt: %w", perr)
    }
}

Try / catch

if _, err := readOrCreatePrivateKey(cfg.NoisePrivateKeyPath); err != nil {
    if errors.Is(err, os.ErrPermission) {
        // fix ownership/mode of the key file and its directory, then restart
    }
    // parse errors on an existing file: rotate the key (nodes must re-register)
}

Prevention

When it happens

Trigger: noise_private_key_path pointing to an unwritable directory (read-only container fs, root-owned dir with headscale running as non-root), an existing key file with corrupt/truncated/hand-edited content, or a file permissions error (mode is deliberately strict on read).

Common situations: Kubernetes/Docker deployments mounting a read-only secret at the key path without the key present; ops staff editing the key file and introducing whitespace/newlines that break parsing; volume permission mismatches after switching the headscale user; SELinux denying reads.

Related errors


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/f93e8be935b0c923. Report an issue: GitHub.