k3s-io/k3s · error
VPN Error. Tailscale requires a JoinKey
Error message
VPN Error. Tailscale requires a JoinKey
What it means
isVPNConfigOK validates the vpnCliAuthInfo parsed from the --vpn-auth flag string (comma-separated key=value pairs: name, joinKey, controlServerURL). For name=tailscale a non-empty joinKey is mandatory — the agent later runs 'tailscale up --authkey <joinKey> --timeout=30s' — so an empty JoinKey fails here before tailscale is ever invoked.
Source
Thrown at pkg/vpn/vpn.go:146
authInfo.JoinKey = vpnKeyValue[1]
case "controlServerURL":
authInfo.ControlServerURL = vpnKeyValue[1]
default:
return vpnCliAuthInfo{}, fmt.Errorf("VPN Error. The passed VPN auth info includes an unknown parameter: %v", vpnKeyValue[0])
}
}
if err := isVPNConfigOK(authInfo); err != nil {
return authInfo, err
}
return authInfo, nil
}
// isVPNConfigOK checks that the config is complete
func isVPNConfigOK(authInfo vpnCliAuthInfo) error {
if authInfo.Name == "tailscale" {
if authInfo.JoinKey == "" {
return errors.New("VPN Error. Tailscale requires a JoinKey")
}
if authInfo.ControlServerURL != "" {
if _, err := url.Parse(authInfo.ControlServerURL); err != nil {
return fmt.Errorf("VPN Error. Invalid control server URL for Tailscale: %w", err)
}
}
return nil
}
return errors.New("Requested VPN: " + authInfo.Name + " is not supported. We currently only support tailscale")
}
// getTailscaleInfo returns the IPs of the interface
func getTailscaleInfo() (*Info, error) {
output, err := util.ExecCommand("tailscale", []string{"status", "--json"})
if err != nil {
return nil, fmt.Errorf("failed to run tailscale status --json: %v", err)
}View on GitHub (pinned to 6ba341e396)
Solutions
- Add the key: --vpn-auth="name=tailscale,joinKey=tskey-auth-xxxxxxxx"
- Create a (reusable) auth key in the Tailscale admin console (or Headscale) first
- If a custom coordination server is used, keep controlServerURL well-formed as well, e.g. name=tailscale,joinKey=...,controlServerURL=https://headscale.example
Example fix
# before --vpn-auth="name=tailscale" # after --vpn-auth="name=tailscale,joinKey=tskey-auth-abcdef123456"
Defensive patterns
Strategy: validation
Validate before calling
func validVPNAuth(s string) bool {
kv := map[string]string{}
for _, p := range strings.Split(s, ",") {
parts := strings.SplitN(p, "=", 2)
if len(parts) != 2 {
return false
}
kv[parts[0]] = parts[1]
}
return kv["name"] == "tailscale" && kv["joinKey"] != ""
} Prevention
- Generate the auth key before enabling the tailscale backend
- Fail deployment templates when the join key variable is empty instead of rendering 'joinKey='
- Remember the accepted keys: name, joinKey, controlServerURL — anything else is rejected as unknown
When it happens
Trigger: --vpn-auth="name=tailscale" with no joinKey entry (or joinKey= with an empty value, e.g. an unset env var interpolated into the flag), typically together with --flannel-backend=tailscale.
Common situations: Enabling the tailscale flannel backend but forgetting the auth key; flag templates that drop the key; joinKey=${TS_KEY} with the variable unset (yields 'joinKey=' which splits to empty).
Related errors
- incorrect netMode for flannel tailscale backend
- Flannel configuration not defined
- tailscale does not provide an ipv6 address
- tailscale does not provide an ipv4 address
- failed to normalize server token; must be in format K10<CA-H
AI-assisted analysis of k3s-io/k3s@6ba341e396 (2026-08-15).
Data as JSON: /api/errors/cd1b6fe053cf35d6.
Report an issue: GitHub.