Use when joining worker or control-plane nodes to a Kubernetes cluster, troubleshooting TLS bootstrap, or debugging node join failures
kubeadm join adds nodes to an existing cluster through bidirectional trust establishment.
Core principle: Join requires two-way trust - the node must verify the control plane (Discovery), and the control plane must verify the node (TLS Bootstrap).
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BIDIRECTIONAL TRUST β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Discovery (Node β Control Plane) β
β βββββββββββββββββββββββββββββββββ β
β "Is this API Server legitimate?" β
β Method: CA cert hash verification or kubeconfig file β
β β
β TLS Bootstrap (Control Plane β Node) β
β ββββββββββββββββββββββββββββββββββββ β
β "Is this node authorized to join?" β
β Method: Bootstrap token β CSR β Certificate issuance β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
From kubeadm init output:
kubeadm join 192.168.10.100:6443 \
--token abcdef.0123456789abcdef \
--discovery-token-ca-cert-hash sha256:xxxx...
| Component | Purpose |
|---|---|
192.168.10.100:6443 |
API Server endpoint |
--token |
Bootstrap token (used for both discovery and TLS bootstrap) |
--discovery-token-ca-cert-hash |
CA certificate fingerprint (prevents MITM) |
1. preflight β Check requirements
2. discovery β Get cluster-info, verify CA
3. TLS bootstrap β Authenticate with token, submit CSR, get certificate
4. kubelet-start β Configure and start kubelet
kubeadm join 192.168.10.100:6443 \
--token 123456.1234567890123456 \
--discovery-token-ca-cert-hash sha256:bd763182...
/etc/kubernetes/
βββ kubelet.conf # kubelet kubeconfig
βββ pki/
βββ ca.crt # Cluster CA (for verification)
/var/lib/kubelet/
βββ config.yaml # kubelet configuration
βββ kubeadm-flags.env # Extra kubelet flags
1. preflight
2. discovery
3. control-plane-prepare β Download certs, generate manifests
4. TLS bootstrap
5. etcd-join β Add to etcd cluster
6. kubelet-start
7. control-plane-join β Apply labels/taints
kubeadm join 192.168.10.100:6443 \
--token 123456.1234567890123456 \
--discovery-token-ca-cert-hash sha256:bd763182... \
--control-plane \
--certificate-key <certificate-key>
# On existing control plane
kubeadm init phase upload-certs --upload-certs
# Outputs: certificate-key: <64-char-hex>
# Or during initial kubeadm init
kubeadm init --upload-certs
kubeadm token list
# Simple
kubeadm token create
# With join command
kubeadm token create --print-join-command
# With custom TTL
kubeadm token create --ttl 2h
openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | \
openssl rsa -pubin -outform der 2>/dev/null | \
openssl dgst -sha256 -hex | sed 's/^.* //'
kubeadm join ... --token xxx --discovery-token-ca-cert-hash sha256:xxx
cluster-info ConfigMap from kube-public namespacekubeadm join ... --discovery-file /path/to/cluster-info.yaml
kubeadm join ... --discovery-token-unsafe-skip-ca-verification
New Node Control Plane
ββββββββ βββββββββββββ
β β
βββββ 1. Get cluster-info βββββββββββΆβ (unauthenticated)
βββββ CA cert + API endpoint βββββββββ
β β
βββββ 2. Authenticate with token ββββΆβ
βββββ Temporary credentials ββββββββββ
β β
βββββ 3. Submit CSR βββββββββββββββββΆβ
β β (auto-approved)
βββββ 4. Signed certificate ββββββββββ
β β
βββββ 5. Use certificate ββββββββββββΆβ (full kubelet access)
β β
# join-config.yaml
apiVersion: kubeadm.k8s.io/v1beta4
kind: JoinConfiguration
discovery:
bootstrapToken:
apiServerEndpoint: "192.168.10.100:6443"
token: "123456.1234567890123456"
caCertHashes:
- "sha256:bd763182..."
nodeRegistration:
kubeletExtraArgs:
- name: node-ip
value: "192.168.10.101"
criSocket: "unix:///run/containerd/containerd.sock"
kubeadm join --config=join-config.yaml
| Symptom | Cause | Fix |
|---|---|---|
| Connection refused to 6443 | Firewall blocking | Open port 6443 on control plane |
| Token expired / "could not find JWS signature" | Default 24h TTL | kubeadm token create --print-join-command |
| CA hash mismatch | Wrong hash or different cluster | Get correct hash from control plane |
| TLS bootstrap timeout | Network/firewall issue | Check connectivity, firewalld |
| Node shows wrong IP | Multi-NIC, wrong default | Set node-ip in kubeletExtraArgs |
| x509 certificate error | Clock skew | Sync NTP on all nodes |
Note: The error "could not find a JWS signature in the cluster-info ConfigMap for token ID" means your bootstrap token has expired. The JWS signature is removed from cluster-info when the token expires.
# From worker node
curl -k https://192.168.10.100:6443/healthz
# Expected: "ok"
# On control plane
kubeadm token list
# Check expiration time
# From worker
ping 192.168.10.100
ss -tlnp | grep 6443 # Should show kubelet after join
# On control plane
systemctl is-active firewalld
# If active, open required ports or disable
firewall-cmd --permanent --add-port=6443/tcp
firewall-cmd --permanent --add-port=10250/tcp
firewall-cmd --reload
journalctl -u kubelet -f
# On the node that failed to join
kubeadm reset -f
rm -rf /etc/kubernetes /var/lib/kubelet
iptables -F && iptables -t nat -F
# Re-attempt join
kubeadm join ...
After successful join:
kubectl get nodeskubelet service running: systemctl status kubeletcrictl ps | grep proxy/etc/kubernetes/kubelet.conf exists/var/lib/kubelet/config.yaml existsThe bootstrap process depends on this publicly-accessible ConfigMap:
# View (works without authentication)
curl -k https://192.168.10.100:6443/api/v1/namespaces/kube-public/configmaps/cluster-info
# Contains:
# - kubeconfig: API Server address + CA certificate
# - jws-kubeconfig-<token>: Signed verification
This is the ONLY Kubernetes resource accessible without authentication.