A self-signed certificate is signed by its own key, not by a certificate authority that browsers trust. It encrypts traffic just as well as any other certificate, but it doesn’t prove identity to anyone who hasn’t explicitly trusted it, so browsers show a warning. It’s useful for internal services, testing and development, and it’s the wrong choice for public websites, where Let’s Encrypt is free.
When self-signed makes sense
- Local development and test environments.
- Internal services on private networks, where you control the clients.
- Temporary certificates before a real one is issued.
- Server-to-server TLS where clients pin the certificate.
For public sites, use Let’s Encrypt. See Certbot for Nginx. For many internal services, a private CA is better than individual self-signed certificates. See private CA with step-ca.
Generate a certificate (one command)
Modern browsers ignore the Common Name and require a Subject Alternative Name (SAN). Include every hostname and IP you’ll use:
openssl req -x509 -newkey rsa:2048 -sha256 -days 825 -nodes \
-keyout /etc/ssl/private/internal.key \
-out /etc/ssl/certs/internal.crt \
-subj "/CN=internal.example.com" \
-addext "subjectAltName=DNS:internal.example.com,DNS:localhost,IP:10.0.0.12"
sudo chmod 600 /etc/ssl/private/internal.key
-x509creates a self-signed certificate (instead of a signing request).-nodesleaves the key unencrypted, so services can start without a passphrase. Protect the file with permissions instead.-days 825is about 27 months. Shorter is fine for test environments.- Use
-newkey ec -pkeyopt ec_paramgen_curve:prime256v1for an ECDSA key.
Inspect it:
openssl x509 -in /etc/ssl/certs/internal.crt -noout -subject -issuer -dates -ext subjectAltName
See openssl commands.
Use it in Nginx
server {
listen 443 ssl;
server_name internal.example.com;
ssl_certificate /etc/ssl/certs/internal.crt;
ssl_certificate_key /etc/ssl/private/internal.key;
}
Apache: SSLCertificateFile and SSLCertificateKeyFile. Debian and Ubuntu also ship a ready-made “snakeoil” pair (/etc/ssl/certs/ssl-cert-snakeoil.pem) from the ssl-cert package, for quick tests.
The browser warning, and how to trust the certificate
Browsers say “Your connection is not private” (NET::ERR_CERT_AUTHORITY_INVALID) because no trusted CA vouches for the certificate. Don’t train users to click through it. Instead, trust the certificate on the machines that need it.
Linux (system trust store):
sudo cp internal.crt /usr/local/share/ca-certificates/internal.crt
sudo update-ca-certificates # Debian/Ubuntu
# RHEL: cp to /etc/pki/ca-trust/source/anchors/ && update-ca-trust
macOS: open the certificate in Keychain Access, add it to the System keychain, and set “Always Trust”.
Windows: run certmgr.msc and import it into “Trusted Root Certification Authorities”. Group Policy works for fleets.
Firefox uses its own store (Settings → Certificates), unless enterprise roots are enabled.
Tools and code: curl --cacert internal.crt https://internal.example.com. In Node, NODE_EXTRA_CA_CERTS=/path/internal.crt. In Python requests, verify="/path/internal.crt".
Never disable certificate verification (curl -k, verify=False) in production code. That makes TLS vulnerable to interception.
mkcert for local development
mkcert creates a local CA, installs it in your system and browser trust stores, and issues certificates that browsers accept on your machine:
mkcert -install
mkcert localhost 127.0.0.1 myapp.test
That’s ideal for developers. Don’t share the mkcert root key.
Self-signed vs private CA vs Let’s Encrypt
| Self-signed | Private CA | Let’s Encrypt | |
|---|---|---|---|
| Trusted by browsers by default | No | No (trust the CA once, then all its certificates) | Yes |
| Good for | Single test or internal service | Many internal services, mTLS | Public websites and APIs |
| Renewal | Manual | Automatable (ACME with step-ca) | Automatic |
| Cost | Free | Free (self-hosted) | Free |
Let’s Encrypt can also issue certificates for internal hostnames under a public domain via the DNS-01 challenge, without exposing the server. See wildcard certificates with DNS validation.
Quick reference
| Task | Command |
|---|---|
| Create cert + key | openssl req -x509 -newkey rsa:2048 -nodes -days 825 -keyout k -out c -subj "/CN=host" -addext "subjectAltName=DNS:host" |
| Inspect | openssl x509 -in c -noout -text |
| Trust on Debian/Ubuntu | copy to /usr/local/share/ca-certificates/ + update-ca-certificates |
| Local dev | mkcert -install && mkcert localhost |
Related: SSL/TLS explained and SSL certificate chain errors.