juanfont/headscale · critical

parsing private key: %w

Error message

parsing private key: %w

What it means

Returned by readOrCreatePrivateKey when the existing key file's content cannot be parsed: machineKey.UnmarshalText(trimmedPrivateKey) fails (hscontrol/app.go:993). The file is expected to contain the textual encoding of a tailscale MachinePrivate key (as produced by MarshalText on first run). Corrupt, truncated, empty, or wrong-format content triggers this; startup aborts to avoid generating a replacement key and silently invalidating every registered node.

Source

Thrown at hscontrol/app.go:995

		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)
	}

	return &machineKey, nil
}

// Change is used to send changes to nodes.
// All change should be enqueued here and empty will be automatically
// ignored.
func (h *Headscale) Change(cs ...change.Change) {
	h.mapBatcher.AddWork(cs...)
}

// HTTPHandler returns an [http.Handler] for the [Headscale] control server.
// The handler serves the Tailscale control protocol including the /key
// endpoint and /ts2021 Noise upgrade path.
func (h *Headscale) HTTPHandler() http.Handler {
	humaMux, _ := apiv1.Handler(apiv1.Backend{
		State:  h.state,

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Inspect the file: cat -A <key_file> — look for stray quotes, CRLF (^M), BOM, or emptiness.
  2. If you have the original key, rewrite the file with exactly the MarshalText output (single line, no quotes, Unix newline, mode 0600).
  3. If the key is unrecoverable and node re-registration is acceptable, stop headscale, remove/rename the key file, start headscale to generate a fresh key, then re-register nodes (their old registrations will no longer validate).
  4. If managed via config management, fix the template so it writes the raw key value with no escaping.

Example fix

# before: kubernetes secret mounted with quoted/DOS content
"tskey-abc..."

# after: raw single-line value, unix newline, no quotes
tskey-abc...
Defensive patterns

Strategy: validation

Validate before calling

// Pre-flight: existing key file must parse before startup.
func keyFileParses(path string) error {
    b, err := os.ReadFile(path)
    if err != nil { return err }
    var k key.MachinePrivate
    return k.UnmarshalText([]byte(strings.TrimSpace(string(b))))
}

Type guard

func isPlainKeyFile(b []byte) bool {
    s := strings.TrimSpace(string(b))
    return s != "" &&
        !strings.ContainsAny(s, "\"'{}\r") &&
        !strings.HasPrefix(s, "{") // reject JSON-wrapped or quoted secrets
}

Try / catch

if err := h.Serve(); err != nil && strings.Contains(err.Error(), "parsing private key") {
    // DO NOT delete and regenerate casually: a new key invalidates every registered node.
    // Restore the original key from backup, or accept re-enrollment of all nodes.
}

Prevention

When it happens

Trigger: File was edited by hand or truncated (empty file, partial paste); wrong key material stored (e.g. a wireguard private key, an x25519 raw base64 without the expected prefix/length, or a JSON blob); encoding mangling — CRLF line endings, BOM, or quotes added around the value; backup restored from a different software version with a different key format.

Common situations: Secrets-management tooling (Vault/Kubernetes secrets) writing the key with extra formatting or quoting; disk-full during a previous write leaving a truncated file; users hand-crafting the key file from a wireguard config; sed edits that introduce whitespace/quotes.

Related errors


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