OpenSSL is the standard command-line toolkit for working with TLS keys and certificates on Linux. Whether you buy a certificate from a commercial authority, run an internal CA for private services or just need to debug why a browser rejects your site, the same handful of OpenSSL commands cover the job. In this tutorial you will use OpenSSL 3 on Ubuntu 24.04 to generate a private key and a certificate signing request (CSR), create a self-signed certificate, build a small private CA, convert between formats, verify certificate chains and check expiration dates of files and live servers.

Prerequisites

To follow this tutorial you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS. The commands are the same on Debian 12 and Rocky Linux 9, which also ship OpenSSL 3.
  • A non-root user with sudo privileges.
  • A domain name, if you plan to request a certificate from a public CA. The examples use your_domain, replace it with your own.

Step 1 - Checking OpenSSL and setting up a working directory

OpenSSL is installed by default on Ubuntu. Check the version:

openssl version
OpenSSL 3.0.13 30 Jan 2024 (Library: OpenSSL 3.0.13 30 Jan 2024)

Create a working directory that only your user can read, since it will hold private keys:

mkdir -p ~/certs
chmod 700 ~/certs
cd ~/certs

All the following commands run from this directory.

Step 2 - Generating a private key

Every certificate is bound to a private key. Two key types are widely supported:

TypeRecommended sizeNotes
ECDSAP-256 curveSmall keys, fast handshakes, supported by all current clients
RSA3072 bits (2048 minimum)Maximum compatibility with very old clients and appliances

Generate an ECDSA P-256 key:

openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out your_domain.key

If you need RSA instead, generate a 3072-bit key:

openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out your_domain.key

Restrict the key's permissions and check it:

chmod 600 your_domain.key
openssl pkey -in your_domain.key -noout -text | head -n 3
Private-Key: (256 bit)
priv:
    3a:9f:0c:...

The key is stored unencrypted, which is what web servers need to start without a passphrase prompt. Protect it with file permissions and never send it to anyone, including the certificate authority.

Step 3 - Creating a certificate signing request

A CSR contains your public key and the names the certificate should cover. You send it to a CA, which returns a signed certificate. Modern browsers only check the Subject Alternative Name (SAN) extension, so always list every hostname there, including the one in the Common Name:

openssl req -new -key your_domain.key -out your_domain.csr \
  -subj "/C=US/O=Your Company/CN=your_domain" \
  -addext "subjectAltName=DNS:your_domain,DNS:www.your_domain"

Verify the CSR's signature and review its contents before you submit it:

openssl req -in your_domain.csr -noout -verify -subject
openssl req -in your_domain.csr -noout -text | grep -A1 "Subject Alternative Name"
Certificate request self-signature verify OK
subject=C = US, O = Your Company, CN = your_domain
            X509v3 Subject Alternative Name:
                DNS:your_domain, DNS:www.your_domain

Paste the content of your_domain.csr into your CA's order form. The CA sends back your certificate and usually one or more intermediate certificates.

Step 4 - Creating a self-signed certificate

For a test environment or a service reached only by you, a self-signed certificate is enough. Clients will show a warning because no trusted CA signed it. Create a new key and a certificate valid for one year in a single command:

openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -noenc \
  -keyout selfsigned.key -out selfsigned.crt -days 365 \
  -subj "/CN=your_domain" \
  -addext "subjectAltName=DNS:your_domain"

-noenc stores the key without a passphrase. Inspect the result:

openssl x509 -in selfsigned.crt -noout -subject -issuer -dates -ext subjectAltName
subject=CN = your_domain
issuer=CN = your_domain
notBefore=Sep 24 10:05:12 2026 GMT
notAfter=Sep 24 10:05:12 2027 GMT
X509v3 Subject Alternative Name:
    DNS:your_domain

Subject and issuer are identical, which is what makes a certificate self-signed.

Step 5 - Running a small private CA for internal services

For internal services such as admin panels, databases or a private registry, a private CA is better than scattered self-signed certificates: you trust the CA once on your machines, and every certificate it signs is accepted.

Create a separate directory for the CA and generate its key:

mkdir -p ~/ca && chmod 700 ~/ca && cd ~/ca
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out ca.key
chmod 600 ca.key

Create the CA certificate, valid for 10 years. The basicConstraints and keyUsage extensions mark it as a CA that may sign certificates:

openssl req -x509 -new -key ca.key -sha256 -days 3650 -out ca.crt \
  -subj "/O=Your Company/CN=Your Company Internal CA" \
  -addext "basicConstraints=critical,CA:TRUE" \
  -addext "keyUsage=critical,keyCertSign,cRLSign"

Sign the CSR from Step 3 with the CA. -copy_extensions copyall keeps the SAN list from the CSR. A validity of 397 days mirrors the limit for public certificates and keeps renewal a routine task:

openssl x509 -req -in ~/certs/your_domain.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out ~/certs/your_domain.crt -days 397 -sha256 -copy_extensions copyall
Certificate request self-signature ok
subject=C = US, O = Your Company, CN = your_domain

To make your servers trust the CA, copy its certificate into the system store. The file name must end in .crt:

sudo cp ~/ca/ca.crt /usr/local/share/ca-certificates/your-company-internal-ca.crt
sudo update-ca-certificates
Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.

Import ca.crt into the browsers and operating systems of the people who use the internal services as well.

Step 6 - Verifying a certificate, its key and its chain

Most TLS problems come from a certificate that does not match its key or a missing intermediate certificate. Check both before installing anything.

Checking that the certificate matches the private key

Compare a hash of the public key inside the certificate with one derived from the private key. This works for both RSA and ECDSA:

cd ~/certs
openssl x509 -in your_domain.crt -noout -pubkey | openssl sha256
openssl pkey -in your_domain.key -pubout | openssl sha256
SHA2-256(stdin)= 5c1e0f9a4b7d...
SHA2-256(stdin)= 5c1e0f9a4b7d...

The two hashes must be identical. If they differ, the certificate was issued for another key.

Verifying the chain

Verify a certificate signed by your private CA:

openssl verify -CAfile ~/ca/ca.crt your_domain.crt
your_domain.crt: OK

For a certificate from a public CA, pass the intermediate certificate as untrusted input. The root is taken from the system trust store:

openssl verify -untrusted intermediate.crt your_domain.crt

If you see unable to get local issuer certificate, the intermediate is missing or is the wrong one.

Building the full chain file

Web servers such as Nginx expect the server certificate followed by the intermediates in a single file. The order matters, server certificate first:

cat your_domain.crt intermediate.crt > your_domain.fullchain.crt

Install the files in a root-owned location and reference them from your web server. For Nginx:

sudo install -m 644 your_domain.fullchain.crt /etc/ssl/certs/
sudo install -m 600 your_domain.key /etc/ssl/private/
ssl_certificate     /etc/ssl/certs/your_domain.fullchain.crt;
ssl_certificate_key /etc/ssl/private/your_domain.key;

Test and reload with sudo nginx -t and sudo systemctl reload nginx.

Step 7 - Converting between certificate formats

OpenSSL on Linux uses PEM files (Base64 text between -----BEGIN ...----- lines). Other platforms expect different formats:

FormatExtensionsTypical use
PEM.pem, .crt, .keyNginx, Apache, HAProxy, most Linux software
DER.der, .cerBinary form, some Java and Windows tools
PKCS#12.pfx, .p12Windows IIS, Java keystores, importing into browsers; key and chain in one password-protected file

Convert a PEM certificate to DER and back:

openssl x509 -in your_domain.crt -outform der -out your_domain.der
openssl x509 -in your_domain.der -inform der -out your_domain.pem

Bundle the key, certificate and intermediate into a PKCS#12 file. OpenSSL asks for an export password:

openssl pkcs12 -export -out your_domain.pfx -inkey your_domain.key -in your_domain.crt -certfile intermediate.crt

Extract the certificate and key from a PKCS#12 file into PEM files:

openssl pkcs12 -in your_domain.pfx -clcerts -nokeys -out extracted.crt
openssl pkcs12 -in your_domain.pfx -nocerts -noenc -out extracted.key
chmod 600 extracted.key

Step 8 - Checking expiration and live servers

Show the expiration date of a certificate file:

openssl x509 -in your_domain.crt -noout -enddate
notAfter=Oct 26 10:12:40 2027 GMT

-checkend tells you whether the certificate expires within a number of seconds, and sets the exit code accordingly, which is useful in monitoring scripts. Check the next 30 days (2,592,000 seconds):

openssl x509 -in your_domain.crt -noout -checkend 2592000
Certificate will not expire

The command exits with 0 when the certificate is still valid after that period and 1 when it expires sooner.

To check what a live server actually sends, connect with s_client. -servername sets the SNI hostname, which servers hosting several sites need to pick the right certificate:

echo | openssl s_client -connect your_domain:443 -servername your_domain 2>/dev/null | openssl x509 -noout -subject -issuer -enddate
subject=CN = your_domain
issuer=C = US, O = Let's Encrypt, CN = R12
notAfter=Dec 23 08:14:02 2026 GMT

To see the whole chain the server presents and whether it validates, look at the start and end of the full output:

echo | openssl s_client -connect your_domain:443 -servername your_domain -showcerts 2>/dev/null | grep -E '^ *[0-9] s:|Verify return code'
 0 s:CN = your_domain
 1 s:C = US, O = Let's Encrypt, CN = R12
Verify return code: 0 (ok)

A server that sends only certificate 0 and returns unable to verify the first certificate is missing its intermediate. Point the web server at the full chain file from Step 6.

You can also confirm which protocol versions a server accepts by forcing one:

echo | openssl s_client -connect your_domain:443 -servername your_domain -tls1_3 2>/dev/null | grep -E '^(New|Protocol)'
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384

Repeat with -tls1_2. If the server does not accept the forced version, the handshake fails and no New line is printed.

Troubleshooting

key values mismatch when Nginx starts. The certificate and key do not belong together, or the full chain file starts with the intermediate instead of your certificate. Compare the public key hashes as in Step 6 and check the order of the chain file.

Browsers report an untrusted certificate but openssl verify works locally. The server does not send the intermediate certificate. Use the full chain file and confirm with s_client -showcerts.

Permission denied reading the key. Web servers read keys as root at startup. Keep keys at mode 600 owned by root, and do not relax permissions to fix this.

Chrome shows NET::ERR_CERT_COMMON_NAME_INVALID. The hostname is missing from the SAN extension. Check with openssl x509 -in your_domain.crt -noout -ext subjectAltName and reissue the certificate with the correct names.

Conclusion

You generated keys and CSRs, created self-signed and private CA certificates, verified keys and chains, converted between formats and checked expiration on files and live servers. These commands are enough to handle most certificate tasks and to diagnose TLS errors quickly. As next steps, automate public certificates with Certbot and Let's Encrypt, add a -checkend test to your monitoring for any certificates that are renewed by hand, and harden your web server's TLS configuration to allow only TLS 1.2 and 1.3.