Back to Guides

Automating HTTP-01 on Nginx, Apache, and IIS

The simplest path to automated certificates

12 minDCV Automation
HTTP-01 automation on Nginx, Apache, and IIS

Watch: HTTP-01 Automation Explained

HTTP-01 is the most widely used ACME challenge type for good reason: no DNS credentials, no API tokens, no complex delegation. If you can serve a file on port 80, you can automate certificate issuance.

HTTP-01 is an ACME Domain Control Validation (DCV) method that proves you control a domain by responding to a challenge via a publicly reachable HTTP endpoint. Unlike legacy email-based DCV (admin@, hostmaster@), HTTP-01 is fully automatable and works with any ACME-compatible CA—not just Let's Encrypt, but also ZeroSSL, Buypass, Google Trust Services, and others.

Who this is for: SysAdmins, DevOps engineers, and web server administrators running Nginx, Apache, or IIS who want zero-touch certificate automation. This guide covers the configurations that actually work in production.

How HTTP-01 Validation Works

When you request a certificate using HTTP-01:

1

ACME client requests certificate

You ask for a cert for example.com.

2

CA provides token

Let's Encrypt returns a random token.

3

Client creates challenge file

Placed at http://example.com/.well-known/acme-challenge/{token}.

4

CA verifies file

Let's Encrypt fetches the URL from multiple vantage points.

5

Certificate issued

Validation passes, you get your cert.

The entire exchange happens over HTTP on port 80. Your ACME client handles everything automatically—you just need to make sure the challenge path is accessible.

Prerequisites

Before automating, verify:

  • Port 80 is open and reachable from the internet
  • Domain DNS points to your server
  • Web server can serve static files from .well-known/acme-challenge/
  • No redirects blocking the challenge path (more on this below)

Nginx

1Certbot with Nginx Plugin (Recommended)

Certbot's Nginx plugin handles everything—challenge files, certificate installation, and config updates.

# Install
sudo apt install certbot python3-certbot-nginx

# Issue and install certificate
sudo certbot --nginx -d example.com -d www.example.com

# Test renewal
sudo certbot renew --dry-run

Certbot automatically creates the challenge response, modifies your Nginx config to add SSL, and sets up auto-renewal via systemd timer. The --dry-run validates your DNS, firewall, and challenge path without hitting rate limits.

2Webroot Mode

If you don't want Certbot modifying your Nginx config:

# Issue certificate only (no auto-install)
sudo certbot certonly --webroot -w /var/www/html -d example.com

Note: The webroot path varies by distribution. Common defaults: /var/www/html (Debian/Ubuntu), /usr/share/nginx/html (RHEL/CentOS). Confirm your actual document root with nginx -T | grep root before running.

Then manually configure Nginx:

server {
    listen 443 ssl;
    server_name example.com;
    
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    
    # Your site config...
}

Production tip: Enable OCSP stapling for faster TLS handshakes. See the Let's Encrypt Best Practices guide.

3Standalone Mode

For servers not running a web server (or during initial setup):

# Certbot spins up its own temporary server on port 80
sudo certbot certonly --standalone -d example.com

Note: This requires stopping Nginx temporarily if it's already bound to port 80.

Challenge Path Configuration

If using webroot mode or having issues, ensure Nginx serves the challenge directory:

server {
    listen 80;
    server_name example.com;
    
    # ACME challenge location - must not redirect
    location ^~ /.well-known/acme-challenge/ {
        root /var/www/html;
        default_type "text/plain";
        try_files $uri =404;
    }
    
    # Redirect everything else to HTTPS
    location / {
        return 301 https://$host$request_uri;
    }
}

The ^~ modifier ensures this location takes priority over other rules.

Apache

1Certbot with Apache Plugin (Recommended)

# Install
sudo apt install certbot python3-certbot-apache

# Issue and install certificate
sudo certbot --apache -d example.com -d www.example.com

# Test renewal
sudo certbot renew --dry-run

Certbot automatically updates your Apache virtual host configuration. The --dry-run validates your DNS, firewall, and challenge path without hitting rate limits.

2Webroot Mode

sudo certbot certonly --webroot -w /var/www/html -d example.com

Manual Apache SSL configuration:

<VirtualHost *:443>
    ServerName example.com
    
    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
    
    # Your site config...
</VirtualHost>

Challenge Path Configuration

Ensure the challenge directory is accessible and not redirected:

<VirtualHost *:80>
    ServerName example.com
    DocumentRoot /var/www/html
    
    # Allow ACME challenges
    <Directory "/var/www/html/.well-known/acme-challenge">
        Options None
        AllowOverride None
        Require all granted
    </Directory>
    
    # Redirect everything else to HTTPS (except challenges)
    RewriteEngine On
    RewriteCond %{REQUEST_URI} !^/.well-known/acme-challenge/
    RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R=301,L]
</VirtualHost>

Note: Virtual host-level redirects (as shown above) are preferred for performance. Use .htaccess only when you cannot modify the main Apache configuration.

If using .htaccess for redirects, add this exception:

RewriteEngine On
RewriteCond %{REQUEST_URI} !^/.well-known/acme-challenge/
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

IIS (Windows)

1win-acme (Recommended)

win-acme (formerly letsencrypt-win-simple) is the standard ACME client for Windows/IIS.

# Download and extract win-acme
# Run as Administrator

.\wacs.exe

Interactive mode walks you through:

  1. Select IIS site
  2. Choose domains from bindings
  3. Validate via HTTP-01
  4. Install certificate to IIS
  5. Configure automatic renewal

Scheduled task created automatically — win-acme sets up renewal in Windows Task Scheduler.

win-acme Command Line

For scripted deployments:

# Simple certificate for single site
wacs.exe --target iis --siteid 1 --installation iis

# Specific bindings
wacs.exe --target iis --host example.com,www.example.com --installation iis

2Posh-ACME (PowerShell Native)

For PowerShell-centric environments:

# Install module
Install-Module -Name Posh-ACME

# Set Let's Encrypt as CA
Set-PAServer LE_PROD

# Get certificate
New-PACertificate -Domain example.com -AcceptTOS

# Certificate files in: $env:LOCALAPPDATA\Posh-ACME\

After issuance, you must bind the certificate to your IIS site. Example using the certificate thumbprint:

# Bind certificate to IIS site (run after certificate issuance)
$cert = Get-PACertificate

# Bind to IIS using netsh
netsh http add sslcert hostnameport="example.com:443" certhash=$cert.Thumbprint appid='{00000000-0000-0000-0000-000000000000}'

# Or use IIS PowerShell module
Import-Module WebAdministration
New-WebBinding -Name "Default Web Site" -Protocol https -Port 443
$binding = Get-WebBinding -Name "Default Web Site" -Protocol https
$binding.AddSslCertificate($cert.Thumbprint, "My")

IIS Challenge Path Configuration

IIS may block extensionless files in the challenge directory. Fix with web.config:

Create /site-root/.well-known/acme-challenge/web.config:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <staticContent>
            <mimeMap fileExtension="." mimeType="text/plain" />
        </staticContent>
        <handlers>
            <clear />
            <add name="StaticFile" path="*" verb="*" 
                 modules="StaticFileModule" 
                 resourceType="Either" 
                 requireAccess="Read" />
        </handlers>
    </system.webServer>
</configuration>

This ensures IIS serves the challenge tokens correctly.

Common Gotchas and Fixes

Port 80 Blocked

Symptom: Connection timeout during validation

Causes:

  • Firewall blocking inbound port 80
  • ISP blocking port 80 (rare, mostly residential)
  • Cloud security group misconfigured

Fix: Open port 80, or use DNS-01 validation instead.

You need port 80 open even if your site is HTTPS-only. The ACME challenge always happens over HTTP. Note: wildcard certificates (*.example.com) always require DNS-01—HTTP-01 cannot validate wildcards.

HTTPS Redirect Breaking Validation

Symptom: "Invalid response" or redirect loop errors

Causes:

  • Blanket HTTP→HTTPS redirect includes the challenge path

Fix: Exclude /.well-known/acme-challenge/ from redirects (see configs above).

Load Balancer in Front

Symptom: Validation fails intermittently

Causes:

  • Challenge file only exists on one backend server; CA hits a different one (common with NGINX, AWS ALB, Cloudflare, F5)

Fix: Use shared storage (NFS, EFS), sticky sessions, DNS-01, or centralized issuance.

Wrong Document Root

Symptom: 404 on challenge URL

Causes:

  • ACME client creating files in wrong directory

Fix: Verify webroot path and specify correct directory with -w flag.

CDN Caching Challenge Response

Symptom: CA sees stale or missing challenge

Causes:

  • CDN caching the 404 or old token

Fix: Exclude /.well-known/ from CDN caching, bypass CDN for validation, or use DNS-01.

Multiple Servers, Same Domain

Issue certificates on one server and distribute to others. For larger estates, use centralized issuance on a single ACME client host and distribute certificates via configuration management tools (Ansible, Puppet, SCCM):

┌─────────────────┐     ┌──────────────────┐
│  ACME Client    │────▶│  Shared Storage  │
│  (one server)   │     │  (NFS/EFS/S3)    │
└─────────────────┘     └────────┬─────────┘
                                 │
                    ┌────────────┼────────────┐
                    ▼            ▼            ▼
               ┌────────┐  ┌────────┐  ┌────────┐
               │ Web 1  │  │ Web 2  │  │ Web 3  │
               └────────┘  └────────┘  └────────┘

Renewal Automation

Linux (systemd)

Certbot installs a systemd timer by default:

# Check timer status
sudo systemctl status certbot.timer

# View timer schedule
sudo systemctl list-timers | grep certbot

Runs twice daily, only renews if expiring within 30 days.

Linux (cron)

If not using systemd:

# Add to root crontab
0 3 * * * certbot renew --quiet --deploy-hook "systemctl reload nginx"

The --deploy-hook only runs when a certificate is actually renewed, not on every check. This prevents unnecessary service reloads.

Windows

win-acme creates a scheduled task automatically. For Posh-ACME or manual setups:

# Create scheduled task
$action = New-ScheduledTaskAction -Execute "PowerShell.exe" `
    -Argument "-File C:\Scripts\renew-certs.ps1"
$trigger = New-ScheduledTaskTrigger -Daily -At 3am
Register-ScheduledTask -TaskName "ACME Renewal" -Action $action -Trigger $trigger

Running daily is safe—ACME clients only renew when the certificate is within 30 days of expiry, so most daily runs exit immediately with no action.

Post-Renewal Hooks

Reload web server after renewal to pick up new certificate:

# Nginx
certbot renew --post-hook "systemctl reload nginx"

# Apache
certbot renew --post-hook "systemctl reload apache2"

Verification and Testing

Test Challenge Path Accessibility

# Create test file
echo "test" | sudo tee /var/www/html/.well-known/acme-challenge/test

# Verify externally
curl -I http://example.com/.well-known/acme-challenge/test

# Should return 200 OK with text/plain content

Browser tip: Also test by opening the challenge URL in your browser. You should see the raw file content with a 200 OK response—no redirects, no HTML wrapper. If you see a redirect or error page, fix your web server config before attempting issuance.

Use Staging Environment First

Let's Encrypt has rate limits. Test with staging:

certbot --staging -d example.com

Staging certs aren't trusted but validate your setup without hitting limits.

Check Certificate Installation

# Verify cert is installed
echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -dates

# Check full chain
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -issuer -subject

Quick Reference

ServerRecommended ToolInstall Command
NginxCertbot + nginx pluginapt install certbot python3-certbot-nginx
ApacheCertbot + apache pluginapt install certbot python3-certbot-apache
IISwin-acmeDownload from win-acme.com
Use caseApproach
Single server, standard setupCertbot with server plugin
Don't want config changesWebroot mode
No web server running yetStandalone mode
Load balanced / multi-serverShared storage or DNS-01
Port 80 blockedDNS-01
Wildcard certificate neededDNS-01 (HTTP-01 can't do wildcards)
Internal / air-gapped systemsInternal CA, manual issuance, or DNS-01 via delegated zone

Related Resources