Self-Signed Certificates: When to Use Them and How to Avoid Warnings
Self-Signed Certificates: When to Use Them and How to Avoid Warnings
A self-signed certificate is signed by the owner's own key, not by a trusted CA. Browsers don't trust it and show ERR_CERT_AUTHORITY_INVALID. Still, self-signed is a valid tool for internal networks, development, and test environments. This guide shows how to create one correctly, when it's appropriate, and how to free your team from constant warnings.
When self-signed is the right choice
- Local development:
localhost,*.local,dev.example.com— Let's Encrypt doesn't cover these, and paid certs are overkill. - Internal services: admin panels, monitoring, proxies — inside the VPN where no outside user ever lands.
- IoT / embedded: limited connectivity, CA installation on clients is under your control.
- mTLS testing: fast generation of client certs without bureaucracy.
- Air-gapped environments: isolated networks without public CA access.
For a public site, self-signed is always a bad idea — users see a red screen, search engines demote you. Use Let's Encrypt.
Creating a self-signed certificate with openssl
A basic 365-day cert for localhost:
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem \
-days 365 -nodes \
-subj "/C=US/ST=CA/O=Dev/CN=localhost"
Modern browsers require SAN (Subject Alternative Name) — without it you get ERR_CERT_COMMON_NAME_INVALID. Proper way — with a config:
# openssl.cnf
[req]
distinguished_name = req
x509_extensions = v3_req
prompt = no
[req]
C = US
CN = localhost
[v3_req]
subjectAltName = @alt_names
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
[alt_names]
DNS.1 = localhost
DNS.2 = dev.example.com
DNS.3 = *.dev.example.com
IP.1 = 127.0.0.1
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem \
-days 365 -nodes -config openssl.cnf
Install in nginx and verify
server {
listen 443 ssl;
server_name localhost;
ssl_certificate /etc/nginx/certs/cert.pem;
ssl_certificate_key /etc/nginx/certs/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
}
curl -k https://localhost/ # -k ignores self-signed
openssl s_client -connect localhost:443 -servername localhost
Removing the browser warning (trust store)
The warning disappears when the cert (or its CA) is added to the system trust store.
macOS:
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain cert.pem
Linux (Debian/Ubuntu):
sudo cp cert.pem /usr/local/share/ca-certificates/dev-cert.crt
sudo update-ca-certificates
Windows (admin PowerShell):
Import-Certificate -FilePath cert.pem `
-CertStoreLocation Cert:\LocalMachine\Root
Firefox has its own trust store — add via Settings → Privacy & Security → Certificates.
The right way: local CA + mkcert
Instead of making certs by hand for every project — create a local CA, install it once, then mint certs under it. The mkcert tool does this in two commands:
# Install (macOS)
brew install mkcert
mkcert -install # Creates and trusts the CA system-wide
# Generate a cert
mkcert localhost 127.0.0.1 dev.example.com "*.dev.example.com"
# Produces localhost+3.pem and localhost+3-key.pem
For a dev team: store the root CA in an encrypted vault (1Password / HashiCorp Vault), each dev imports once, then mkcert mints certs on demand.
Internal PKI for organizations
At mid/large-size companies, ad-hoc self-signed doesn't scale. You need a PKI:
- Root CA (offline, on an isolated machine) — signs only intermediates.
- Intermediate CA (online) — issues server certs.
- Tools: HashiCorp Vault (PKI secrets engine), Step CA, cert-manager for Kubernetes.
- Trust distribution: root CA pushed via AD group policy / MDM to all workstations.
This mirrors how public CAs work, just inside your company. Certs auto-renew via ACME (like Let's Encrypt) or Vault API документацию.
Risks and anti-patterns
- ❌ Disabling cert verification (
curl -k,verify_ssl=False) in production code — kills TLS protection. - ❌ Copying the CA private key between developers without encryption.
- ❌ Self-signed on a public API — clients will suffer.
- ✅ Use mkcert or Vault instead of raw openssl.
- ✅ Store CA private keys in HSM or encrypted Secrets Manager.
- ✅ Rotate certs — don't set 10-year validity.
Frequently asked questions
Why doesn't Let's Encrypt work for localhost?
Let's Encrypt requires public validation (DNS-01 or HTTP-01). Not possible for localhost or private IPs. Workaround — DNS-01 against a real domain like dev.example.com with DNS pointing to 127.0.0.1.
Is self-signed more secure than HTTP?
Technically yes — traffic is encrypted. But without trust you can't detect MITM; an attacker can swap the self-signed cert for theirs and you won't notice.
Can I reuse one self-signed cert for multiple domains?
Yes via SAN. Add all domains to alt_names. Wildcard works too: DNS.1 = *.example.local. See wildcard vs SAN.
What validity period should I set?
365 days is fine for dev. For internal PKI use 90 days max (forced rotation). Public CAs haven't issued more than 398 days since 2020 — good reference.
Conclusion
Self-signed is a tool, not a sin — appropriate for local dev, internal services, and IoT. Just automate via mkcert or a PKI, and never use on public sites. The enterno.io SSL Checker catches accidental self-signed deploys to prod, and Monitors tracks expiry for internal certs.
X.509 spec — RFC 5280. mkcert — github.com/FiloSottile/mkcert.
Check your website right now
Check now →