Using limit_except to deny all except GET, HEAD and POST

How does one use nginx's limit_except to deny all except typical http methods (i.e. GET and POST) in a typical website nginx configuration (e.g. a blog)? Imagine it were to reside in the server block and the blocks were configured to redirect all traffic to https and www.

An illustrative example of how to accomplish this would be great.

I understand that the somewhat controversial way to do it has been:

add_header Allow "GET, POST, HEAD" always;
if ( $request_method !~ ^(GET|POST|HEAD)$ ) {
return 405;
}

However, given if is evil, it makes sense to explore the alternative method.


There really isnt much more to do other than adding whats already mentioned in the docs to your server block:

server {
  server_name www.example.org;
  root /var/www/blog;

  ...
  # add this line (HEAD is implicit - it's a just special case of GET)
  limit_except GET POST { deny  all; }
  ...

}

It is not necessarily the best idea to just do that, though:

Before choosing to configure either method, note the difference between 405 and 403.

  • 403 refers to the accessing client not being authorized to do that request.
  • 405 refers to the server not allowing that method on that uri.

Using a combination of both is valid: you might want to tell users that PROPFIND is not allowed here, but then as the client tries PUT still tell him that while that might be an understood method, the specific request is still forbidden.

What you can configure with limit_except is only the the subset of restrictions that can lead to 403 - i do not see a way to trigger 405 using limit_except that would be more clear than just using if right away.

Here is an (untested) example that combines the 401, 403 and 405 responses and should clarify their precedence in a typical configuration:

server {
    listen 192.0.2.1 ssl http2;
    server_name example.org;
    ssl_certificate_key /etc/ssl/private/example.org.key;

    # nobody shall be able to delete anything on this server
    if ($request_method = DELETE)
    {
        # the concerns about using if are not applicable
        # if the block only contains "return" or "rewrite .. last"
        return 405;
    }

    root /var/www/html;

    location /.well-known {
        alias /var/www/well-known;
    }
    location / {
        # logging in from specific IPs grants acces without HBA login
        satisfy any;
        allow 203.0.113.0/24;
        deny all;
        auth_basic_user_file /etc/nginx/users.passwd;
        auth_basic "join VPN or hit enter on this prompt:";

        limit_except GET {
            # block does not inherit the access limitations from above
            deny all;
        }

        location /uploading {
            limit_except GET POST {
               deny all;
            }
            proxy_pass http://unix:/run/backend.sock:/upload/;
        }
    }
}

Example requests:

  1. DELETE requests method will return 405 (a standards-compliant configuration should add an Allow header, omitted here)
  2. GET from 203.0.113.0/24 will always reply based on /var/www/html
  3. PROPFIND from 203.0.113.0/24 will return 403
  4. any request from another IP missing HBA headers will return 401
  5. any POST request having valid HBA outside of /writable will return 403
  6. but inside /writable, POST requests would be proxied to the backend
  7. PROPFIND requests having valid HBA will return 403
  8. any method other than DELETE will reply based on /var/www/well-known