Let's Encrypt Free SSL Certificate Complete Setup Guide - HTTPS Implementation in Practice
Let's Encrypt Free SSL Certificate Complete Setup Guide
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.comcan 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) |
- 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
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!) |
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
- SSL Labs (Qualys): https://www.ssllabs.com/ssltest/ - The most authoritative SSL inspection tool. Aim for an A+ rating.
- Mozilla Observatory: https://observatory.mozilla.org/ - Comprehensive security score
- testssl.sh: A CLI tool you can run locally
- SSL Configuration Generator: https://ssl-config.mozilla.org/ - Automatically generates optimal settings
# 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
--stagingflag 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) orhttps:// - 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.comopen without warnings? - [x] Padlock icon check: Is there a padlock icon in the address bar?
- [x] HTTP redirect check: Does
http://example.comautomatically redirect to HTTPS? - [x] SSL Labs rating check: At least A, preferably A+
- [x] Auto-renewal test: Does
certbot renew --dry-runsucceed? - [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.pemmust 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.