Generate a Self-Signed SSL Certificate with openssl
Create a certificate and private key for local development, internal tools, and test environments where a CA-signed certificate is not necessary.
A self-signed certificate provides the same encryption as one issued by a certificate authority — the difference is that browsers and clients do not trust it automatically and show a warning. For local development and internal services where you control the client, that trade-off is fine.
Generate a certificate and key in one command
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \
-sha256 -days 365 -nodes \
-subj "/CN=localhost"
What this produces:
key.pem— the private keycert.pem— the self-signed certificate
What the flags do:
-x509— output a self-signed certificate instead of a certificate signing request-newkey rsa:4096— generate a new RSA key with a 4096-bit key size-sha256— sign the certificate with SHA-256-days 365— certificate valid for one year-nodes— do not encrypt the private key (no passphrase). Necessary when the key is used by a server process that cannot prompt for input.-subj "/CN=localhost"— set the Common Name without the interactive prompt
Add Subject Alternative Names
Modern browsers and TLS clients require the hostname to appear in the Subject Alternative Names (SAN) extension, not just the Common Name. A certificate without SANs is rejected by Chrome, Safari, and many libraries even if the CN matches.
Create a config file:
# san.cnf
[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_req
prompt = no
[req_distinguished_name]
CN = myapp.internal
[v3_req]
subjectAltName = @alt_names
[alt_names]
DNS.1 = myapp.internal
DNS.2 = localhost
IP.1 = 127.0.0.1
IP.2 = 192.168.1.50
Generate the certificate using the config:
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \
-sha256 -days 365 -nodes -config san.cnf
Now the certificate covers both the hostname and IP addresses in the alt_names block.
Inspect a certificate
Verify what you generated:
openssl x509 -in cert.pem -text -noout
The output shows the validity period, the subject, and — most importantly — the Subject Alternative Names section. If X509v3 Subject Alternative Name is missing or does not list your hostname, the certificate will be rejected by modern clients.
Check the expiry date specifically:
openssl x509 -in cert.pem -noout -dates
Trust the certificate locally
Self-signed certificates produce browser warnings because they are not in the system's certificate trust store. Add them to stop the warnings on your own machine.
macOS:
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain cert.pem
Ubuntu / Debian:
sudo cp cert.pem /usr/local/share/ca-certificates/myapp.crt
sudo update-ca-certificates
Windows (PowerShell):
Import-Certificate -FilePath cert.pem -CertStoreLocation Cert:\LocalMachine\Root
Restart your browser after adding the certificate. Chrome caches trust state aggressively — a full restart (not just a new tab) is required.
Use the certificate with a web server
Nginx:
server {
listen 443 ssl;
server_name myapp.internal;
ssl_certificate /etc/ssl/myapp/cert.pem;
ssl_certificate_key /etc/ssl/myapp/key.pem;
}
Node.js (https module):
const https = require('https');
const fs = require('fs');
const server = https.createServer({
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem'),
}, app);
server.listen(443);
Protect the private key
The private key must stay private. If someone obtains it, they can impersonate your server for the certificate's validity period.
chmod 600 key.pem
chown root:root key.pem
Do not commit key.pem to version control. Add it to .gitignore:
key.pem
*.pem
For production services reachable from the internet, use a certificate from Let's Encrypt instead of a self-signed one. certbot automates issuance and renewal, the certificates are free, and clients trust them without any manual configuration.
SysEmperor