--- title: "How to Manage SSL/TLS Certificates with OpenSSL on Ubuntu" description: "Use OpenSSL on Ubuntu 24.04 to create keys and CSRs, issue self-signed and private CA certificates, convert formats, verify chains and check expiration." category: "Server Security" language: en source: https://cubepath.com/docs/server-security/ssl-tls-certificate-management-with-openssl updated: 2026-09-25 --- # How to Manage SSL/TLS Certificates with OpenSSL on Ubuntu 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. > Note: For public websites, a free, automatically renewed certificate from Let's Encrypt (for example with Certbot) is usually the better choice. Use the manual workflow in this guide for commercial certificates such as OV/EV, for internal services and for troubleshooting. ## Step 1 - Checking OpenSSL and setting up a working directory OpenSSL is installed by default on Ubuntu. Check the version: ```bash openssl version ``` ```output 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: ```bash 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: | Type | Recommended size | Notes | |------|------------------|-------| | ECDSA | P-256 curve | Small keys, fast handshakes, supported by all current clients | | RSA | 3072 bits (2048 minimum) | Maximum compatibility with very old clients and appliances | Generate an ECDSA P-256 key: ```bash openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out your_domain.key ``` If you need RSA instead, generate a 3072-bit key: ```bash openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out your_domain.key ``` Restrict the key's permissions and check it: ```bash chmod 600 your_domain.key openssl pkey -in your_domain.key -noout -text | head -n 3 ``` ```output 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: ```bash 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: ```bash openssl req -in your_domain.csr -noout -verify -subject openssl req -in your_domain.csr -noout -text | grep -A1 "Subject Alternative Name" ``` ```output 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: ```bash 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: ```bash openssl x509 -in selfsigned.crt -noout -subject -issuer -dates -ext subjectAltName ``` ```output 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: ```bash 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: ```bash 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: ```bash 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 ``` ```output 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`: ```bash sudo cp ~/ca/ca.crt /usr/local/share/ca-certificates/your-company-internal-ca.crt sudo update-ca-certificates ``` ```output 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. > Important: Anyone with `ca.key` can issue certificates your systems trust. Keep it offline or on a dedicated, well-protected machine, never on the web servers that use the certificates. ## 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: ```bash cd ~/certs openssl x509 -in your_domain.crt -noout -pubkey | openssl sha256 openssl pkey -in your_domain.key -pubout | openssl sha256 ``` ```output 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: ```bash openssl verify -CAfile ~/ca/ca.crt your_domain.crt ``` ```output 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: ```bash 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: ```bash 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: ```bash sudo install -m 644 your_domain.fullchain.crt /etc/ssl/certs/ sudo install -m 600 your_domain.key /etc/ssl/private/ ``` ```nginx 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: | Format | Extensions | Typical use | |--------|-----------|-------------| | PEM | `.pem`, `.crt`, `.key` | Nginx, Apache, HAProxy, most Linux software | | DER | `.der`, `.cer` | Binary form, some Java and Windows tools | | PKCS#12 | `.pfx`, `.p12` | Windows IIS, Java keystores, importing into browsers; key and chain in one password-protected file | Convert a PEM certificate to DER and back: ```bash 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: ```bash 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: ```bash 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 ``` > Note: A `.pfx` file created with OpenSSL 3 uses modern encryption that some older Windows or Java versions cannot read. If an import fails with a password or format error on such a system, re-export adding the `-legacy` option. ## Step 8 - Checking expiration and live servers Show the expiration date of a certificate file: ```bash openssl x509 -in your_domain.crt -noout -enddate ``` ```output 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): ```bash openssl x509 -in your_domain.crt -noout -checkend 2592000 ``` ```output 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: ```bash echo | openssl s_client -connect your_domain:443 -servername your_domain 2>/dev/null | openssl x509 -noout -subject -issuer -enddate ``` ```output 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: ```bash echo | openssl s_client -connect your_domain:443 -servername your_domain -showcerts 2>/dev/null | grep -E '^ *[0-9] s:|Verify return code' ``` ```output 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: ```bash echo | openssl s_client -connect your_domain:443 -servername your_domain -tls1_3 2>/dev/null | grep -E '^(New|Protocol)' ``` ```output 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.