Keys, CSRs and certificates
OpenSSL key generation: RSA, ECDSA, Ed25519 and ML-DSA
In OpenSSL 3.x, openssl genpkey generates every private key type: RSA, ECDSA, Ed25519 and, from OpenSSL 3.5, the post-quantum ML-DSA and ML-KEM. The commands below also encrypt, inspect and match keys. Outputs come from OpenSSL 3.6.1; commands marked "3.5+" fail on older versions.
Check your version first with openssl version. On macOS the system openssl is LibreSSL, so use Homebrew's $(brew --prefix openssl@3)/bin/openssl.
Generate an RSA private key
Use genpkey with rsa_keygen_bits; 2048 bits is the minimum public CAs accept and 3072 gives extra margin.
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out example.com.key
# 3072 or 4096 bits
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out example.com.key
..+.+.....+...+....+..+...+++++++++++++++++++++++++++++++*..+...+......
...+.+............+...+..+++++++++++++++++++++++++++++++*...........+++
The dots and plus signs are progress output from prime search and can be ignored; recent 3.x releases accept -quiet to hide them. The file starts with -----BEGIN PRIVATE KEY----- (PKCS#8). 4096-bit keys take noticeably longer to generate and make every handshake slower, with little practical benefit over 3072.
Generate an RSA key with genrsa
genrsa still works and is what most older tutorials use; in 3.x it writes the same PKCS#8 format as genpkey.
openssl genrsa -out example.com.key 3072
head -1 example.com.key
-----BEGIN PRIVATE KEY-----
Before OpenSSL 3.0, genrsa wrote -----BEGIN RSA PRIVATE KEY----- (PKCS#1). If a guide tells you to look for that header, the key is not broken; it uses the newer container. See "Convert a key to the traditional format" below if software insists on PKCS#1.
Generate an ECDSA P-256 key
P-256 (OpenSSL name prime256v1) is the recommended default for TLS: a 256-bit key is roughly as strong as RSA 3072 and much faster to sign with.
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out example.com.key
# older equivalent, writes SEC1 "BEGIN EC PRIVATE KEY"
openssl ecparam -name prime256v1 -genkey -noout -out example.com.key
-----BEGIN PRIVATE KEY-----
genpkey writes PKCS#8; ecparam -genkey writes the traditional SEC1 format (BEGIN EC PRIVATE KEY). Nginx, Apache, HAProxy and certbot accept both. Some old guides add -param_enc explicit; do not, because explicit curve parameters are rejected by many TLS stacks.
Generate a P-384 key and list curves
Use P-384 when a policy (for example a government profile) requires it; otherwise P-256 is enough.
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-384 -out example.com.key
openssl ecparam -list_curves | grep -E "prime256v1|secp384r1|secp521r1"
secp384r1 : NIST/SECG curve over a 384 bit prime field
secp521r1 : NIST/SECG curve over a 521 bit prime field
prime256v1: X9.62/SECG curve over a 256 bit prime field
Public CAs issue TLS certificates for P-256, P-384 and P-521 keys only. secp256k1 and Brainpool curves appear in the list but browsers do not accept them for TLS certificates.
Generate an Ed25519 or Ed448 key
Ed25519 keys are tiny and fast, but they are only useful for private PKI, SSH-style tooling and mTLS inside your own systems.
openssl genpkey -algorithm ED25519 -out device.key
cat device.key
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIPOx/N3NUcmxmTHOy4dbMZNVy+UQ0e5PhLTGx/frIr34
-----END PRIVATE KEY-----
Use -algorithm ED448 for Ed448. As of November 2026 no publicly trusted CA issues Ed25519 server certificates, because the CA/Browser Forum Baseline Requirements allow only RSA and ECDSA. Ed25519 keys cannot take -pkeyopt options; there is nothing to choose.
Encrypt a private key with a passphrase
Add a cipher option when you write the key; AES-256 is the usual choice.
# new key, encrypted
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 \
-aes-256-cbc -out example.com.key
# encrypt an existing key
openssl pkey -in example.com.key -aes256 -out example.com.enc.key
head -1 example.com.enc.key
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
-----BEGIN ENCRYPTED PRIVATE KEY-----
In scripts pass the password with -pass pass:... (genpkey) or -passout file:secret.txt; pass: puts the secret in your shell history and process list, so prefer file: or env:VAR. A web server cannot start unattended with an encrypted key unless you configure a passphrase helper (for example ssl_password_file in Nginx).
Remove the passphrase from a key
Read the encrypted key and write it back without a cipher option.
openssl pkey -in example.com.enc.key -out example.com.key
Enter pass phrase for example.com.enc.key:
If you see Could not find private key ... UI_process:processing error, OpenSSL could not ask for the password (for example in a non-interactive shell); pass it with -passin file:secret.txt. Set the unencrypted file to mode 600 and keep it out of Git.
Extract the public key
The public key is derived from the private key; this is what a certificate or CSR will contain.
openssl pkey -in example.com.key -pubout -out example.com.pub
cat example.com.pub
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3ESOpoc5gEeieqQ2KT82rP89m/8d
pKpjwWt960B2G9vF7f/s1cTOn3iPwpPxflR8Z6cmTwDqBHraRNtAD7D8Xg==
-----END PUBLIC KEY-----
The public key is safe to share. You need it for public-key pinning, DANE TLSA records and for proving which key a certificate was issued for.
View and check a private key
-text -noout prints the key parameters without the PEM block; -check validates the math.
openssl pkey -in example.com.key -noout -text | head -12
openssl pkey -in example.com.key -noout -check
Private-Key: (256 bit)
priv:
71:90:82:72:65:87:d7:a5:c1:76:04:09:c2:8f:00:
...
pub:
04:dc:44:8e:a6:87:39:80:47:a2:7a:a4:36:29:3f:
...
ASN1 OID: prime256v1
NIST CURVE: P-256
Key is valid
The first line tells you the algorithm and size: Private-Key: (2048 bit, 2 primes) for RSA, ED25519 Private-Key: for Ed25519. Never paste this output anywhere: the priv field is the secret itself.
Check that a key matches a certificate or CSR
Compare a SHA-256 hash of the public key from each file; equal hashes mean they belong together.
openssl pkey -in example.com.key -pubout | openssl sha256
openssl x509 -in example.com.crt -noout -pubkey | openssl sha256
openssl req -in example.com.csr -noout -pubkey | openssl sha256
SHA2-256(stdin)= 1b4738615ecd9017a0dfbb8fe1d9c7654f80c8ee25cac47d06f9f818c9879dd6
SHA2-256(stdin)= 1b4738615ecd9017a0dfbb8fe1d9c7654f80c8ee25cac47d06f9f818c9879dd6
SHA2-256(stdin)= 1b4738615ecd9017a0dfbb8fe1d9c7654f80c8ee25cac47d06f9f818c9879dd6
The older -modulus | openssl md5 trick only works for RSA; the public-key hash works for RSA, EC, Ed25519 and ML-DSA. A mismatch is what causes Nginx's key values mismatch error. The key matcher tool does the same comparison in your browser.
Convert a key to the traditional format
Some old Java, PHP and appliance software only reads BEGIN RSA PRIVATE KEY or BEGIN EC PRIVATE KEY; -traditional writes those.
openssl pkey -in example.com.key -traditional -out example.com.pkcs1.key
head -1 example.com.pkcs1.key
# and back to PKCS#8
openssl pkey -in example.com.pkcs1.key -out example.com.key
-----BEGIN RSA PRIVATE KEY-----
PKCS#1 (RSA) and SEC1 (EC) are the "traditional" formats; PKCS#8 wraps any algorithm with an OID, which is why newer tools prefer it. Ed25519 and ML-DSA keys exist only in PKCS#8. The format converter and the format conversion recipes cover DER and PFX as well.
Generate an ML-DSA key (OpenSSL 3.5+)
ML-DSA (FIPS 204) is the NIST post-quantum signature algorithm; OpenSSL 3.5 and later can generate it with genpkey.
openssl genpkey -algorithm ML-DSA-65 -out pq.key
openssl pkey -in pq.key -noout -text | head -4
ML-DSA-65 Private-Key:
seed:
e0:ea:60:31:dd:5d:33:68:6b:ca:d0:c3:41:cb:3b:
...
Parameter sets are ML-DSA-44, ML-DSA-65 and ML-DSA-87. The public key of ML-DSA-65 is 1,952 bytes and each signature is 3,309 bytes, so a self-signed ML-DSA-65 certificate made with these commands came out at 5,520 bytes of DER versus 468 bytes for P-256. Use these keys for lab work and private PKI; see post-quantum TLS for why public certificates are not there yet.
Generate an ML-KEM key (OpenSSL 3.5+)
ML-KEM (FIPS 203) is a key-encapsulation mechanism, not a signature scheme, so it never goes into a TLS server certificate.
openssl genpkey -algorithm ML-KEM-768 -out kem.key
openssl list -kem-algorithms | grep -i mlkem
{ 2.16.840.1.101.3.4.4.2, id-alg-ml-kem-768, ML-KEM-768, MLKEM768 } @ default
X25519MLKEM768 @ default
X448MLKEM1024 @ default
SecP256r1MLKEM768 @ default
In TLS, ML-KEM is used for ephemeral key exchange: since 3.5, OpenSSL's default client keyshares include the hybrid group X25519MLKEM768, and you never create those keys by hand. A standalone ML-KEM key is useful for testing HPKE-style encryption or interoperability. Check what is available with openssl list -signature-algorithms and openssl list -kem-algorithms.
FAQ
Which key type should I use for a TLS certificate?
ECDSA P-256 is the best default: small, fast and accepted by every public CA. Use RSA 2048 or 3072 only when you must support very old clients or hardware that cannot do ECDSA. Many servers run both an ECDSA and an RSA certificate at the same time.
What is the difference between genrsa and genpkey?
genrsa only makes RSA keys and is kept for compatibility. genpkey handles every algorithm with the same options. In OpenSSL 3.x both write PKCS#8 (BEGIN PRIVATE KEY) by default, so the output is interchangeable.
Can I get a public certificate for an ML-DSA key?
Not as of November 2026. The CA/Browser Forum Baseline Requirements only allow RSA and ECDSA keys in publicly trusted TLS certificates. ML-DSA keys are useful today for private PKI experiments and testing; see post-quantum TLS.