The Error
You hit this when connecting to a service by IP address โ a Kubernetes API server, an internal gRPC endpoint, a custom HTTPS service, anything:
x509: cannot validate certificate for 10.0.0.1 because it doesn't contain any IP SANs
The TLS handshake fails. Go refuses to proceed, and so does kubectl, grpc-go, and every other Go-based client.
Why This Happens
TLS certificates identify the server through two mechanisms: the legacy Common Name (CN) field and the modern Subject Alternative Names (SAN) extension. SANs hold DNS names (DNS:) or IP addresses (IP:).
Whoever generated this cert set CN=10.0.0.1 and stopped there โ no SAN entries at all. That worked for years because TLS clients used to fall back to the CN when no SANs were present. Go 1.15 deprecated that fallback. Go 1.16 removed it entirely. Now if a cert has no IP SAN matching the address you're connecting to, Go rejects it โ no override short of disabling verification altogether.
Confirm this is the issue:
openssl s_client -connect 10.0.0.1:6443 </dev/null 2>&1 | openssl x509 -noout -text | grep -A1 "Subject Alternative"
No output below the header means no SANs. Root cause confirmed.
Quick Fix โ Skip Verification (Dev/Testing Only)
Need to unblock a dev environment right now? Disabling certificate verification is an option. Never use this in production.
In Go code:
import "crypto/tls"
import "net/http"
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
InsecureSkipVerify: true,
}
With curl:
curl -k https://10.0.0.1:6443/healthz
With kubectl:
kubectl --insecure-skip-tls-verify get nodes
Treat this as a stopgap. The cert still needs to be regenerated properly.
Permanent Fix โ Regenerate the Certificate with IP SANs
Option 1: Self-Signed Cert via openssl (non-Kubernetes)
The key is a config file with an explicit [alt_names] section. Passing SANs as command-line flags is error-prone; a .cnf file makes them impossible to forget:
# san.cnf
[req]
default_bits = 2048
prompt = no
default_md = sha256
req_extensions = req_ext
distinguished_name = dn
[dn]
CN = 10.0.0.1
[req_ext]
subjectAltName = @alt_names
[alt_names]
IP.1 = 10.0.0.1
IP.2 = 127.0.0.1
DNS.1 = myservice.internal
Generate the key and certificate:
openssl req -x509 -newkey rsa:2048 -sha256 -days 365 \
-keyout server.key \
-out server.crt \
-config san.cnf \
-nodes
Verify the SAN was embedded:
openssl x509 -in server.crt -noout -text | grep -A2 "Subject Alternative Name"
Expected output:
X509v3 Subject Alternative Name:
IP Address:10.0.0.1, IP Address:127.0.0.1, DNS:myservice.internal
Option 2: Kubernetes API Server (kubeadm)
This is the most common trigger for the error with kubectl. The API server cert was issued without the node's IP in certSANs โ usually because no one thought to add it before running kubeadm init.
Check your current kubeadm config first:
kubectl -n kube-system get configmap kubeadm-config -o yaml
Create a patch config listing every IP the API server is reachable at:
# kubeadm-patch.yaml
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
apiServer:
certSANs:
- "10.0.0.1"
- "127.0.0.1"
- "kubernetes"
- "kubernetes.default"
- "kubernetes.default.svc"
- "kubernetes.default.svc.cluster.local"
Back up the existing certs, then force-regenerate:
sudo mv /etc/kubernetes/pki/apiserver.crt /etc/kubernetes/pki/apiserver.crt.bak
sudo mv /etc/kubernetes/pki/apiserver.key /etc/kubernetes/pki/apiserver.key.bak
sudo kubeadm init phase certs apiserver --config kubeadm-patch.yaml
Restart the API server. It runs as a static pod, so deleting the container lets kubelet recreate it:
sudo crictl rm $(sudo crictl ps --name kube-apiserver -q)
# kubelet restarts it automatically within a few seconds
Option 3: cert-manager (in-cluster)
Add the IP to the ipAddresses field in your Certificate resource:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: my-service-cert
spec:
secretName: my-service-tls
ipAddresses:
- 10.0.0.1
dnsNames:
- myservice.internal
issuerRef:
name: my-ca-issuer
kind: ClusterIssuer
Apply it and cert-manager issues a new certificate with the correct SANs. No manual openssl steps required.
Verification
After swapping the certificate, confirm the SANs are present:
openssl s_client -connect 10.0.0.1:6443 </dev/null 2>&1 | openssl x509 -noout -text | grep -A3 "Subject Alternative"
For Kubernetes, drop the insecure flag โ a clean response here means the cert is valid:
kubectl get nodes
For a Go HTTP client, confirm a normal TLS connection works without InsecureSkipVerify:
resp, err := http.Get("https://10.0.0.1:6443/healthz")
if err != nil {
log.Fatal(err) // should not trigger anymore
}
One more gotcha: if you're using a custom CA, your Go client needs the CA cert loaded into its trust store โ not just the server cert. A missing CA cert produces a different error (x509: certificate signed by unknown authority), but it's a common stumble at this stage.
Prevent It Next Time
- Always generate certs from a
.cnffile. CN-only certs have been broken in Go since 1.16 โ there is no good reason to create them. - Include
127.0.0.1andlocalhostin[alt_names]even for remote services. It costs nothing and avoids a separate debugging session during local testing. - For Kubernetes clusters, list every IP the API server might be reached at โ load balancer VIP, node IP, internal cluster IP โ in
certSANsbefore the firstkubeadm init. Adding SANs after the fact means a full cert rotation. - Consider
cfsslorstep-cliover rawopenssl. Both have cleaner SAN handling and are much harder to misconfigure.

