This guide explains, in plain English, how to use a tool called Lego to get, install, and automatically renew SSL certificates for your website or application. Think of an SSL certificate like a security padlock for your website — it tells visitors that your site is safe and that no one is secretly reading the data sent between them and your server.
Lego is a powerful, lightweight program (a single file — no extra software needed!) that runs on Windows, Mac, and Linux. It works with more than 100 DNS providers and handles everything for you: getting the certificate, installing it, and renewing it automatically before it expires.
|
? PREREQUISITE Before following this guide, make sure you have completed the Lego Installation Guide and have your ACME server credentials (Email, EAB Key ID, and HMAC Key) ready. |
Before we get started, here is a quick plain-English glossary of the technical words you will see:
| Term | What It Means (Plain English) |
|---|---|
| SSL Certificate | A digital file that proves your website is authentic and encrypts all data sent to/from it. Like a passport for your website. |
| ACME Server | The online service that issues (creates and approves) your certificate. Lego talks to this behind the scenes. |
| EAB Credentials | Your unique login key pair for the ACME server. Think of it as a username and password, but more secure. |
| DNS Provider | The company that manages your domain name records (e.g. Cloudflare, Route53). Lego uses it to prove you own the domain. |
| DNS Challenge | A security check where Lego adds a temporary record to your DNS to prove you control the domain. |
| Run Hook | A script that runs automatically after a certificate is issued or renewed — for example, to restart your web server. |
| Cron Job | A scheduled task on Linux/Mac that runs at set times automatically (e.g. every night at 2 AM). |
| Task Scheduler | The Windows equivalent of a cron job — runs scripts on a schedule. |
| SAN Certificate | A certificate that covers multiple domain names at once (e.g. example.com AND www.example.com). |
| Wildcard Certificate | A certificate that covers ALL sub-domains under a domain (e.g. *.example.com covers api.example.com, mail.example.com, etc.). |
This section explains how to ask Lego to request a brand-new certificate from the ACME server. This is the very first step — like applying for a new passport.
By default, Lego waits up to 30 seconds for the ACME server to create your certificate once all security checks pass. Some ACME servers take longer — if yours does, Lego will stop waiting and show an error before your certificate is ready.
To fix this, you can tell Lego to wait longer by adding --cert.timeout followed by the number of seconds. This option is available from Lego version 4.7.0 and above.
|
? RECOMMENDED Always set |
Common timeout values:
Use this if you need a certificate for exactly one website address (e.g. demo.example.com only).
What you should see when it works:
[INFO] [demo.example.com] acme: Obtaining certificate
[INFO] [demo.example.com] acme: Validating domain
[INFO] [demo.example.com] The server validated our request
[INFO] [demo.example.com] acme: Requesting certificate
[INFO] [demo.example.com] Server responded with a certificate.
|
???? TIP To see extra detail about what Lego is doing, add |
A SAN (Subject Alternative Name) certificate covers several domain names at once — very useful if you have multiple addresses pointing to the same server. Just repeat --domains for each address you want to cover.
A wildcard certificate covers your main domain and ALL its sub-domains. For example, *.example.com covers api.example.com, mail.example.com, app.example.com, and any others. It is best to include the root domain (example.com) alongside the wildcard to cover everything.
You can combine both wildcards and specific extra domains in a single certificate. This is the most flexible option for complex setups.
lego --email "$EMAIL" \
--server "$ACME_SERVER" \
--eab --kid "$EAB_KEY_ID" --hmac "$EAB_HMAC_KEY" \
--dns cloudflare \
--domains "*.example.com" \
--domains example.com \
--domains www.example.com \
--domains mail.example.com \
--cert.timeout 300 \
run
A key type is the mathematical algorithm used to protect your certificate. Different types offer different trade-offs between compatibility and speed.
| Key Type | Flag | What It Means | When to Use |
|---|---|---|---|
| RSA 4096 | --key-type rsa4096 |
Most compatible with older systems. Slightly slower. | Default choice for maximum compatibility. |
| RSA 2048 | --key-type rsa2048 |
Compatible, slightly faster than RSA 4096. | Use for older systems or performance-sensitive setups. |
| ECDSA P-256 | --key-type ec256 |
Modern, fast, and small. Best performance. | Recommended for new setups on modern servers. |
| ECDSA P-384 | --key-type ec384 |
More secure than P-256, slightly slower. | Use for high-security environments. |
Example — ECDSA P-256 (recommended for performance):
lego --email "$EMAIL" \
--server "$ACME_SERVER" \
--eab --kid "$EAB_KEY_ID" --hmac "$EAB_HMAC_KEY" \
--dns cloudflare \
--domains demo.example.com \
--key-type ec256 \
run
After Lego successfully gets your certificate, it saves it on your computer. Here is exactly where to find each file.
What each file is for:
| File | Purpose |
|---|---|
.crt |
The full certificate chain (certificate + intermediates). This is what you give to Apache, Nginx, IIS, etc. |
.key |
NEVER share this file, upload it publicly, or commit it to version control (like Git). |
.issuer.crt |
Just the intermediate certificate. Some older applications need this file separately. |
.json |
Metadata about your certificate: expiry date, covered domains, and the certificate's URL on the ACME server. |
|
???? IMPORTANT SECURITY NOTE The |
Getting the certificate is only half the job. You also need to install it on your web server so that your website actually uses it. Lego can do this automatically using "run hooks" — small scripts that run right after a certificate is issued or renewed.
A run hook is a command or script that Lego automatically runs every time it successfully gets or renews a certificate. This is how you tell Lego: "After you get the certificate, please copy it to the right folder and restart my web server."
Simple example — restart Nginx automatically after getting a certificate:
lego --email "$EMAIL" \
--server "$ACME_SERVER" \
--eab --kid "$EAB_KEY_ID" --hmac "$EAB_HMAC_KEY" \
--dns cloudflare \
--domains demo.example.com \
--run-hook "systemctl reload nginx" \
run
You can chain multiple hooks (they run in order):
--run-hook "/path/to/deploy-script.sh" \
--run-hook "systemctl reload nginx" \
--run-hook "docker restart web-app" \
Follow these steps to automatically deploy your certificate to an Apache web server every time it is issued or renewed.
Step A — Create the deployment script:
sudo nano /usr/local/bin/lego-deploy-apache.sh
Copy and paste the following into the file:
#!/bin/bash
DOMAIN="demo.example.com"
CERT_SRC="$HOME/.lego/certificates"
CERT_DEST="/etc/apache2/ssl"
mkdir -p "$CERT_DEST"
cp "$CERT_SRC/$DOMAIN.crt" "$CERT_DEST/$DOMAIN.crt"
cp "$CERT_SRC/$DOMAIN.key" "$CERT_DEST/$DOMAIN.key"
cp "$CERT_SRC/$DOMAIN.issuer.crt" "$CERT_DEST/$DOMAIN.issuer.crt"
chmod 644 "$CERT_DEST/$DOMAIN.crt"
chmod 644 "$CERT_DEST/$DOMAIN.issuer.crt"
chmod 600 "$CERT_DEST/$DOMAIN.key"
systemctl reload apache2
echo "$(date): Apache certificates deployed for $DOMAIN" >> /var/log/lego-deploy.log
Step B — Make the script executable (so the system can run it):
sudo chmod +x /usr/local/bin/lego-deploy-apache.sh
Step C — Get your certificate and run the deploy hook:
lego --email "$EMAIL" \
--server "$ACME_SERVER" \
--eab --kid "$EAB_KEY_ID" --hmac "$EAB_HMAC_KEY" \
--dns cloudflare \
--domains demo.example.com \
--run-hook "/usr/local/bin/lego-deploy-apache.sh" \
run
Step D — Configure Apache to use the certificate:
sudo tee /etc/apache2/sites-available/demo.example.com-ssl.conf > /dev/null << 'EOF'
<VirtualHost *:443>
ServerName demo.example.com
DocumentRoot /var/www/demo.example.com
SSLEngine on
SSLCertificateFile /etc/apache2/ssl/demo.example.com.crt
SSLCertificateKeyFile /etc/apache2/ssl/demo.example.com.key
SSLProtocol -all +TLSv1.2 +TLSv1.3
Header always set Strict-Transport-Security "max-age=63072000"
</VirtualHost>
EOF
sudo a2enmod ssl headers
sudo a2ensite demo.example.com-ssl
sudo systemctl reload apache2
Follow these steps to automatically deploy your certificate to an Nginx web server.
Step A — Create the deployment script:
sudo nano /usr/local/bin/lego-deploy-nginx.sh
Script content:
#!/bin/bash
DOMAIN="demo.example.com"
CERT_SRC="$HOME/.lego/certificates"
CERT_DEST="/etc/nginx/ssl"
mkdir -p "$CERT_DEST"
cp "$CERT_SRC/$DOMAIN.crt" "$CERT_DEST/$DOMAIN.crt"
cp "$CERT_SRC/$DOMAIN.key" "$CERT_DEST/$DOMAIN.key"
chmod 644 "$CERT_DEST/$DOMAIN.crt"
chmod 600 "$CERT_DEST/$DOMAIN.key"
nginx -t && systemctl reload nginx
echo "$(date): Nginx certificates deployed for $DOMAIN" >> /var/log/lego-deploy.log
Step B — Make the script executable:
sudo chmod +x /usr/local/bin/lego-deploy-nginx.sh
Step C — Get your certificate with the deploy hook:
lego --email "$EMAIL" \
--server "$ACME_SERVER" \
--eab --kid "$EAB_KEY_ID" --hmac "$EAB_HMAC_KEY" \
--dns cloudflare \
--domains demo.example.com \
--run-hook "/usr/local/bin/lego-deploy-nginx.sh" \
run
Step D — Configure Nginx to use SSL:
server {
listen 443 ssl http2;
server_name demo.example.com;
root /var/www/demo.example.com;
ssl_certificate /etc/nginx/ssl/demo.example.com.crt;
ssl_certificate_key /etc/nginx/ssl/demo.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
add_header Strict-Transport-Security "max-age=63072000" always;
}
server {
listen 80;
server_name demo.example.com;
return 301 https://$host$request_uri; # Redirect HTTP to HTTPS
}
sudo nginx -t # Test config first — check for errors
sudo systemctl reload nginx # Apply changesOn Windows, IIS uses the Windows Certificate Store rather than plain files. The script below handles the conversion and import for you.
Step A — Save this script as C:\Scripts\lego-deploy-iis.ps1:
$Domain = "demo.example.com"
$CertSrc = "$env:USERPROFILE\.lego\certificates"
$CertPath = "$CertSrc\$Domain.crt"
$KeyPath = "$CertSrc\$Domain.key"
$PfxPath = "$env:TEMP\$Domain.pfx"
$PfxPassword = ConvertTo-SecureString -String "TempPassword123!" -Force -AsPlainText
# Convert certificate files to PFX format (required by Windows)
& "C:\Program Files\Git\usr\bin\openssl.exe" pkcs12 -export `
-out $PfxPath -inkey $KeyPath -in $CertPath -password pass:TempPassword123!
# Import into Windows Certificate Store
$Cert = Import-PfxCertificate -FilePath $PfxPath `
-CertStoreLocation Cert:\LocalMachine\My -Password $PfxPassword
# Bind to IIS site
Import-Module WebAdministration
$Binding = Get-WebBinding -Name "Default Web Site" -Protocol https
if ($Binding) { $Binding.AddSslCertificate($Cert.Thumbprint, "My") }
Remove-Item $PfxPath -Force # Clean up temporary file
Add-Content -Path "C:\Scripts\lego-deploy.log" `
-Value "$(Get-Date): IIS certificate deployed for $Domain"
Step B — Issue certificate with the IIS deploy hook:
lego --email "$env:EMAIL" `
--server "$env:ACME_SERVER" `
--eab --kid "$env:EAB_KEY_ID" --hmac "$env:EAB_HMAC_KEY" `
--dns cloudflare `
--domains demo.example.com `
--run-hook "powershell.exe -ExecutionPolicy Bypass -File C:\Scripts\lego-deploy-iis.ps1" `
run
If you need to deploy to multiple folders, multiple web servers, or even remote servers all at once, use this advanced script. It includes error checking and logging.
Save as /usr/local/bin/lego-deploy-custom.sh:
#!/bin/bash
set -e # Stop immediately if anything fails
DOMAIN="$LEGO_CERT_DOMAIN" # Lego provides this automatically
CERT_PATH="$LEGO_CERT_PATH"
KEY_PATH="$LEGO_CERT_KEY_PATH"
LOG_FILE="/var/log/lego-deploy.log"
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"; }
log "Starting deployment for: $DOMAIN"
# Check files exist before proceeding
if [[ ! -f "$CERT_PATH" ]] || [[ ! -f "$KEY_PATH" ]]; then
log "ERROR: Certificate or key file not found"; exit 1
fi
# Deploy to multiple local directories
for location in "/etc/nginx/ssl" "/etc/apache2/ssl" "/opt/custom-app/ssl"; do
if [[ -d "$location" ]]; then
cp "$CERT_PATH" "$location/$DOMAIN.crt"
cp "$KEY_PATH" "$location/$DOMAIN.key"
chmod 644 "$location/$DOMAIN.crt"
chmod 600 "$location/$DOMAIN.key"
log "Deployed to $location"
fi
done
# Reload all running web services
for service in nginx apache2 custom-app; do
if systemctl is-active --quiet "$service"; then
systemctl reload "$service"; log "Reloaded $service"
fi
done
# Copy to remote servers (requires SSH key access)
for server in server1.local server2.local; do
scp "$CERT_PATH" "root@$server:/etc/ssl/certs/$DOMAIN.crt"
scp "$KEY_PATH" "root@$server:/etc/ssl/private/$DOMAIN.key"
ssh "root@$server" "systemctl reload nginx"
log "Deployed to remote $server"
done
log "Deployment completed for $DOMAIN"
sudo chmod +x /usr/local/bin/lego-deploy-custom.sh
lego --email "$EMAIL" \
--server "$ACME_SERVER" \
--eab --kid "$EAB_KEY_ID" --hmac "$EAB_HMAC_KEY" \
--dns cloudflare \
--domains demo.example.com \
--run-hook "/usr/local/bin/lego-deploy-custom.sh" \
run
SSL certificates expire — they have a "use by" date. Lego can check your certificates automatically and renew them before they run out, so you never get a scary "Certificate Expired" warning on your website. By default, Lego renews when there are 30 days left.
The renew command checks if your certificate needs renewing. If it does, it renews it and runs any deploy hooks. If it doesn't need renewing yet, it does nothing — safe to run any time.
|
???? HOW IT WORKS The |
A cron job is a scheduled task that runs a command at set times. We will set one up to run every day at 2 AM to check and renew your certificates.
Step A — Create the renewal script:
sudo nano /usr/local/bin/lego-renew.sh
Paste this content into the file:
#!/bin/bash
source "$HOME/.lego/config/acme-server.env"
DOMAINS=("demo.example.com" "example.com" "*.wildcard-example.com")
LOG_FILE="/var/log/lego-renewal.log"
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"; }
log "Starting renewal check"
for domain in "${DOMAINS[@]}"; do
log "Checking: $domain"
lego --email "$EMAIL" \
--server "$ACME_SERVER" \
--dns cloudflare \
--domains "$domain" \
--run-hook "/usr/local/bin/lego-deploy-nginx.sh" \
renew --days 30 >> "$LOG_FILE" 2>&1
[ $? -eq 0 ] && log "Done: $domain" || log "ERROR: $domain"
done
log "Renewal check finished"
Step B — Make the script executable:
sudo chmod +x /usr/local/bin/lego-renew.sh
Option 1 — Add a Cron Job (runs daily at 2 AM):
sudo crontab -e
# Add this line inside the editor:
0 2 * * * /usr/local/bin/lego-renew.sh >> /var/log/lego-cron.log 2>&1
Option 2 — Use Systemd Timer (recommended on modern Linux):
Systemd timers are more reliable than cron jobs on modern Linux systems.
Create a service file at /etc/systemd/system/lego-renewal.service:
[Unit]
Description=Lego Certificate Renewal
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/lego-renew.sh
User=root
Create a timer file at /etc/systemd/system/lego-renewal.timer:
[Unit]
Description=Lego Certificate Renewal Timer
Requires=lego-renewal.service
[Timer]
OnCalendar=daily
RandomizedDelaySec=1h # Runs at a random time within 1 hour to avoid server overload
Persistent=true # Re-runs missed jobs if the machine was off
[Install]
WantedBy=timers.target
Enable and start the timer:
sudo systemctl daemon-reload
sudo systemctl enable lego-renewal.timer
sudo systemctl start lego-renewal.timer
# Check that it is active:
sudo systemctl list-timers lego-renewal.timer
On Windows, we use Task Scheduler to run the renewal script automatically every day.
Step A — Create the PowerShell renewal script at C:\Scripts\lego-renew.ps1:
. "$env:USERPROFILE\.lego\config\acme-server.ps1"
$Domains = @("demo.example.com", "example.com", "*.wildcard-example.com")
$LogFile = "C:\Scripts\lego-renewal.log"
function Write-Log { param($Message)
"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - $Message" |
Tee-Object -FilePath $LogFile -Append
}
Write-Log "Starting renewal check"
foreach ($Domain in $Domains) {
Write-Log "Checking: $Domain"
lego --email "$env:EMAIL" `
--server "$env:ACME_SERVER" `
--dns cloudflare `
--domains $Domain `
--run-hook "powershell.exe -File C:\Scripts\lego-deploy-iis.ps1" `
renew --days 30 2>&1 | Tee-Object -FilePath $LogFile -Append
if ($LASTEXITCODE -eq 0) { Write-Log "Done: $Domain" }
else { Write-Log "ERROR: $Domain" }
}
Write-Log "Renewal check finished"
Step B — Register the Task Scheduler task (run PowerShell as Administrator):
$Action = New-ScheduledTaskAction -Execute "powershell.exe" `
-Argument "-ExecutionPolicy Bypass -File C:\Scripts\lego-renew.ps1"
$Trigger = New-ScheduledTaskTrigger -Daily -At 2AM
$Principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" `
-LogonType ServiceAccount -RunLevel Highest
$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries -StartWhenAvailable
Register-ScheduledTask -TaskName "Lego Certificate Renewal" `
-Action $Action -Trigger $Trigger -Principal $Principal `
-Settings $Settings `
-Description "Daily renewal check for Lego SSL certificates"
# Confirm it was created:
Get-ScheduledTask -TaskName "Lego Certificate Renewal"
This section covers more advanced situations: deploying to multiple servers, Docker containers, Kubernetes, and Azure Key Vault.
If you run multiple web servers and want all of them to get the new certificate automatically, use this script. It copies the certificate to each server using SSH.
|
?? PREREQUISITE SSH key-based authentication must be set up between your main server and the remote servers. The remote servers must be accessible via SSH without a password prompt. |
Save as /usr/local/bin/lego-deploy-multi.sh:
#!/bin/bash
DOMAIN="$LEGO_CERT_DOMAIN"
CERT_PATH="$LEGO_CERT_PATH"
KEY_PATH="$LEGO_CERT_KEY_PATH"
REMOTE_SERVERS=("server1.local" "server2.local" "server3.local")
LOG_FILE="/var/log/lego-multi-deploy.log"
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"; }
log "Multi-server deployment for: $DOMAIN"
for server in "${REMOTE_SERVERS[@]}"; do
log "Deploying to $server"
scp "$CERT_PATH" "root@$server:/etc/ssl/certs/$DOMAIN.crt"
scp "$KEY_PATH" "root@$server:/etc/ssl/private/$DOMAIN.key"
ssh "root@$server" "chmod 644 /etc/ssl/certs/$DOMAIN.crt"
ssh "root@$server" "chmod 600 /etc/ssl/private/$DOMAIN.key"
ssh "root@$server" "systemctl reload nginx"
log "Done: $server"
done
log "Multi-server deployment complete"
If your web server runs inside a Docker container, use this script to copy certificates to the Docker volume directory and restart the containers.
Save as /usr/local/bin/lego-deploy-docker.sh:
#!/bin/bash
DOMAIN="$LEGO_CERT_DOMAIN"
CERT_PATH="$LEGO_CERT_PATH"
KEY_PATH="$LEGO_CERT_KEY_PATH"
DOCKER_VOL="/var/docker/ssl"
mkdir -p "$DOCKER_VOL"
cp "$CERT_PATH" "$DOCKER_VOL/$DOMAIN.crt"
cp "$KEY_PATH" "$DOCKER_VOL/$DOMAIN.key"
chmod 644 "$DOCKER_VOL/$DOMAIN.crt"
chmod 600 "$DOCKER_VOL/$DOMAIN.key"
# Restart the containers so they pick up the new certificate
docker restart nginx-proxy
docker restart web-app
echo "$(date): Docker containers updated for $DOMAIN" >> /var/log/lego-docker.log
Kubernetes stores certificates as "Secrets". This script creates or updates a TLS Secret in your cluster whenever a certificate is renewed.
Save as /usr/local/bin/lego-deploy-k8s.sh:
#!/bin/bash
DOMAIN="$LEGO_CERT_DOMAIN"
CERT_PATH="$LEGO_CERT_PATH"
KEY_PATH="$LEGO_CERT_KEY_PATH"
NAMESPACE="production"
SECRET_NAME="tls-secret-$(echo $DOMAIN | tr '.' '-')"
# Create or update the TLS Secret in Kubernetes
kubectl create secret tls "$SECRET_NAME" \
--cert="$CERT_PATH" \
--key="$KEY_PATH" \
--namespace="$NAMESPACE" \
--dry-run=client -o yaml | kubectl apply -f -
# Restart the ingress controller to pick up the new certificate
kubectl rollout restart deployment/nginx-ingress-controller -n ingress-nginx
echo "$(date): Kubernetes secret updated for $DOMAIN" >> /var/log/lego-k8s.log
Azure Key Vault is a secure cloud storage service for secrets and certificates. This script converts your certificate to PFX format (required by Azure) and uploads it directly.
Save as /usr/local/bin/lego-deploy-azure-keyvault.sh:
#!/bin/bash
DOMAIN="$LEGO_CERT_DOMAIN"
CERT_PATH="$LEGO_CERT_PATH"
KEY_PATH="$LEGO_CERT_KEY_PATH"
VAULT_NAME="mykeyvault"
CERT_NAME=$(echo "$DOMAIN" | tr '.' '-') # Azure names cannot contain dots
PFX_PASSWORD="SecurePassword123!"
# Step 1: Convert to PFX format (required by Azure Key Vault)
PFX_FILE="/tmp/$CERT_NAME.pfx"
openssl pkcs12 -export \
-out "$PFX_FILE" \
-inkey "$KEY_PATH" \
-in "$CERT_PATH" \
-password "pass:$PFX_PASSWORD"
# Step 2: Upload to Azure Key Vault
az keyvault certificate import \
--vault-name "$VAULT_NAME" \
--name "$CERT_NAME" \
--file "$PFX_FILE" \
--password "$PFX_PASSWORD"
# Step 3: Delete the temporary PFX file (keep it secure)
rm -f "$PFX_FILE"
echo "$(date): Certificate uploaded to Azure Key Vault for $DOMAIN" >> /var/log/lego-azure.log
After everything is set up, you need to keep an eye on your certificates to make sure renewals are working and to catch any issues early.
List all certificate files:
| ???? Linux / macOS | ???? Windows (PowerShell) |
|---|---|
ls -la ~/.lego/certificates/ |
dir "$env:USERPROFILE\.lego\certificates\" |
View full certificate details (domains, dates, issuer):
View certificate metadata (expiry date, covered domains):
| ???? Linux / macOS | ???? Windows (PowerShell) |
|---|---|
cat ~/.lego/certificates/demo.example.com.json | jq |
Get-Content "$env:USERPROFILE\.lego\certificates\demo.example.com.json" | ConvertFrom-Json |
|
|