You fire up your local dev server, hit https://localhost:3000 in Safari, and get slapped with "This Connection Is Not Private" — even though you know full well it's your own machine.
Here's the fix, then why it happens.
Step 1: Confirm the cert is actually expired
Don't guess. Open Terminal and check:
openssl x509 -in /path/to/your/cert.pem -noout -dates
You'll get something like:
notBefore=Jan 4 10:22:01 2024 GMT
notAfter=Jan 4 10:22:01 2025 GMT
If today's date is past notAfter, that's your culprit. If you don't know where your cert lives, common spots are ./certs/, ./ssl/, ~/.localhost-certs/, or wherever your dev server config points. Webpack Dev Server, Vite, and Next.js each default to slightly different filenames.
Step 2: Regenerate the cert
Quickest path if you don't have mkcert installed:
mkdir -p ~/.localhost-certs
cd ~/.localhost-certs
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout localhost-key.pem \
-out localhost-cert.pem \
-days 825 \
-subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
The -addext flag matters. Safari is stricter than Chrome about SANs — a cert with only CN=localhost and no subjectAltName will still trigger the warning on recent macOS versions. Modern Safari on Sonoma effectively ignores CN entirely.
If you already use mkcert, even better:
mkcert -install
mkcert localhost 127.0.0.1 ::1
That drops two files in your current directory and installs the local CA into your system keychain automatically. Skips Step 3 entirely.
Step 3: Trust the cert in Keychain (if you didn't use mkcert)
Open Keychain Access (Applications → Utilities). Drag your localhost-cert.pem into the System keychain. It'll show up as "localhost." Double-click it, expand Trust, and set "When using this certificate" to Always Trust. Close the window, enter your password.
Now point your dev server at the new key and cert files. In Vite that's:
// vite.config.js
server: {
https: {
key: fs.readFileSync('~/.localhost-certs/localhost-key.pem'),
cert: fs.readFileSync('~/.localhost-certs/localhost-cert.pem'),
}
}
Restart the server. Hard-refresh Safari with Cmd+Shift+R. Done.
Why this works
Browsers treat certificates as valid only within a time window. When your self-signed cert's notAfter date passes, every request to that origin fails the TLS handshake at the certificate validation step. Safari shows "This Connection Is Not Private" and refuses to proceed — no "Advanced" bypass link like Chrome gives you on localhost.
Regenerating gives you a fresh validity window. Adding the SAN extension tells Safari the cert is legitimately for both localhost and 127.0.0.1, which Chrome infers but Safari doesn't. Trusting it in the System keychain promotes it from "self-signed, unknown authority" to "explicitly trusted by the user" — Safari's highest trust tier short of a real CA.
The reason trusting the cert alone doesn't fix an expired one: trust and validity are separate checks. You can trust a cert forever, but if it expired yesterday, Safari won't touch it.
Less common variations
The cert is valid but Safari still complains
Check the clock. A VM with a skewed system time will make every cert look expired or not-yet-valid. Run date in Terminal. If it's off by more than a few minutes, fix NTP:
sudo sntp -sS time.apple.com
You regenerated but Safari still shows the old cert
Safari aggressively caches TLS state. Quit it fully (Cmd+Q, not just close the window), then reopen. If that doesn't work, clear the cert cache:
sudo rm -rf ~/Library/Caches/com.apple.Safari
killall -HUP Safari
You're on a .local hostname instead of localhost
Bonjour-resolved names like myapp.local need that name in the SAN list too. Reissue with -addext "subjectAltName=DNS:localhost,DNS:myapp.local,IP:127.0.0.1". Otherwise Safari does a reverse lookup and flags a mismatch.
Node's built-in https module
If you're running your own https server instead of a dev framework, the same key/cert paths go in the https.createServer() options object. Nothing else changes.
Chrome is fine, Safari isn't
Almost always the SAN issue. Chrome falls back to CN matching; Safari on macOS 13+ requires SAN. Reissue with the -addext flag above and you're sorted.
Prevention
Stop hand-rolling certs. Install mkcert once and forget about expiry for years — it issues certs valid for 825 days by default, and you can extend that:
mkcert -cert-file localhost-cert.pem -key-file localhost-key.pem \
-days 3650 localhost 127.0.0.1 ::1
Add a note to your project README with the regeneration command. I had a client last month whose entire local dev setup died because nobody documented how their certs were generated — the original dev had left the company, and the rest of the team spent a day reverse-engineering a webpack config. Don't be that team.
Also: don't reuse production certs for local dev. Ever. If it leaks, you're revoking a live cert and breaking real users.
Set a calendar reminder 30 days before expiry if you're stuck with manual certs. That's the difference between a two-minute fix and a lost afternoon.