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
  • -x509 creates a self-signed certificate (instead of a signing request).
  • -nodes leaves the key unencrypted, so services can start without a passphrase. Protect the file with permissions instead.
  • -days 825 is about 27 months. Shorter is fine for test environments.
  • Use -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 for 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.

  • SSL
  • Security
  • OpenSSL