This came up from time here and there but no question properly covers this use case.
The relevant section of http
:
map $http_origin $origin_with_default {
default '*';
~. $http_origin;
}
map $request_method $es_target {
default '';
POST 'search';
GET 'search';
HEAD 'search';
OPTIONS 'options';
}
root /app;
The relevant section of server
:
server {
location ~* /(.*)/_search {
limit_except OPTIONS {
auth_basic "Read Users";
auth_basic_user_file /etc/nginx/htpasswd_read;
}
rewrite ^ /internal/$es_target;
}
location /internal {
return 405;
}
location /internal/search/ {
internal;
proxy_pass http://elasticsearch/;
proxy_http_version 1.1;
proxy_set_header Connection "Keep-Alive";
proxy_set_header Proxy-Connection "Keep-Alive";
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Credentials;
include "cors.headers";
}
location /internal/options {
internal;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
add_header 'Access-Control-Max-Age' 1728000;
include "cors.headers";
return 204;
}
}
Finally, the cors.headers
file:
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Allow-Origin $origin_with_default always;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS' always;
add_header Access-Control-Allow-Headers 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always;
POST https://example.com/index_name/_search
gives 401. This is as expected.OPTIONS https://example.com/index_name/_search
returns the options headers. This is also as expected.- However,
POST https://u:[email protected]/index_name/_search
gives a 404 and the server log containsopen() "/app/index_name/_search" failed (2: No such file or directory),
. Before I added therewrite ^ /internal/$es_target;
and thelocation /internal/search/
section, when theproxy_pass
was just afterlimit_except
insidelocation ~* /(.*)/_search {
it did work. Because of 1) and 2) I believe the rewrite and the location matching works. But why does it try to serve a file instead of doing a proxy pass?
2
Answers
Here's a working config. It needed
limit_except
removed, every location switched to regex matching consuming everything -- and the last location being last is important to be so otherwise it gets into a rewrite loop. The "If the proxy_pass directive is specified with a URI, then when a request is passed to the server, the part of a normalized request URI matching the location is replaced by a URI specified in the directive" part of the rewrite module is not something I was able to get working.We still need the
http
section:And then comes
server
The following answer will require the minimal understanding of two nginx base concepts – the request processing phases (described in the development guide) and the location content handler (which can be either local, when the request is served using some local file contents, or not, when the response is coming from some kind of upstream – an HTTP proxy, FastCGI or uWSGI daemon, etc.).
Despite the fact that I have quite extensive experience in configuring nginx,
limit_except
isn’t a directive I’d used to use frequently. To understand its behavior I did a couple of tests. Here is the list of nginx directives I’m going to use and the request processing phases where they are registering their handlers, in order of execution:rewrite
–NGX_HTTP_REWRITE_PHASE
auth_basic
–NGX_HTTP_ACCESS_PHASE
try_files
–NGX_HTTP_PRECONTENT_PHASE
proxy_pass
–NGX_HTTP_CONTENT_PHASE
From all the above directives only
auth_basic
andproxy_pass
are allowed to use inside thelimit_except
block. Thetry_files "" <location>
trick I’m going to use described in this answer at ServerFault, so I would skip its detailed description here.TL;DR The solution will be provided at the next part of the answer; the
limit_except
directive cannot be used to solve the problem.The
limit_except
directive behavior analysisI will use the following config to analyze the
limit_except
directive behavior:Under the
/var/www/html
directory I’ll place a singleindex.html
file with the single text lineindex
.Here we go.
For the
GET
request nginx uses the local content handler. For thePOST
request nginx uses thehttp_proxy_module
content handler.Here nginx uses the defined
http_proxy_module
content handler for both requests. We didn’t find anything we can’t be expect yet. Lets go further.The
rewrite
rules are completely ignored if the request falls under thelimit_except
condition. This looks like something we did not expect. However a quick search gives us the nginx trac ticket referring the following comment:Now let’s check the
try_files
directive behavior. To do it we will add the followingmap
blockand two named locations
to our configuration.
The unconditional jump to the named location works as expected.
This is also expected, since the
NGX_HTTP_PRECONTENT_PHASE
wheretry_files
attaches its handler is executed before theNGX_HTTP_CONTENT_PHASE
one.Looks like the nginx tries to use local content handler for the
POST
request.Bad news. The
NGX_HTTP_PRECONTENT_PHASE
handler defined in the main location did not get executed if the request falls under thelimit_except
condition. This looks similar to the nested locations behavior, although in contradistinction to the nested location we can’t use thetry_files
directive inside thelimit_except
block.Looks like the
limit_except
directive has some kind of limited use cases. Does it mean the question problem is not solvable? No. It means thelimit_except
directive cannot be used to solve it and we need to find some other way. Never give up 🙂Solution
You can optionally enable/disable basic authentication using the technique I just described here. Add the additional
map
block to your configuration:Now you can enable conditional basic auth in a following way:
However you can’t use the
rewrite
directive in this block since that directive is executed during theNGX_HTTP_REWRITE_PHASE
, andauth_basic
directive register its handler at the laterNGX_HTTP_ACCESS_PHASE
. While for the regularallow
/deny
directives there is a way to do all the checks using only the rewrite module directives (generic example is here), there is no such a way for basic auth. Fortunately we still can use ourtry_files
trick (which will be executed at the laterNGX_HTTP_PRECONTENT_PHASE
). If by chance you are using OpenResty bundle or lua-nginx-module, you have an additional options described in the aforementioned answer.I can see you already faced the problems with the correct URI that should be passed to the upstream. Your original
proxy_pass http://elasticsearch/;
will pass the/
for every request, and theproxy_pass http://elasticsearch;
will pass the rewritten URI. While your original request URI is always available via the$request_uri
variable (which does not get changed with therewrite
directive unlike the$uri
one), and something likeproxy_pass http://elasticsearch$request_uri;
should work too, we will use the named locations (we are not limited to, but that way we should prevent any URI changes at all). Here is the whole solution (I slightly optimize your firstmap
block to prevent (some kind of expensive) regex library call):