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
- Inspect the file: cat -A <key_file> — look for stray quotes, CRLF (^M), BOM, or emptiness.
- If you have the original key, rewrite the file with exactly the MarshalText output (single line, no quotes, Unix newline, mode 0600).
- 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).
- 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
- Write the key file exactly as headscale produces it: single line, no quotes, no CRLF, no JSON.
- Back up the private key file together with the database.
- Add a secret-format lint (no quotes/BOM/CRLF) to config-management pipelines.
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
- DERP server private key and noise private key are the same:
- configuration error: %w
- reading or creating Noise protocol private key: %w
- init state: %w
- building trusted_proxies middleware: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/dbc1500c0d20930e.
Report an issue: GitHub.