···11111212## Background
13131414-I use [Linuxserver.io's (LSIO)](https://www.linuxserver.io/) dockerized nginx reverse-proxy solution, [SWAG](https://docs.linuxserver.io/general/swag/), as the point of ingress for public-facing services in my homelab. In addition to being easy to configure LSIO containers can be sideloaded with ["Docker Mods"](https://www.linuxserver.io/blog/2019-09-14-customizing-our-containers#docker-mods) that can provide additional functionality to the main service. Some of these are [generic](https://mods.linuxserver.io/?mod=universal) but most are specific to the container they are running on.
1414+I use [Linuxserver.io's (LSIO)](https://www.linuxserver.io/) dockerized nginx reverse-proxy solution, [SWAG](https://docs.linuxserver.io/general/swag/), as the point of ingress for public-facing services in my homelab. In addition to being easy to configure LSIO containers can be sideloaded with [Docker Mods](https://www.linuxserver.io/blog/2019-09-14-customizing-our-containers#docker-mods) that can provide additional functionality to the main service. Some of these are [generic](https://mods.linuxserver.io/?mod=universal) but most are specific to the container they are running on.
15151616One of thes docker mods specific to SWAG installs and configures an [nginx bouncer](https://github.com/linuxserver/docker-mods/tree/swag-crowdsec) for [Crowdsec](https://crowdsec.net/), a community-driven quasi [WAF](https://www.cloudflare.com/learning/ddos/glossary/web-application-firewall-waf/). This mod, along with [LSIO's guide for setting up a full Crowdsec solution](https://www.linuxserver.io/blog/blocking-malicious-connections-with-crowdsec-and-swag), is a large facet of my homelab's defensive design.
1717···33333434### Current Nginx Configuration
35353636-Let's take a look at my abridged configuration...this is foreshadowing. The problem is present below but trickily not immediately obvious as it is a valid config.
3636+Let's take a look at my abridged configuration...this is foreshadowing. The problem is present below but, trickily, is not immediately obvious as it is a valid config.
37373838-* `nginx.conf` is setup to include `http.d` where the crowdsec bouncer is configured
3939-* `site-confs/default.conf` is largely the same as SWAG sets it up with these changes
3838+* `nginx.conf` is setup to include `http.d`, where the crowdsec bouncer is configured
3939+* `site-confs/default.conf` is largely the same as SWAG sets it up, but with these changes
40404141```nginx
4242# ...
···66666767The non-intercepting behavior seems suspiciously consistent with what the redirect is supposed to do...
68686969-But let me tell you it was a rabbit hole to get to this point. I debugged the crowdsec side of things thoroughly thinking maybe it was an issue with scenario buckets not overflowing...or the bouncer not getting IP ban decisions in time. It wasn't until I turned on debug-level logging in nginx with `error_log ... debug;` that I was able to see the bouncer not logging anything that it clued me in that it might be my nginx directive instead.
6969+But let me tell you it was a rabbit hole to get to this point. I debugged the crowdsec side of things thoroughly thinking maybe it was an issue with scenario buckets not overflowing...or the bouncer not getting IP ban decisions in time. It wasn't until I turned on debug-level logging in nginx with `error_log ... debug;`, and was able to see the bouncer not logging anything, that it clued me in that it might be my nginx directive instead.
70707171## Well There's Your Problem
72727373-It wasn't until I got some feedback from the helpful devs on the LSIO discord server, who suspected it might be an order-of-operations issue, that I really started digging into what the nginx bouncer did and how nginx directives are processed.
7373+Once I got some feedback from the helpful devs on the LSIO discord server, who suspected it might be an order-of-operations issue, I really started digging into what the nginx bouncer did and how nginx directives are processed.
74747575-Nginx procceses request in a [sequences of **phases**](https://nginx.org/en/docs/dev/development_guide.html#http_phases). If an earlier phase ends execution or causes a response to sent then all subsequent phases are not run.
7575+Nginx procceses requests in a [sequences of **phases**](https://nginx.org/en/docs/dev/development_guide.html#http_phases). If an earlier phase ends execution or causes a response to be sent then all subsequent phases are not run.
76767777-The `return` directive in the `location` block is part of the [**rewrite phase**](https://nginx.org/en/docs/http/ngx_http_rewrite_module.html) but the nginx bouncer runs in an [`access_by_lua_block`](https://github.com/openresty/lua-nginx-module?tab=readme-ov-file#access_by_lua_block) [handler](https://github.com/crowdsecurity/cs-nginx-bouncer/blob/main/nginx/crowdsec_nginx.conf#L19) that is part of the **access phase**. Critically, the rewrite phase runs before the access phase.
7777+The `return` directive in the `location` block is part of the [**rewrite phase**](https://nginx.org/en/docs/http/ngx_http_rewrite_module.html), but the nginx bouncer runs in an [`access_by_lua_block`](https://github.com/openresty/lua-nginx-module?tab=readme-ov-file#access_by_lua_block) [handler](https://github.com/crowdsecurity/cs-nginx-bouncer/blob/main/nginx/crowdsec_nginx.conf#L19) that is part of the **access phase**. Critically, **the rewrite phase runs before the access phase.**
78787979-So, since the `location /` block was catching all these non-existing routes it was returning a response using a phase that occurred before the nginx bouncer ever run!
7979+So, since the `location /` block was catching all these non-existing routes it was returning a response using a phase that occurred before the nginx bouncer ever ran!
80808181## The Solution
8282···84848585### The Easy Way
86868787-Well, we already know that `proxy_pass` works since it's what we've been using with LSIO. Yep, random helpful internetizen on stackoverflow confirms[`proxy_pass` is part of the **content phase**](https://stackoverflow.com/a/78595091/1469797) which does run after access phase.
8787+Well, we already know that `proxy_pass` works since it's what we've been using with LSIO. Yep, random helpful netizen on stackoverflow confirms [`proxy_pass` is part of the **content phase**](https://stackoverflow.com/a/78595091/1469797) which does run after access phase.
88888989-So let's create a dummy server inline and park our `return directive` behind a proxy.
8989+So let's create a simple, inline server and park our `return` directive behind a `proxy_pass` to it.
90909191```nginx
9292server {
···116116117117What's that? You want to be clever? Don't want to create new `server` blocks? Do it all in the same `location` block? Fine. But you asked for it.
118118119119-Let's revisit that [lua handler](https://github.com/crowdsecurity/cs-nginx-bouncer/blob/main/nginx/crowdsec_nginx.conf#L19) provided by the crowdsec bouncer. If we do some digging we can determine that re-defining the same handler within a nested directive (http -> server -> location) will cause the ["most specific" handler to be used.](https://groups.google.com/g/openresty-en/c/0RmRy6Q2DOA). Essentially, we can override crowdsec's handler with our own.
119119+Let's revisit that [lua handler](https://github.com/crowdsecurity/cs-nginx-bouncer/blob/main/nginx/crowdsec_nginx.conf#L19) provided by the crowdsec bouncer. If we do some digging we can determine that re-defining the same handler within a nested directive (http -> server -> location) will cause the [_most specific_ handler to be used.](https://groups.google.com/g/openresty-en/c/0RmRy6Q2DOA) Essentially, we can override crowdsec's handler with our own.
120120121121So let's copy-paste their handle into our location block and add some code to do the redirect after cs runs.
122122···139139 end
140140 }
141141 }
142142+}
142143```
143144{: file='site-confs/default.conf'}
144145145145-But wait..this isn't working? And if we add debug logging we can see `ngx.redirect` never runs! What gives?
146146+But wait..this isn't working? If we add debug logging after `cs.Allow` we can see `ngx.redirect` never runs! What gives?
146147147147-Crowdsec's bouncer code is running [`ngx.exit(ngx.DECLINED)`](https://github.com/crowdsecurity/lua-cs-bouncer/blob/main/lib/crowdsec.lua#L662) when the IP is not blocked. This causes the handler to exit early and not run out code after `cs.Allow`.
148148+Crowdsec's bouncer code is running [`ngx.exit(ngx.DECLINED)`](https://github.com/crowdsecurity/lua-cs-bouncer/blob/main/lib/crowdsec.lua#L662) when the IP is not blocked. This causes the handler to exit early and not run our code after `cs.Allow`.
148149149150So lets replace that exit with a return...
150151···154155+ return
155156end
156157```
158158+{: file='crowdsec.lua#L662'}
159159+157160158161Now our redirect is running! Hooray!
159162160160-But now you need to deal with patching this file any time the docker-mod is updated. Manging this dependency would be a PITA but it is possible. [I've commented on an issue on the bouncer to make this easier](https://github.com/crowdsecurity/lua-cs-bouncer/issues/95) but it's still not as end-user friendly as the proxy_pass solution. Good luck with that.163163+But now you need to deal with patching this file any time the docker-mod is updated or the container is recreated. Manging this dependency would be a PITA but it is possible. [I've commented on an issue on the bouncer to make this easier](https://github.com/crowdsecurity/lua-cs-bouncer/issues/95) but it's still not as end-user friendly as the proxy_pass solution. Good luck with that.