Skip to content

Talos worker nodes fail to register: bootstrap-token secret missing from workload cluster kube-system #3875

Closed

Description

@IvanHunters

Summary

Fresh Talos worker VMs come up healthy, apid responds, kubelet service is Running/OK, but nodes never register in the workload cluster. All API calls from kubelet are silently rejected as system:anonymous, including the CSR that would give kubelet its client certificate. Machines stay Provisioned indefinitely; the workload cluster ends up with zero Nodes.

Root cause: the workload cluster's kube-system namespace does not contain the bootstrap-token-<id> Secret that matches the token embedded in the TalosConfig bootstrap data. The token is generated and referenced by TalosConfig (cluster.token: <id>.<secret>), but nothing on the Cozystack side creates the corresponding bootstrap-token Secret inside the workload cluster.

Symptom overlaps with #3513 (0 nodes registered, machines Provisioned, VMIs healthy), but the failure mode is deterministic on affected clusters, not flaky. #3513 tracks other manifestations of the same "no nodes register" outcome.

Reproduction / evidence

On an affected tenant Kubernetes CR (a prod cluster that ran for weeks with the same misconfig):

$ kubectl --context host --namespace tenant-x get machines
NAME                CLUSTER         NODENAME   PROVIDERID           PHASE         AGE   VERSION
ht-apps-abc-def                                kubevirt://...       Provisioned   11d   v1.33.13
ht-apps-abc-ghi                                kubevirt://...       Provisioned   11d   v1.33.13

$ kubectl --context tenantkubeconfig get nodes
No resources found

$ kubectl --context host --namespace tenant-x get vmi
# all Running, agent connected, IPs assigned

talosctl service kubelet on each worker:

STATE    Running
HEALTH   OK
EVENTS   [Waiting]: task "kubelet" failed: exit code 1
         (last log "No valid client certificate is found and the server is
          responsive, exiting.")

talosctl logs kubelet (looping every ~7s):

"Failed to ensure lease exists":
  leases.coordination.k8s.io "ht-apps-abc-def" is forbidden:
  User "system:anonymous" cannot get resource "leases" ...

"Unable to register node with API server":
  nodes "ht-apps-abc-def" is forbidden:
  User "system:anonymous" cannot get resource "nodes" ...

"Failed while requesting a signed certificate from the control plane":
  cannot create certificate signing request: Unauthorized

bootstrap-token present in the TalosConfig secret:

$ kubectl --context host --namespace tenant-x \
    get secret ht-apps-abc-def-bootstrap-data \
    --output=jsonpath='{.data.value}' | base64 -d | grep -E 'token:'
    token: <id>.<secret>          # machine.token (Talos join)
    token: mcvbbi.<secret>        # cluster.token (kubelet bootstrap)  <- this one

Same mcvbbi id is in ${release}-talos-secrets under bootstrapTokenId.

bootstrap-token-mcvbbi Secret in workload kube-system:

$ kubectl --context tenantkubeconfig --namespace kube-system \
    get secret --field-selector=type=bootstrap.kubernetes.io/token
No resources found in kube-system namespace.

Comparison with a working cluster

Same chart version, another tenant that has been healthy since day one:

$ kubectl --context tenantkubeconfig-staging --namespace kube-system \
    get secret --field-selector=type=bootstrap.kubernetes.io/token
NAME                     TYPE                            DATA   AGE
bootstrap-token-ac7loi   bootstrap.kubernetes.io/token   6      11d

ac7loi matches bootstrapTokenId in that tenant's ${release}-talos-secrets. The difference between working and broken clusters is exactly whether this Secret exists.

Fix that unblocks the cluster

Manually create the missing Secret in the workload cluster:

apiVersion: v1
kind: Secret
metadata:
  name: bootstrap-token-<id>
  namespace: kube-system
type: bootstrap.kubernetes.io/token
stringData:
  token-id: <id>
  token-secret: <secret>
  usage-bootstrap-authentication: "true"
  usage-bootstrap-signing: "true"
  auth-extra-groups: system:bootstrappers:kubeadm:default-node-token

Values taken from ${release}-talos-secrets.data.bootstrapTokenId / bootstrapTokenSecret.

After applying: kubelet immediately transitions to authenticated, CSR is auto-approved by kubeadm:node-autoapprove-bootstrap (present in the workload cluster), nodes become Ready, workloads move.

Suggested cozystack change

Either:

  • talos-reconcile Job should also kubectl apply a bootstrap-token Secret against the workload cluster (it already assembles the same bootstrapTokenId/Secret values), or

  • The tenant Kubernetes chart should provision this Secret at cluster-init time and re-provision it if the underlying ${release}-talos-secrets rotates.

Currently the CSR-approver ClusterRoleBinding (kubeadm:node-autoapprove-bootstrap) is present but useless without the matching bootstrap-token Secret.

Environment

  • Cozystack v1.6.x, Kamaji-backed tenant Kubernetes clusters
  • Chart version: 0.0.0+7c18a8ec4e4d
  • Reproduced on two independent tenant clusters (long-running prod, not a fresh e2e install)
  • Not reproduced on a sibling tenant that was created earlier and has the Secret present

Notes

Activity

  1. IvanHunters commented on Aug 17, 2026

    @IvanHunters
    CollaboratorAuthor

    Root cause located in cozystack code

    The chart does ship this Job — packages/apps/kubernetes/templates/talos/bootstrap-token-tenant-job.yaml (added in 39cdc88 on 2026-06-16, hardened in b5cc2ae and a4c6a07). So the intent is present.

    The problem is the hook lifecycle wired to it:

    annotations:
      "helm.sh/hook": post-install,post-upgrade
      "helm.sh/hook-weight": "10"
      "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded

    post-upgrade runs after Helm has finished waiting for the release's resources to become Ready. In the tenant kubernetes release, that wait includes MachineDeployment reaching Ready, which requires nodes to register, which requires the bootstrap-token-<id> Secret this very Job is meant to create.

    The dependency chain deadlocks:

    Helm upgrade  → apply templates
                  → wait for resources Ready
                      → MachineDeployment waits on nodes to register
                          → kubelets need bootstrap-token to auth CSR
                              → bootstrap-token would be created by post-upgrade hook
                                  → post-upgrade hook fires only after wait completes
                                      → wait cannot complete without the token
    

    On the two affected tenant clusters we saw HR.status.upgradeFailures = 5726 — Helm was retrying the upgrade for ~1.5 days, never reaching the post-upgrade phase.

    The sibling tenant that works (htstaging in our setup) was installed before this Job template landed, went through an early install where nodes came up by other means, and only afterwards had the post-upgrade hook fire successfully on a subsequent HR reconcile — so the Secret exists there, and the release loop is self-sustaining. Any freshly installed tenant Kubernetes release, or any pre-existing one whose HR sits in UpgradeFailed, ends up in the deadlocked state we hit.

    Suggested fix

    Move the hook to pre-install,pre-upgrade (with a higher weight so it runs after the admin kubeconfig Secret is materialised). The Job only needs <release>-admin-kubeconfig and <release>-talos-secrets to exist — both are rendered before workloads, so the hook can safely run before the wait phase.

    Alternatively, drop the hook annotation entirely and let the Job be a regular resource with ttlSecondsAfterFinished — Helm will apply it in the normal sync and it won't be gated on the release becoming Ready.

    Either shape breaks the deadlock. pre-install,pre-upgrade is closer to the current author's intent (idempotent Job that runs on every reconcile).

    Note

    #3519 (talos-reconcile Job OOM at 256Mi) also affects the same chart, but the two are independent. Even with #3519 fixed, this hook-ordering deadlock still traps any freshly installed tenant whose nodes cannot yet register.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

triage/needs-triageIndicates an issue needs triage by a maintainer

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions