The cluster was fine yesterday
You run a routine kubectl get pods and get slapped with this:
Unable to connect to the server: tls: certificate signed by unknown authority
Nothing changed β or so you thought. kubectl is refusing to trust the certificate the API server presented. The TLS handshake failed before a single byte of cluster data came back.
Four things typically cause this: the cluster was rebuilt, certificates were rotated, you copied a kubeconfig from another machine, or the API server cert quietly expired overnight.
Debug: figure out which cert is the problem
Step 1 β Check what kubeconfig you're actually using
kubectl config view --raw | grep server
echo $KUBECONFIG
A set KUBECONFIG env var overrides the default ~/.kube/config. Confirm you're looking at the right file before going further.
Step 2 β Test the TLS connection directly
# Get the API server address from kubeconfig
kubectl config view --raw -o jsonpath='{.clusters[0].cluster.server}'
# Then probe it with openssl
openssl s_client -connect <api-server-host>:6443 2>&1 | head -30
Scan the certificate chain in the output. It shows whether the cert is self-signed, what the CN/SAN fields say, and the exact expiry date.
Step 3 β Check certificate expiry
# On a control plane node
openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -dates
# Or check all certs at once (kubeadm clusters)
kubeadm certs check-expiration
See a Not After date in the past? That's your culprit.
Step 4 β Inspect the CA cert embedded in kubeconfig
# Extract and decode the CA cert from kubeconfig
kubectl config view --raw -o jsonpath='{.clusters[0].cluster.certificate-authority-data}' \
| base64 -d | openssl x509 -noout -text | grep -A2 'Validity\|Issuer\|Subject'
Compare the issuer here against what the API server is actually presenting. A mismatch means the kubeconfig is stale β the cluster moved on without it.
Solutions β pick the one that matches your situation
Scenario A: Kubeconfig is stale (most common)
The cluster's CA or API server cert was rotated, but your local kubeconfig still carries the old CA data.
# If you have SSH access to the control plane node
scp root@<control-plane-ip>:/etc/kubernetes/admin.conf ~/.kube/config
# Or on the control plane itself
cat /etc/kubernetes/admin.conf
Grab the fresh admin.conf and replace your local kubeconfig. Fastest fix on this list.
Scenario B: API server certificate expired (kubeadm)
# Renew all certificates
sudo kubeadm certs renew all
# Verify renewal
kubeadm certs check-expiration
# Restart control plane components to pick up new certs
sudo systemctl restart kubelet
# Refresh your kubeconfig
sudo cp /etc/kubernetes/admin.conf ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config
Static pods for the API server restart automatically β usually within 60 seconds. If they don't move, force it:
sudo crictl ps | grep kube-apiserver
# Note the container ID, then:
sudo crictl stop <container-id>
Scenario C: Self-signed cert not trusted (new cluster setup)
First-time connection to a cluster, and the CA cert isn't in your kubeconfig. Or someone handed you a raw endpoint URL with no CA bundle attached.
# Option 1: Add the CA cert to your kubeconfig
kubectl config set-cluster <cluster-name> \
--certificate-authority=/path/to/ca.crt \
--embed-certs=true
# Option 2: Use insecure-skip-tls-verify (TEMPORARY DEBUG ONLY)
kubectl --insecure-skip-tls-verify get nodes
Do not leave insecure-skip-tls-verify: true in a kubeconfig permanently. Use the flag only to confirm the server is reachable while you sort out the cert β then remove it.
Scenario D: API server SAN mismatch
The IP or hostname you're connecting through isn't listed in the certificate's Subject Alternative Names. This breaks after adding a load balancer in front of the API server, or after the control plane node gets a new IP.
# Check what SANs the current cert covers
openssl s_client -connect <api-server-ip>:6443 2>/dev/null \
| openssl x509 -noout -text | grep -A1 'Subject Alternative'
Missing IP or hostname? Reissue the cert with the correct SANs. For kubeadm:
# Delete the existing apiserver cert
sudo rm /etc/kubernetes/pki/apiserver.{crt,key}
# Add the new IP/hostname and regenerate
sudo kubeadm init phase certs apiserver \
--apiserver-cert-extra-sans=<new-ip>,<new-hostname>
# Restart kubelet
sudo systemctl restart kubelet
Scenario E: Managed cluster (EKS/GKE/AKS) β stale kubeconfig
# EKS β refresh kubeconfig
aws eks update-kubeconfig --name <cluster-name> --region <region>
# GKE
gcloud container clusters get-credentials <cluster-name> --zone <zone>
# AKS
az aks get-credentials --resource-group <rg> --name <cluster-name>
Managed clusters rotate CA data on their own schedule. Always pull a fresh kubeconfig from the cloud CLI. Copying kubeconfigs between machines is how you end up back here.
Verify the fix
# Basic connectivity
kubectl cluster-info
# Should return node list without TLS errors
kubectl get nodes
# Double-check cert validity
kubeadm certs check-expiration # if kubeadm cluster
When kubectl cluster-info shows the API server URL and the CoreDNS line with no TLS warnings, you're done.
Set a reminder before it happens again
Default kubeadm certificate validity is 1 year β 365 days, to the hour. Without automation, this exact error comes back next year at the worst possible time.
# Add to crontab on the control plane node β runs cert check monthly
0 9 1 * * kubeadm certs check-expiration 2>&1 | mail -s "K8s cert check" ops@yourcompany.com
# Or set up auto-renewal 30 days before expiry
0 9 1 * * bash -c 'kubeadm certs check-expiration 2>&1 | grep -q "<30d" && kubeadm certs renew all'
Plenty of teams just run kubeadm certs renew all in a quarterly maintenance window and call it done. That works too. Both beat getting paged at 2 AM because a cert aged out.

