Fix x509: certificate relies on legacy Common Name field, use SANs instead in Go

intermediate๐Ÿ”’ SSL/TLS2026-05-11| Go 1.15+, any OS (Linux, macOS, Windows), TLS/HTTPS client connections

Error Message

x509: certificate relies on legacy Common Name field, use SANs instead
#golang#ssl#x509#san#tls

TL;DR

Your certificate only has a Common Name (CN) field โ€” no Subject Alternative Name (SAN) extension. Go 1.15 made SANs mandatory. Regenerate the cert with a SAN and you're done.

The error

x509: certificate relies on legacy Common Name field, use SANs instead

This happens when your Go app โ€” acting as a TLS client โ€” connects to a server whose certificate has no SAN extension. Go 1.15 started enforcing RFC 2818 strictly: hostname verification must go through SANs, not CN. Browsers dropped CN-only support years earlier; Go caught up in 1.15.

Where you'll typically see this:

  • Internal services using self-signed certs from old openssl req one-liners
  • Dev certs generated without -ext flags
  • Legacy corporate PKI that predates the SAN requirement
  • Any tool older than ~2018 that only populated CN

Root cause

Old TLS clients had a fallback: if a certificate had no SAN, match the hostname against Common Name instead. Go 1.15 removed that fallback entirely. Now the cert must carry a Subject Alternative Name DNS entry (or IP address) that matches whatever host you're connecting to.

Run this to confirm the cert is missing SANs:

# Check against a live server
openssl s_client -connect yourhost:443 2>/dev/null | openssl x509 -noout -text | grep -A1 "Subject Alternative Name"

# Or inspect a local cert file
openssl x509 -in server.crt -noout -text | grep -A1 "Subject Alternative Name"

Empty output โ€” no DNS: or IP Address: lines โ€” confirms the problem.

Fix 1 โ€” Regenerate the certificate with a SAN (recommended)

Regenerating is the only real fix. Here are three ways to do it.

Option A: openssl with a config file

Create san.cnf:

[req]
default_bits       = 2048
prompt             = no
default_md         = sha256
distinguished_name = dn
req_extensions     = v3_req

[dn]
CN = yourhost.internal

[v3_req]
subjectAltName = @alt_names

[alt_names]
DNS.1 = yourhost.internal
DNS.2 = localhost
IP.1  = 127.0.0.1

Then generate the key and cert:

openssl req -x509 -nodes -days 825 \
  -newkey rsa:2048 \
  -keyout server.key \
  -out server.crt \
  -config san.cnf \
  -extensions v3_req

Option B: mkcert (fastest for local dev)

mkcert sets up a local CA, injects it into your system trust store, and bakes in the right SANs โ€” no config files needed:

brew install mkcert       # macOS
# or: apt install mkcert  # Debian/Ubuntu

mkcert -install
mkcert yourhost.internal localhost 127.0.0.1

You get yourhost.internal+2.pem and yourhost.internal+2-key.pem, trusted by your OS and browser out of the box.

Option C: Generate programmatically with Go's crypto/x509

package main

import (
    "crypto/ecdsa"
    "crypto/elliptic"
    "crypto/rand"
    "crypto/x509"
    "crypto/x509/pkix"
    "encoding/pem"
    "math/big"
    "net"
    "os"
    "time"
)

func main() {
    key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)

    template := &x509.Certificate{
        SerialNumber: big.NewInt(1),
        Subject:      pkix.Name{CommonName: "localhost"},
        NotBefore:    time.Now(),
        NotAfter:     time.Now().Add(365 * 24 * time.Hour),
        KeyUsage:     x509.KeyUsageDigitalSignature,
        ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
        // SANs โ€” this is what Go requires
        DNSNames:    []string{"localhost", "yourhost.internal"},
        IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
    }

    certDER, _ := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)

    certFile, _ := os.Create("server.crt")
    pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: certDER})
    certFile.Close()

    keyDER, _ := x509.MarshalECPrivateKey(key)
    keyFile, _ := os.Create("server.key")
    pem.Encode(keyFile, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
    keyFile.Close()
}

Fix 2 โ€” Temporary workaround (Go 1.15โ€“1.16 only)

Stuck waiting on a cert renewal? There's an escape hatch โ€” but a short-lived one. Set the GODEBUG flag to re-enable the old CN fallback:

GODEBUG=x509ignoreCN=0 go run main.go

Or in code, before any TLS calls:

import "os"
os.Setenv("GODEBUG", "x509ignoreCN=0")

Hard limit: x509ignoreCN=0 was pulled in Go 1.17. It does nothing on 1.17+. Fix the cert.

Fix 3 โ€” Skip verification entirely (tests only)

For automated tests or internal tooling where you own both ends:

import (
    "crypto/tls"
    "net/http"
)

client := &http.Client{
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{
            InsecureSkipVerify: true, // never in production
        },
    },
}

InsecureSkipVerify: true bypasses all certificate validation โ€” hostname, expiry, trust chain. Fine for local tests. A security hole everywhere else.

Verify the fix

After regenerating, check that SANs actually landed in the cert:

openssl x509 -in server.crt -noout -text | grep -A2 "Subject Alternative Name"

Expected output:

X509v3 Subject Alternative Name:
    DNS:yourhost.internal, DNS:localhost, IP Address:127.0.0.1

Run your Go program โ€” the error should be gone. For a quick live test:

curl --cacert server.crt https://yourhost.internal:8443/health

A 200 OK means the TLS handshake completed cleanly.

Summary

  • Go 1.15+ requires SANs โ€” CN-only certs are rejected, full stop.
  • Regenerate using openssl with a config file, mkcert, or Go's crypto/x509.
  • GODEBUG=x509ignoreCN=0 buys time on Go 1.15โ€“1.16 only.
  • InsecureSkipVerify belongs in tests, not production.

Related Error Notes