CrowdSec with NGINX Proxy Manager
NGINX Proxy Manager (or from now on just ‘NPM’) is a popular Docker-based, easy-to-use ‘webproxy-in-a-box’ solution. And having CrowdSec support for it would be awesome as it would add another layer of protection to whatever website you’re using it with. And one can never have too much protection.
But how about native support for CrowdSec? That would be cool, right? Luckily someone had that thought already on May 28, 2021, when they created an issue about it on the project’s Github repo. Back then CrowdSec was nowhere as widespread as it is today so for the first months after that nothing really happened other than other GitHub users were in there to post comments to back up the original poster of the issue.
In October 2021 someone tried experimenting with adding CrowdSec but at that time there was no suitable log parser (for reasons the NPM project changed the logfile format so they couldn’t be parsed with the normal NGINX log parser) so that didn’t really work. In January 2022 all that changed as the CrowdSec developer team finally got around to creating one. And when a bouncer came for OpenResty (the web platform based on NGINX which NPM utilizes) the fun part could really take off!
This is around the time where the beauty of FOSS is shown:
Luckily two engaged CrowdSec community members took it upon themselves to fork the NPM Docker container to build in the OpenResty bouncers and what else is needed. Meet Baudneo and LePresidente who independently of each other decided to create a fork of NPM. Both forks can be used as drop-in replacements for installations of the original NPM project. That’s also when the similarities stop. Baudneo’s fork included ModSecurity (which is pretty cool if I may say so) as well as GeoIP2 with the lib-MindMax DB module. LePresidente’s fork is more vanilla as the plan here is to get changes merged into the upstream NPM project (which is also pretty cool!). Maybe at some point integrating ModSecurity would be part of the upstream project as well, if nothing else as an add-on of sorts. One can always hope 🙂
I won’t be describing the process of setting up NPM as many have done before me (and I am sure they do a much better job than I would) so instead, I’ll just link you to the project’s documentation as well as a video (by our friends at IBRACORP) on that subject.
In my setup I’m using MariaDB as back end, configured as described in the documentation:
version: "3" services: app: image: 'jc21/nginx-proxy-manager:latest' restart: unless-stopped ports: # These ports are in format <host-port>:<container-port> - '80:80' # Public HTTP Port - '443:443' # Public HTTPS Port - '81:81' # Admin Web Port # Add any other Stream port you want to expose # - '21:21' # FTP environment: DB_MYSQL_HOST: "db" DB_MYSQL_PORT: 3306 DB_MYSQL_USER: "npm" DB_MYSQL_PASSWORD: "xxxxx" DB_MYSQL_NAME: "npm" # Uncomment this if IPv6 is not enabled on your host # DISABLE_IPV6: 'true' volumes: - ./data:/data - ./letsencrypt:/etc/letsencrypt depends_on: - db db: image: 'jc21/mariadb-aria:latest' restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: 'npm' MYSQL_DATABASE: 'npm' MYSQL_USER: 'npm' MYSQL_PASSWORD: 'xxxxx' volumes: - ./data/mysql:/var/lib/mysql
Preparing CrowdSec agent
By default, the CrowdSec agent listens on localhost only. Obviously, that won’t work as we want it to be reachable by the OpenResty bouncer so the first step is to make the CrowdSec agent listen on all IP addresses. Edit
/etc/crowdsec/config.yaml with your favorite editor and change the line with
listen_uri in the server section to:
Afterwards restart the CrowdSec agent:
$ sudo systemctl restart crowdsec
Next, you will need to create an API key for the OpenResty bouncer that comes with either of the forks:
$ sudo cscli bouncers add npm-proxy
Make sure to note down the API key as you can get it printed out again without re-adding the bouncer.
TIP: Having the CrowdSec agent listening on all interfaces makes it accessible for all connections coming from the internet. You probably don’t want that. So make sure to install a host firewall. I can recommend ufw as it’s so simple even if I can figure out the basics. The only caveat is that you need to specifically allow connections from the NPM container to the CrowdSec agent.
In my example, I have configured the NPM to connect to the default interface of npm_default (which is created by the NPM container (which on my test setup is 172.18.0.0/16. It will most likely differ depending on your setup. If you don’t do that, the OpenResty bouncer won’t be able to talk to the CrowdSec agent and it won’t work!
Either of the CrowdSec-enabled NPM forks can be used as drop-in replacements with no need to change the current configuration of NPM. What will be needed are three things:
A running CrowdSec agent. This can either be directly on your host, on another server on your network or in another Docker container on the same host. Either works the same.
A suitable docker-compose file. You can’t download one that is guaranteed to work so instead you’ll have to edit your current docker-compose.yaml. But chances are that it’ll look a lot like mine.
Configuring the OpenResty bouncer that’s downloaded alongside the new NPM Docker image. I’ll show you later how.
So to emphasize: the following example is just an example of how it could be done. It may not work in your setup.
In my setup, the CrowdSec agent is installed on the host (a Debian server) as packages from packagecloud.io. It could also be installed in another Docker container.
Next, edit your
docker-compose.yaml. The first thing you need to do is to determine which NPM fork you want to go with. In my example, I have chosen to go with Baudneo’s fork as I really want to play with ModSecurity. The author’s own documentation on how to use the fork is available on Docker Hub. If you want to try out LePresidente’s fork instead, instructions are at the Github repo. The procedure is more or less the same (except for the ModSecurity specific part).
Switching to the new Docker image is literally a matter of changing the
image: line in
docker-compose.yaml so it points to the new image and editing the bouncer config. Do so by replacing the existing image line with this:
We’ll do the rest of the configuration shortly as we need to deploy the new image to get the config file we need to edit. Please keep in mind that the documentation on Baudneo’s fork says that it needs three environmental variables (or edit in the
crowdsec-openresty-bouncer.conf to work). I couldn’t get the bouncer to start unless editing the config file. I have already informed Baudneo of this.
One of the cool features of the OpenResty bouncer as opposed to the generic firewall bouncer is the ability to mitigate threats without dropping traffic on the network level. In certain cases, you really don’t want to shut users out (think e-commerce). Instead, you can force connections that trigger a scenario to go through a CAPTCHA to prove they’re not a bot. The OpenResty bouncer comes with the ability to challenge users with a Google reCAPTCHA v2 challenge. To do that you need to set up an account and a site with Google. This is done here. This is what I did to set it up:
Make sure to note
SITE KEY and
SECRET KEY as you’ll need them for configuring the bouncer. But for the bouncer config file to be available for editing now is the time to deploy your new
$ docker-compose up -d
Wait a few seconds for the new docker image to download and deploy (depending on your internet connection). Then edit the
data/crowdsec/crowdsec-openresty-bouncer.conf file. The conf file must contain the following information:
ENABLED=true API_URL=http://<ip of crowdsec agent>:8080 API_KEY=<bouncer api key from before> # ReCaptcha Secret Key SECRET_KEY=<from recaptcha setup> # Recaptcha Site key SITE_KEY=<from recaptcha setup>
Restart the container to deploy your new config file:
$ docker-compose restart
To verify that it works you need to view logs from the NPM docker container:
$ docker logs --follow <name/id of container. Find it using ‘docker container ls’>
If you see a line like this you’re in luck:
nginx: [alert] [lua] init_by_lua:11: [Crowdsec] Initialisation done
When you introduce another bouncer into an existing CrowdSec configuration, chances are that a bouncer already exists. Oftentimes it’s the firewall bouncer. You’ll want to control which scenarios trigger which mitigating actions (read: which bouncer reacts to what). This is where
profiles.yaml comes into the picture. This configuration file is part of the CrowdSec agent which in my setup resides on the docker host in
/etc/crowdsec/profiles.yaml. Insert the following before the existing content of the file:
name: captcha_remediation filters: - Alert.Remediation == true && Alert.GetScope() == "Ip" && Alert.GetScenario() startsWith "crowdsecurity/http-" decisions: - type: captcha duration: 4h on_success: break ---
Basically, this instructs the agent to mitigate everything from scenarios that begin with HTTP using the OpenResty bouncer and to do so with a reCAPTCHA challenge.
Before you do anything else, make sure the
base-http-scenarios collection is installed. If not install it with
$ sudo cscli collections install crowdsecurity/base-http-scenarios
Next, reload the CrowdSec agent:
$ sudo systemctl reload crowdsec
By default, ModSecurity WAF is installed and loaded. The OWASP-CoreRuleSet is installed and used as the default ruleset. To enable it all you need to do is to instruct NPM where to deploy it.
I choose to deploy it on all HTTP sites by adding the following to
data/nginx/custom/http_top.conf (neither the custom directory nor the file exists in advance):
modsecurity on; modsecurity_rules_file /etc/nginx/modsec/main.conf;
To deploy the changes, restart the NPM container once again:
$ docker-compose restart
To verify everything works, watch the
Congratulations! You now have a CrowdSec-enabled NPM installation with even more security! I hope you enjoyed the ride and are able to make it work without too much fuzz.