nginx as rate limiter based on http body - nginx

I'm evaluating nginx to act as rate limiter for a multi tenancy REST API system. I need to limit API calls by tenant-id.
For example i want to allow 100 r/s for tenant1 and only 50 r/s for tenant2.
It can be easily achived when there are differant urls like: "me.com/tenant1/api" and "me.com/tenant2/api" (with the location directive).
But, in my case the urls are the same for all tenants "me.com/api" (I can't change this).
To find the tenant-id I need to extract a JSON attribute from the Body of the request, and then check the DB for the real tenant-id.
Is it possible to limit_req with my requirements?
Thank for the help!

I decided to build another service getTenant for parsing the body and extracting the Tenant from the DB. This service is called internally by Nginx.
I'm not sure if that is the best nginx (/openresty) solution, but this is what i came up with:
limit_req_zone t1Limit zone=t1Zone:10m rate=200r/s;
limit_req_zone t2Limit zone=t2Zone:10m rate=90r/s;
server {
location /api{
content_by_lua_block {
ngx.req.read_body();
local reqBody = ngx.req.get_body_data()
local res = ngx.location.capture("/getTenant", {method=ngx.HTTP_POST,body=reqBody});
local tenantId= res.body;
if tenantId== "none" then
ngx.log(ngx.ERR, "Tenant not found!");
ngx.say(tenantId);
else
ngx.req.set_header("x_myTenantId", tenantId)
local res2 = ngx.location.capture("/" .. tenantId .."/doApi", {method=ngx.HTTP_POST,body=reqBody});
if res2.status == ngx.HTTP_OK then
ngx.say(res2.body);
ngx.exit(res2.status);
else
ngx.status = res2.status
ngx.exit(res2.status)
end
end;
}
}
location /getTenant {
internal; #this is not accessible from outside.
proxy_pass http://UpStream1/getCustomer;
proxy_set_header X-Original-URI $request_uri;
}
location /tenant1/doApi {
internal; #this is not accessible from outside.
# Proxy all requests to the AReqUpStream server group
proxy_pass http://UpStream2/doApi;
limit_req zone=tenant1Zone burst=25;
limit_req_log_level notice;
}
location /tenant2/doApi {
internal; #this is not accessible from outside.
# Proxy all requests to the AReqUpStream server group
proxy_pass http://UpStream2/doApi;
limit_req zone=tenant2Zone burst=10 ;#nodelay;
limit_req_status 409;
limit_req_log_level notice;
}
}
Basically, when me.com/api is called, a new subrequest is issued to service /getTenant. The response of that call is used to build another subrequest call to the /tenant[X]/doApi service. That way i can define locations per tenant and provide different rate_limis to each.
Comments on that are more than welcome!

Related

Nginx - Decode URL query parameter and forward it as request header

I need to send some basic auth credentials (es. user:pass) to nginx in the form of query parameter (es. http://example.com?BASIC_AUTH=dXNlcjpwYXNz) and being able to forward them in the more usual Authorization: Basic dXNlcjpwYXNz header form to a target server behind the proxy.
I'm already able to retrieve the value of the encoded auth string with a regular expression. The problem is that very often that value may contain some character that need to be percent-encoded in the URL. Es. user:pass! -> ?BASIC_AUTH=dXNlcjpwYXNzIQ== becomes ?BASIC_AUTH=dXNlcjpwYXNzIQ%3D%3D
Therefore, when I forward the request to the target server, I end up specifing Authorization: Basic dXNlcjpwYXNzIQ%3D%3D which the target server will reject, giving a 401 Unauthorized.
How can I force nginx to decode the auth string before setting the Authorization header? Thanks in advance for your help.
Note: I can't send the auth string in the Authorization header in the first place due to some application-specific constraints.
"Pure" nginx solution
Unfortunately nginx does not provide a rich string operations set. I think there isn't a way to do global search-and-replace through some string (which can be a solution if we could replace all %2B with +, %2F with / and %3D with =). However there are circumstances under which nginx performs an urldecoding of some string - when this string becomes a part of an URI which will be forwarded to an upstream proxy server.
So we can add a value of a BASIC_AUTH request argument to the URI and make a proxy request to ourself:
# Main server block
server {
listen 80 default_server;
...
location / {
if ($arg_basic_auth) {
# "basic_auth" request argument is present,
# append "/decode_basic_auth/<BASE64_token>" to the URI
# and go to the next location block
rewrite ^(.*)$ /decode_basic_auth/$arg_basic_auth$1 last;
}
# No "basic_auth" request argument present,
# can do a proxy call from here without setting authorization headers
...
}
location /decode_basic_auth/ {
# This will be an internal location only
internal;
# Remove "basic_auth" request argument from the list of arguments
if ($args ~* (.*)(^|&)basic_auth=[^&]*(\2|$)&?(.*)) {
set $args $1$3$4;
}
# Some hostname for processing proxy subrequests
proxy_set_header Host internal.basic.auth.localhost;
# Do a subrequest to ourselfs, preserving other request arguments
proxy_pass http://127.0.0.1$uri$is_args$args;
}
}
# Additional server block for proxy subrequests processing
server {
listen 80;
server_name internal.basic.auth.localhost;
# Got URI in form "/decode_basic_auth/<BASE64_token>/<Original_URI>"
location ~ ^/decode_basic_auth/([^/]+)(/.*)$ {
proxy_set_header Authorization "Basic $1";
# Setup other HTTP headers here
...
proxy_pass http://<upstream_server>$2$is_args$args;
}
# Do not serve other requests
location / {
return 444;
}
}
Maybe this is not a very elegant solution, but it is tested and works.
OpenResty / ngx_http_lua_module
This can be easily solved with openresty or ngx_http_lua_module using ngx.escape_uri function:
server {
listen 80 default_server;
...
location / {
set $auth $arg_basic_auth;
if ($args ~* (.*)(^|&)basic_auth=[^&]*(\2|$)&?(.*)) {
set $args $1$3$4;
}
rewrite_by_lua_block {
ngx.var.auth = ngx.unescape_uri(ngx.var.auth)
}
proxy_set_header Authorization "Basic $auth";
# Setup other HTTP headers here
...
proxy_pass http://<upstream_server>;
}
}

Calling external api in Nginx location section

I am trying to resolve proxy_pass value dynamically (through web api) in nginx.
I need something like below;
Example taken from: https://www.nginx.com/resources/wiki/start/topics/depth/ifisevil/
location /proxy-pass-uri {
set $urlToProxy = CallWebAPI("http://localhost:8081/resolver?url=" + $url);
proxy_pass $urlToProxy;
}
So, my question is that, is it possible to make HTTP request or to write method such as CallWebAPI?
I know it might be a bad practice, but the website I am dealing with has thousands of web urls, which are mapped as key-value pairs, and 90% of them does not obey any specific regex rules. So I have content mapped database, and I need to fetch incoming url with content dynamically.
I am trying to use a very light web service to look up URLs from redis, and return proxy url.
Would this be a valid scenario, or is there any other built in solution in nginx like this?
I doubt this can be done with "pure" nginx, but this definitely can be done with openresty or ngx_http_lua_module with the help of ngx.location.capture method. For example:
resolver 8.8.8.8;
location ~/proxy-pass-uri(/.*)$ {
set $url $1;
set $proxy "";
access_by_lua_block {
res = ngx.location.capture("http://localhost:8081/resolver?url=" .. ngx.var.url)
ngx.var.proxy = res.body
}
proxy_pass $proxy$url;
}
There is also an ngx_http_js_module (documentation, GitHub) which have an ability to do subrequests (example), but I never used it and cannot tell if it can be used this way.
Important update
After almost a three years since this answer was written, it comes that I needed the similar functionality myself, and it turns out that the above answer is completely broken and unworkable. You can't do a subrequest via ngx.location.capture to anything else but to some other nginx location. So the correct (checked and confirmed to be workable) example for the above question is
resolver 8.8.8.8;
location /resolver {
internal;
proxy_pass http://localhost:8081;
}
location ~ ^/proxy-pass-uri(/.*)$ {
set $url $1;
set $proxy "";
access_by_lua_block {
res = ngx.location.capture("/resolver?url=" .. ngx.var.url)
if res.status == ngx.HTTP_OK then
ngx.var.proxy = res.body
else
ngx.exit(res.status)
end
}
proxy_pass $proxy$url$is_args$args;
}
The above example assumes that the proxy resolution service is really expecting request in a /resolver?url=<uri> form. The location /resolver { ... } while being internal behaves like any other prefix location, so if the /resolver prefix for that location cannot be used for some reason, the same can be written as
resolver 8.8.8.8;
location /get_proxy {
internal;
proxy_pass http://localhost:8081/resolver;
}
location ~ ^/proxy-pass-uri(/.*)$ {
set $url $1;
set $proxy "";
access_by_lua_block {
res = ngx.location.capture("/get_proxy?url=" .. ngx.var.url)
if res.status == ngx.HTTP_OK then
ngx.var.proxy = res.body
else
ngx.exit(res.status)
end
}
proxy_pass $proxy$url$is_args$args;
}

conditional routing with nginx based on referer

I need to route traffic based on the http request origin. I have two environments and we need to redirect every http request for "/us-en" to Environment1 and others to Environment2 using "$http_referer".
Redirection based on location works.
location ~ /us-en {
proxy_pass Environment1;
proxy_set_header Host Environment1;
}
With '$http_referer' the below option does not work. Request your suggestion on the same.
if ($http_referer ~ ^https?://dev.xyz.com/us-en){
rewrite ^/us-en(/*)$ HOME_PAGE$1 break;
proxy_pass Environment1;
}
Error: nginx: [emerg] "proxy_pass" directive is not allowed here in /opt/nginx/conf/nginx.conf.
Note: By default all the traffic goes to Environment2 as an upstream configuration is present.
# needed if your proxy destination specified with domain name instead of IP address
resolver 8.8.8.8;
location /home/ {
proxy_set_header Host HOST1;
# setup other proxied headers if needed
if ($http_referer ~ ^https?://dev.xyz.com/home) {
rewrite ^/home(/.*)$ HOME_PAGE$1 break;
proxy_pass https://HOST1:8080; # this can be specified by IP address
}
}
With such configuration requests to your_domain.com/home/path/file from dev.xyz.com/home/... (but not from dev.xyz.com/any/other/path!) will be proxied to https://HOST1:8080/HOME_PAGE/path/file. If you specify your proxy destination with domain name instead of IP address, you'll need to specify the additional parameter resolver in your server config. You can use your local name server if you have one, or use something external like Google public DNS (8.8.8.8) or DNS provided for you by your ISP. Anyway such configuration leads to additional DNS lookups, so if you can, specify your proxy destination with IP address.
Update
There is another way to do it with the valid_referers directive:
# needed if your proxy destination specified with domain name instead of IP address
resolver 8.8.8.8;
location /home/ {
proxy_set_header Host HOST1;
# setup other proxied headers if needed
valid_referers example.com/home;
if ($invalid_referer = "") {
rewrite ^/home(/.*)$ HOME_PAGE$1 break;
proxy_pass https://HOST1:8080; # this can be specified by IP address
}
}
Update # 2020.11.11
Besides this answer somehow achieved a score of 5, the given solution has an extremely bad design (it isn't a good approach to have different content handlers in the location and the nested if block; moreover, having an if block with any directive other than from the nginx rewrite module should be avoided if possible) and won't work at all on early nginx versions (I wanna cry when I look at some of my early answers). An original OP question was
The logic should be like below but has some syntax mistakes.
if ($http_origin ~ '^http?://(dev.xyz.com/home)') {
set $flag 'true';
}
if ($flag = 'true') {
location /home/ {
proxy_pass "https://HOST1:8080/HOME PAGE/";
}
}else{
Do Not proxy pass
}
It is unclear what do not proxy pass means. If it means returning some HTTP error (for example, HTTP 403 Forbidden), it can be done with the following configuration:
location /home/ {
if ($http_referer !~ ^https?://dev.xyz.com/home) {
return 403;
}
rewrite ^/home(/.*)$ HOME_PAGE$1 break;
proxy_set_header Host HOST1;
# setup other proxied headers if needed
proxy_pass https://HOST1:8080; # this can be specified by IP address
}
If do not proxy pass means to serve the request locally, the solution is more complex:
map $http_referer $loc {
~^https?://dev.xyz.com/home loc_proxy;
default loc_local;
}
server {
...
location /home/ {
try_files /dev/null #$loc;
}
location #loc_proxy {
rewrite ^/home(/.*)$ HOME_PAGE$1 break;
proxy_set_header Host HOST1;
# setup other proxied headers if needed
proxy_pass https://HOST1:8080;
}
location #loc_local {
rewrite ^/home(/.*)$ HOME_PAGE$1 break;
root /path/to/required/page;
...
}
The try_files /dev/null #the_named_location; trick is taken from this excellent answer.
However now the edited OP's question states for a different requirements, which also could be achieved with the map directive help:
map $http_referer $environment {
~^https?://dev.xyz.com/home Environment1;
default Environment2;
}
server {
...
location /home/ {
rewrite ^/home(/.*)$ HOME_PAGE$1 break;
proxy_set_header Host $environment;
# setup other proxied headers if needed
proxy_pass https://$environment;
}

nginx set remaining count for limit_req in X-RateLimit-Remaining header

I'm actually a little surprised that I couldn't find anything after a couple hours of googling, but the problem is as follows:
I want nginx to serve as my throttle for my API.
My config file contains a well-cited example of limit_req_zone:
limit_req_zone $binary_remote_addr zone=limit:2m rate=10r/m;
along with my location directive containing the expected limit_req zone=limit nodelay;
I would love to have nginx attach headers to the response message for both the X-RateLimit-Remaining and X-RateLimit-Reset attributes. Basically have nginx use the active count of the rate=10r/m to populate X-RateLimit-Remaining and timeframe of the same rate=10r/m value to populate X-RateLimit-Reset with how many seconds are left before a refresh.
http {
limit_req_zone $binary_remote_addr zone=login:10m rate=2r/s;
limit_req_status 429;
limit_conn_status 429;
server {
listen 80;
server_name [removed];
location / {
limit_req zone=limit nodelay;
proxy_pass http://reverse-proxy-example;
add_header X-RateLimit-Remaining [nginx variable?];
add_header X-RateLimit-Reset [nginx variable?]
}
}
Thoughts? Possible? Would love to avoid hitting the application to get these numbers.
I would say that this isn't possible with the upstream version of nginx.
You can find the documentation for the limit_req directive through http://nginx.org/r/limit_req, which redirects to http://nginx.org/docs/http/ngx_http_limit_req_module.html#limit_req, which conclusively shows that the module doesn't have any known variables within it.
Looking at http://ngx.su/src/http/modules/ngx_http_limit_req_module.c confirms the conjecture.
Another option is to look at http://nginx.org/docs/varindex.html, which lists all the variables — looking for limit will only get you to $limit_rate, which is an unrelated variable.
P.S. Consider that the limit_req is done through a leaky bucket method.
https://en.wikipedia.org/wiki/Leaky_bucket
Without going into further details or making stuff up (the wikipedia article is huge!), I'd guess that it may not be entirely trivial to present this information to the end user in a consistent and actionable manner.
Just like #cnst's answer, there is no an approprivate varible to save the value you want, if you really want the message, you can implement a lua function to save this message.
The nginx config would like this:
http {
limit_req_zone $binary_remote_addr zone=login:10m rate=2r/s;
limit_req_status 429;
limit_conn_status 429;
server {
listen 80;
server_name [removed];
set $x-ratelimit-ramaining 0;
set $x-ratelimit-reset 0;
access_by_lua_file your_file_name.lua;
location / {
limit_req zone=limit nodelay;
proxy_pass http://reverse-proxy-example;
add_header X-RateLimit-Remaining $x-rate-limit-remaining;
add_header X-RateLimit-Reset $x-ratelimit-reset;
}
}
thus, each request will trigger the lua script, and you can update the variables $x-rate-limit-remaining $x-ratelimit-reset in your lua function.
Packngo from Packet has a function that might be a good fit for what you need to do.
Sample from packngo:
func (r *Response) populateRate() {
// parse the rate limit headers and populate Response.Rate
if limit := r.Header.Get(headerRateLimit); limit != "" {
r.Rate.RequestLimit, _ = strconv.Atoi(limit)
}
if remaining := r.Header.Get(headerRateRemaining); remaining != "" {
r.Rate.RequestsRemaining, _ = strconv.Atoi(remaining)
}
if reset := r.Header.Get(headerRateReset); reset != "" {
if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 {
r.Rate.Reset = Timestamp{time.Unix(v, 0)}
}
}
}

configuring proxy_pass multiple params with ngnix

I need to set proxy_pass for the below url pattern.
location ~ ^/hosts/bu/(.*)/app/(.*)$ {
proxy_pass http://appserver.cnma.com:3000/hosts/bu/$1/app/$2;
}
When I try with the URL it does not pass the second parameter correctly. Please let me know where I am going wrong.
Orig docs say:
If proxy_pass is specified without a URI, the request URI is passed to the server in the same form as sent by a client when the original request is processed, or the full normalized request URI is passed when processing the changed URI:
location /some/path/ {
proxy_pass http://127.0.0.1;
}
So there is enough:
proxy_pass http://appserver.cnma.com:3000$request_uri;
you can use named capture in nginx while using regular expressions
location ~ ^/hosts/bu/(.*)/app/(.*)$ {
proxy_pass http://appserver.cnma.com:3000/hosts/bu/$1/app/$2;
}
You can modify block to below like this
location ~ ^/hosts/bu/(?<loc1>.+)/app/(?<loc2>.+)$ {
proxy_pass http://appserver.cnma.com:3000/hosts/bu/$1/app/$loc2;
}

Resources