Troubleshooting acme.sh: Why Nginx Mode Fails with Redirects (and How to Fix It with Webroot)

If you use acme.sh to manage your Let’s Encrypt certificates on an Nginx server, you might appreciate the convenience of the --nginx mode. It promises to automatically edit your configuration, handle the validation, and revert the changes.

However, if you enforce HTTP to HTTPS redirects (or use HSTS), Nginx mode often fails, resulting in frustrated timeout errors or 404 Not Found responses during renewal.

In this post, we’ll explore why the automatic Nginx mode breaks in these scenarios and how to switch to the robust Webroot mode for 100% reliability.

The Problem: “Invalid Response” and Timeouts

You attempt to renew your certificates using the standard Nginx mode command:

Bash

acme.sh --renew-all
# OR
acme.sh --issue -d example.com --nginx

Instead of a success message, you get an error log that looks like this:

Plaintext

[Tue Feb 10 15:21:31 GMT 2026] Error renewing mail.unki.net_ecc.
[Tue Feb 10 15:21:31 GMT 2026] invalid status, verification error details:
Invalid response from http://example.com/.well-known/acme-challenge/xhq...: 404

Or worse, you might hit Let’s Encrypt rate limits because the automatic verification keeps failing.

The Root Cause: The “Intrusive” Config vs. 301 Redirects

The issue lies in how acme.sh‘s Nginx mode interacts with 301 redirects.

  1. How Nginx Mode works: It parses your nginx.conf and temporarily injects a location block to handle the .well-known/acme-challenge/ request, pointing it to a temporary internal directory.
  2. Your Config: Most secure sites have a global catch-all block to force HTTPS:Nginxserver { listen 80; return 301 https://$host$request_uri; }
  3. The Conflict: When Let’s Encrypt hits your HTTP (port 80) URL, Nginx prioritizes the return 301 directive. The request is redirected to HTTPS before acme.sh‘s temporary injection can handle it.
  4. The Failure: The validator follows the redirect to HTTPS. If your HTTPS block doesn’t explicitly know how to handle the challenge (because acme.sh only injected the rule into port 80, or the SSL handshake fails), it returns a 404.

The Solution: Webroot Mode with Explicit Nginx Config

The fix is to switch from the “intrusive” Nginx mode to Webroot mode. Instead of letting the script guess how to modify your config, we will explicitly tell Nginx how to handle validation requests and simply ask acme.sh to write the file to disk.

Step 1: Prepare Nginx for the Challenge (HTTP & HTTPS)

We need to create a “backdoor” in your Nginx config that allows Let’s Encrypt to access the verification files.

Crucially, we must add this to BOTH the HTTP (port 80) and HTTPS (port 443) blocks. Why HTTPS? Because if a validator follows a redirect or if you have HSTS enabled, the validation request will arrive at port 443. If this block is missing there, it will fail (often with a 404).

Edit your nginx.conf:

Nginx

# --- 1. HTTP Server Block ---
server {
    listen 80;
    server_name example.com;

    # Priority Rule: Handle ACME challenge immediately
    # The "^~" modifier stops Nginx from checking other regex locations (like redirects)
    location ^~ /.well-known/acme-challenge/ {
        default_type "text/plain";
        root /usr/share/nginx/html;  # Your actual web root path
    }

    # Redirect everything else to HTTPS
    location / {
        return 301 https://$host$request_uri;
    }
}

# --- 2. HTTPS Server Block ---
server {
    listen 443 ssl;
    server_name example.com;

    # SSL Configuration...
    ssl_certificate /etc/ssl/certs/example.com.cer;
    ssl_certificate_key /etc/ssl/certs/example.com.key;

    # Redundancy Rule: Handle ACME challenge here too!
    # Essential for renewals where redirects or HSTS force the connection to HTTPS
    location ^~ /.well-known/acme-challenge/ {
        default_type "text/plain";
        root /usr/share/nginx/html;  # Must match the path above
    }

    # Your normal application logic
    location / {
        try_files $uri $uri/ /index.php?$args;
    }
}

Don’t forget to reload Nginx:

Bash

sudo nginx -t
sudo systemctl reload nginx

Step 2: Verify the Fix

Before running the renewal, test if the “backdoor” works. Create a dummy file:

Bash

mkdir -p /usr/share/nginx/html/.well-known/acme-challenge/
echo "success" > /usr/share/nginx/html/.well-known/acme-challenge/test.txt

Now, test both HTTP and HTTPS access:

Bash

# Should return "success" directly (no 301 redirect)
curl http://example.com/.well-known/acme-challenge/test.txt

# Should return "success" (proving the HTTPS block handles it too)
curl https://example.com/.well-known/acme-challenge/test.txt

Step 3: Issue Certificate using Webroot Mode

Now, tell acme.sh to use the path you defined (-w) instead of trying to modify Nginx (--nginx).

Bash

acme.sh --issue -d example.com -w /usr/share/nginx/html --server letsencrypt --force

Once successful, install the cert. This ensures Nginx reloads automatically in the future:

Bash

acme.sh --install-cert -d example.com \
--key-file       /etc/ssl/certs/example.com.key  \
--fullchain-file /etc/ssl/certs/example.com.full.cer \
--reloadcmd     "systemctl reload nginx"

Conclusion: By installing the certificate this way, acme.sh saves the configuration for you. In the future, the automatic cron job (acme.sh --renew-all) will remember to use Webroot mode, ensuring your certificates renew successfully every time without any manual intervention.

Summary

While acme.sh‘s Nginx mode is great for simple configurations, Webroot mode is the gold standard for stability in production environments. By manually configuring the .well-known location block in both your HTTP and HTTPS server blocks, you eliminate ambiguity and ensure that your SSL renewals survive Nginx updates, complex redirects, and strict HSTS policies.

Leave a Reply