---
title: "OpenSSL"
description: "A practical OpenSSL reference for TLS and certificate work. Generating keys and CSRs, self-signed certs, inspecting certificates, verifying chains, testing live handshakes with s_client, converting between PEM, DER, PKCS#12 and JKS, and checking that a key matches its certificate."
author: "Mohammad Abu Mattar"
canonical: https://mkabumattar.com/cheatsheets/openssl
---

# OpenSSL

Most OpenSSL use falls into two very different jobs. One is creating things: keys, CSRs, an internal CA. The other is answering "why does this TLS connection not work", which is where the tool earns its reputation for being unfriendly, because the useful invocations are long and the error messages point at the wrong layer.

The single most common failure is worth internalising before the reference below, because it explains a symptom that otherwise makes no sense: the certificate works in your browser and fails in `curl`.

_The handshake, and the chain that has to verify_

Two details from that diagram cause most wasted time. Hostname matching uses the Subject Alternative Name, not the Common Name, so a certificate with a perfect CN and no SAN simply fails. And `s_client` without `-servername` sends no SNI, so the server hands back its default certificate and you end up debugging a mismatch you created yourself.

On AWS, where the certificate lives changes what OpenSSL is even for.

_Where TLS terminates, and who holds the key_

The reference below is grouped the way the work actually splits: keys, then CSRs and certificates, then inspecting, then the verification and live-handshake commands you reach for when something is broken.

## Keys

Generating and inspecting private keys.

### Generating private keys

RSA for compatibility, EC for anything new.

**Keywords:** genrsa, genpkey, ecparam

#### Modern key generation

```bash
# EC P-256: smaller, faster, and fine everywhere in 2026
openssl ecparam -name prime256v1 -genkey -noout -out key.pem

# RSA 2048, the safe default when something old is involved
openssl genrsa -out key.pem 2048

# RSA 4096 if a policy demands it (slower handshakes)
openssl genrsa -out key.pem 4096

# genpkey is the modern front end and works for both
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out key.pem
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out key.pem
```

Prefer EC (P-256) unless you have a specific compatibility reason. It gives equivalent security to RSA 3072 with far less CPU per handshake.

- Never commit a private key. `chmod 600` it and treat a leak as a full rotation.

#### Encrypted keys, and removing the passphrase

```bash
# Generate with a passphrase
openssl genpkey -algorithm RSA -aes-256-cbc -out key.enc.pem

# Strip the passphrase (nginx and friends cannot prompt at boot)
openssl rsa -in key.enc.pem -out key.pem

# Add one to an existing key
openssl rsa -in key.pem -aes-256-cbc -out key.enc.pem
```

An encrypted key cannot be read by a service that starts unattended, which is why production keys are usually unencrypted on disk and protected by file permissions and a secret manager instead.

#### Inspecting a key

```bash
openssl pkey -in key.pem -noout -text        # any key type
openssl rsa  -in key.pem -noout -text        # RSA specifically
openssl pkey -in key.pem -pubout              # derive the public key
```

_exec_
```bash
openssl pkey -in key.pem -noout -text | head -3
```

_output_
```text
Private-Key: (2048 bit, 2 primes)
modulus:
    00:c4:1f:9a:...
```

Reading the bit length confirms you generated what you meant to. It is a common surprise to find a 1024-bit key inherited from an old runbook.

**Best practices:**

- Generate EC P-256 for new services; keep RSA 2048 for legacy compatibility.
- Store keys in a secret manager and mount them at deploy time rather than baking them into an image.

## CSRs and Certificates

Requesting certificates, and creating your own for internal use.

### Certificate signing requests

What you send to a CA, and how to get the SAN right.

**Keywords:** req, CSR, subjectAltName

#### Key and CSR in one command, with SANs

```bash
openssl req -new -newkey rsa:2048 -nodes \
  -keyout key.pem -out request.csr \
  -subj "/C=JO/ST=Amman/L=Amman/O=Example Ltd/CN=example.com" \
  -addext "subjectAltName=DNS:example.com,DNS:www.example.com"
```

`-nodes` means no passphrase on the key. `-addext subjectAltName` is the part that matters: without a SAN the certificate is rejected by every modern client regardless of the CN.

- CN is legacy. Every hostname you need must appear in the SAN, including the bare domain.

#### A config file, for anything you will regenerate

```bash
cat > csr.cnf <<'CNF'
[req]
default_bits       = 2048
prompt             = no
distinguished_name = dn
req_extensions     = ext

[dn]
C  = JO
O  = Example Ltd
CN = example.com

[ext]
subjectAltName = @alt

[alt]
DNS.1 = example.com
DNS.2 = www.example.com
DNS.3 = api.example.com
CNF

openssl req -new -config csr.cnf -newkey rsa:2048 -nodes \
  -keyout key.pem -out request.csr
```

Put the config in version control. Regenerating a CSR from a remembered command line is where SANs get dropped.

#### Verify a CSR before sending it

```bash
openssl req -in request.csr -noout -text -verify
openssl req -in request.csr -noout -subject
```

_exec_
```bash
openssl req -in request.csr -noout -text | grep -A2 "Alternative Name"
```

_output_
```text
X509v3 Subject Alternative Name:
    DNS:example.com, DNS:www.example.com
```

Always confirm the SAN is present before paying a CA. Finding out afterwards costs a reissue.

**Common errors:**

- **Browser reports NET::ERR_CERT_COMMON_NAME_INVALID even though the CN is right.**: The certificate has no SAN, or the hostname is missing from it. Add it with `-addext subjectAltName=DNS:...` and reissue; the CN is not consulted.

### Self-signed and internal CAs

For local development and internal services.

#### A self-signed certificate in one line

```bash
openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
  -keyout key.pem -out cert.pem \
  -subj "/CN=localhost" \
  -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
```

Fine for local development. Clients will not trust it unless you add it to their trust store, which is the point of the next example.

#### A small internal CA, then a cert signed by it

```bash
# 1. the CA (keep ca.key somewhere safe)
openssl req -x509 -newkey rsa:4096 -nodes -days 3650 \
  -keyout ca.key -out ca.crt -subj "/CN=Example Internal CA"

# 2. a key + CSR for the service
openssl req -new -newkey rsa:2048 -nodes \
  -keyout svc.key -out svc.csr -subj "/CN=svc.internal" \
  -addext "subjectAltName=DNS:svc.internal"

# 3. sign it, copying the SAN across (this is the step people miss)
openssl x509 -req -in svc.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -days 365 -out svc.crt \
  -extfile <(printf "subjectAltName=DNS:svc.internal")
```

`openssl x509 -req` does NOT copy extensions from the CSR. Without `-extfile` the signed certificate has no SAN and will be rejected, even though the CSR had one.

- Distribute `ca.crt` to clients; never distribute `ca.key`.

**Common errors:**

- **A cert signed by your internal CA has no SAN even though the CSR did.**: `openssl x509 -req` drops extensions. Pass them again with `-extfile`, or use `openssl ca` which honours a config section.

## Inspecting Certificates

Reading what a certificate actually says.

### Reading a certificate

Dates, names, issuer, extensions.

**Keywords:** x509, dates, subjectAltName, fingerprint

#### The fields you actually check

```bash
openssl x509 -in cert.pem -noout -text              # everything
openssl x509 -in cert.pem -noout -subject -issuer   # who, signed by whom
openssl x509 -in cert.pem -noout -dates             # validity window
openssl x509 -in cert.pem -noout -ext subjectAltName
openssl x509 -in cert.pem -noout -fingerprint -sha256
openssl x509 -in cert.pem -noout -serial
```

_exec_
```bash
openssl x509 -in cert.pem -noout -dates -subject
```

_output_
```text
notBefore=Sep  1 00:00:00 2026 GMT
notAfter=Nov 30 23:59:59 2026 GMT
subject=CN = example.com
```

`-noout` suppresses the re-printed PEM, which is what makes these usable in scripts.

#### Is it expiring soon?

```bash
# exit 0 if the cert is still valid in 30 days, 1 if not
openssl x509 -in cert.pem -noout -checkend $((30*86400))

# readable form for a monitoring script
openssl x509 -in cert.pem -noout -enddate | cut -d= -f2
```

`-checkend` is the whole expiry-monitoring problem solved in one flag. Wire it into a cron check and stop finding out from customers.

- Check the whole chain, not just the leaf. An expired intermediate breaks the site just as thoroughly.

#### Does this key match this certificate?

```bash
# RSA: the moduli must be identical
openssl x509 -noout -modulus -in cert.pem | openssl sha256
openssl rsa  -noout -modulus -in key.pem  | openssl sha256

# EC keys: compare the public keys instead
openssl x509 -in cert.pem -noout -pubkey | openssl sha256
openssl pkey -in key.pem -pubout          | openssl sha256
```

Two identical hashes means the pair matches. This is the first thing to check when a server refuses to start with a key-mismatch error.

**Advanced notes:**

- **Reading a certificate straight from a server:** You do not need the file. `openssl s_client -connect host:443 -servername host </dev/null 2>/dev/null | openssl x509 -noout -dates -subject` pulls the live certificate and inspects it in one pipeline. This is the fastest way to answer "what is actually deployed", which is often different from what is in the repository.

## Verifying and Debugging

Chains, live handshakes, and why a client disagrees with your browser.

### Chain verification

The missing-intermediate problem, and how to prove it.

**Keywords:** verify, s_client, showcerts, CAfile

#### Verify a chain offline

```bash
# against the system trust store
openssl verify cert.pem

# supplying the intermediate the server should be sending
openssl verify -untrusted intermediate.pem cert.pem

# against a private CA
openssl verify -CAfile ca.crt svc.crt
```

_exec_
```bash
openssl verify -untrusted intermediate.pem cert.pem
```

_output_
```text
cert.pem: OK
```

`-untrusted` is confusingly named: it means "here are intermediate certificates to build the path with", not "do not trust these".

- "unable to get local issuer certificate" almost always means a missing intermediate, not an untrusted root.

#### What is the server actually serving?

```bash
# -servername sends SNI. Without it you may get the wrong cert entirely.
openssl s_client -connect example.com:443 -servername example.com

# count the certificates in the chain: one leaf only is a bug
openssl s_client -connect example.com:443 -servername example.com \
  -showcerts </dev/null 2>/dev/null | grep -c "BEGIN CERTIFICATE"

# verify against a specific CA bundle
openssl s_client -connect example.com:443 -servername example.com \
  -CAfile /etc/ssl/certs/ca-certificates.crt
```

_exec_
```bash
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null | grep -c "BEGIN CERTIFICATE"
```

_output_
```text
2
```

Two or three certificates is normal (leaf plus intermediates). One means the server is not sending its intermediate, which works in browsers and fails in curl, Java and Alpine.

- Always pass `-servername`. Omitting SNI is the most common self-inflicted TLS debugging wound.
- `</dev/null` stops s_client hanging waiting for input.

#### Protocol and cipher checks

```bash
# is TLS 1.2 still enabled? is 1.3 available?
openssl s_client -connect host:443 -tls1_2 </dev/null 2>&1 | grep -E "Protocol|Cipher"
openssl s_client -connect host:443 -tls1_3 </dev/null 2>&1 | grep -E "Protocol|Cipher"

# confirm an old protocol is actually refused
openssl s_client -connect host:443 -tls1_1 </dev/null 2>&1 | tail -3

# STARTTLS for mail, and other protocols
openssl s_client -starttls smtp -connect mail:587
openssl s_client -starttls postgres -connect db:5432
```

A handshake failure on `-tls1_1` is the result you want: it proves the old protocol is disabled rather than merely unpreferred.

**Common errors:**

- **curl says "unable to get local issuer certificate" but the site loads fine in Chrome.**: The server is not sending its intermediate. Browsers cache intermediates from other sites and paper over it. Fix the server to serve the full chain (leaf + intermediate, not the root).
- **Hostname mismatch when testing with s_client, but the site works in a browser.**: You omitted `-servername`, so no SNI was sent and the server returned its default certificate. Add `-servername <host>`.
- **s_client appears to hang with no output.**: It is waiting on stdin. Append `</dev/null`.

## Converting Formats

PEM, DER, PKCS#12 and Java keystores.

### Format conversions

The same certificate, different envelopes.

**Keywords:** pkcs12, der, pem, keystore

#### PEM, DER, and PKCS#12

```bash
# PEM (base64, "-----BEGIN") <-> DER (binary)
openssl x509 -in cert.pem -outform der -out cert.der
openssl x509 -in cert.der -inform der -out cert.pem

# bundle cert + key + chain into one .p12 (Windows, Java, browsers)
openssl pkcs12 -export -out bundle.p12 \
  -inkey key.pem -in cert.pem -certfile intermediate.pem \
  -name "example.com"

# and back out again
openssl pkcs12 -in bundle.p12 -nokeys  -out cert.pem
openssl pkcs12 -in bundle.p12 -nocerts -nodes -out key.pem
```

PEM is text and safe to paste; DER is binary and used by Java and Windows tooling. PKCS#12 carries the key and the chain together in one password-protected file.

- Check which you have with `file cert.crt`, or just look for `-----BEGIN`.

#### Java keystores

```bash
# the supported route: build a PKCS#12, then convert
openssl pkcs12 -export -out bundle.p12 -inkey key.pem -in cert.pem
keytool -importkeystore -srckeystore bundle.p12 -srcstoretype PKCS12 \
        -destkeystore keystore.jks -deststoretype JKS

# inspect what a keystore holds
keytool -list -v -keystore keystore.jks
```

OpenSSL cannot write JKS directly. Go through PKCS#12; `keytool` handles the last step.

**Best practices:**

- Keep the canonical copy in PEM and convert on demand, rather than maintaining several formats.

## Everyday Utilities

The parts of OpenSSL that have nothing to do with certificates.

### Hashes, random, and base64

Handy when you have OpenSSL and not much else.

#### Random values and digests

```bash
openssl rand -hex 32              # 32 random bytes as hex
openssl rand -base64 24           # a decent password
openssl dgst -sha256 file.tar.gz  # checksum
openssl base64 -in f -out f.b64   # encode
openssl base64 -d -in f.b64       # decode
```

`openssl rand -hex 32` is a reliable way to generate a secret on a box with no other tooling.

- For password hashing use a real KDF (argon2, bcrypt, scrypt), not `dgst`.

#### Quick symmetric encryption

```bash
openssl enc -aes-256-cbc -pbkdf2 -salt -in f -out f.enc
openssl enc -d -aes-256-cbc -pbkdf2 -in f.enc -out f
```

Fine for moving a file past an untrusted channel. Always include `-pbkdf2` and `-salt`; the old defaults were weak.

**Advanced notes:**

- **Benchmarking, and why EC wins:** `openssl speed rsa2048 ecdsap256` reports signatures per second for each. On typical hardware EC P-256 does an order of magnitude more signing operations per second than RSA 2048, which is why it matters on a busy TLS terminator doing thousands of handshakes.
