tailscale/tailscale · error
server_key is missing
Error message
server_key is missing
What it means
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.
Source
Thrown at control/tsp/nodefile.go:102
panic(fmt.Sprintf("NodeFile.AsJSON: %v", err)) // unreachable: all fields marshal successfully
}
return append(out, '\n')
}
// Check reports whether nf has all required fields set.
// It returns an error describing the first zero-valued field, if any.
func (nf NodeFile) Check() error {
if nf.NodeKey.IsZero() {
return fmt.Errorf("node_key is missing")
}
if nf.MachineKey.IsZero() {
return fmt.Errorf("machine_key is missing")
}
if nf.URL == "" {
return fmt.Errorf("server_url is missing")
}
if nf.ServerInfo.Key.IsZero() {
return fmt.Errorf("server_key is missing")
}
return nil
}
View on GitHub (pinned to 6e0912f979)
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:..."
Example fix
// before
nf := tsp.NodeFile{NodeKey: nk, MachineKey: mk, ServerInfo: tsp.ServerInfo{URL: u}}
tsp.WriteNodeFile(path, nf) // invalid NodeFile: server_key is missing
// after
srvKey, err := tsp.DiscoverServerKey(ctx, u)
if err != nil {
return err
}
nf := tsp.NodeFile{NodeKey: nk, MachineKey: mk, ServerInfo: tsp.ServerInfo{URL: u, Key: srvKey}}
tsp.WriteNodeFile(path, nf) Defensive patterns
Strategy: validation
Validate before calling
if nf.ServerInfo.Key.IsZero() {
k, err := tsp.DiscoverServerKey(ctx, nf.URL)
if err != nil {
return err
}
nf.ServerInfo.Key = k
}
err := tsp.WriteNodeFile(path, nf) Type guard
func hasServerKey(nf tsp.NodeFile) bool {
return !nf.ServerInfo.Key.IsZero()
} Try / catch
if err := tsp.WriteNodeFile(path, nf); err != nil {
if strings.Contains(err.Error(), "server_key is missing") {
// discover then retry the write
if k, derr := tsp.DiscoverServerKey(ctx, nf.URL); derr == nil {
nf.ServerInfo.Key = k
return tsp.WriteNodeFile(path, nf)
}
}
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: Bootstrap code that registers and immediately tries to save credentials, forgetting the server key step; reusing a NodeFile template that never included server_key.
Understand the failure class
Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.
Related errors
- machine_key is missing
- invalid NodeFile: %w
- node_key is missing
- server_url is missing
- cannot use and advertise exit node at same time
AI-assisted analysis of tailscale/tailscale@6e0912f979 (2026-08-18).
Data as JSON: /api/errors/0052dd3f14bc2954.
Report an issue: GitHub.