Keys, CSRs and certificates

OpenSSL self-signed certificates: SAN, localhost and trust

By · Last verified Tested macOS · OpenSSL 3.6.1

OpenSSL 3.x creates a self-signed certificate and its key with a single openssl req -x509 command. The certificate is signed by its own key instead of a CA. It encrypts like any other certificate, but no client trusts it until you install it: right for development, lab devices and internal tests, wrong for public sites. Outputs are from OpenSSL 3.6.1.

Create a self-signed certificate with SANs in one command

req -x509 skips the CSR and writes a certificate; -newkey generates the key at the same time.

openssl req -x509 -newkey rsa:2048 -sha256 -days 365 -noenc \
  -keyout example.com.key -out example.com.crt \
  -subj "/CN=example.com" \
  -addext "subjectAltName=DNS:example.com,DNS:*.example.com"
openssl x509 -in example.com.crt -noout -subject -dates -ext subjectAltName
subject=CN=example.com
notBefore=Nov  9 18:17:00 2026 GMT
notAfter=Nov  9 18:17:00 2027 GMT
X509v3 Subject Alternative Name: 
    DNS:example.com, DNS:*.example.com

The SAN is what clients check; a certificate with the name only in CN fails in every current browser. -addext needs OpenSSL 1.1.1 or later. The self-signed certificate generator produces the same files in your browser.

Create an ECDSA self-signed certificate

Swap the key type to ec with a P-256 curve for a smaller, faster certificate.

openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -noenc -days 365 \
  -keyout example.com.key -out example.com.crt \
  -subj "/CN=example.com" \
  -addext "subjectAltName=DNS:example.com,DNS:www.example.com,IP:203.0.113.10"
openssl x509 -in example.com.crt -noout -text | grep -E "Signature Algorithm|Public-Key|IP Address" | head -3
        Signature Algorithm: ecdsa-with-SHA256
                Public-Key: (256 bit)
                DNS:example.com, DNS:www.example.com, IP Address:203.0.113.10

Every modern client supports ECDSA P-256. Ed25519 self-signed certificates also work with -newkey ed25519, but most browsers do not accept Ed25519 for TLS, so keep those for internal mTLS.

Self-sign an existing key or CSR

Use -key with req -x509 to reuse a key, or x509 -req to turn an existing CSR into a certificate.

# existing key
openssl req -x509 -key example.com.key -days 365 -subj "/CN=example.com" \
  -addext "subjectAltName=DNS:example.com" -out example.com.crt
# existing CSR (copy its SANs into the certificate)
openssl x509 -req -in example.com.csr -key example.com.key -days 365 \
  -copy_extensions copy -out example.com.crt
Certificate request self-signature ok
subject=CN=example.com

x509 -req ignores the CSR's extensions unless you add -copy_extensions copy (OpenSSL 3.0+) or pass them with -extfile. This is the most common reason a self-signed certificate "has no SAN".

Set the validity period

-days sets the lifetime; without it req -x509 gives you only 30 days.

openssl req -x509 -newkey rsa:2048 -noenc -keyout n.key -out n.crt -subj "/CN=example.com"
openssl x509 -in n.crt -noout -dates
notBefore=Nov  9 18:17:00 2026 GMT
notAfter=Dec  9 18:17:00 2026 GMT

The 30-day default surprises people when a test environment breaks a month later. Recent 3.x versions also accept -not_before and -not_after with absolute dates (check openssl req -help). Stay at or below 825 days, because Apple platforms reject longer TLS server certificates even when trusted manually.

Create a localhost certificate for development

Mark it as a non-CA server certificate and include both the name and loopback IPs.

openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -noenc -days 825 \
  -keyout localhost.key -out localhost.crt -subj "/CN=localhost" \
  -addext "subjectAltName=DNS:localhost,IP:127.0.0.1,IP:::1" \
  -addext "basicConstraints=critical,CA:FALSE" \
  -addext "keyUsage=critical,digitalSignature" \
  -addext "extendedKeyUsage=serverAuth"
openssl x509 -in localhost.crt -noout -ext basicConstraints,extendedKeyUsage,subjectAltName
X509v3 Subject Alternative Name: 
    DNS:localhost, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1
X509v3 Basic Constraints: critical
    CA:FALSE
X509v3 Extended Key Usage: 
    TLS Web Server Authentication

With the default config, req -x509 adds CA:TRUE; the explicit basicConstraints above overrides it. Apple requires the serverAuth EKU for TLS server certificates. For an RSA key, use keyUsage=critical,digitalSignature,keyEncipherment.

Use -noenc instead of -nodes

OpenSSL 3.0 renamed the "do not encrypt the private key" flag from -nodes to -noenc.

openssl req -help 2>&1 | grep -E "noenc|nodes"
 -noenc                Don't encrypt private keys
 -nodes                Don't encrypt private keys; deprecated

Both still work in 3.x, so old scripts are fine. Use -noenc in new scripts; on OpenSSL 1.1.1 only -nodes exists. Without either flag, OpenSSL asks for a passphrase and the web server will prompt for it at every start.

Test the certificate with s_server and curl

Run a throwaway TLS server and connect with --cacert to prove the certificate and key work together.

openssl s_server -accept 8443 -cert localhost.crt -key localhost.key -www &
curl https://localhost:8443/ -o /dev/null
curl --cacert localhost.crt https://localhost:8443/ -o /dev/null -w "%{http_code}\n"
echo | openssl s_client -connect localhost:8443 -servername localhost 2>&1 | grep "Verify return"
curl: (60) SSL certificate problem: unable to get local issuer certificate
200
Verify return code: 18 (self-signed certificate)

Error 60 without --cacert is the expected result; see curl 60 for the details. Verify code 18 means the chain ends in a certificate that signed itself and is not in the trust store. kill %1 stops the test server.

Trust the certificate on macOS

Add it to the System keychain as a trusted root; Safari and Chrome use the macOS trust settings.

sudo security add-trusted-cert -d -r trustRoot \
  -k /Library/Keychains/System.keychain localhost.crt
(no output on success; macOS may ask for your password or Touch ID)

Remove it later in Keychain Access, or with sudo security remove-trusted-cert -d localhost.crt. Firefox keeps its own store; import the file under Settings, Privacy & Security, Certificates, View Certificates, Authorities.

Trust the certificate on Windows

Import it into the Local Machine "Trusted Root Certification Authorities" store from an elevated prompt.

certutil -addstore -f Root localhost.crt
# or in PowerShell
Import-Certificate -FilePath .\localhost.crt -CertStoreLocation Cert:\LocalMachine\Root
Root "Trusted Root Certification Authorities"
Signature matches Public Key
Certificate "localhost" added to store.
CertUtil: -addstore command completed successfully.

Edge and Chrome use the Windows store. You can also double-click the .crt file and choose "Install Certificate", then pick "Trusted Root Certification Authorities" by hand; certmgr.msc (current user) and certlm.msc (local machine) show and remove it.

Trust the certificate on Ubuntu, Debian and RHEL

Copy the PEM file into the system anchor directory and rebuild the bundle.

# Ubuntu / Debian: the file must end in .crt
sudo cp localhost.crt /usr/local/share/ca-certificates/localhost.crt
sudo update-ca-certificates
# RHEL / Rocky / Fedora
sudo cp localhost.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust
Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.

This covers curl, wget, Python requests with the system bundle, Go and most CLI tools. Java uses its own cacerts keystore and Node.js uses a bundled list unless you set NODE_EXTRA_CA_CERTS. Chrome on Linux reads an NSS database in ~/.pki/nssdb.

Understand why browsers still warn

Trust is only one of several checks, and each failure has its own error.

Symptom Cause Fix
NET::ERR_CERT_AUTHORITY_INVALID Not installed in the store this browser uses Install it as above, or into Firefox's own store
NET::ERR_CERT_COMMON_NAME_INVALID Hostname missing from SAN Re-create with every hostname and IP in subjectAltName
MOZILLA_PKIX_ERROR_CA_CERT_USED_AS_END_ENTITY Certificate has CA:TRUE Add basicConstraints=critical,CA:FALSE
Rejected on macOS or iOS only Valid for more than 825 days, or no serverAuth EKU Shorter -days, add extendedKeyUsage=serverAuth
Works in curl but not on a phone Mobile OS needs a separate install and, on iOS, full trust enabled Install the profile and enable trust in settings

Firefox's self-signed error is described in MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT. Domains on the HSTS preload list, such as anything under .dev, never let you click through a certificate warning.

Use a private CA instead for teams

When several machines or developers need trusted certificates, create one root and sign leaf certificates with it.

# one-time: trust ca.crt everywhere, then issue as many leaf certificates as needed
openssl x509 -req -in app.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -days 90 -extfile server.ext -out app.crt
openssl verify -CAfile ca.crt app.crt
Certificate request self-signature ok
subject=CN=example.com
app.crt: OK

With a CA you install one root per device and rotate leaf certificates freely, instead of re-trusting every new self-signed certificate. The full recipe, including name constraints and intermediates, is in private CA.

FAQ

Is a self-signed certificate less secure than a CA-signed one?

The encryption is identical. What is missing is third-party identity: nobody vouches that the key belongs to the name, so every client must be told explicitly to trust it. That is fine for development and closed systems and wrong for public websites, where a free Let's Encrypt certificate costs nothing.

How long should a self-signed development certificate be valid?

One year or less is a sensible default. Since iOS 13 and macOS 10.15, Apple platforms reject TLS server certificates issued after 1 July 2019 that are valid for more than 825 days, even when you trust them manually, so never go above that.

Can I make a self-signed certificate for an IP address?

Yes: add IP:203.0.113.10 (or IP:127.0.0.1) to the subjectAltName. Clients compare IP addresses only against IP-type SAN entries, not DNS entries or the Common Name.

Sources

  1. OpenSSL manual: openssl-req
  2. OpenSSL 3.0 manual: x509v3_config
  3. Microsoft Learn: certutil
  4. RFC 5280: X.509 certificate profile