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 reqone-liners - Dev certs generated without
-extflags - 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
opensslwith a config file,mkcert, or Go'scrypto/x509. GODEBUG=x509ignoreCN=0buys time on Go 1.15โ1.16 only.InsecureSkipVerifybelongs in tests, not production.

