DEV Community

Rasika Dangamuwa
Rasika Dangamuwa

Posted on

Why Modern Browsers Reject Your Self-Signed Certs: 5 TLS Traps Every Dev Hits

You need HTTPS on localhost. Maybe you are debugging an OAuth redirect URI, testing a Service Worker, or working with cookies that require SameSite=None; Secure. You run the classic one-liner:

openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 365
Enter fullscreen mode Exit fullscreen mode

You drop the keys into Vite or Nginx, fire up Chrome, and hit a red wall: NET::ERR_CERT_COMMON_NAME_INVALID or ERR_CERT_INVALID. Clicking "Advanced" might not even offer an option to proceed.

The command that worked years ago produces certificates modern TLS stacks reject on sight. Here are the five real-world traps you hit with self-signed certificates and how modern standards enforce them.


1. The Common Name (CN) Is Dead — Subject Alternative Name (SAN) Is Mandatory

The classic OpenSSL prompt asks for Common Name (e.g. server FQDN) []: localhost. You enter it and assume you are covered.

It worked in 2012. Today, it fails immediately. RFC 2818 deprecated relying on the Common Name field for domain verification, and Chromium-based browsers (Chrome, Edge, Brave) as well as Safari enforce this strictly: if your certificate lacks a subjectAltName (SAN) extension, the browser rejects it, regardless of what is in the CN.

Your OpenSSL command must inject X.509 v3 extensions:

# req.cnf
[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_req
prompt = no

[req_distinguished_name]
CN = localhost

[v3_req]
subjectAltName = @alt_names

[alt_names]
DNS.1 = localhost
IP.1 = 127.0.0.1
Enter fullscreen mode Exit fullscreen mode

2. IP SANs vs. DNS SANs

Notice the difference between DNS.1 and IP.1 above.

When your frontend connects to https://localhost:3000, the client performs a DNS match against DNS.1. But if your mobile emulator, curl script, or API client connects to https://127.0.0.1:3000, TLS hostname verification will fail if you only specified DNS.1 = localhost.

IP addresses must be explicitly declared as IP entries in the SAN table:

  • DNS.1 = localhost
  • IP.1 = 127.0.0.1
  • IP.2 = ::1

If you are testing container-to-container calls inside Docker, you also need entries for bridge hostnames or host.docker.internal.

3. The macOS 825-Day Lifetime Ceiling

A common habit when creating local test certificates is setting -days 3650 (10 years) to avoid expiration headaches.

On macOS and iOS, this causes an immediate ERR_CERT_INVALID that cannot be bypassed. Starting with macOS 10.15 and iOS 13, Apple introduced strict TLS baseline rules: all server certificates must have a validity period of 825 days or fewer. If your certificate validity exceeds 825 days, Apple Keychain and Safari reject the TLS handshake before examining any other trust flags.

Keep your local certificates under 365 days.

4. Missing Extended Key Usage (EKU)

A raw certificate generated without explicit usage extensions may default to no usage constraints, causing client rejection.

Modern TLS clients require the server certificate to explicitly declare its purpose:

extendedKeyUsage = serverAuth
Enter fullscreen mode Exit fullscreen mode

If you are setting up mutual TLS (mTLS) where both sides authenticate, client certificates must declare extendedKeyUsage = clientAuth. Without serverAuth, Go HTTP servers, Node.js tls modules, and modern browsers will abort handshakes with ERR_SSL_KEY_USAGE_INCOMPATIBLE.

When generating quick testing certificates or debugging TLS configs for Docker and staging environments, you can generate compliant RSA/ECDSA keypairs with correct SANs and EKU directly in your browser using the Nutilz Self-Signed Certificate Generator. It builds valid X.509 v3 extensions client-side without sending private keys over any network.

5. Leaf Certificate vs. Local Root CA

If you create a standalone self-signed certificate and import it directly into your OS trust store, you will run into basicConstraints issues:

  • A Certificate Authority (CA) requires basicConstraints = critical, CA:TRUE and keyCertSign.
  • An End-Entity (Server) certificate requires basicConstraints = critical, CA:FALSE and digitalSignature, keyEncipherment.

If your server certificate contains CA:TRUE, modern security scanners and browsers flag it as insecure. If it contains CA:FALSE, importing it as a trusted Root Authority will fail validation checks because it cannot sign a certificate chain.

The proper local setup is a two-tier hierarchy:

  1. Create a local root CA with CA:TRUE. Install that CA once into your operating system trust store (or use tools like mkcert).
  2. Issue server certificates signed by that local CA with CA:FALSE, extendedKeyUsage = serverAuth, and proper SAN entries.

Summary Checklist for Local HTTPS

Next time you configure local HTTPS or internal microservice TLS, ensure your certificate meets these requirements:

  • Has a subjectAltName listing both DNS and IP targets.
  • Validity does not exceed 365 days (well below the 825-day ceiling).
  • Declares extendedKeyUsage = serverAuth.
  • Uses basicConstraints = CA:FALSE for leaf server certs.

Taking two minutes to verify your SANs and X.509 extensions—whether via OpenSSL configuration files or utilities like Nutilz—saves hours of debugging cryptic browser TLS errors.

Top comments (0)