Fixing Kubeadm 'certificate signed by unknown authority' Error During Node Join
Threat/Impact Level: CRITICAL | Exploitability/Downtime Risk: HIGH | Time to Fix: 10–20 mins
TL;DR
- What broke: The worker node's TLS handshake to the API server failed because
kubeadm joincould not verify the control plane certificate against a trusted CA — caused by a wrong--discovery-token-ca-cert-hash, a rotated or expired cluster CA, or a stale bootstrap token. - How to fix it: Regenerate the correct CA cert hash from the live control plane, issue a fresh bootstrap token, and re-run
kubeadm joinwith the corrected values. - Fast path: Use our Client-Side Sandbox below to auto-refactor your failing
kubeadm joincommand orKubeadmConfigSpec— paste it in, get the corrected output instantly.
The Incident (What Does the Error Mean?)
Raw error output from kubeadm join:
W0615 03:12:44.123456 14321 join.go:346] [preflight] WARNING: JoinControlPane.controlPlane settings will be ignored when control-plane flag is not set.
error execution phase preflight/control-plane-prepare/download-certs
[preflight] Running pre-flight checks
error: unable to fetch the kubeadm-config ConfigMap: Get "https://192.168.1.10:6443/api/v1/namespaces/kube-system/configmaps/kubeadm-config":
x509: certificate signed by unknown authority
Immediate consequence: The node join is hard-blocked. The worker cannot authenticate the API server's TLS certificate, so it refuses the connection entirely. No kubelet is registered. Your cluster cannot scale. If this is a rolling node replacement during an incident, you are now short on capacity.
The Attack Vector / Blast Radius
This is not just an annoyance — it is a security control working correctly against a misconfiguration that could be exploited.
If you bypass CA verification (the wrong fix that gets copy-pasted from Stack Overflow), you open the join process to a man-in-the-middle attack. An attacker on the same network segment can present a self-signed certificate, intercept the bootstrap token, and register a rogue node into your cluster. That rogue node receives pod scheduling, mounts secrets via projected service account tokens, and can exfiltrate workloads silently.
The three real root causes, in order of frequency:
- Stale
--discovery-token-ca-cert-hash— The hash was generated from an old CA or copied incorrectly. The hash must be a SHA-256 of the control plane's currentca.crtpublic key. - Rotated PKI — You ran
kubeadm certs renewor rebuilt the control plane. The CA changed. Every previously generated join command is now invalid. - Expired bootstrap token — Tokens expire after 24h by default. An expired token causes a different error path but can cascade into this x509 failure depending on kubeadm version.
How to Fix It (The Solution)
Basic Fix: Regenerate the Correct Join Command on the Control Plane
Step 1: Create a new bootstrap token
kubeadm token create --print-join-command
This outputs a fully formed kubeadm join command with a fresh token AND the correct --discovery-token-ca-cert-hash. Use this output directly.
Step 2: If you need the hash independently (for automation)
openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt \
| openssl rsa -pubin -outform der 2>/dev/null \
| openssl dgst -sha256 -hex \
| sed 's/^.* //'
Prefix the output with sha256: when passing to --discovery-token-ca-cert-hash.
Enterprise Best Practice: Corrected kubeadm Join (Diff)
- kubeadm join 192.168.1.10:6443 \
- --token abcdef.0123456789abcdef \
- --discovery-token-ca-cert-hash sha256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ kubeadm join 192.168.1.10:6443 \
+ --token $(kubeadm token create) \
+ --discovery-token-ca-cert-hash sha256:$(openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //')
For JoinConfiguration YAML (GitOps/Ansible use case):
apiVersion: kubeadm.k8s.io/v1beta3
kind: JoinConfiguration
discovery:
bootstrapToken:
apiServerEndpoint: "192.168.1.10:6443"
- token: "abcdef.0123456789abcdef"
- caCertHashes:
- - "sha256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ token: "<output of: kubeadm token create>"
+ caCertHashes:
+ - "sha256:<output of openssl hash command above>"
unsafeSkipCAVerification: false
⚠️ Never set
unsafeSkipCAVerification: true. This is the nuclear option that disables the entire trust chain. Any node on your network can MITM the join. This has been the root cause of multiple real-world cluster compromises.
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. Automate Hash Generation — Never Hardcode It
In Ansible, Terraform, or any node-provisioning pipeline, always derive the CA hash at runtime from the live control plane:
# Ansible task example
- name: Get kubeadm CA cert hash
shell: |
openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt \
| openssl rsa -pubin -outform der 2>/dev/null \
| openssl dgst -sha256 -hex | sed 's/^.* //'
register: ca_cert_hash
delegate_to: "{{ control_plane_host }}"
2. Token TTL Policy
Do not use long-lived tokens in automated pipelines. Set explicit short TTLs and rotate:
kubeadm token create --ttl 1h --print-join-command
3. OPA/Kyverno: Block unsafeSkipCAVerification
Enforce a Kyverno ClusterPolicy to reject any JoinConfiguration with unsafeSkipCAVerification: true in your GitOps repo:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: block-unsafe-kubeadm-join
spec:
validationFailureAction: Enforce
rules:
- name: deny-unsafe-ca-skip
match:
resources:
kinds: ["ConfigMap"]
validate:
message: "unsafeSkipCAVerification must never be true in JoinConfiguration."
deny:
conditions:
- key: "{{ request.object.data.JoinConfiguration | contains(@, 'unsafeSkipCAVerification: true') }}"
operator: Equals
value: true
4. Certificate Expiry Monitoring
Add kubeadm certs check-expiration to your monitoring pipeline. Alert at 30 days before expiry. A CA rotation mid-incident is the primary cause of this error in mature clusters.
# Cron or CI check
kubeadm certs check-expiration | grep -v 'CERTIFICATE\|^$' | awk '$NF < 30 {print "EXPIRING SOON: " $0}'