Fix x509: cannot validate certificate for IP because it doesn't contain any IP SANs

intermediate๐Ÿ”’ SSL/TLS2026-07-10| Go 1.15+, Kubernetes, kubectl, kubeadm, any TLS client connecting via IP address

Error Message

x509: cannot validate certificate for 10.0.0.1 because it doesn't contain any IP SANs
#go#kubernetes#x509#san

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 .cnf file. CN-only certs have been broken in Go since 1.16 โ€” there is no good reason to create them.
  • Include 127.0.0.1 and localhost in [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 certSANs before the first kubeadm init. Adding SANs after the fact means a full cert rotation.
  • Consider cfssl or step-cli over raw openssl. Both have cleaner SAN handling and are much harder to misconfigure.

Related Error Notes