OpenSSL, CSRs and Self-Signed Certificates: What Each Piece Is For

OpenSSL, CSRs and Self-Signed Certificates: What Each Piece Is For

Three things get called “the certificate” and they are not the same object. Most TLS confusion starts there.

The three pieces

The private key is a secret you generate. It never leaves the machine, it is never sent anywhere, and anyone who has it can impersonate you.

The CSR, or certificate signing request, contains your public key plus identity details, signed with your private key. That signature proves you hold the matching private key without revealing it.

The certificate is what comes back. It contains your public key and identity, signed by a certificate authority. The signature is what makes other people willing to believe it.

The private key never travels. Only the CSR goes out, and only the certificate comes back.

Generating a key

# ECDSA, the sensible default for anything new
openssl ecparam -genkey -name prime256v1 -out server.key

# RSA, when you need compatibility with older clients
openssl genrsa -out server.key 2048

chmod 600 server.key

ECDSA P-256 is the right choice for new deployments. It gives security comparable to RSA 3072 with faster handshakes and smaller certificates. RSA 2048 remains fine, and is what you pick when something ancient has to connect.

The chmod 600 is not optional. A private key readable by other users on the machine is not private, and many TLS servers will refuse to start if the permissions are loose.

Creating a CSR, with SANs

This is where most hand-made certificates go wrong.

cat > csr.cnf <<'EOF'
[req]
distinguished_name = dn
req_extensions     = ext
prompt             = no

[dn]
CN = example.internal
O  = Example Ltd
C  = GB

[ext]
subjectAltName = @alt

[alt]
DNS.1 = example.internal
DNS.2 = www.example.internal
IP.1  = 10.0.0.10
EOF

openssl req -new -key server.key -out server.csr -config csr.cnf

Common Name is dead. Browsers stopped reading it years ago and rely entirely on the Subject Alternative Name extension. A certificate whose CN matches perfectly but which has no SAN entry will be rejected, and the error message rarely says so clearly.

List every name the service will be reached by. A certificate for example.internal does not cover www.example.internal, and one covering a hostname does not cover the IP address unless you add it.

Check before you send it:

openssl req -in server.csr -noout -text | grep -A3 'Alternative Name'

Self-signing, and when that is fine

openssl x509 -req -in server.csr -signkey server.key \
  -out server.crt -days 365 \
  -extfile csr.cnf -extensions ext

Note -extfile and -extensions. Without them the SANs from your CSR are silently dropped and you get a certificate that fails for reasons you already thought you had handled. This is the most common self-signed certificate mistake.

A self-signed certificate provides exactly the same encryption as one from a public CA. What it does not provide is third-party identity verification, which is a different property.

That makes it appropriate when you control both ends: internal services, development, machine-to-machine links inside your own network. It is inappropriate for anything the public reaches, and clicking through a browser warning trains exactly the habit you do not want.

For anything public, use Let’s Encrypt. It is free, automated and trusted everywhere.

A small internal CA, which is usually better

Self-signing every service means adding an exception per service. Running one CA and signing with it means one trust decision total.

# the CA, kept somewhere safe
openssl ecparam -genkey -name prime256v1 -out ca.key
openssl req -x509 -new -key ca.key -sha256 -days 3650 \
  -out ca.crt -subj "/CN=Example Internal CA/O=Example Ltd"

# sign a service CSR with it
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -out server.crt -days 365 -sha256 \
  -extfile csr.cnf -extensions ext

Distribute ca.crt once to every machine that needs to trust your services:

# Debian and Ubuntu
sudo cp ca.crt /usr/local/share/ca-certificates/example-ca.crt
sudo update-ca-certificates

# Fedora and RHEL
sudo cp ca.crt /etc/pki/ca-trust/source/anchors/example-ca.crt
sudo update-ca-trust

Firefox keeps its own trust store and ignores the system one, so it needs the CA added through its own settings. That catches people out constantly.

The CA private key is now the most valuable secret you own. Anyone with it can issue a certificate for any name your machines will trust. Keep it off the servers that use it, and treat it like the material covered in our secrets management guide.

Inspecting and verifying

# what is in a certificate
openssl x509 -in server.crt -noout -text

# the short version
openssl x509 -in server.crt -noout -subject -issuer -dates -ext subjectAltName

# does the key match the certificate
openssl x509 -noout -pubkey -in server.crt | openssl sha256
openssl pkey -pubout -in server.key | openssl sha256

# what a live server is actually serving
openssl s_client -connect example.com:443 -servername example.com </dev/null

The two sha256 commands must produce the same hash. If they do not, you have paired the wrong key with the certificate, which produces a server that refuses to start or a handshake that fails in a confusing way.

-servername matters: without it, OpenSSL omits SNI and a virtual-hosted server will hand you the wrong certificate.

The chain problem

A certificate is usually signed by an intermediate, which is signed by a root. Clients trust roots, so the server must supply the intermediates to connect the two.

# server certificate first, then intermediates, no root needed
cat server.crt intermediate.crt > fullchain.crt

Order matters and the root is unnecessary, since the client already has it.

Get this wrong and you get the classic symptom: works in one browser, fails in another, fails in curl. Browsers frequently cache intermediates from other sites and paper over the gap. curl does not, which makes it the better test.

curl -v https://example.internal 2>&1 | grep -i 'certificate\|verify'
openssl s_client -connect example.internal:443 -showcerts </dev/null

Practical notes

Certificates expire, and the expiry will surprise you. Monitor it rather than remembering it. Our Prometheus alerting guide covers turning that into an alert that fires weeks ahead rather than an outage.

Short lifetimes are better. Public CAs have been shortening maximum validity for years, and the reason is that a compromised key with a three-month certificate is a much smaller problem than one with a three-year certificate. Automate renewal and the length stops mattering to you.

A CSR is disposable. Once you have the certificate, the CSR has no further use. The key and the certificate are what you keep.

Our OpenSSL CSR builder assembles the config and commands above if you would rather fill in fields than write the .cnf by hand.

Frequently Asked Questions

What is the difference between a private key, a CSR and a certificate?

The private key is the secret you generate and never share. The CSR is a request containing your public key and identity details, signed by your private key to prove you hold it. The certificate is what a CA returns: your public key and identity, signed by the CA so others will trust it.

Do I still need to set the Common Name in a certificate?

No. Every current browser and most modern clients ignore Common Name entirely and read only the Subject Alternative Name extension. A certificate without SANs will be rejected regardless of what the CN says, which is the single most common reason a hand-made certificate fails.

When is a self-signed certificate acceptable?

When you control both ends of the connection and can distribute trust yourself. Internal services, development, and machine-to-machine links within your own infrastructure are all reasonable. Anything a browser or a third party must trust needs a certificate from a CA they already trust.

Why does my certificate work in curl but not in a browser?

Usually a missing intermediate certificate. Browsers often cache intermediates from previous sites and fill the gap silently, while curl does not. If the server does not send the full chain, it will work inconsistently depending on what the client already knows.

Should I use RSA or ECDSA for a new key?

ECDSA with the P-256 curve for almost everything new. It gives equivalent security to a much larger RSA key with faster handshakes and smaller certificates. RSA 2048 remains the compatibility choice for old clients or hardware that predates widespread ECDSA support.

How do I make my browser trust my own internal CA?

Import the CA certificate, not the server certificate, into the trust store. On Linux that usually means copying it into the system anchors directory and running update-ca-certificates or update-ca-trust. Firefox keeps its own store and needs the CA added separately.