2

I've spent the last few hours trying to understand what is happening with the nginx location documentation and questions by other people on serverfault (like this one). From everything I gathered, my solution should work, but it just doesn't.

So as the title says, I'd like to configure nginx such that it will serve the challange file that acme.sh creates, but redirect everything else to a specific https domain of mine configured in the same server (this one works perfectly fine).

My server block:

server {
    listen      80 default_server;
    listen [::]:80 default_server;
    server_name _;
    root /usr/share/nginx/html;

    location ^~ /.well-known/acme-challenge/ {
        try_files $uri /;
    }
    location / {
        return 301 https://example.com$request_uri;
    }
}

For testing purposes, I've created a file myself in this directory: /usr/share/nginx/html/.well-known/acme-challange/somefile. The testing curl command: curl http://example.com/.well-known/acme-challange/somefile

Problem is: No matter which request, everything is always redirected. The moment I remove the / location block, however, the curl command will give me the contents of my test file. Obviously no redirects will be done in that case.

From the nginx documentation, my understanding is with the modifier I put, once the acme prefix matches (which it obviously does since it works when it is the only location block), it should not try to match anything else. I've also tried simply removing the try_files directive, but it makes no difference. Instead of return 301 ... I've also tried to do a rewrite, which also has the exact same behavior: There will always be a redirect no matter which URI is used.

For some reason the / location block is always used once it's there, even if there are more specific locations that match and even if said locations have the ^~ modifier.

Another variant I just tried:

server {
    listen      80 default_server;
    listen [::]:80 default_server;
    server_name _;
    root /usr/share/nginx/html;
    location /.well-known/acme-challenge/ {
        try_files $uri =404;
    }
    return 301 https://example.com$request_uri;
}

Very similar behavior: If the return is present -> redirect. If I comment it out: my test file is served to me with the curl call. One curious thing is that I cannot move the root directive inside the location blocks. If I do, nginx starts to look for files in /etc/nginx/html, which is apparently the default root, and will throw an error in the logs ("no such file or directory") when I do the http request.

Btw, I did not remove anything from this server block, I just changed the domain name to example.com. It is also the only server block listening on port 80 and no other configs are included in the file. Of course, after every change, I also restarted the nginx service. If it matters, my nginx version is 1.18.0

Output of nginx -T as requested in comments (domain name was changed and mime.types file was cut due to character limits here):

2021/04/23 02:21:00 [warn] 88558#88558: could not build optimal types_hash, you should increase either types_hash_max_size: 1024 or types_hash_bucket_size: 64; ignoring types_hash_bucket_size
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
# configuration file /etc/nginx/nginx.conf:
user html;
worker_processes  auto;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include mime.types;
    default_type application/octet-stream;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    ssl_prefer_server_ciphers on;
    ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES128+EECDH:AES256+EECDH:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256";
    ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
    ssl_dhparam /etc/ssl/certs/dhparam.pem;
    ssl_ecdh_curve secp384r1:prime256v1;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    gzip  on;

    # for certificate verification
    server {
        listen      80 default_server;
        listen [::]:80 default_server;
        server_name _;
        root /usr/share/nginx/html;
        location /.well-known/acme-challenge/ {
            try_files $uri =404;
        }
        #return 301 https://example.com$request_uri;
    }

    server {
        listen       443 ssl http2;
        listen  [::]:443 ssl http2;
        server_name  example.com;
        ssl_certificate ssl/example.com.crt;
        ssl_certificate_key ssl/example.com.key;

        #access_log  logs/host.access.log  main;

        root   /usr/share/nginx/html;
        index index.html index.php;
        location / {
            fastcgi_param SCRIPT_FILENAME $document_root/src/index.php;
            include fastcgi_params;
            # override SCRIPT_NAME which was set in fastcgi_params
            fastcgi_param SCRIPT_NAME /src/index.php;
            fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
        }
        location /public/ {
            allow all;
        }
        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        #error_page   500 502 503 504  /50x.html;
        #location = /50x.html {
        #    root   /usr/share/nginx/html;
        #}
    }
}


# configuration file /etc/nginx/mime.types:
types {
 #REMOVED BECAUSE OF CHARACTER LIMIT
}

# configuration file /etc/nginx/fastcgi_params:

fastcgi_param  QUERY_STRING       $query_string;
fastcgi_param  REQUEST_METHOD     $request_method;
fastcgi_param  CONTENT_TYPE       $content_type;
fastcgi_param  CONTENT_LENGTH     $content_length;

fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
fastcgi_param  REQUEST_URI        $request_uri;
fastcgi_param  DOCUMENT_URI       $document_uri;
fastcgi_param  DOCUMENT_ROOT      $document_root;
fastcgi_param  SERVER_PROTOCOL    $server_protocol;
fastcgi_param  REQUEST_SCHEME     $scheme;
fastcgi_param  HTTPS              $https if_not_empty;

fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;

fastcgi_param  REMOTE_ADDR        $remote_addr;
fastcgi_param  REMOTE_PORT        $remote_port;
fastcgi_param  SERVER_ADDR        $server_addr;
fastcgi_param  SERVER_PORT        $server_port;
fastcgi_param  SERVER_NAME        $server_name;

# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param  REDIRECT_STATUS    200;
Anpan
  • 123
  • 5
  • Could you add the output of: `nginx -T` ? Remove/edit any sensitive data. E.g. the website should be called www.example.com – Mircea Vutcovici Apr 23 '21 at 00:12
  • @MirceaVutcovici added. Had to remove the mime.types stuff tho due to size restrictions here. But that shouldn't be relevant as it's the stock file delivered by the nginx package on arch. – Anpan Apr 23 '21 at 00:31
  • FYI - your first server block example does not work because the slash in the return location block is a prefix match which takes precedence over the `^~` non-regular expression match, thus the letsencrypt location block is never selected and the return is always executed. The second one fails because the return is at the server level and thus takes precedence over all location blocks - again, the letsencrypt location block is never selected. – Simon Hampel Sep 10 '22 at 21:49
  • Related: https://serverfault.com/questions/1017545/unable-to-use-letsencrypt-certbot-when-http-to-https-redirect-is-setup – Jesse Nickles Dec 11 '22 at 18:24

1 Answers1

3

I made this to work with the following vhost configuration:

server {
    listen 80;

    server_name example.com;

    location /.well-known/ {
        root /whatever/i/specified/in/certbot/-d/argument;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

(the SSL vhost configuration is not important here)

What is different from yours is that I have root inside location. This setup works very well on multitude of reverse proxy and origin servers.

Nikita Kipriyanov
  • 10,947
  • 2
  • 24
  • 45
  • 1
    This worked! Now, do you have any idea why this works and my attempts above don't? I'd really like to know for future reference. – Anpan Apr 24 '21 at 12:50
  • 1
    @Anpan refer to the [Pitfalls and Common Mistakes](https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/#incorrect-return-context) page - Incorrect return context section. When a return directive is placed at a server level, it takes precedence over all location blocks and thus the letsencrypt location never gets selected. By wrapping the return in a location block, all location blocks are evaluated for the best match and letsencrypt will work because it uses a more specific match than the slash. – Simon Hampel Sep 10 '22 at 21:43