Introduction: HTTPS Is No Longer Optional

As of 2026, HTTPS is an essential part of any website. Every major browser (Google Chrome, Firefox, Safari, etc.) displays a "Not Secure" warning for HTTP sites, and Google rewards HTTPS sites with higher search rankings. Furthermore, modern web technologies such as HTTP/2, HTTP/3, and Service Workers only work over HTTPS.

In the past, SSL certificates used to cost hundreds of dollars per year, but Let's Encrypt completely changed the landscape. Founded in 2015, Let's Encrypt is a non-profit Certificate Authority (CA) that allows anyone to get an SSL certificate completely free of charge. As of 2026, it is the most widely used free SSL certificate in the world, with millions of certificates issued every day.

This guide covers the entire process of applying HTTPS to a real service using Let's Encrypt. From installing Certbot to configuring Nginx/Apache, wildcard certificates, auto-renewal, and troubleshooting common issues — everything is explained in a way you can apply directly in production.

1. Let's Encrypt and SSL Certificate Basics

1.1 Key Features of Let's Encrypt

  • Completely free: Issuance, renewal, and reissuance are all free
  • Automation: The ACME protocol enables automated issuance and renewal
  • 90-day validity: Shorter than commercial certificates (1-2 years), but handled by auto-renewal
  • Domain Validation (DV) only: Organization Validation (OV) and Extended Validation (EV) are not supported
  • Wildcard supported: Certificates in the form of *.example.com can be issued
  • Trusted by all major browsers: Chrome, Firefox, Safari, and Edge all trust it automatically

1.2 Understanding Validation Methods

Let's Encrypt issues certificates using challenge mechanisms to verify domain ownership:

Challenge Type Validation Method Use Case
HTTP-01 Validate by placing a file in a specific path on the web server Most common, single domain
DNS-01 Validate via a DNS TXT record Required for wildcard certificates, can be automated
TLS-ALPN-01 Validate during the TLS handshake Special environments (e.g., port 80 blocked)
Which method should you choose?
- Single domain (example.com, www.example.com): HTTP-01 (simplest)
- Wildcard (*.example.com): DNS-01 required
- Internal servers, port 80 blocked: DNS-01

1.3 Prerequisites Before Issuance

  • Domain ownership: You must have a domain you actually control
  • DNS configuration: The domain must properly resolve to your server's IP address
  • Ports 80/443 open: For the HTTP-01 method, port 80 must be reachable from the outside
  • Root privileges: Required to install certificates and modify web server configuration

2. Installing Certbot

Certbot is the official Let's Encrypt client developed by the EFF (Electronic Frontier Foundation). It automates certificate issuance and management.

2.1 Ubuntu/Debian Installation

# snap method (officially recommended)
sudo snap install core
sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

# Or apt method (simple)
sudo apt update
sudo apt install certbot -y
sudo apt install python3-certbot-nginx -y    # Nginx plugin
sudo apt install python3-certbot-apache -y   # Apache plugin (if needed)

# Check version
certbot --version

2.2 RHEL/Rocky Linux/CentOS Installation

# Enable the EPEL repository (RHEL/CentOS 7)
sudo yum install epel-release -y
sudo yum install certbot python3-certbot-nginx -y

# RHEL 9 / Rocky Linux 9
sudo dnf install epel-release -y
sudo dnf install certbot python3-certbot-nginx -y

# snap method (common across versions)
sudo dnf install snapd -y
sudo systemctl enable --now snapd.socket
sudo ln -s /var/lib/snapd/snap /snap
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

3. Applying an SSL Certificate to Nginx

3.1 Automatic Configuration (Easiest Method)

Using Certbot's Nginx plugin, you can issue a certificate and update the Nginx configuration in a single step:

# Prerequisite: Nginx must already have a server block for the domain
# e.g., server_name example.com www.example.com;

# Issue certificate + auto-configure Nginx
sudo certbot --nginx -d example.com -d www.example.com

# When run, it prompts you for the following:
# 1. Email address (for expiration notifications)
# 2. Terms of Service agreement (Y)
# 3. EFF newsletter subscription (N is fine)
# 4. HTTP -> HTTPS redirect configuration (option 2 recommended)

On success, you will see a message similar to:

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/example.com/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/example.com/privkey.pem
This certificate expires on 2026-07-05.

Deploying certificate
Successfully deployed certificate for example.com
Successfully deployed certificate for www.example.com
Your existing server block has been modified to redirect HTTP traffic to HTTPS.

3.2 Manual Configuration (Certificate Only)

If you prefer to write the Nginx configuration yourself, you can just issue the certificate and configure the server manually:

# webroot method (while Nginx is running)
sudo certbot certonly --webroot -w /var/www/example.com/html \
  -d example.com -d www.example.com

# standalone method (temporarily stop Nginx during issuance)
sudo systemctl stop nginx
sudo certbot certonly --standalone -d example.com -d www.example.com
sudo systemctl start nginx
# Example of a manual Nginx configuration
# /etc/nginx/conf.d/example.com.conf

# HTTP -> HTTPS redirect
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

# Main HTTPS configuration
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com www.example.com;

    # Let's Encrypt certificate paths
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Optimal SSL settings
    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_session_tickets off;

    # OCSP Stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8 8.8.4.4 valid=300s;

    # HSTS (force HTTPS)
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

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

    location / {
        try_files $uri $uri/ =404;
    }
}
# Validate and apply the configuration
sudo nginx -t
sudo systemctl reload nginx

4. Applying an SSL Certificate to Apache

4.1 Automatic Configuration

# Enable Apache SSL modules (Ubuntu/Debian)
sudo a2enmod ssl
sudo a2enmod headers
sudo systemctl reload apache2

# Auto-configure with the Certbot Apache plugin
sudo certbot --apache -d example.com -d www.example.com

4.2 Manual Apache Configuration

# /etc/apache2/sites-available/example.com-ssl.conf
<IfModule mod_ssl.c>
<VirtualHost *:443>
    ServerName example.com
    ServerAlias www.example.com
    DocumentRoot /var/www/example.com/html

    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem

    # SSL protocols (TLS 1.2 or higher)
    SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
    SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
    SSLHonorCipherOrder off
    SSLSessionTickets off

    # OCSP Stapling
    SSLUseStapling on
    SSLStaplingCache "shmcb:logs/ssl_stapling(32768)"

    # HSTS
    Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"

    ErrorLog ${APACHE_LOG_DIR}/example.com-ssl-error.log
    CustomLog ${APACHE_LOG_DIR}/example.com-ssl-access.log combined
</VirtualHost>
</IfModule>

# HTTP -> HTTPS redirect
<VirtualHost *:80>
    ServerName example.com
    ServerAlias www.example.com
    Redirect permanent / https://example.com/
</VirtualHost>
# Enable the site
sudo a2ensite example.com-ssl.conf

# Validate and apply the configuration
sudo apachectl configtest
sudo systemctl reload apache2

5. Issuing a Wildcard Certificate (DNS-01)

A wildcard certificate (*.example.com) is very convenient because a single certificate can cover all subdomains (app.example.com, api.example.com, etc.). However, only the DNS-01 challenge is supported.

5.1 Manual DNS-01 Issuance

# Manual DNS challenge
sudo certbot certonly --manual --preferred-challenges=dns \
  -d example.com -d "*.example.com"

# Certbot will print a message like the following:
# Please deploy a DNS TXT record under the name:
# _acme-challenge.example.com
# with the following value:
# xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# Manually register the above TXT record in your DNS panel

# Verify propagation before pressing Enter
dig -t txt _acme-challenge.example.com

# Once DNS propagation is confirmed, press Enter to continue validation
Caution: DNS TXT records can take time to propagate (up to 24 hours, usually a few minutes to tens of minutes). Use the dig command to verify propagation before pressing Enter. Pressing it too early will cause validation to fail.

5.2 DNS API Automation (Cloudflare Example)

If you use Cloudflare, Route53, Google Cloud DNS, or similar services, you can automate issuance via API:

# Install the Cloudflare plugin
sudo snap install certbot-dns-cloudflare
# or
sudo apt install python3-certbot-dns-cloudflare -y

# Store the Cloudflare API token (/root/.secrets/cloudflare.ini)
sudo mkdir -p /root/.secrets
sudo cat > /root/.secrets/cloudflare.ini << 'EOF'
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN
EOF
sudo chmod 600 /root/.secrets/cloudflare.ini

# Automatically issue a wildcard certificate
sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /root/.secrets/cloudflare.ini \
  -d example.com -d "*.example.com"
# Route53 plugin (AWS)
sudo snap install certbot-dns-route53

sudo certbot certonly \
  --dns-route53 \
  -d example.com -d "*.example.com"
# (IAM credentials must be available via environment variables or ~/.aws/credentials)

6. Automatic Certificate Renewal

Let's Encrypt certificates must be renewed every 90 days. Certbot supports automatic renewal out of the box.

6.1 Verifying Auto-Renewal

# Test auto-renewal (simulation without actually renewing)
sudo certbot renew --dry-run

# Force renewal (even if more than 30 days remain before expiration)
sudo certbot renew --force-renewal

# Renew only a specific domain
sudo certbot renew --cert-name example.com

# Check the auto-renewal timer status (systemd)
sudo systemctl status certbot.timer

# cron method (automatically configured by the snap version)
systemctl list-timers | grep certbot

6.2 Auto-Renewal Architecture

Certbot sets up an automatic renewal mechanism during installation:

  • systemd timer: /etc/systemd/system/certbot.timer (snap/modern installs)
  • cron: /etc/cron.d/certbot (apt install)
  • Execution frequency: Twice a day (every 12 hours)
  • Renewal condition: Only certificates expiring within 30 days are actually renewed
# Manual cron registration (if needed)
sudo crontab -e

# Check for auto-renewal every day at 3 AM
0 3 * * * /usr/bin/certbot renew --quiet --post-hook "systemctl reload nginx"

# Run a specific command after renewal (hook)
sudo certbot renew --post-hook "systemctl reload nginx"
sudo certbot renew --deploy-hook "systemctl reload nginx"

6.3 Configuring Renewal Failure Notifications

# Change the notification email
sudo certbot update_account --email new@example.com

# Run a script when renewal fails
# /etc/letsencrypt/renewal-hooks/deploy/notify.sh
#!/bin/bash
echo "Certificate renewed: $RENEWED_DOMAINS" | mail -s "SSL Renewal" admin@example.com

sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/notify.sh

7. Certificate Management Commands

7.1 Frequently Used Certbot Commands

# List issued certificates
sudo certbot certificates

# Delete a specific certificate
sudo certbot delete --cert-name example.com

# Reissue certificate (add another domain)
sudo certbot certonly --expand -d example.com -d www.example.com -d blog.example.com

# Use an ECDSA key (faster and smaller than RSA)
sudo certbot certonly --nginx --key-type ecdsa --elliptic-curve secp384r1 \
  -d example.com -d www.example.com

# Register with a specific email
sudo certbot register --email admin@example.com --agree-tos

# View detailed certificate information
openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -text -noout

# Check the expiration date
echo | openssl s_client -connect example.com:443 2>/dev/null | \
  openssl x509 -noout -dates

7.2 Certificate File Structure

File Name Description
cert.pem Server certificate only (standalone)
chain.pem Intermediate CA certificate chain
fullchain.pem Server certificate + intermediate chain (for Nginx/Apache)
privkey.pem Private key (never expose!)
Security Warning: privkey.pem must never be uploaded to Git repositories, shared folders, or external services. If leaked, immediately reissue the certificate and revoke the old one.

8. Verifying and Optimizing Your HTTPS Configuration

8.1 Online SSL Inspection Tools

# Run a local check with testssl.sh
git clone https://github.com/drwetter/testssl.sh.git
cd testssl.sh
./testssl.sh https://example.com

# Check the certificate chain
curl -vI https://example.com 2>&1 | grep -i "SSL\|certificate"

# Check protocols/ciphers
nmap --script ssl-enum-ciphers -p 443 example.com

8.2 Additional Settings for an A+ Rating

# Generate strong DH parameters (takes some time)
sudo openssl dhparam -out /etc/nginx/dhparam.pem 2048

# Add to the Nginx configuration
ssl_dhparam /etc/nginx/dhparam.pem;

# Security headers (required for SSL Labs A+)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

8.3 Registering for HSTS Preload

Registering in the HSTS preload list lets browsers force HTTPS even for first-time visitors to your site:

# Registration requirements (all must be met)
1. A valid certificate
2. HTTP -> HTTPS redirect
3. All subdomains served over HTTPS
4. max-age=63072000 (2 years) or more in the HSTS header
5. includeSubDomains included in the HSTS header
6. preload included in the HSTS header

# Registration site: https://hstspreload.org/

9. Common Issues and Solutions

9.1 "Connection refused" Error

Problem: Failed authorization procedure.
example.com (http-01): urn:ietf:params:acme:error:connection :: The server could not connect to the client to verify the domain :: Fetching http://example.com/.well-known/acme-challenge/xxx: Connection refused

Cause: Port 80 is not accessible from the outside
Solution:

  • Allow port 80 in the firewall: sudo ufw allow 80/tcp
  • Allow HTTP in your cloud provider's Security Group
  • Confirm the domain resolves to the correct IP: dig example.com

9.2 Rate Limit Exceeded

Problem: too many certificates already issued for exact set of domains

Cause: You hit Let's Encrypt's issuance limit
Key Let's Encrypt rate limits:

  • 5 duplicate certificates per week for the same set of domains
  • 50 certificates per week per registered domain
  • 5 failed validation attempts per hour

Solution:

  • Use the --staging flag in test environments (no limits)
  • Wait and retry later (usually one week)
  • Change the domain set by adding a new domain
# Test in the staging environment
sudo certbot --nginx --staging -d example.com

# Staging certificates are not trusted by real browsers
# Issue a real certificate once configuration is complete:
sudo certbot --nginx --force-renewal -d example.com

9.3 DNS Validation Failure

Problem: DNS problem: NXDOMAIN looking up TXT for _acme-challenge.example.com

Cause: The DNS TXT record has not propagated yet
Solution:

# Check the TXT record
dig -t txt _acme-challenge.example.com

# Check against Google DNS
dig -t txt _acme-challenge.example.com @8.8.8.8

# Check against Cloudflare DNS
dig -t txt _acme-challenge.example.com @1.1.1.1

# If all return the same value, propagation is complete

9.4 Mixed Content Warnings

If an HTTPS page loads HTTP resources (images, scripts, etc.), the browser will show a warning.

Solution:

  • In HTML, change http:// to // (protocol-relative) or https://
  • Auto-upgrade using a CSP header: upgrade-insecure-requests
# Auto-upgrade in Nginx
add_header Content-Security-Policy "upgrade-insecure-requests" always;

9.5 Certificate Renewal Failure

# Check renewal logs
sudo journalctl -u certbot.timer
sudo cat /var/log/letsencrypt/letsencrypt.log

# Common causes
# 1. Web server configuration error -> nginx -t / apachectl configtest
# 2. Firewall blocking -> ufw status / firewall-cmd --list-all
# 3. Disk full -> df -h
# 4. DNS changes -> dig example.com

# Force renewal manually
sudo certbot renew --force-renewal --cert-name example.com

10. Production Operations Checklist

10.1 Post-Issuance Verification

  • [x] Browser access check: Does https://example.com open without warnings?
  • [x] Padlock icon check: Is there a padlock icon in the address bar?
  • [x] HTTP redirect check: Does http://example.com automatically redirect to HTTPS?
  • [x] SSL Labs rating check: At least A, preferably A+
  • [x] Auto-renewal test: Does certbot renew --dry-run succeed?
  • [x] Expiration monitoring: Set up notifications 30 days before expiration
  • [x] Backup: Regularly back up the /etc/letsencrypt/ directory

10.2 Certificate Monitoring Script

#!/bin/bash
# /usr/local/bin/check_ssl_expiry.sh - Certificate expiration monitoring

DOMAINS=("example.com" "api.example.com" "blog.example.com")
WARNING_DAYS=30
ALERT_EMAIL="admin@example.com"

for domain in "${DOMAINS[@]}"; do
    expiry_date=$(echo | openssl s_client -servername "$domain" \
        -connect "$domain:443" 2>/dev/null | \
        openssl x509 -noout -enddate | cut -d= -f2)

    expiry_epoch=$(date -d "$expiry_date" +%s)
    now_epoch=$(date +%s)
    days_left=$(( (expiry_epoch - now_epoch) / 86400 ))

    echo "$domain: $days_left days remaining"

    if [ $days_left -lt $WARNING_DAYS ]; then
        echo "WARNING: $domain expires in $days_left days!" | \
            mail -s "SSL Expiry Alert: $domain" "$ALERT_EMAIL"
    fi
done
# Grant execute permission and register with cron
sudo chmod +x /usr/local/bin/check_ssl_expiry.sh

# Run every morning at 9 AM
sudo crontab -e
# 0 9 * * * /usr/local/bin/check_ssl_expiry.sh

Conclusion: HTTPS for Every Website with Let's Encrypt

Let's Encrypt is more than just a free SSL certificate — it is an innovation that raised the security bar of the entire web. Countless sites that previously gave up on HTTPS due to cost are now all served securely over HTTPS, a change that benefits both users and service providers.

Key takeaways from this guide:

  • Automate with Certbot - Manual issuance and renewal leave too much room for mistakes. Always take advantage of Certbot's auto-renewal.
  • Use DNS-01 for wildcards - If you manage many subdomains, a wildcard certificate is far more convenient.
  • Set up monitoring - Even with auto-renewal configured, it can still fail, so expiration monitoring is essential.
  • Aim for SSL Labs A+ - Don't just enable HTTPS — apply an optimized SSL configuration as well.
  • Be cautious with HSTS - Once enabled, it is hard to roll back, so only apply it once everything is in place.
  • Protect your private key - privkey.pem must never be exposed externally.

With Let's Encrypt and Certbot, you can build production-grade HTTPS in just a few minutes. Adapt the examples in this guide to your own environment and run a secure web service with confidence.