Nginx – Not Picking Location with Longest Prefix

nginxssl-certificate

In this simplified nginx setup, I have two nginx location blocks, one for SSL certificate renewals, and one for fast proxy (for uwsgi/Django ).

When a SSL certificate renewal happens via acme.sh, it places files in /var/www/example.com/public/.well-known/acme-challenge/ and then tests if they are served up. However the fast proxy (Django) is handling the requests to /.well-known/acme-challenge/....... instead of the location = /\.well-known/acme-challenge/ location block.

How do I tell nginx to use the correct block? If I delete the fast proxy location block, then the location = /\.well-known/acme-challenge/ block works fine.

Here's the config:

server { 
    listen 443 ssl http2; 
    listen [::]:443 ssl http2; # For IP6 
    server_name www.example.com; 
    charset utf-8; 
    root /var/www/example.com/public; 

    # acme.sh SSL certificate issues
    location /\.well-known/acme-challenge/ {
    }

    # Finally, send all non-media requests to the Django server.
    location / {
        uwsgi_pass django;
        include /home/michael/project/conf/uwsgi/params;  
    } 

The nginx docs say:

When nginx selects a location block to serve a request it first checks location directives that specify prefixes, remembering location with the longest prefix, and then checks regular expressions. If there is a match with a regular expression, nginx picks this location or, otherwise, it picks the one remembered earlier.

To me /\.well-known/acme-challenge/ is the longest prefix, so it should "win".

Best Answer

Did you look at the documentation for the location directive?

A location can either be defined by a prefix string, or by a regular expression. Regular expressions are specified with the preceding “~*” modifier (for case-insensitive matching), or the “~” modifier (for case-sensitive matching).

To find location matching a given request, nginx first checks locations defined using the prefix strings (prefix locations). Among them, the location with the longest matching prefix is selected and remembered. Then regular expressions are checked, in the order of their appearance in the configuration file. The search of regular expressions terminates on the first match, and the corresponding configuration is used. If no match with a regular expression is found then the configuration of the prefix location remembered earlier is used.

Your location /\.well-known/acme-challenge/ is a prefix string, but you are trying to escape a . as it was a regular expression. This does not work.

For URL https://www.example.com/.well-known/acme-challenge/foo...

server { 
    listen 443 ssl http2; 
    server_name www.example.com; 

    # 1. Prefix string, NO MATCH.
    location /\.well-known/acme-challenge/ {
    }

    # 2. Longest matching prefix string.
    location /.well-known/acme-challenge/ {
    }

    # 3. First case sensitive regex match.
    location ~ /\.well-known/acme-challenge/ {
    }

    # 4. Another case sensitive regex match.
    location ~ ^/\.well-known/acme-challenge/ {
    }

    # 5. Matching prefix string, but not the longest.
    location / {
    }
}

If you had all these, the regex match #3 would be used, as it is the first matching regex. It would also match with /any/path/having/.well-known/acme-challenge/in/it unlike the #4 that matches only at the beginning of the URI path, similar to the prefix string matching.

TL;DR: #2 & #4 are equivalent and with both you could achieve your goal.

For completeness, the evaluation order would be:

  1. #1 Prefix, no match
  2. #2 Prefix, match (length=28)
  3. #5 Prefix, match (length=1)
  4. #3 Regex, match => chosen

The #4 would not be evaluated. Without #3 & #4, the #2 would win.

Related Topic