Set up automated SSL certificate provisioning and renewal for NGINX using Let's Encrypt Certbot with systemd timers, monitoring, and failure alerting for production environments.
Prerequisites
- Root or sudo access
- Domain name pointing to your server
- NGINX web server
- Internet connectivity for Let's Encrypt validation
What this solves
Manual SSL certificate management creates security risks and service interruptions when certificates expire unexpectedly. This tutorial automates SSL certificate provisioning, renewal, and monitoring for NGINX using Let's Encrypt Certbot with systemd timers, ensuring your web services maintain valid certificates without manual intervention.
Step-by-step configuration
Install NGINX and Certbot
Install NGINX web server and Certbot for Let's Encrypt certificate management.
sudo apt update
sudo apt install -y nginx certbot python3-certbot-nginx
Configure NGINX virtual host
Create a basic NGINX virtual host configuration for your domain before requesting SSL certificates.
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example.com;
index index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
location /.well-known/acme-challenge/ {
root /var/www/example.com;
allow all;
}
}
Create web root directory
Set up the web root directory with proper permissions for NGINX and Certbot validation.
sudo mkdir -p /var/www/example.com
sudo chown -R www-data:www-data /var/www/example.com
sudo chmod -R 755 /var/www/example.com
echo "Welcome to example.com
" | sudo tee /var/www/example.com/index.html
Enable site and test configuration
Enable the virtual host and verify NGINX configuration syntax.
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Request SSL certificate with Certbot
Use Certbot to request and automatically configure SSL certificates for your domain.
sudo certbot --nginx -d example.com -d www.example.com --email admin@example.com --agree-tos --no-eff-email
Verify SSL certificate installation
Check that Certbot successfully modified your NGINX configuration with SSL settings.
sudo nginx -t
sudo systemctl reload nginx
sudo certbot certificates
Create certificate renewal script
Create a renewal script that handles certificate renewal and NGINX reloading with logging.
#!/bin/bash
Certbot renewal script with logging and error handling
LOG_FILE="/var/log/certbot-renewal.log"
DATE=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$DATE] Starting certificate renewal check" >> "$LOG_FILE"
Attempt renewal
if /usr/bin/certbot renew --quiet --nginx >> "$LOG_FILE" 2>&1; then
echo "[$DATE] Certificate renewal check completed successfully" >> "$LOG_FILE"
# Test nginx configuration
if /usr/sbin/nginx -t >> "$LOG_FILE" 2>&1; then
# Reload nginx if config is valid
/bin/systemctl reload nginx >> "$LOG_FILE" 2>&1
echo "[$DATE] NGINX reloaded successfully" >> "$LOG_FILE"
else
echo "[$DATE] ERROR: NGINX configuration test failed" >> "$LOG_FILE"
exit 1
fi
else
echo "[$DATE] ERROR: Certificate renewal failed" >> "$LOG_FILE"
exit 1
fi
echo "[$DATE] Renewal process completed" >> "$LOG_FILE"
Make renewal script executable
Set proper permissions for the renewal script.
sudo chmod +x /usr/local/bin/certbot-renewal.sh
sudo chown root:root /usr/local/bin/certbot-renewal.sh
Create systemd service for renewal
Create a systemd service unit for certificate renewal.
[Unit]
Description=Certbot SSL Certificate Renewal
After=network.target
[Service]
Type=oneshot
User=root
ExecStart=/usr/local/bin/certbot-renewal.sh
StandardOutput=journal
StandardError=journal
Create systemd timer for automatic renewal
Configure a systemd timer to run certificate renewal checks twice daily.
[Unit]
Description=Run Certbot SSL renewal check twice daily
Requires=certbot-renewal.service
[Timer]
OnCalendar=--* 06,18:00:00
RandomizedDelaySec=3600
Persistent=true
[Install]
WantedBy=timers.target
Enable and start the renewal timer
Enable the systemd timer to start automatically and begin running renewal checks.
sudo systemctl daemon-reload
sudo systemctl enable certbot-renewal.timer
sudo systemctl start certbot-renewal.timer
sudo systemctl status certbot-renewal.timer
Create certificate monitoring script
Create a monitoring script to check certificate expiration and send alerts.
#!/bin/bash
Certificate monitoring script
WARN_DAYS=30
CRIT_DAYS=7
LOG_FILE="/var/log/cert-monitor.log"
DATE=$(date '+%Y-%m-%d %H:%M:%S')
Function to check certificate expiration
check_cert() {
local domain="$1"
local exp_date
local exp_epoch
local now_epoch
local days_until_exp
exp_date=$(echo | openssl s_client -servername "$domain" -connect "$domain:443" 2>/dev/null | \
openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
if [ -z "$exp_date" ]; then
echo "[$DATE] ERROR: Could not retrieve certificate for $domain" >> "$LOG_FILE"
return 1
fi
exp_epoch=$(date -d "$exp_date" +%s)
now_epoch=$(date +%s)
days_until_exp=$(( (exp_epoch - now_epoch) / 86400 ))
echo "[$DATE] Certificate for $domain expires in $days_until_exp days" >> "$LOG_FILE"
if [ "$days_until_exp" -lt "$CRIT_DAYS" ]; then
echo "[$DATE] CRITICAL: Certificate for $domain expires in $days_until_exp days!" >> "$LOG_FILE"
logger -p daemon.crit "SSL Certificate CRITICAL: $domain expires in $days_until_exp days"
elif [ "$days_until_exp" -lt "$WARN_DAYS" ]; then
echo "[$DATE] WARNING: Certificate for $domain expires in $days_until_exp days" >> "$LOG_FILE"
logger -p daemon.warning "SSL Certificate WARNING: $domain expires in $days_until_exp days"
fi
}
Check certificates for all configured domains
for cert_file in /etc/letsencrypt/live/*/cert.pem; do
if [ -f "$cert_file" ]; then
cert_dir=$(dirname "$cert_file")
domain=$(basename "$cert_dir")
check_cert "$domain"
fi
done
Make monitoring script executable
Set permissions for the certificate monitoring script.
sudo chmod +x /usr/local/bin/cert-monitor.sh
sudo chown root:root /usr/local/bin/cert-monitor.sh
Create monitoring systemd service and timer
Set up systemd service and timer for certificate expiration monitoring.
[Unit]
Description=SSL Certificate Expiration Monitor
After=network.target
[Service]
Type=oneshot
User=root
ExecStart=/usr/local/bin/cert-monitor.sh
StandardOutput=journal
StandardError=journal
[Unit]
Description=Run SSL certificate monitoring daily
Requires=cert-monitor.service
[Timer]
OnCalendar=daily
RandomizedDelaySec=1800
Persistent=true
[Install]
WantedBy=timers.target
Enable certificate monitoring
Enable and start the certificate monitoring timer.
sudo systemctl daemon-reload
sudo systemctl enable cert-monitor.timer
sudo systemctl start cert-monitor.timer
sudo systemctl status cert-monitor.timer
Configure log rotation
Set up log rotation for certificate renewal and monitoring logs to prevent disk space issues.
/var/log/certbot-renewal.log /var/log/cert-monitor.log {
weekly
rotate 12
compress
delaycompress
missingok
notifempty
create 644 root root
}
Verify your setup
Test your SSL certificate automation and monitoring configuration.
# Check SSL certificate status
sudo certbot certificates
Test renewal process (dry run)
sudo certbot renew --dry-run
Check systemd timers
sudo systemctl list-timers | grep cert
Test certificate monitoring
sudo /usr/local/bin/cert-monitor.sh
Check logs
sudo tail -f /var/log/certbot-renewal.log
sudo tail -f /var/log/cert-monitor.log
Test SSL configuration
curl -I https://example.com
openssl s_client -connect example.com:443 -servername example.com
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Certbot validation fails | DNS not pointing to server or firewall blocking port 80 | Verify DNS with dig example.com and check firewall rules |
| NGINX fails to reload after renewal | Invalid NGINX configuration | Test config with sudo nginx -t and fix syntax errors |
| Certificate renewal timer not running | Timer not enabled or systemd issues | Check with sudo systemctl status certbot-renewal.timer |
| Monitoring script shows SSL errors | Certificate not properly installed or expired | Re-run Certbot with sudo certbot --nginx -d example.com |
| Log files growing too large | Logrotate not configured properly | Test logrotate with sudo logrotate -d /etc/logrotate.d/certbot-logs |
Next steps
- Configure NGINX rate limiting and DDoS protection with advanced security rules
- Monitor nginx performance with Prometheus and Grafana using nginx-prometheus-exporter
- Set up NGINX monitoring with Prometheus and Grafana for web server observability
- Configure NGINX wildcard SSL certificates with DNS validation
- Set up Certbot webhook notifications for Slack and email alerts
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Global variables
DOMAIN=""
EMAIL=""
WEB_ROOT=""
NGINX_CONF_DIR=""
SITES_AVAILABLE=""
SITES_ENABLED=""
# Usage function
usage() {
echo "Usage: $0 -d DOMAIN -e EMAIL"
echo " -d DOMAIN Domain name (e.g., example.com)"
echo " -e EMAIL Email for Let's Encrypt notifications"
echo " -h Show this help message"
exit 1
}
# Logging functions
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Cleanup function for rollback
cleanup() {
if [[ $? -ne 0 ]]; then
log_error "Script failed. Cleaning up..."
if [[ -n "$DOMAIN" && -f "$SITES_AVAILABLE/$DOMAIN" ]]; then
rm -f "$SITES_AVAILABLE/$DOMAIN"
[[ -L "$SITES_ENABLED/$DOMAIN" ]] && rm -f "$SITES_ENABLED/$DOMAIN"
fi
[[ -d "$WEB_ROOT" ]] && rm -rf "$WEB_ROOT"
systemctl reload nginx 2>/dev/null || true
fi
}
trap cleanup ERR
# Parse command line arguments
while getopts "d:e:h" opt; do
case $opt in
d) DOMAIN="$OPTARG" ;;
e) EMAIL="$OPTARG" ;;
h) usage ;;
*) usage ;;
esac
done
# Validate required arguments
if [[ -z "$DOMAIN" || -z "$EMAIL" ]]; then
log_error "Domain and email are required"
usage
fi
# Validate email format
if [[ ! "$EMAIL" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
log_error "Invalid email format"
exit 1
fi
# Check if running as root
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root"
exit 1
fi
# Auto-detect distro and set package manager
if [[ -f /etc/os-release ]]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update"
WEB_USER="www-data"
SITES_AVAILABLE="/etc/nginx/sites-available"
SITES_ENABLED="/etc/nginx/sites-enabled"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf update -y"
WEB_USER="nginx"
SITES_AVAILABLE="/etc/nginx/conf.d"
SITES_ENABLED="/etc/nginx/conf.d"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum update -y"
WEB_USER="nginx"
SITES_AVAILABLE="/etc/nginx/conf.d"
SITES_ENABLED="/etc/nginx/conf.d"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
else
log_error "Cannot detect Linux distribution"
exit 1
fi
WEB_ROOT="/var/www/$DOMAIN"
NGINX_CONF_DIR="/etc/nginx"
log_info "[1/9] Updating package repositories..."
$PKG_UPDATE
log_info "[2/9] Installing NGINX and Certbot..."
$PKG_INSTALL nginx certbot python3-certbot-nginx
log_info "[3/9] Creating web root directory..."
mkdir -p "$WEB_ROOT"
chown -R "$WEB_USER:$WEB_USER" "$WEB_ROOT"
chmod -R 755 "$WEB_ROOT"
echo "Welcome to $DOMAIN" > "$WEB_ROOT/index.html"
chown "$WEB_USER:$WEB_USER" "$WEB_ROOT/index.html"
chmod 644 "$WEB_ROOT/index.html"
log_info "[4/9] Creating NGINX virtual host configuration..."
if [[ "$PKG_MGR" == "apt" ]]; then
# Debian/Ubuntu style
cat > "$SITES_AVAILABLE/$DOMAIN" << EOF
server {
listen 80;
server_name $DOMAIN www.$DOMAIN;
root $WEB_ROOT;
index index.html index.htm;
location / {
try_files \$uri \$uri/ =404;
}
location /.well-known/acme-challenge/ {
root $WEB_ROOT;
allow all;
}
}
EOF
ln -sf "$SITES_AVAILABLE/$DOMAIN" "$SITES_ENABLED/$DOMAIN"
else
# RHEL/CentOS style
cat > "$SITES_AVAILABLE/$DOMAIN.conf" << EOF
server {
listen 80;
server_name $DOMAIN www.$DOMAIN;
root $WEB_ROOT;
index index.html index.htm;
location / {
try_files \$uri \$uri/ =404;
}
location /.well-known/acme-challenge/ {
root $WEB_ROOT;
allow all;
}
}
EOF
fi
log_info "[5/9] Testing NGINX configuration and starting service..."
nginx -t
systemctl enable nginx
systemctl start nginx
systemctl reload nginx
# Configure firewall if present
if command -v ufw >/dev/null 2>&1; then
ufw allow 'Nginx Full' 2>/dev/null || true
elif command -v firewall-cmd >/dev/null 2>&1; then
firewall-cmd --permanent --add-service=http --add-service=https 2>/dev/null || true
firewall-cmd --reload 2>/dev/null || true
fi
log_info "[6/9] Requesting SSL certificate with Certbot..."
certbot --nginx -d "$DOMAIN" -d "www.$DOMAIN" --email "$EMAIL" --agree-tos --non-interactive --no-eff-email
log_info "[7/9] Creating certificate renewal script..."
cat > /usr/local/bin/certbot-renewal.sh << 'EOF'
#!/bin/bash
# Certbot renewal script with logging and error handling
LOG_FILE="/var/log/certbot-renewal.log"
DATE=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$DATE] Starting certificate renewal check" >> "$LOG_FILE"
# Attempt renewal
if /usr/bin/certbot renew --quiet --nginx >> "$LOG_FILE" 2>&1; then
echo "[$DATE] Certificate renewal check completed successfully" >> "$LOG_FILE"
# Test nginx configuration
if /usr/sbin/nginx -t >> "$LOG_FILE" 2>&1; then
# Reload nginx if config is valid
/bin/systemctl reload nginx >> "$LOG_FILE" 2>&1
echo "[$DATE] NGINX reloaded successfully" >> "$LOG_FILE"
else
echo "[$DATE] ERROR: NGINX configuration test failed" >> "$LOG_FILE"
exit 1
fi
else
echo "[$DATE] ERROR: Certificate renewal failed" >> "$LOG_FILE"
exit 1
fi
echo "[$DATE] Renewal process completed" >> "$LOG_FILE"
EOF
chmod 755 /usr/local/bin/certbot-renewal.sh
chown root:root /usr/local/bin/certbot-renewal.sh
log_info "[8/9] Creating systemd service and timer for automatic renewal..."
cat > /etc/systemd/system/certbot-renewal.service << EOF
[Unit]
Description=Certbot SSL Certificate Renewal
After=network.target
[Service]
Type=oneshot
User=root
ExecStart=/usr/local/bin/certbot-renewal.sh
StandardOutput=journal
StandardError=journal
EOF
cat > /etc/systemd/system/certbot-renewal.timer << EOF
[Unit]
Description=Run Certbot renewal twice daily
Requires=certbot-renewal.service
[Timer]
OnCalendar=*-*-* 00,12:00:00
Persistent=true
RandomizedDelaySec=3600
[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable certbot-renewal.timer
systemctl start certbot-renewal.timer
log_info "[9/9] Verifying installation..."
nginx -t
systemctl reload nginx
# Verify SSL certificate
if certbot certificates | grep -q "$DOMAIN"; then
log_info "SSL certificate successfully installed for $DOMAIN"
else
log_error "SSL certificate verification failed"
exit 1
fi
# Verify systemd timer
if systemctl is-active --quiet certbot-renewal.timer; then
log_info "Automatic renewal timer is active"
else
log_error "Automatic renewal timer failed to start"
exit 1
fi
log_info "SSL certificate automation setup completed successfully!"
log_info "Certificate will be automatically renewed twice daily"
log_info "Check renewal logs at: /var/log/certbot-renewal.log"
log_info "Your site is now available at: https://$DOMAIN"
Review the script before running. Execute with: bash install.sh