KubernetesLXXIV · kubeadmkubeadm
kubeadm join — control-plane and worker node bootstrapping
What you'll learn
- Run kubeadm join for worker and control-plane nodes
- Trace the join process (bootstrap, CSR, kubelet)
- Reason about node labelling and taints
- Plan cluster growth with --control-plane
Prerequisites
Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16
kubeadm join adds a node to an existing cluster. For a
worker, the bootstrap is straightforward; for a
control-plane, the join also establishes an etcd member
and an API server on the new host. This lesson walks
both flows and the production discipline.
The join command
After kubeadm init on the first control-plane host:
kubeadm join loadbalancer.example:6443 \
--token abcdef.0123456789abcdef \
--discovery-token-ca-cert-hash sha256:abc...
This is the join command for both worker and additional control-plane hosts. The flags:
| Flag | Purpose |
|---|---|
loadbalancer.example:6443 | The cluster’s API server endpoint |
--token | Bootstrap token issued at init time (24h TTL) |
--discovery-token-ca-cert-hash | The API server’s CA cert hash |
The worker join
On a worker host:
sudo kubeadm join loadbalancer.example:6443 \
--token abcdef.0123456789abcdef \
--discovery-token-ca-cert-hash sha256:abc...
This command:
- Installs the kubelet binary (already there typically).
- Reads the bootstrap token.
- Connects to the cluster’s API server.
- Submits a CSR (CertificateSigningRequest) for the node’s identity.
- Once approved (auto-approved for nodes), installs the kubelet’s cert.
- Configures the kubelet with the cluster info.
sequenceDiagram
autonumber
participant N as New node
participant AS as API server
participant KC as kube-controller-manager
participant K as kubelet on new node
N->>AS: GET /api/v1/nodes (use bootstrap token)
AS-->>N: cluster info (CA, endpoint)
N->>AS: POST CSR (system:node:<node-name>)
AS-->>KC: watch CSR
KC->>KC: auto-approve (nodes have CSR auto-approval)
KC->>AS: approve CSR
N->>AS: GET approved cert
N->>N: install client cert for kubelet
K->>AS: register Node (heartbeat)
K->>K: start running Pods (when scheduled)
The new node is now a worker in the cluster.
The control-plane join
For an additional control-plane host:
sudo kubeadm join loadbalancer.example:6443 \
--token abcdef.0123456789abcdef \
--discovery-token-ca-cert-hash sha256:abc... \
--control-plane \
--certificate-key abcdef1234567890...
The --control-plane flag triggers additional steps:
- Install etcd member on the new host.
- Add the etcd member to the cluster.
- Install kube-apiserver static pod.
- Install kube-controller-manager static pod.
- Install kube-scheduler static pod.
sequenceDiagram
autonumber
participant N as New control-plane host
participant AS as API server (existing)
participant E as etcd cluster
N->>AS: bootstrap token
N->>N: install etcd member
N->>AS: etcdctl member add
N->>N: install kube-apiserver static pod
N->>N: install kube-controller-manager static pod
N->>N: install kube-scheduler static pod
N->>AS: register Node (with control-plane label)
The new host becomes a control-plane node.
The node labelling
After join, the node is labelled:
kubectl get nodes --show-labels
NAME STATUS ROLES LABELS
cp-1 Ready control-plane <control-plane labels>
cp-2 Ready control-plane <control-plane labels>
cp-3 Ready control-plane <control-plane labels>
worker-1 Ready <none> <default labels>
Control-plane nodes have taints (no Pod placement by default):
kubectl describe node cp-1 | grep Taints
# Taints: node-role.kubernetes.io/control-plane:NoSchedule
To run control-plane Pods on the node, set
--register-with-taints differently at init / join, or
remove the taint manually.
What the join does not do
- Install the CNI plugin (presumably already done).
- Install add-ons (ingress, cert-manager, etc.).
- Configure the kubelet’s runtime (presumably preset).
- Schedule Pods (the cluster scheduler runs after the kubelet registers).
The join assumes the kubelet and runtime are installed before running.
The post-join sanity
# Substitute your own value before running:
NODE=cp-02 # the node name as it appears in `kubectl get nodes`
# 1. The node is listed
kubectl get nodes
# 2. The node reaches Ready
kubectl wait node/"$NODE" --for=condition=Ready --timeout=60s
# 3. (control-plane) etcd has the new member
kubectl -n kube-system exec "etcd-$NODE" -- etcdctl member list
# 4. (control-plane) The API server is running
kubectl -n kube-system get pod -l component=kube-apiserver
$ kubectl get nodes -o wide...Common join issues
| Issue | Cause | Fix |
|---|---|---|
| Token expired | The bootstrap token TTL elapsed | Run kubeadm token create --print-join-command |
| Hash mismatch | The CA cert hash is wrong | Verify against the original init output |
| Network unreachable to API server | LB / firewall | Verify connectivity from new node |
| Cert approval failed | RBAC denied | Check the controller-manager logs |
| kubelet fails to register | Versions mismatch | Verify kubelet version matches API server |
The bootstrap token rotation
For long-running clusters, the bootstrap token expires:
# After 24 hours:
sudo kubeadm token create --print-join-command
The token is reusable while it has TTL; the printed join command is for workers.
The multi-control-plane join sequence
For a 5-control-plane production cluster:
# Values from `kubeadm token create --print-join-command` and
# `kubeadm init phase upload-certs --upload-certs`:
JOIN_TOKEN=9x7k2f.7bd1a9c0e4f6238a
CA_CERT_HASH=1f3c8a7e5b0d94f26a1c7e3b8d05f4a29c6e1b7d3f80a5c2e9b4d6f1a8c3e5b7
CERT_KEY=b41d7e6c2a95f803d1e7b4a6c90f52d8b3e7a1c46f9027b5d8e3a1c6f4b20e94
# On each cp-2, cp-3, cp-4, cp-5:
sudo kubeadm join loadbalancer.example:6443 \
--token "$JOIN_TOKEN" \
--discovery-token-ca-cert-hash "sha256:$CA_CERT_HASH" \
--control-plane \
--certificate-key "$CERT_KEY"
Each control-plane join runs in parallel (with care not to overload the network).
The certificate-key rotation
The certificate-key is set at init time. To rotate:
# Generate a fresh key with `kubeadm certs certificate-key`, then:
NEW_CERT_KEY=6d2f9b81c47e0a3f5b8d16c2e94a7f03b5c8e1d47a2f960b3e8c5d1a7f4b2e60
sudo kubeadm init phase upload-certs --upload-certs --certificate-key="$NEW_CERT_KEY"
This invalidates old certificate keys.
The discipline of join
- Capture the join command immediately. It is only printed once (at init).
- Issue new tokens as needed. Tokens expire.
- Verify post-join. All nodes Ready, etcd member count, control-plane pods running.
- Document the cluster’s node map. Each node’s role, AZ, IP, install date.
- Restrict who can join. The bootstrap token is a cluster-issuing credential; rotate after the initial join.
Quiz
Knowledge check · 4 questions
Q1. What does kubeadm join do differently for control-plane vs worker nodes?
Q2. The bootstrap token issued by `kubeadm init` is valid forever.
Q3. kubeadm join fails with 'token expired'. Walk the remediation.
Cluster was initialised 2 days ago. Today the team tries to join a new control-plane host; the join fails because the original token has expired.
Q4. What is the difference between worker and control-plane bootstrap, and why does it matter?
Passing score: 75%. Answers are checked in this browser.
Production discipline
- Capture the join command. It’s only printed once; store it for the team’s reference.
- Issue fresh tokens as needed. Tokens expire; rotate the procedure.
- Verify post-join. Nodes Ready, etcd member count, control-plane pods running.
- Document the node map. Each node’s role, AZ, IP, installation date.
- Restrict join access. Tokens are cluster-issuing credentials; treat them carefully.
kubeadm join is the cluster’s growth mechanism. Operating
it well is keeping the cluster’s growth controlled and
verified.