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

  1. Add the key: --vpn-auth="name=tailscale,joinKey=tskey-auth-xxxxxxxx"
  2. Create a (reusable) auth key in the Tailscale admin console (or Headscale) first
  3. 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

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


AI-assisted analysis of k3s-io/k3s@6ba341e396 (2026-08-15). Data as JSON: /api/errors/cd1b6fe053cf35d6. Report an issue: GitHub.