3

It's probably easiest to just show by example where I'm hitting issues, so I'll jump right in...

This snippet will work as-is for Nginx with rewrite/auth modules enabled. So hopefully this issue is quick & easy reproduce on pretty much any Nginx install...

server {
  listen 8081;
  add_header x-user bar;
  return 200;
}

server {
  listen 8080;

  location = /auth {
    internal;
    proxy_pass http://localhost:8081;
  }

  location / {
    auth_request /auth;
    auth_request_set $foo $upstream_http_x_user;

    add_header x-test $foo;
    add_header success true;

    proxy_pass http://example.com/;
  }
}

The above example Nginx site config does the following:

  1. Sends any request to /auth via an auth_request call
  2. The /auth location sends the request to another server which adds a header x-user bar
  3. auth_request_set sets a new var $foo based on the upstream header value of x-user set in step 2 above.
  4. A new header is set x-test which is set to the value of $foo
  5. The request proceededs to an external destination.

Response is exactly how I would expect, and confirmed the $foo var was set correctly:

$ curl -s --head http://localhost:8080 | grep -E 'HTTP|x-test'
HTTP/1.1 200 OK
x-test: bar

So, here comes the problem...
I need to adjust this config so that it'll return 403 if the value of the upstream header is incorrect.

Seemed like a simple task. So I added an if{} conditional to check the header:

  location / {
    auth_request /auth;
    auth_request_set $foo $upstream_http_x_user;

    # this 'if' is the only part added to the original config
    if ($foo != bar) {
      return 403;
    }

    add_header x-test $foo;
    add_header success true;

    proxy_pass http://example.com/;
  }

The if conditional evaluated true so I got a 403, which is not what I was expecting. So, this does not work:

$ curl -s --head http://localhost:8080 | grep -E 'HTTP|x-test'
HTTP/1.1 403 Forbidden

I realize that if is evil however I seem to be using it just to return which should be ok. I'm open to using any method to accomplish the same goal - with or without if, so I'm open to ideas!!

I have tried doing things like moving the auth_request and/or the if statements into the server{} block but nothing seems to make this evaluate the way I was expecting.


Further Troubleshooting / Details:

I have verified the problem is that the if is evaluated BEFORE the auth_request_set is

  location / {
    auth_request /auth;
    auth_request_set $foo $upstream_http_x_user;

    if ($foo != bar) {
      # x-test never gets set because $foo is null when if evaluates
      add_header x-test $foo always;
      add_header success false always;
      return 403;
    }

    add_header x-test $foo;
    add_header success true;

    proxy_pass http://example.com/;
  }
$ curl -s --head http://localhost:8080 | grep -E 'HTTP|x-test|success'
HTTP/1.1 403 Forbidden
success: false

I have verified this is not an issue if using set instead of auth_request_set. This works (but doesn't accomplish the goal):

  # set works, but not auth_request_set
  location / {
    set $foo bar;

    if ($foo != bar) {
      return 403;
    }

    add_header x-test $foo;
    add_header success true;

    proxy_pass http://example.com/;
  }

This config works. set is evaluated before if:

$ curl -s --head http://localhost:8080 | grep -E 'HTTP|x-test'
HTTP/1.1 200 OK
x-test: bar

The behavior persists even if the auth_request is in the server{} context:

server {
  listen 8081;
  add_header x-user bar;
  return 200;
}

server {
  listen 8080;

  auth_request /auth;
  auth_request_set $foo $upstream_http_x_user;

  location = /auth {
    internal;
    proxy_pass http://localhost:8081;
  }

  location / {

    if ($foo != bar) {
      return 403;
    }

    add_header x-test $foo;
    add_header success true;

    proxy_pass http://example.com/;
  }

}

$ curl -s --head http://localhost:8080 | grep -E 'HTTP|x-test|success'
HTTP/1.1 403 Forbidden
success: false

I've reviewed the following docs and questions:

2
  • Is there a reason that you cannot put the auth_request in the server context? Commented Feb 2, 2023 at 8:49
  • I've tried that and it has the same behavior. I've tried both auth_request and auth_request_set in server context, and a mix with only auth_request in server and auth_request_set in location. All return 403 every time.
    – Rino Bino
    Commented Feb 2, 2023 at 9:16

2 Answers 2

3
+100

The if is a problematic, because it is an imperative construct in otherwise declarative configuration.

Therefore it doesn't always work as expected. More information can be found in IfIsEvil article.

In this case, the set and auth_request_set happen in different stages of nginx request processing, and the if processing happens between those two stages.

Unfortunately I don't know how to actually do what you want in nginx. Maybe that needs to be done in the upstream server where you proxy the request to.

2
  • 1
    Thanks for the reply, if you have any outside the box ideas, please let me know!! The upstream server has a 1:many relationship with a bunch of these nginx listeners in question, and each one has a different "valid" value for the var being checked, so I would really like to avoid moving that config to the upstream. I'm still hammering at it. I was thinking of some sort of map to replace the if but can't seem to work out viable logic for that.
    – Rino Bino
    Commented Feb 3, 2023 at 22:57
  • This is technically the correct answer to my question. You simply cannot do it with if. My specific issue/scenario/use-case isn't necessarily solved, but the question itself is answered.
    – Rino Bino
    Commented Feb 7, 2023 at 6:19
1

This doesn't answer the question, see Tero's answer here for the actual answer (spoiler: if is truly evil, even if using it with return).

However, as a functional answer providing a workaround: Here is at least a way to make invalidated requests get sent a 403. The proxy_pass endpoint is dynamic depending on the tested header. This seems to solve the use-case...

upstream barhost {
  server example.com;
}


map $foo $choose_upstream {
  default 127.0.0.1:8999;

  # cannot use hostnames here since they don't resolve. Using an upstream instead. 
  bar barhost;
}

server {
  listen 8999;
  return 403;
}

server {
  listen 8081;
  add_header x-user bar;
  return 200;
}

server {
  listen 8080;

  auth_request /auth;

  location = /auth {
    internal;
    proxy_pass http://localhost:8081;
  }

  location / {
    auth_request_set $foo $upstream_http_x_user;
    proxy_set_header Host example.com;
    proxy_pass http://$choose_upstream/;
  }
}

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .