# OpenSSL self-signed certificates: SAN, localhost and trust

> One-line OpenSSL 3.x commands for self-signed certificates with SANs, localhost dev certs, and adding them to macOS, Windows and Linux trust stores.

Source: https://ssl.sh/cookbook/self-signed/
Last updated: 2026-11-10

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.

```bash
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
```

```text
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](https://ssl.sh/tools/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.

```bash
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
```

```text
        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.

```bash
# 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
```

```text
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.

```bash
openssl req -x509 -newkey rsa:2048 -noenc -keyout n.key -out n.crt -subj "/CN=example.com"
openssl x509 -in n.crt -noout -dates
```

```text
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.

```bash
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
```

```text
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`.

```bash
openssl req -help 2>&1 | grep -E "noenc|nodes"
```

```text
 -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.

```bash
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"
```

```text
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](https://ssl.sh/errors/curl-60-ssl-certificate-problem/) 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.

```bash
sudo security add-trusted-cert -d -r trustRoot \
  -k /Library/Keychains/System.keychain localhost.crt
```

```text
(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.

```bash
certutil -addstore -f Root localhost.crt
# or in PowerShell
Import-Certificate -FilePath .\localhost.crt -CertStoreLocation Cert:\LocalMachine\Root
```

```text
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.

```bash
# 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
```

```text
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](https://ssl.sh/errors/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.

```bash
# 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
```

```text
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](https://ssl.sh/cookbook/private-ca/).
