--- title: "How to Install a Commercial SSL Certificate on Nginx" description: "Generate a private key and CSR with OpenSSL, install a commercial or self-signed SSL certificate and its chain on Nginx or Apache in Ubuntu 24.04." category: "Web Servers" language: en source: https://cubepath.com/docs/web-servers/manual-ssl-tls-configuration-with-own-certificates updated: 2026-09-25 --- # How to Install a Commercial SSL Certificate on Nginx Let's Encrypt covers most websites, but sometimes you need to install a certificate yourself: an OV or EV certificate bought from a commercial certificate authority (CA), a certificate issued by your company's internal CA, or a self-signed certificate for a lab or internal service. The process is always the same: generate a private key, create a certificate signing request (CSR), receive the signed certificate, and install it together with its intermediate chain. In this tutorial you will do all of that with OpenSSL on Ubuntu 24.04 and configure Nginx, with the Apache equivalent, to serve the certificate. ## Prerequisites To follow this tutorial you need: - A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with a non-root user that has `sudo` privileges. - Nginx (or Apache) installed and serving your site over HTTP. - A domain name with DNS records pointing to the server. This guide uses `your_domain` as a placeholder. - Access to your certificate provider's control panel to submit the CSR and complete domain validation, if you're buying a certificate. - Port 443 open in the firewall: `sudo ufw allow 'Nginx Full'` or `sudo ufw allow 'Apache Full'`. 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) ``` ## Step 1 - Generating the private key and CSR The private key never leaves your server; the CSR contains the public key and the names you want in the certificate, and it is what you send to the CA. Create a directory for the certificate files and move into it. Certificates are public, so the directory can be world-readable; only the key file will be restricted: ```bash sudo mkdir -p /etc/ssl/your_domain cd /etc/ssl/your_domain ``` Generate a 2048-bit RSA key and the CSR in a single command. Browsers ignore the Common Name and validate only the Subject Alternative Name (SAN) extension, so list every hostname in `subjectAltName`: ```bash sudo openssl req -new -newkey rsa:2048 -nodes \ -keyout your_domain.key \ -out your_domain.csr \ -subj "/C=ES/O=Your Company/CN=your_domain" \ -addext "subjectAltName=DNS:your_domain,DNS:www.your_domain" ``` - `-nodes` leaves the key unencrypted so Nginx can start without asking for a passphrase. The file permissions protect it instead. - `-subj` fills in the subject non-interactively. For DV certificates only the CN matters; for OV/EV certificates use your legal organization name exactly as the CA will validate it. - For a wildcard certificate, use `CN=*.your_domain` and `subjectAltName=DNS:*.your_domain,DNS:your_domain`. If your CA accepts ECDSA certificates, you can use a smaller and faster key with `-newkey ec -pkeyopt ec_paramgen_curve:prime256v1` instead of `-newkey rsa:2048`. Lock down the private key: ```bash sudo chmod 600 your_domain.key ``` Check the CSR before you submit it: ```bash sudo openssl req -in your_domain.csr -noout -subject -verify sudo openssl req -in your_domain.csr -noout -text | grep -A1 "Subject Alternative Name" ``` ```output Certificate request self-signature verify OK subject=C = ES, O = Your Company, CN = your_domain X509v3 Subject Alternative Name: DNS:your_domain, DNS:www.your_domain ``` Print the CSR and paste the whole block, including the `BEGIN` and `END` lines, into your CA's order form: ```bash sudo cat your_domain.csr ``` Complete the validation the CA asks for (an email to an admin address, a DNS TXT record or a file under `/.well-known/pki-validation/`). The CA then sends you the signed certificate. > Note: Publicly trusted TLS certificates now have a maximum validity of 200 days, and that limit keeps shrinking in the coming years. A "multi-year" purchase means the CA will reissue the certificate during the subscription, so you'll repeat Steps 2 to 4 more often than you may expect. ## Step 2 - Building the certificate chain The CA normally delivers your server certificate plus one or more intermediate certificates (often called a "CA bundle"). Browsers need the intermediates to build a path to a trusted root, so the server must send them. Copy the files to the server, for example with `scp`, and place them in `/etc/ssl/your_domain/`: - `your_domain.crt`: your server certificate. - `ca_bundle.crt`: the intermediate certificate or certificates. Confirm what each file contains: ```bash sudo openssl x509 -in your_domain.crt -noout -subject -issuer -enddate sudo openssl x509 -in ca_bundle.crt -noout -subject -issuer ``` ```output subject=CN = your_domain issuer=C = US, O = Example CA, CN = Example CA RSA Intermediate notAfter=Apr 12 23:59:59 2027 GMT subject=C = US, O = Example CA, CN = Example CA RSA Intermediate issuer=C = US, O = Example CA, CN = Example CA Root ``` The issuer of your certificate must match the subject of the first intermediate. Verify that the chain validates against Ubuntu's trusted root store: ```bash sudo openssl verify -untrusted ca_bundle.crt your_domain.crt ``` ```output your_domain.crt: OK ``` Nginx expects the server certificate followed by the intermediates in one file. Order matters: your certificate first, then the intermediates. ```bash sudo sh -c 'cat your_domain.crt ca_bundle.crt > fullchain.crt' ``` If the CA's files don't end with a newline, the `END CERTIFICATE` and `BEGIN CERTIFICATE` lines may end up on the same line. Check with `sudo grep -c "BEGIN CERTIFICATE" fullchain.crt`: the number must equal the certificates you concatenated. ## Step 3 - Checking that the key matches the certificate A certificate only works with the private key its CSR was created from. Compare the public key in both files; the two hashes must be identical: ```bash sudo openssl x509 -in your_domain.crt -noout -pubkey | openssl sha256 sudo openssl pkey -in your_domain.key -pubout | openssl sha256 ``` ```output SHA2-256(stdin)= 3c1f9e0b7a... SHA2-256(stdin)= 3c1f9e0b7a... ``` This comparison works for both RSA and ECDSA keys. If the hashes differ, you are using a key from a different CSR, and Nginx will refuse to start with a `key values mismatch` error. ## Step 4 - Configuring Nginx Open your site's server block: ```bash sudo nano /etc/nginx/sites-available/your_domain ``` Replace its contents with an HTTP server that redirects to HTTPS and an HTTPS server that uses the new files. Adjust `root` to your site's document root: ```nginx server { listen 80; listen [::]:80; server_name your_domain www.your_domain; return 301 https://$host$request_uri; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name your_domain www.your_domain; ssl_certificate /etc/ssl/your_domain/fullchain.crt; ssl_certificate_key /etc/ssl/your_domain/your_domain.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; root /var/www/your_domain; index index.html; location / { try_files $uri $uri/ =404; } } ``` TLS 1.2 and 1.3 with OpenSSL's default cipher list is a secure baseline for modern clients. The `listen ... http2` syntax is the one supported by Nginx 1.24, the version in Ubuntu 24.04. Enable the site if it isn't already, test the configuration and reload: ```bash sudo ln -s /etc/nginx/sites-available/your_domain /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx ``` ```output nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful ``` If the symlink already exists, `ln` reports `File exists`, which is fine. ### Configuring Apache instead On Apache, enable the SSL module: ```bash sudo a2enmod ssl ``` Create a virtual host for HTTPS: ```bash sudo nano /etc/apache2/sites-available/your_domain-ssl.conf ``` ```apache ServerName your_domain ServerAlias www.your_domain DocumentRoot /var/www/your_domain SSLEngine on SSLCertificateFile /etc/ssl/your_domain/fullchain.crt SSLCertificateKeyFile /etc/ssl/your_domain/your_domain.key SSLProtocol -all +TLSv1.2 +TLSv1.3 ``` Since Apache 2.4.8, `SSLCertificateFile` can contain the certificate followed by its intermediates, so the same `fullchain.crt` works and the old `SSLCertificateChainFile` directive is no longer needed. In your port 80 virtual host, add `Redirect permanent / https://your_domain/` to send visitors to HTTPS. Then enable the site and reload: ```bash sudo a2ensite your_domain-ssl sudo apache2ctl configtest sudo systemctl reload apache2 ``` ## Step 5 - Verifying the installation Check what the server actually sends, including the chain. The `-servername` option sets SNI, which is required when several sites share one IP address: ```bash echo | openssl s_client -connect your_domain:443 -servername your_domain 2>/dev/null | grep -E "^ *[0-9] s:|Verify return code" ``` ```output 0 s:CN = your_domain 1 s:C = US, O = Example CA, CN = Example CA RSA Intermediate Verify return code: 0 (ok) ``` Depth `0` is your certificate and depth `1` the intermediate. `Verify return code: 0 (ok)` means a client with the standard root store trusts the chain. A code of `20` or `21` (`unable to get local issuer certificate`) means the intermediates are missing from `fullchain.crt`. Confirm the dates and names: ```bash echo | openssl s_client -connect your_domain:443 -servername your_domain 2>/dev/null | openssl x509 -noout -subject -enddate -ext subjectAltName ``` For a complete external check of the chain, protocols and ciphers, run your domain through the SSL Labs Server Test. ## Creating a self-signed certificate for internal use For a service that only you or your team access (a lab, an admin panel behind a VPN), a self-signed certificate encrypts traffic without buying anything. Browsers will show a warning because no trusted CA signed it, so never use one on a public website. Generate the key and certificate in one command: ```bash sudo openssl req -x509 -newkey rsa:2048 -nodes -days 365 \ -keyout /etc/ssl/your_domain/selfsigned.key \ -out /etc/ssl/your_domain/selfsigned.crt \ -subj "/CN=internal.your_domain" \ -addext "subjectAltName=DNS:internal.your_domain,IP:your_server_ip" sudo chmod 600 /etc/ssl/your_domain/selfsigned.key ``` Including the IP address in the SAN lets clients connect by IP without a name mismatch. Point `ssl_certificate` and `ssl_certificate_key` to these two files; there's no chain to build. To avoid browser warnings, import `selfsigned.crt` into the trust store of the machines that access the service. Copy the file to the client and, on an Ubuntu client, run: ```bash sudo cp selfsigned.crt /usr/local/share/ca-certificates/internal-your_domain.crt sudo update-ca-certificates ``` ## Tracking expiry Nothing renews a manually installed certificate for you. Check how many days remain: ```bash sudo openssl x509 -in /etc/ssl/your_domain/your_domain.crt -noout -enddate ``` `-checkend` returns a non-zero exit code if the certificate expires within the given number of seconds, which makes it easy to use from a monitoring system: ```bash sudo openssl x509 -in /etc/ssl/your_domain/your_domain.crt -noout -checkend 2592000 ``` ```output Certificate will not expire ``` `2592000` seconds is 30 days. When a renewal is due, generate a new key and CSR (Step 1), install the new files, run Steps 3 and 4 again and reload the web server. Keep the old files until the new certificate is verified. ## Troubleshooting **`SSL_CTX_use_PrivateKey ... key values mismatch` when testing Nginx.** The key doesn't belong to the certificate, or the files in `fullchain.crt` are in the wrong order. Run the hash comparison from Step 3 and make sure your server certificate is the first block in `fullchain.crt`. **Works in the browser, fails in `curl` or mobile apps.** Desktop browsers sometimes fetch missing intermediates on their own; other clients don't. Check the chain with the `s_client` command in Step 5 and add the intermediates. **`Permission denied` reading the key.** Nginx reads keys as root before dropping privileges, so `root:root` with mode `600` is correct. Check that the path in the configuration is right with `sudo ls -l /etc/ssl/your_domain/`. **`PEM_read_bio_X509_AUX ... no start line`.** The file isn't in PEM format. If the CA sent a DER file (`.cer` or `.der`), convert it with `openssl x509 -inform der -in your_domain.cer -out your_domain.crt`. ## Conclusion You generated a private key and CSR with OpenSSL, built the certificate chain, verified that the key matches, and configured Nginx or Apache to serve the certificate over TLS 1.2 and 1.3. Keep a note of the expiry date, because manual certificates are renewed by hand. As next steps, add expiry checks to your monitoring, add a `Strict-Transport-Security` header once the whole site works over HTTPS, and consider Let's Encrypt with Certbot for sites that don't require a commercial certificate.