{"record":{"id":"0052dd3f14bc2954","repo":"tailscale/tailscale","slug":"server-key-is-missing","errorCode":null,"errorMessage":"server_key is missing","messagePattern":"server_key is missing","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"control/tsp/nodefile.go","lineNumber":102,"sourceCode":"\t\tpanic(fmt.Sprintf(\"NodeFile.AsJSON: %v\", err)) // unreachable: all fields marshal successfully\n\t}\n\treturn append(out, '\\n')\n}\n\n// Check reports whether nf has all required fields set.\n// It returns an error describing the first zero-valued field, if any.\nfunc (nf NodeFile) Check() error {\n\tif nf.NodeKey.IsZero() {\n\t\treturn fmt.Errorf(\"node_key is missing\")\n\t}\n\tif nf.MachineKey.IsZero() {\n\t\treturn fmt.Errorf(\"machine_key is missing\")\n\t}\n\tif nf.URL == \"\" {\n\t\treturn fmt.Errorf(\"server_url is missing\")\n\t}\n\tif nf.ServerInfo.Key.IsZero() {\n\t\treturn fmt.Errorf(\"server_key is missing\")\n\t}\n\treturn nil\n}\n","sourceCodeStart":84,"sourceCodeEnd":106,"githubUrl":"https://github.com/tailscale/tailscale/blob/6e0912f97994f927632b34ae9e63b53d6516a6ac/control/tsp/nodefile.go#L84-L106","documentation":"The fourth failure mode of NodeFile.Check: ServerInfo.Key (json field \"server_key\"), the server's Noise public key, is the zero key.MachinePublic. Persisting it lets future sessions skip key discovery and prevents MITM on first contact, so Check refuses to write credentials without it. Note: a present-but-malformed server_key string makes ReadNodeFile fail at unmarshal instead; this error is specifically absent/empty.","triggerScenarios":"Writing the NodeFile before DiscoverServerKey has run and without SetControlPublicKey; JSON whose \"server_key\" is \"\" or omitted; the struct was built from a partial registration response.","commonSituations":"Bootstrap code that registers and immediately tries to save credentials, forgetting the server key step; reusing a NodeFile template that never included server_key.","solutions":["Fetch the key before persisting: k, err := DiscoverServerKey(ctx, serverURL), then ServerInfo{URL: serverURL, Key: k}","Or hard-code the vendor's known Noise public key and pass it via SetControlPublicKey + the NodeFile","If loading existing JSON, add \"server_key\": \"mkey:...\""],"exampleFix":"// before\nnf := tsp.NodeFile{NodeKey: nk, MachineKey: mk, ServerInfo: tsp.ServerInfo{URL: u}}\ntsp.WriteNodeFile(path, nf) // invalid NodeFile: server_key is missing\n\n// after\nsrvKey, err := tsp.DiscoverServerKey(ctx, u)\nif err != nil {\n    return err\n}\nnf := tsp.NodeFile{NodeKey: nk, MachineKey: mk, ServerInfo: tsp.ServerInfo{URL: u, Key: srvKey}}\ntsp.WriteNodeFile(path, nf)","handlingStrategy":"validation","validationCode":"if nf.ServerInfo.Key.IsZero() {\n    k, err := tsp.DiscoverServerKey(ctx, nf.URL)\n    if err != nil {\n        return err\n    }\n    nf.ServerInfo.Key = k\n}\nerr := tsp.WriteNodeFile(path, nf)","typeGuard":"func hasServerKey(nf tsp.NodeFile) bool {\n    return !nf.ServerInfo.Key.IsZero()\n}","tryCatchPattern":"if err := tsp.WriteNodeFile(path, nf); err != nil {\n    if strings.Contains(err.Error(), \"server_key is missing\") {\n        // discover then retry the write\n        if k, derr := tsp.DiscoverServerKey(ctx, nf.URL); derr == nil {\n            nf.ServerInfo.Key = k\n            return tsp.WriteNodeFile(path, nf)\n        }\n    }\n}","preventionTips":["Call DiscoverServerKey during enrollment and persist the result immediately","Prefer pinning the server key over re-discovering on every start","Include the server_key check in credential-file linters"],"tags":["validation","server-key","nodefile","missing-field","noise"],"backgroundTag":"missing-required-argument","analyzedSha":"6e0912f97994f927632b34ae9e63b53d6516a6ac","analyzedAt":"2026-08-18T08:17:25.280Z","contentChangedAt":"2026-08-18T08:17:25.280Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}