Your Kubernetes cluster is running, your workloads are growing, and now you need more capacity. Maybe you need another worker node to schedule more pods, or another master node so the control plane survives a machine failure. Either way, the answer is kubeadm join, and either way, the join command you saved from the day you initialized the cluster stopped working long ago: the bootstrap token in it expires after 24 hours.
This tutorial shows you how to generate fresh join credentials and add both node types to an existing kubeadm cluster: the simple case (a worker) and the slightly involved case (an additional master, which needs a certificate key on top of the token). It is written for sysadmins and DevOps engineers running self-managed Kubernetes on Ubuntu.
If you run K3s instead of full Kubernetes, the process is different and simpler, I covered it separately in How to Join a Node to an Existing K3s Cluster.
What Actually Happens During a Join
Understanding the moving parts makes the errors self-explanatory later:
- The bootstrap token (
--token) is a short-lived credential that lets a brand-new node talk to the API server just long enough to register itself. It expires after 24 hours by default, which is why you regenerate it instead of reusing the original. - The CA cert hash (
--discovery-token-ca-cert-hash) lets the joining node verify it is talking to the right cluster and not an impostor. It is a fingerprint of the cluster’s certificate authority and does not expire. - The certificate key (
--certificate-key, masters only) decrypts a copy of the control plane certificates that kubeadm temporarily stores as a secret inside the cluster. A new master needs those certificates to serve the API itself. The uploaded copy is deleted after 2 hours, so this key is even more short-lived than the token.
A worker join needs the first two. A master join needs all three, plus a control plane endpoint that is not a single machine’s IP.
Prerequisites
- A running kubeadm-based Kubernetes cluster and admin access (
kubectlworking) on an existing master node - A new node running Ubuntu with the same versions of
kubeadm,kubelet, and container runtime as the cluster, with swap disabled and the required kernel modules loaded
If the new node is not prepared yet, follow my installation tutorial up to the point where kubeadm, kubelet, and kubectl are installed (do not run kubeadm init on it): How to Install Kubernetes with a Single Master.
Check the version match before anything else, it prevents the most annoying class of join failures:
# on an existing master
kubeadm version -o short
# on the new node
kubeadm version -o short
The new node’s version may not be newer than the control plane’s. Same version is the safe choice.
Part 1: Join a Worker Node
Step 1: Generate the Join Command (on an Existing Master)
One command creates a fresh token and prints the complete join command with the current CA hash:
kubeadm token create --print-join-command
Output:
kubeadm join 10.14.10.39:6443 --token 55ekme.0ahjmz09920bqsyt --discovery-token-ca-cert-hash sha256:636786a58df07625167bd7305d56c4cc75bcbedcc474313934aa08c09dc603af
10.14.10.39:6443 is your control plane endpoint. The token in this command is valid for 24 hours; if you add another worker next month, just run the command again.
Step 2: Run the Join Command (on the New Worker)
Copy the entire command to the new node and run it with root privileges:
sudo kubeadm join 10.14.10.39:6443 --token 55ekme.0ahjmz09920bqsyt --discovery-token-ca-cert-hash sha256:636786a58df07625167bd7305d56c4cc75bcbedcc474313934aa08c09dc603af
A successful join ends with:
This node has joined the cluster:
* Certificate signing request was sent to apiserver and a response was received.
* The Kubelet was informed of the new secure connection details.
Run 'kubectl get nodes' on the control-plane to see this node join the cluster.
Step 3: Verify (on the Master)
kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION
master-1 Ready control-plane 120d v1.33.2
worker-1 Ready <none> 90d v1.33.2
worker-2 Ready <none> 2m v1.33.2
The new node shows NotReady for a minute or two while the CNI plugin’s pods start on it. If it stays NotReady beyond that, jump to the troubleshooting section.
That is the whole worker flow. Masters take a little more work.
Part 2: Join a Master (Control Plane) Node
One Requirement Before You Start
A multi-master cluster needs a stable control plane endpoint: a DNS name or virtual IP that fronts all masters, usually a load balancer. If you initialized your cluster with --control-plane-endpoint pointing at such an address, you are ready. If your cluster was initialized without it (the API server address is just the first master’s IP), additional masters cannot cleanly join; you would need to reconfigure the cluster’s endpoint first.
Check what your cluster uses:
kubectl get cm kubeadm-config -n kube-system -o jsonpath='{.data.ClusterConfiguration}' | grep controlPlaneEndpoint
controlPlaneEndpoint: k8s-endpoint:6443
If you still need to build the load balancer itself, I wrote a full guide using HAProxy and keepalived: How To Install HAProxy for Kubernetes Load Balancer.
Step 1: Upload the Control Plane Certificates (on an Existing Master)
A new master needs the cluster’s certificates. Instead of copying files around with scp, let kubeadm re-upload them as an encrypted secret and hand you the decryption key:
sudo kubeadm init phase upload-certs --upload-certs
[upload-certs] Storing the certificates in Secret "kubeadm-certs" in the "kube-system" Namespace
[upload-certs] Using certificate key:
6a2f496e172b16584f3700da0427d6e87b3ff06a67383c1bb05cf504128e4465
That last line is the certificate key. It is valid for 2 hours, so plan to complete the join within that window (re-running the command later generates a fresh key, no harm done).
Step 2: Generate the Join Command (on an Existing Master)
Same command as for workers:
kubeadm token create --print-join-command
kubeadm join k8s-endpoint:6443 --token 4iegnp.x2tfqdd9gl93zz1o --discovery-token-ca-cert-hash sha256:636786a58df07625167bd7305d56c4cc75bcbedcc474313934aa08c09dc603af
Step 3: Run the Combined Join Command (on the New Master)
First, make sure the new node can resolve the control plane endpoint. If k8s-endpoint is internal-only, add it to the hosts file, pointing at the load balancer IP:
echo "192.168.7.25 k8s-endpoint" | sudo tee -a /etc/hosts
Then combine the join command from Step 2 with the control plane flags and the certificate key from Step 1:
sudo kubeadm join k8s-endpoint:6443 --token 4iegnp.x2tfqdd9gl93zz1o \
--discovery-token-ca-cert-hash sha256:636786a58df07625167bd7305d56c4cc75bcbedcc474313934aa08c09dc603af \
--control-plane \
--certificate-key 6a2f496e172b16584f3700da0427d6e87b3ff06a67383c1bb05cf504128e4465
The --control-plane flag is what tells kubeadm this node becomes a master rather than a worker. Success looks like:
This node has joined the cluster and a new control plane instance was created.
To start administering your cluster from this node, you need to run the following as a regular user:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
Run those three lines if you want to use kubectl from the new master.
Step 4: Verify (on Any Master)
kubectl get nodes
NAME STATUS ROLES AGE VERSION
master-1 Ready control-plane 120d v1.33.2
master-2 Ready control-plane 3m v1.33.2
worker-1 Ready <none> 90d v1.33.2
Also confirm etcd is healthy with a new member:
kubectl get pods -n kube-system | grep etcd
Both etcd pods should be Running. Keep in mind that etcd quorum math applies to masters: 1 or 3 masters are sensible, 2 is actually worse than 1 for availability, because losing either node breaks quorum.
Common Mistakes and Troubleshooting
could not find a JWS signature in the cluster-info ConfigMap. The token in your command is expired or mistyped. Generate a fresh one with kubeadm token create --print-join-command and try again.
error execution phase control-plane-prepare/download-certs: the Secret does not include the required certificate key. More than 2 hours passed since upload-certs, or you reused an old certificate key. Re-run Step 1 on an existing master and use the new key.
The join hangs on [preflight] Running pre-flight checks. Almost always connectivity: the new node cannot reach the control plane endpoint on port 6443. Test with nc -zv k8s-endpoint 6443 from the new node and check firewalls in between.
Preflight errors about swap or the container runtime. Kubelet refuses to run with swap enabled: sudo swapoff -a and remove the swap line from /etc/fstab. If the error mentions the CRI socket, containerd is not running or not configured; revisit the node preparation steps from the installation tutorial.
CA cert hash does not match. You mixed a token from one cluster with a hash from another (easy to do when you manage several clusters). Always take the whole join command from a single --print-join-command run.
The node joined but stays NotReady. Check the CNI pods on that node: kubectl get pods -n kube-system -o wide | grep <node-name>. Image pulls for the network plugin are the usual delay; a pull error or a pod CIDR conflict is the usual real problem.
You ran the join on the wrong machine or want to redo it. Reset the node and start over: sudo kubeadm reset -f, then remove /etc/cni/net.d/* and rejoin. On the master side, also kubectl delete node <name> if it half-registered.
Best Practices
- Treat join credentials as secrets. A valid token plus network access to port 6443 is enough to add a node to your cluster. Do not paste join commands into shared chats or wikis, and delete long-lived test tokens with
kubeadm token delete <token>. - Match versions before joining, and upgrade the control plane before you ever upgrade node components.
- Keep master counts odd. Go from 1 master to 3, not to 2, so etcd quorum works in your favor.
- Label and taint new workers right after joining (
kubectl label node worker-2 role=apps) so scheduling stays intentional as the cluster grows. - Recheck certificate expiry after scaling the control plane. Every master has its own set of certificates to renew yearly; my guide on How To Renew Kubernetes Certificates covers it.
Conclusion
You can now grow a kubeadm cluster in both directions: workers with a single regenerated join command, and masters with the extra certificate-key step and a proper control plane endpoint in front of them. You also know why each credential exists and what its lifetime is, which turns most join errors from mysteries into checklist items.
Good next steps: put a highly available load balancer in front of your control plane with HAProxy for Kubernetes Load Balancer, and keep the cluster’s certificates healthy with How To Renew Kubernetes Certificates.