{"record":{"id":"e2459befe9294b92","repo":"k3s-io/k3s","slug":"token-is-required-to-join-a-cluster","errorCode":null,"errorMessage":"token is required to join a cluster","messagePattern":"token is required to join a cluster","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"pkg/cluster/bootstrap.go","lineNumber":124,"sourceCode":"\t\t\t// cluster they need to delete the database.\n\t\t\tlogrus.Infof(\"Managed %s cluster bootstrap already complete and initialized\", c.managedDB.EndpointName())\n\t\t\t// This is a workaround for an issue that can be caused by terminating the cluster bootstrap before\n\t\t\t// etcd is promoted from learner. Odds are we won't need this info, and we don't want to fail startup\n\t\t\t// due to failure to retrieve it as this will break cold cluster restart, so we ignore any errors.\n\t\t\tif c.config.JoinURL != \"\" && c.config.Token != \"\" {\n\t\t\t\tc.clientAccessInfo, _ = clientaccess.ParseAndValidateToken(c.config.JoinURL, c.config.Token, opts...)\n\t\t\t}\n\t\t\treturn false, true, nil\n\t\t} else if c.config.JoinURL == \"\" {\n\t\t\t// Not initialized, not joining - must be initializing (cluster-init)\n\t\t\tlogrus.Infof(\"Managed %s cluster initializing\", c.managedDB.EndpointName())\n\t\t\treturn false, false, nil\n\t\t}\n\n\t\t// Not initialized, but have a Join URL - fail if there's no token; if there is then validate it.\n\t\t// Note that this is the path taken by control-plane-only nodes every startup, as they have a non-nil managedDB that is never initialized.\n\t\tif c.config.Token == \"\" {\n\t\t\treturn false, false, errors.New(\"token is required to join a cluster\")\n\t\t}\n\n\t\t// Fail if the token isn't syntactically valid, or if the CA hash on the remote server doesn't match\n\t\t// the hash in the token. The password isn't actually checked until later when actually bootstrapping.\n\t\tinfo, err := clientaccess.ParseAndValidateToken(c.config.JoinURL, c.config.Token, opts...)\n\t\tif err != nil {\n\t\t\treturn false, false, errors.WithMessage(err, \"failed to validate token\")\n\t\t}\n\t\tc.clientAccessInfo = info\n\n\t\tif c.config.DisableETCD {\n\t\t\tlogrus.Infof(\"Managed %s disabled on this node\", c.managedDB.EndpointName())\n\t\t} else {\n\t\t\tlogrus.Infof(\"Managed %s cluster not yet initialized\", c.managedDB.EndpointName())\n\t\t}\n\t}\n\n\t// No errors and no bootstrap stamp, need to bootstrap.","sourceCodeStart":106,"sourceCodeEnd":142,"githubUrl":"https://github.com/k3s-io/k3s/blob/6ba341e396edc16b8dcae978a7c5e3ac7ee5606e/pkg/cluster/bootstrap.go#L106-L142","documentation":"During bootstrap, when a managed database (sqlite or embedded etcd) exists but is not initialized and a JoinURL is configured, k3s requires a token to pull bootstrap data from the remote server. Control-plane nodes take this path on every startup, so an empty token is fatal: the node cannot anonymously join a cluster.","triggerScenarios":"`k3s server --server https://10.0.0.10:6443` with no --token/K3S_TOKEN and no token in local storage; adding a second (HA) server to a cluster without passing the shared cluster token.","commonSituations":"New control-plane nodes in embedded-etcd HA clusters where the operator assumes only agents need the token; K3S_TOKEN empty in the systemd unit; token secret not yet populated when the server pod starts.","solutions":["Pass the shared cluster token via --token or K3S_TOKEN (same value as the seed server's /var/lib/rancher/k3s/server/token or a `k3s token create` output)","Verify the token actually reaches the process: `systemctl show k3s -p Environment` and the unit drop-ins","If this node was meant to seed a new cluster, remove --server so it initializes instead of joins"],"exampleFix":"# before\nk3s server --server https://10.0.0.10:6443\n\n# after\nK3S_TOKEN=<cluster-token> k3s server --server https://10.0.0.10:6443","handlingStrategy":"validation","validationCode":"// fail fast before cluster bootstrap\nif c.config.JoinURL != \"\" && strings.TrimSpace(c.config.Token) == \"\" && !c.storageReady() {\n    return errors.New(\"token is required to join a cluster: set K3S_TOKEN/--token\")\n}","typeGuard":"func hasJoinCredentials(joinURL, token string) bool {\n    return joinURL == \"\" || strings.TrimSpace(token) != \"\"\n}","tryCatchPattern":"if err != nil {\n    if strings.Contains(err.Error(), \"token is required to join a cluster\") {\n        // surface an actionable message and stop retrying until the secret is populated\n        return fmt.Errorf(\"K3S_TOKEN missing; populate the token secret and restart\")\n    }\n    return err\n}","preventionTips":["Provisioning for HA control-plane nodes must include the shared cluster token, not just agent tokens","Add init-container/readiness checks that block start until the token secret is non-empty","Centralize token distribution (secret store) so joins never depend on manual copy"],"tags":["bootstrap","token","cluster-join","ha"],"backgroundTag":null,"analyzedSha":"6ba341e396edc16b8dcae978a7c5e3ac7ee5606e","analyzedAt":"2026-08-15T16:27:54.286Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}