liminfo

SSL Certificate Setup and Auto-Renewal

Issue free SSL certificates with Let's Encrypt, configure wildcard certificates, set up automatic renewal, and build certificate expiry monitoring for a complete HTTPS infrastructure

SSL certificate setupLet's Encryptcertbot usagewildcard SSLcertificate auto-renewalHTTPS configurationSSL certificatecertificate expiry monitoring

Problem

You need to apply HTTPS to a service operating multiple subdomains (api.example.com, app.example.com, admin.example.com). Issuing individual certificates for each subdomain complicates management, so a wildcard certificate (*.example.com) is needed. Since certificate expiration makes the service inaccessible, auto-renewal is essential, and you need alerts when renewal fails. Additionally, SSL security settings (TLS versions, cipher suites, HSTS) must be optimized alongside Nginx.

Required Tools

Certbot (Let's Encrypt)

The official client that automatically issues and renews free SSL/TLS certificates from the Let's Encrypt CA.

Nginx

A reverse proxy that handles SSL termination. Responsible for SSL certificate application and security header configuration.

OpenSSL

A command-line tool for querying, verifying, and converting SSL/TLS certificates. Essential for certificate status checks and debugging.

DNS Provider API

DNS management API needed to automate the DNS-01 challenge required for wildcard certificate issuance.

cron / systemd timer

Schedulers for periodically running certificate auto-renewal and monitoring scripts.

Solution Steps

1

Install Certbot and issue a single-domain certificate

Start by issuing a basic certificate for a single domain. Certbot can automatically configure with Nginx/Apache plugins, or run independently in standalone mode. The DNS A record must point to the server IP and port 80 must be open for the HTTP-01 challenge to succeed.

# Install Certbot (Ubuntu 22.04+)
sudo apt update
sudo apt install certbot python3-certbot-nginx -y

# Method 1: Nginx plugin (auto-configuration)
sudo certbot --nginx -d example.com -d www.example.com

# Method 2: Standalone (without Nginx, needs port 80)
sudo certbot certonly --standalone -d example.com

# Method 3: Webroot (using existing web server)
sudo certbot certonly --webroot -w /var/www/html -d example.com

# Verify issuance
sudo certbot certificates

# Certificate file locations
ls -la /etc/letsencrypt/live/example.com/
# cert.pem      - Server certificate
# chain.pem     - Intermediate CA certificate
# fullchain.pem - cert.pem + chain.pem (used by Nginx)
# privkey.pem   - Private key

# Check certificate info with OpenSSL
openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -text -noout | head -20

# Check certificate expiry date
openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -enddate -noout
2

Issue wildcard certificate (DNS-01 challenge)

Wildcard certificates (*.example.com) can only be issued via DNS-01 challenge. Automation is possible using DNS management APIs. Certbot plugins exist for major DNS providers like Cloudflare and AWS Route53 that automatically handle DNS record creation/deletion.

# === Cloudflare DNS authentication ===

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

# Set up Cloudflare API credentials
sudo mkdir -p /etc/letsencrypt
cat << 'EOF' | sudo tee /etc/letsencrypt/cloudflare.ini
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN
EOF
sudo chmod 600 /etc/letsencrypt/cloudflare.ini

# Issue wildcard certificate
sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d example.com \
  -d "*.example.com" \
  --preferred-challenges dns-01

# === AWS Route53 authentication ===
# sudo apt install python3-certbot-dns-route53 -y
# sudo certbot certonly \
#   --dns-route53 \
#   -d example.com \
#   -d "*.example.com"

# === Manual DNS authentication (cannot be automated) ===
# sudo certbot certonly --manual --preferred-challenges dns \
#   -d example.com -d "*.example.com"
# -> Must manually add _acme-challenge.example.com TXT record

# Verify issuance
sudo certbot certificates
3

Nginx SSL optimization settings

Apply the SSL certificate to Nginx and optimize TLS versions, cipher suites, and OCSP Stapling. Reduce TLS handshake overhead with ssl_session_cache and ssl_session_tickets, and enforce HTTPS with HSTS headers.

# /etc/nginx/snippets/ssl-params.conf
# Common SSL settings separated into snippets for reuse

# TLS protocol versions (allow 1.2+ only)
ssl_protocols TLSv1.2 TLSv1.3;

# Strong cipher suites
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 caching (improves handshake performance)
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
ssl_session_tickets off;

# OCSP Stapling (accelerates certificate validation)
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;

# --- Nginx site configuration ---
server {
    listen 80;
    server_name example.com *.example.com;
    return 301 https://$host$request_uri;
}

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

    # Wildcard certificate
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Include common SSL settings
    include /etc/nginx/snippets/ssl-params.conf;

    # Security headers
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
4

Configure and verify auto-renewal

Let's Encrypt certificates expire every 90 days, making auto-renewal essential. Automate renewal with certbot's systemd timer or cron, and set up a hook to reload Nginx after renewal. Always verify correct operation with a renewal simulation (--dry-run).

# === Check systemd timer (included with certbot package) ===
sudo systemctl status certbot.timer
sudo systemctl list-timers | grep certbot

# Enable if timer doesn't exist
sudo systemctl enable --now certbot.timer

# === Set up renewal hooks ===
# Reload Nginx after successful renewal
cat << 'EOF' | sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
#!/bin/bash
systemctl reload nginx
echo "$(date): Nginx reloaded after certificate renewal" >> /var/log/certbot-renewal.log
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

# === Renewal simulation (must run!) ===
sudo certbot renew --dry-run

# === Setting up via cron instead ===
# (unnecessary if using systemd timer)
# sudo crontab -e
# 0 3 * * * certbot renew --quiet --deploy-hook "systemctl reload nginx"

# === Check renewal logs ===
sudo journalctl -u certbot.service --since "7 days ago"

# === Check current certificate status ===
sudo certbot certificates
5

Certificate expiry monitoring script

Since auto-renewal can fail, write a script that monitors certificate expiry dates and sends alerts when below a threshold (e.g., 14 days). Checking actual SSL connections from outside can also detect certificate chain issues.

#!/bin/bash
# ssl-monitor.sh - SSL certificate expiry monitoring

ALERT_DAYS=14
DOMAINS=(
  "example.com"
  "api.example.com"
  "app.example.com"
)

SLACK_WEBHOOK="${SLACK_WEBHOOK_URL}"

check_ssl() {
  local domain=$1
  local port=${2:-443}

  # Check certificate via actual SSL connection from outside
  local expiry_date
  expiry_date=$(echo | openssl s_client -servername "$domain" \
    -connect "$domain:$port" 2>/dev/null | \
    openssl x509 -noout -enddate 2>/dev/null | \
    cut -d= -f2)

  if [ -z "$expiry_date" ]; then
    echo "ERROR: Cannot connect to $domain:$port"
    return 1
  fi

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

  if [ "$days_left" -le 0 ]; then
    echo "CRITICAL: $domain - Certificate EXPIRED!"
    send_alert "CRITICAL" "$domain" "Certificate has EXPIRED!"
  elif [ "$days_left" -le "$ALERT_DAYS" ]; then
    echo "WARNING: $domain - $days_left days until expiry"
    send_alert "WARNING" "$domain" "Expires in $days_left days"
  else
    echo "OK: $domain - $days_left days remaining"
  fi
}

send_alert() {
  local level=$1 domain=$2 message=$3
  if [ -n "$SLACK_WEBHOOK" ]; then
    curl -s -X POST "$SLACK_WEBHOOK" \
      -H 'Content-Type: application/json' \
      -d "{"text": "[$level] SSL: $domain - $message"}"
  fi
}

echo "=== SSL Certificate Monitor ($(date)) ==="
for domain in "${DOMAINS[@]}"; do
  check_ssl "$domain"
done

# Cron: run daily at 9 AM
# 0 9 * * * /opt/scripts/ssl-monitor.sh >> /var/log/ssl-monitor.log 2>&1
6

SSL security audit and troubleshooting

Covers SSL Labs grade testing, certificate chain verification, and diagnosing/resolving common SSL issues. Includes troubleshooting for frequently encountered production issues like missing intermediate certificates, mixed content, and SNI problems.

# === Certificate chain verification ===
# Check certificate chain returned by server
openssl s_client -connect example.com:443 -showcerts </dev/null 2>/dev/null

# Verify certificate chain validity
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt \
  /etc/letsencrypt/live/example.com/fullchain.pem

# === Common issue diagnostics ===

# 1. Check if certificate and private key match
CERT_MD5=$(openssl x509 -noout -modulus -in cert.pem | openssl md5)
KEY_MD5=$(openssl rsa -noout -modulus -in privkey.pem | openssl md5)
[ "$CERT_MD5" = "$KEY_MD5" ] && echo "MATCH" || echo "MISMATCH!"

# 2. Test SSL protocol connection
openssl s_client -connect example.com:443 -tls1_2 </dev/null 2>&1 | head -5
openssl s_client -connect example.com:443 -tls1_3 </dev/null 2>&1 | head -5

# 3. Test specific cipher suite
openssl s_client -connect example.com:443 \
  -cipher ECDHE-RSA-AES256-GCM-SHA384 </dev/null 2>&1 | head -5

# 4. Check HSTS header
curl -sI https://example.com | grep -i strict-transport

# 5. Find mixed content
curl -s https://example.com | grep -oP 'http://[^"'"'"'\s>]+' | sort -u

# 6. Verify SNI support
openssl s_client -servername example.com -connect example.com:443 </dev/null 2>&1 | \
  grep "subject="

# === Nginx SSL configuration test ===
sudo nginx -t
sudo nginx -T | grep ssl_

Core Code

Core commands for wildcard SSL certificate issuance, Nginx application, auto-renewal, and monitoring. Uses the Let's Encrypt + Certbot + Cloudflare DNS combination.

#!/bin/bash
# === Core: SSL certificate issuance + auto-renewal + monitoring ===

# 1. Issue wildcard certificate (Cloudflare DNS)
sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d example.com -d "*.example.com"

# 2. Nginx SSL configuration
# ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# ssl_protocols TLSv1.2 TLSv1.3;
# add_header Strict-Transport-Security "max-age=63072000" always;

# 3. Auto-renewal (systemd timer)
sudo systemctl enable --now certbot.timer

# 4. Post-renewal Nginx reload hook
echo 'systemctl reload nginx' > \
  /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

# 5. Test renewal
sudo certbot renew --dry-run

Common Mistakes

Wildcard certificate renewal fails because DNS plugin credentials have expired

Periodically verify that Cloudflare API tokens or AWS IAM keys have not expired or had permissions changed. Register an email with certbot for renewal failure notifications, and track expiry dates with a monitoring script.

Setting only cert.pem instead of fullchain.pem in Nginx, missing the intermediate certificate

Always specify fullchain.pem (server certificate + intermediate CA) for ssl_certificate. Using only cert.pem causes certificate verification failures on some clients (especially mobile devices).

Not running certbot renew --dry-run test, discovering failures only during actual renewal

Always run sudo certbot renew --dry-run immediately after certificate issuance to verify the renewal process works correctly. Wildcard certificates especially require DNS authentication, so plugin configuration must be validated.

Related liminfo Services