4

I have nginx with a protected reverse proxy to a local IP:

server { server_name protected.example.com; client_max_body_size 100M; listen 0.0.0.0:443 ssl; ssl_certificate /path/to/lets-encrypt.crt; ssl_certificate_key /path/to/lets-encrypt.key; ssl_client_certificate /path/to/ClientAuthCA.pem; ssl_verify_client on; ssl_verify_depth 2; location / { proxy_pass http://10.90.0.2:3000; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; proxy_redirect off; } } 

This config works fine.

I wanted to add a custom error page for when the ssl client auth fails. ChatGPT suggested the following:

// 495: an error has occurred during the client certificate verification; // 496: a client has not presented the required certificate; error_page 495 496 = @client_cert_fail; location @client_cert_fail { // serve file } 

During testing I was able to reach the reverse proxy even though I didn't provide a certificate to the webserver.

It seems like this happens whenever location @client_cert_fail doesn't respond to the request, for example:

location @client_cert_fail { add_header Content-Type text/html; root /var/www/; index client_cert_fail.html; } 

(ignoring whether this location block is valid or not) it allows users past the client certificate authentication.

Doing something like:

location @client_cert_fail { return 400 "Client Cert Authentication failed"; } 

Fixes the problem. But my question is, why is a client able to bypass the client certificate authentication?

Why does it matter what's inside location @client_cert_fail when it's just for error handling?

I'm shocked nginx ignores the client authentication if my error handling is incorrect / wrong (?).

EDIT: Could it be that if location @client_cert_fail doesn't terminate the request, nginx is just looking for the next location block which happens to be the one who's proxying requests to the private IP?

1 Answer 1

5

That's because many people don't understand how the index directive actually works. If the request URI maps to a physical directory under the web server root, and a suitable index file is found in that directory, the index directive performs an internal redirect to the same URI with the index file name appended. This behavior is explicitly described in the documentation:

It should be noted that using an index file causes an internal redirect, and the request can be processed in a different location. For example, with the following configuration:

location = / { index index.html; } location / { ... } 

a / request will actually be processed in the second location as /index.html.

Thus, if the incoming request is for / and certificate authentication fails, nginx passes request processing to your named location @client_cert_fail. There, the index directive finds an index file client_cert_fail.html in the /var/www directory and performs an internal redirect to /client_cert_fail.html. At that point, the request is re-evaluated, and the best matched prefix or regex location block is selected to serve the new URI. For requests other than /, you're likely to get a 404 Not Found error while the request is still being processed within the named location, since the original request URI remains unchanged.

You can easily fix this by rewriting any request URI to /client_cert_fail.html inside your named location:

location @client_cert_fail { root /var/www; rewrite ^ /client_cert_fail.html break; } 

If you want to allow the client_cert_fail.html file to load additional assets, you can change the above configuration as follows:

location @client_cert_fail { root /var/www; try_files $uri /client_cert_fail.html =404; } 

The assets must, of course, be located under the same /var/www directory or its subdirectories. (The solution was originally authored by Richard Smith.)


An addition to the answer, addressing the OP's ongoing questions from the comments:

This perfectly explains why the config using index doesn't work! However, using an empty location @client_cert_fail {} block also allows clients to bypass client authentication. Is this because @client_cert_fail doesn't terminate the request and nginx is just looking for the next location block than can satisfy the request?

A request can leave the location it was initially assigned to only through an internal rewrite. This can happen implicitly in two main cases:

  1. Through the index directive (either inherited from a higher level or by implying the default index.html), if an index file is found in the directory mapped from the request URI and web server root.
  2. Through an error_page directive, if an error occurs during processing the request. However, this is less likely here, since you're already handling a 495/496 error. For this to happen, recursive_error_pages on; must be explicitly enabled.

Looking at the error log it shows me that when I leave the location @client_cert_fail {} block empty, it's generating an internal redirect to /usr/share/nginx/html/index.html which is then redirected to /index.html?. I'm not entirely sure why that happens (the redirect from /usr/share/nignx/html/index.html to /index.html?), but it confirms what you said: the location block is left by an index directive that must be implicit because I don't have an index directive anywhere in my nginx config.

You're not interpreting the error log entries quite correctly.

Both index and root directives have their defaults. For the index directive, the default is:

index index.html; 

Unfortunately, there's no way in nginx to completely disable the index directive, even though this is actually desired in some cases (see this thread for an example). Having a directive like noindex would really be helpful (I've even considered writing a patch for the nginx index module to add such a directive).

The root directive defaults to the html directory, relative to the prefix compile-time option value. You can check the actual prefix value for your nginx binary using the nginx -V command. The output will contain a line like:

configure arguments: --prefix=... 

For most packaged nginx distributions, the prefix is usually either /var/www or /usr/share/nginx, as in your case.

Back to your error log: nginx checks for the presence of the default index file index.html under the default document root /usr/share/nginx/html, finds it, and performs an internal redirect to the URI /index.html?, appending the (empty) query string to the rewritten URI. This behavior — preserving the query string for the rewritten URI — is particularly important when a PHP file such as index.php is specified as an index file using the index directive.

I suppose I should put additional checks (like if ($ssl_client_verify != SUCCESS) {}) in sensitive location blocks.

This is exactly what you should try to avoid by all means. An if directive, when used within a location block, has nothing to do with the traditional if ... then construction from imperative programming languages. The internal implementation of the if directive declared in the server and location contexts is completely different, and If is Evil... when used in the location context (if you're curious, you can read about how it actually works here).

You only need to ensure the request won't be internally rewritten when processed by your @client_cert_fail named location. Both provided examples guarantee this won't happen due to the index directive. In the first example, any request is rewritten to /client_cert_fail.html, and the index module does not process any request where the request URI does not end with a slash. In the second example, the try_files directive ensures the requested URI maps to a physical file under the /var/www directory, or it will be rewritten to /client_cert_fail.html otherwise. This directive cannot pass a request that maps to a directory to the next request processing phase, where the index module is actually invoked, due to the fact that the $uri/ parameter is not present.

7
  • I see. This perfectly explains why the config using index doesn't work! However, using an empty location @client_cert_fail {} block also allows clients to bypass client authentication. Is this because @client_cert_fail doesn't terminate the request and nginx is just looking for the next location block than can satisfy the request? Commented Aug 1 at 5:05
  • @Marco A request can leave the location it was initially assigned to only through an internal rewrite. This can happen implicitly in two main cases: 1. Through the index directive (either inherited from a higher level or by implying the default index.html), if an index file is found in the directory mapped from the request URI and web server root. 2. Through an error_page directive, if an error occurs during processing the request. However, this is less likely here, since you're already handling a 495/496 error. For this to happen, recursive_error_pages on; must be explicitly enabled. Commented Aug 1 at 9:31
  • Yeah, looking at the error.log it shows me that when I leave the location @client_cert_fail {} block empty, it's generating an internal redirect to /usr/share/nginx/html/index.html which is then redirected to /index.html?. I'm not entirely sure why that happens (the redirect from /usr/share/nignx/html/index.html to /index.html?), but it confirms what you said: the location block is left by an index directive that must be implicit because I don't have an index directive anywhere in my nginx config. Commented Aug 2 at 9:34
  • I suppose I should put additional checks (like if ($ssl_client_verify != SUCCESS) {}) in sensitive location blocks ... I didn't know one can jump out of a location directive that easily. Commented Aug 2 at 9:35
  • 1
    @Marco You'd better not. See the update to the answer. Commented Aug 2 at 14:57

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.