KB
SSL & Certificates

SSL Certificate Generation and Installation for Apache and Nginx

6 min read1186 words24 code blocks

At a glance#

  • Purpose: Generate a CSR, install a commercial certificate, and set up automatic Let's Encrypt renewal on Apache and Nginx.
  • Applies to: Apache 2.4 and Nginx 1.18+ on Ubuntu and RHEL-family systems.
  • Risk: Medium — a bad certificate or config error takes HTTPS down until corrected.
  • Time: 30 minutes for Let's Encrypt; 1–2 days for a commercial certificate including issuance.

Overview#

There are two routes, and choosing correctly saves a lot of work:

Let's EncryptCommercial CA
CostFreePaid
Validity90 days1 year
RenewalAutomaticManual
ValidationDomain onlyDomain, organisation or extended
Requires public reachabilityYes (HTTP-01)No
WildcardsYes (DNS-01 only)Yes

Use Let's Encrypt for anything publicly reachable. Use a commercial certificate where the organisation must be named in the certificate, or where the host is not reachable from the internet.

Part 1 — Commercial certificate: generating the CSR#

1. Generate a private key and CSR#

bash
sudo mkdir -p /etc/ssl/example.com
cd /etc/ssl/example.com

sudo openssl req -new -newkey rsa:2048 -nodes \
  -keyout example.com.key \
  -out example.com.csr

Answer the prompts:

PromptValue
Country NameNP
State or ProvinceBagmati
LocalityKathmandu
Organization NameExact registered company name
Organizational UnitIT
Common NameThe exact FQDN, e.g. www.example.com
Email AddressLeave blank
Challenge passwordLeave blank
Warning: Leave the challenge password empty. A passphrase on the key means the web server cannot start unattended — it will prompt for the passphrase on every boot.
Note: Common Name must match exactly how users reach the site. example.com and www.example.com are different names. Most CAs now include both automatically, but confirm before submitting.

2. Protect the private key#

bash
sudo chmod 600 /etc/ssl/example.com/example.com.key
sudo chown root:root /etc/ssl/example.com/example.com.key
Warning: The private key never leaves this server and is never emailed. If it is exposed, the certificate must be revoked and reissued.

3. Verify the CSR before submitting#

bash
openssl req -in example.com.csr -noout -text | head -20

Confirm the Common Name and organisation are correct. A typo here means paying for a reissue.

4. Submit to the CA#

Paste the contents of example.com.csr into the CA's order form:

bash
cat /etc/ssl/example.com/example.com.csr

Complete domain validation — usually an email to admin@example.com or a DNS TXT record.

5. Install the issued certificate#

The CA returns your certificate and an intermediate bundle. Place both:

bash
sudo cp example_com.crt /etc/ssl/example.com/example.com.crt
sudo cp example_com.ca-bundle /etc/ssl/example.com/example.com.ca-bundle
sudo chmod 644 /etc/ssl/example.com/*.crt /etc/ssl/example.com/*.ca-bundle

Confirm the certificate matches the key before restarting anything:

bash
openssl x509 -noout -modulus -in example.com.crt | openssl md5
openssl rsa  -noout -modulus -in example.com.key | openssl md5

Both hashes must be identical.

Part 2 — Installing on Nginx#

Nginx needs the certificate and intermediates in one file, certificate first:

bash
cd /etc/ssl/example.com
sudo bash -c 'cat example.com.crt example.com.ca-bundle > example.com.fullchain.crt'

Edit the site configuration:

bash
sudo nano /etc/nginx/sites-available/example.com
nginx
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    ssl_certificate     /etc/ssl/example.com/example.com.fullchain.crt;
    ssl_certificate_key /etc/ssl/example.com/example.com.key;

    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_stapling        on;
    ssl_stapling_verify on;

    add_header Strict-Transport-Security "max-age=31536000" always;

    root /var/www/example.com;
    index index.html;
}

Test and reload:

bash
sudo nginx -t
sudo systemctl reload nginx
Warning: Always run nginx -t first. A reload with a broken config leaves the previous configuration running, but a restart with a broken config leaves the site down.

Part 3 — Installing on Apache#

Apache takes the certificate and chain as separate directives:

bash
sudo nano /etc/apache2/sites-available/example.com-ssl.conf     # Debian/Ubuntu
# or /etc/httpd/conf.d/example.com-ssl.conf                     # RHEL
apache
<VirtualHost *:80>
    ServerName example.com
    ServerAlias www.example.com
    Redirect permanent / https://example.com/
</VirtualHost>

<VirtualHost *:443>
    ServerName example.com
    ServerAlias www.example.com
    DocumentRoot /var/www/example.com

    SSLEngine on
    SSLCertificateFile      /etc/ssl/example.com/example.com.crt
    SSLCertificateKeyFile   /etc/ssl/example.com/example.com.key
    SSLCertificateChainFile /etc/ssl/example.com/example.com.ca-bundle

    SSLProtocol             -all +TLSv1.2 +TLSv1.3
    SSLCipherSuite          ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
    SSLHonorCipherOrder     off

    Header always set Strict-Transport-Security "max-age=31536000"
</VirtualHost>

Enable the required modules and site:

bash
sudo a2enmod ssl headers
sudo a2ensite example.com-ssl
sudo apache2ctl configtest
sudo systemctl reload apache2

On RHEL:

bash
sudo httpd -t
sudo systemctl reload httpd

Part 4 — Let's Encrypt with certbot#

1. Install certbot#

bash
# Ubuntu / Debian
sudo apt update
sudo apt install certbot python3-certbot-nginx python3-certbot-apache -y

# RHEL / Rocky / AlmaLinux
sudo dnf install epel-release -y
sudo dnf install certbot python3-certbot-nginx python3-certbot-apache -y

2. Obtain and install the certificate#

Certbot edits the web server config for you:

bash
# Nginx
sudo certbot --nginx -d example.com -d www.example.com

# Apache
sudo certbot --apache -d example.com -d www.example.com

To obtain a certificate without touching the config:

bash
sudo certbot certonly --webroot -w /var/www/example.com -d example.com -d www.example.com

Certificates are written to /etc/letsencrypt/live/example.com/.

Note: HTTP-01 validation requires port 80 open to the internet. For internal hosts or wildcards, use DNS-01 instead: ``bash sudo certbot certonly --manual --preferred-challenges dns -d "*.example.com" ``

3. Confirm automatic renewal#

The packages install a systemd timer. Verify it:

bash
systemctl list-timers | grep certbot
sudo certbot renew --dry-run

A successful dry run is the only proof renewal will work. Certificates last 90 days — an unverified renewal is a scheduled outage.

Add a reload hook so the web server picks up the new certificate:

bash
sudo nano /etc/letsencrypt/renewal-hooks/deploy/reload-web.sh
bash
#!/bin/bash
systemctl reload nginx 2>/dev/null || systemctl reload apache2 2>/dev/null || systemctl reload httpd
bash
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-web.sh

Verification#

Check the served certificate and chain:

bash
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null | \
  openssl x509 -noout -subject -issuer -dates

Confirm the chain validates:

bash
echo | openssl s_client -connect example.com:443 -servername example.com 2>&1 | grep "Verify return code"

Verify return code: 0 (ok) is the target. Anything else usually means a missing intermediate.

Confirm the HTTP redirect and protocol support:

bash
curl -sI http://example.com | head -1          # expect 301
curl -sI --tlsv1.2 https://example.com | head -1

Set a calendar reminder 30 days before expiry for commercial certificates.

Troubleshooting#

SymptomCause and fix
SSL_ERROR_BAD_CERT_DOMAINCertificate CN/SAN does not cover the hostname used. Reissue with correct names.
Browser warns of incomplete chainIntermediates missing. Nginx needs them concatenated; Apache needs SSLCertificateChainFile.
key values mismatch on restartCertificate and key are from different CSRs. Compare moduli.
Nginx PEM_read_bio_X509 errorWrong file order in the fullchain, or a stray blank line. Certificate must come first.
Apache prompts for a passphrase at bootKey is encrypted. Remove it: openssl rsa -in enc.key -out plain.key
certbot: "Could not bind to port 80"Web server already using it. Use --nginx/--apache plugins or --webroot rather than --standalone.
certbot: "Timeout during connect"Port 80 not reachable from the internet. Check the firewall, or switch to DNS-01.
Renewal succeeded but old certificate still servedWeb server was not reloaded. Add the deploy hook above.
Rate limit from Let's EncryptFive failures per hour per account. Use --dry-run while testing.