23 Stateless Mode
Frank W edited this page 2026-10-14 13:46:53 +02:00

Stateless Mode

Configure your webserver to respond statelessly to challenges for a given account key. This requires nothing more than a one-time web server configuration change and no "moving parts".

  1. First get your account key thumbprint:

    root@ed:~# acme.sh --register-account
    [Mon Feb  6 21:40:18 CST 2017] Registering account
    [Mon Feb  6 21:40:19 CST 2017] Already registered
    [Mon Feb  6 21:40:21 CST 2017] Update success.
    [Mon Feb  6 21:40:21 CST 2017] ACCOUNT_THUMBPRINT='6fXAG9VyG0IahirPEU2ZerUtItW2DHzDzD9wZaEKpqd'
    

    Remember the thumbprint in the last line: 6fXAG9VyG0IahirPEU2ZerUtItW2DHzDzD9wZaEKpqd

  2. Configure the web server to return the account key thumbprint:

    NGINX

    Add something similar to your nginx.conf:

    http {
    ...
      server {
      ...
        location ~ ^/\.well-known/acme-challenge/([-_a-zA-Z0-9]+)$ {
          default_type text/plain;
          return 200 "$1.6fXAG9VyG0IahirPEU2ZerUtItW2DHzDzD9wZaEKpqd";
        }
      ...
      }
    }
    

    CADDY

    Add something similar to your Caddyfile:

    example.com {
      @achallenge {
        path_regexp ch ^/\.well-known/acme-challenge/([-_a-zA-Z0-9]+)$
     }
     respond @achallenge "{re.ch.1}.6fXAG9VyG0IahirPEU2ZerUtItW2DHzDzD9wZaEKpqd"
    

    APACHE

    Tested on Apache 2.4.62

    Add something similar to your httpd.conf:

    ...
    <VirtualHost *:80>
        ...
        <LocationMatch "/.well-known/acme-challenge/(?<challenge>[-_a-zA-Z0-9]+)">
            RewriteEngine On
            RewriteRule "^([-_a-zA-Z0-9]+)$" "$1" [E=challenge:$1]
            ErrorDocument 200 "%{ENV:MATCH_CHALLENGE}.6fXAG9VyG0IahirPEU2ZerUtItW2DHzDzD9wZaEKpqd"
            RewriteRule ^ - [L,R=200]
        </LocationMatch>
        ...
    </VirtualHost>
    ...
    

    HAPROXY

    Add the http-request return rule to your configuration:

    global
        setenv ACCOUNT_THUMBPRINT '6fXAG9VyG0IahirPEU2ZerUtItW2DHzDzD9wZaEKpqd'
        log stderr local0
        stats socket /var/run/haproxy.sock level admin mode 0666
    
    frontend web
        log global
        option httplog
        mode  http
        bind :80
        bind :443 ssl crt /etc/haproxy/certs/
        http-request return status 200 content-type text/plain lf-string "%[path,field(-1,/)].${ACCOUNT_THUMBPRINT}\n" if { path_reg '^/.well-known/acme-challenge/[-_a-zA-Z0-9]+$' }
    

    IIS

    The solution for the IIS (Microsoft) is a bit more difficult as it does not support returning a custom body directly in a web.config. So we need two parts, first the web.config file in the .well-known/acme-challenge/ folder:

    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
      <system.webServer>
        <httpRedirect enabled="false" />
        <validation validateIntegratedModeConfiguration="false" />
        <staticContent>
          <clear />
          <mimeMap fileExtension="." mimeType="text/json" />
          <mimeMap fileExtension=".*" mimeType="text/json" />
        </staticContent>	
    	<rewrite>
    	    <rules>
              <rule name="ACME Stateless Mode" stopProcessing="true">
                <match url="^([A-Za-z0-9_-]+)$" />
                <action type="Rewrite" url="acme-challenge.ashx?token={R:1}" appendQueryString="false" />
              </rule>
            </rules>
    	</rewrite>
      </system.webServer>
      <system.web>
        <authorization>
          <allow users="*" />
        </authorization>
      </system.web>
    </configuration>
    

    The config above also includes some useful configuration entries for IIS when using it for the ACME challenge. As second part we use a ASP.NET handler to return the correct content, store this as acme-challenge.ashx in the same folder, do not forget to adjust the thumbprint:

    <%@ WebHandler Language="C#" Class="AcmeChallengeHandler" %>
    
    using System;
    using System.Text.RegularExpressions;
    using System.Web;
    
    public class AcmeChallengeHandler : IHttpHandler
    {
        // Your ACME account thumbprint (e.g. from `acme.sh --register-account`).
        private const string AccountThumbprint = "YOUR_THUMBPRINT_HERE";
    
        // ACME tokens are base64url: letters, digits, '-' and '_'.
        private static readonly Regex TokenPattern =
            new Regex("^[A-Za-z0-9_-]{1,256}$", RegexOptions.Compiled);
    
        public bool IsReusable
        {
            get { return true; }
        }
    
        public void ProcessRequest(HttpContext context)
        {
            HttpResponse response = context.Response;
            response.Clear();
            response.ContentType = "text/plain";
            response.Cache.SetCacheability(HttpCacheability.NoCache);
    
            string token = context.Request.QueryString["token"] ?? string.Empty;
    
            if (!TokenPattern.IsMatch(token))
            {
                response.StatusCode = 404;
                response.Write("Not found");
                return;
            }
    
            response.StatusCode = 200;
            response.Write(token + "." + AccountThumbprint);
        }
    }
    
  3. Ok, you can issue cert now.

acme.sh --issue -d example.com  --stateless