{"record":{"id":"dbc1500c0d20930e","repo":"juanfont/headscale","slug":"parsing-private-key-w","errorCode":null,"errorMessage":"parsing private key: %w","messagePattern":"parsing private key: %w","errorType":"console","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"hscontrol/app.go","lineNumber":995,"sourceCode":"\t\terr = os.WriteFile(path, machineKeyStr, privateKeyFileMode)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\n\t\t\t\t\"saving private key to disk at path %q: %w\",\n\t\t\t\tpath,\n\t\t\t\terr,\n\t\t\t)\n\t\t}\n\n\t\treturn &machineKey, nil\n\t} else if err != nil {\n\t\treturn nil, fmt.Errorf(\"reading private key file: %w\", err)\n\t}\n\n\ttrimmedPrivateKey := strings.TrimSpace(string(privateKey))\n\n\tvar machineKey key.MachinePrivate\n\tif err = machineKey.UnmarshalText([]byte(trimmedPrivateKey)); err != nil { //nolint:noinlineerr\n\t\treturn nil, fmt.Errorf(\"parsing private key: %w\", err)\n\t}\n\n\treturn &machineKey, nil\n}\n\n// Change is used to send changes to nodes.\n// All change should be enqueued here and empty will be automatically\n// ignored.\nfunc (h *Headscale) Change(cs ...change.Change) {\n\th.mapBatcher.AddWork(cs...)\n}\n\n// HTTPHandler returns an [http.Handler] for the [Headscale] control server.\n// The handler serves the Tailscale control protocol including the /key\n// endpoint and /ts2021 Noise upgrade path.\nfunc (h *Headscale) HTTPHandler() http.Handler {\n\thumaMux, _ := apiv1.Handler(apiv1.Backend{\n\t\tState:  h.state,","sourceCodeStart":977,"sourceCodeEnd":1013,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/app.go#L977-L1013","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before: kubernetes secret mounted with quoted/DOS content\n\"tskey-abc...\"\r\n\n# after: raw single-line value, unix newline, no quotes\ntskey-abc...","handlingStrategy":"validation","validationCode":"// Pre-flight: existing key file must parse before startup.\nfunc keyFileParses(path string) error {\n    b, err := os.ReadFile(path)\n    if err != nil { return err }\n    var k key.MachinePrivate\n    return k.UnmarshalText([]byte(strings.TrimSpace(string(b))))\n}","typeGuard":"func isPlainKeyFile(b []byte) bool {\n    s := strings.TrimSpace(string(b))\n    return s != \"\" &&\n        !strings.ContainsAny(s, \"\\\"'{}\\r\") &&\n        !strings.HasPrefix(s, \"{\") // reject JSON-wrapped or quoted secrets\n}","tryCatchPattern":"if err := h.Serve(); err != nil && strings.Contains(err.Error(), \"parsing private key\") {\n    // DO NOT delete and regenerate casually: a new key invalidates every registered node.\n    // Restore the original key from backup, or accept re-enrollment of all nodes.\n}","preventionTips":["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."],"tags":["crypto-keys","configuration","data-corruption","startup"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}