Why switch at all?
For a long time I used the well-known Nginx Proxy Manager as a reverse proxy for external access. It always worked reliably, but something always bothered me: it’s “just” a reverse proxy with no additional security features.
At some point I started wondering whether I could at least integrate Fail2Ban - and whether that was even feasible. Since I hadn’t touched Fail2Ban in a while, I first did some research to see if it was still the way to go. It didn’t take long before I came across a more modern alternative: CrowdSec
I’d never heard of CrowdSec before and started reading up on it. The sheer amount of information was a bit overwhelming at first and everything seemed super complicated. But it really isn’t. Let me try to break it down simply:
CrowdSec - a quick overview
At its core, CrowdSec is similar to Fail2Ban: log files are monitored and suspicious IPs get blocked. But unlike Fail2Ban, CrowdSec is built around a decentralized architecture. It consists of multiple components - at minimum these three:
- LAPI: the Local API, the central coordinator.
- Log Processor: monitors the log files.
- Remediation Component (Bouncer): enforces decisions and blocks the offending IPs.
The key advantage is that decentralization. All three components can run on the same system, but they don’t have to. There’s usually only one LAPI, but you can have multiple log processors and bouncers. Log processors can run on different systems and monitor their respective logs. When suspicious activity is detected, they send alerts to the LAPI. The LAPI then creates decisions as needed and pushes them to the bouncers - so if one log processor spots a suspicious IP, all bouncers will block it automatically.
There are different types of bouncers: they can sit at Layer 3 directly in iptables, or at Layer 7 inside a reverse proxy.
A special form of bouncer is the AppSec Component: it acts as a Web Application Firewall (WAF) directly within CrowdSec, analyzing HTTP traffic at the application layer - not just based on IP addresses, but also based on the requests themselves.
The component that really unlocks CrowdSec’s full potential is the Cloud Console: once you connect it to your LAPI, all decisions get reported there as well. That’s also where blocklists come in. Once you’ve created a CrowdSec account and connected the console, you’re automatically subscribed to the Community Blocklist: if enough community members report an IP, it lands on the blocklist and gets automatically blocked by all connected CrowdSec installations. This means most known malicious IPs are blocked before they even attempt an attack on your system.
The free plan also lets you subscribe to up to 3 additional specialized blocklists - for example ones tailored to mail servers or web servers.
Setting up NPMplus + CrowdSec
But enough theory - time to get to the actual goal: integrating CrowdSec into Nginx Proxy Manager.
After some research I came across a fork of NPM with significantly more features: NPMplus. And one of its best features is the native CrowdSec integration, including a collection created by Zoey specifically for NPMplus and AppSec.
Since I only had a handful of hosts in my NPM setup, I decided to start fresh with NPMplus from scratch. I had been running NPM in an LXC container on one of my Proxmox nodes. NPMplus and its additional components are designed for Docker, though. There is a Proxmox Community Script for NPMplus that apparently installs Docker inside an LXC, but I didn’t try that. Running Docker inside an LXC can cause problems, and Zoey explicitly states in the NPMplus docs that you shouldn’t do it. So I spun up a Debian VM, installed Docker, and got to work.
I used the guide published by Zoey on the CrowdSec blog as a reference: Enhancing Web Server Security with NPMplus and CrowdSec. The steps I followed are described below.
Preparing and starting the stack
First, download the current compose.yaml from the NPMplus GitHub repo:
curl -L https://raw.githubusercontent.com/ZoeyVid/NPMplus/refs/heads/develop/compose.yaml -o compose.yamlThis file needs to be adapted to your needs. It can be a bit overwhelming at first with all the configuration options - I stripped it down significantly for my setup. That said, you should still take a look at the original file and what it offers. Here’s what my current compose file looks like:
name: npmplus
services:
npmplus:
container_name: npmplus
image: docker.io/zoeyvid/npmplus:2026-04-12-r1
restart: always
network_mode: host
volumes:
- /opt/npmplus:/data
environment:
- TZ=Europe/Berlin
- ACME_EMAIL=<admin@example.com>
- LOGROTATE=true
- GOA=false
- GOA_LISTEN_LOCALHOST=true
- NGINX_TRUST_SECPR1=true # needed for Alexa AWS skill -> HomeAssistant communication
crowdsec:
container_name: crowdsec
image: docker.io/crowdsecurity/crowdsec:latest
restart: always
network_mode: bridge
ports:
- 127.0.0.1:7422:7422
- 8080:8080
environment:
- TZ=Europe/Berlin
- USE_WAL=true
- COLLECTIONS=ZoeyVid/npmplus crowdsecurity/http-dos
volumes:
- /opt/crowdsec/conf:/etc/crowdsec
- /opt/crowdsec/data:/var/lib/crowdsec/data
- /opt/npmplus/nginx/logs:/opt/npmplus/nginx/logs:ro
networks: {}Port 8080 (CrowdSec) is intentionally not bound to localhost in my setup, since additional log processors and bouncers on other systems communicate with the LAPI. If you don’t plan on doing that, use 127.0.0.1:8080:8080 here.
I’m also intentionally not using docker.io/zoeyvid/npmplus:latest - releases tend to bring significant changes and I prefer to update deliberately.
Next, create the directory /opt/crowdsec/conf/acquis.d and create the following file inside it: npmplus.yaml
The file should contain:
filenames:
- /opt/npmplus/nginx/logs/*.log
labels:
type: npmplus
---
listen_addr: 0.0.0.0:7422
appsec_config: crowdsecurity/appsec-default
name: appsec
source: appsec
labels:
type: appsec
# if you use openappsec you can enable this
#---
#source: file
#filenames:
# - /opt/openappsec/logs/cp-nano-http-transaction-handler.log*
#labels:
# type: openappsecThe current version of this file can also be found in the NPMplus repo: npmplus.yaml.
Now you can start the stack:
docker compose up -dAfter the first start, check the logs to get the initial password:
docker logs npmplusSetting up NPMplus as a bouncer
NPMplus now needs to be registered as a Remediation Component (Bouncer) so that decisions can be enforced directly by NPMplus at Layer 7.
Run the following command and copy the output API key:
docker exec crowdsec cscli bouncers add npmplus -o rawThe API key needs to be added to /opt/npmplus/crowdsec/crowdsec.conf and ENABLED needs to be set to true:
ENABLED=true
API_URL=http://127.0.0.1:8080
# Authentication options, either using an API key or TLS client certificate.
API_KEY=<APIKEY>
USE_TLS_AUTH=false
TLS_CLIENT_CERT=/path/to/client.crt
TLS_CLIENT_KEY=/path/to/client.key
CACHE_EXPIRATION=1
# bounce for all type of remediation that the bouncer can receive from the local API
BOUNCING_ON_TYPE=all
FALLBACK_REMEDIATION=ban
REQUEST_TIMEOUT=3000
UPDATE_FREQUENCY=10
# By default internal requests are ignored, such as any path affected by rewrite rule.
# set ENABLE_INTERNAL=true to allow checking on these internal requests.
ENABLE_INTERNAL=false
# live or stream
MODE=live
# exclude the bouncing on those location
EXCLUDE_LOCATION=
#those apply for "ban" action
# /!\ REDIRECT_LOCATION and RET_CODE can't be used together. REDIRECT_LOCATION take priority over RET_CODE
BAN_TEMPLATE_PATH=/data/crowdsec/ban.html
REDIRECT_LOCATION=
RET_CODE=
#those apply for "captcha" action
#valid providers are recaptcha, hcaptcha, turnstile
CAPTCHA_PROVIDER=
# Captcha Secret Key
SECRET_KEY=
# Captcha Site key
SITE_KEY=
CAPTCHA_TEMPLATE_PATH=/data/crowdsec/captcha.html
CAPTCHA_EXPIRATION=3600
APPSEC_URL=http://127.0.0.1:7422
APPSEC_FAILURE_ACTION=deny
APPSEC_CONNECT_TIMEOUT=
APPSEC_SEND_TIMEOUT=1000
APPSEC_PROCESS_TIMEOUT=30000
ALWAYS_SEND_TO_APPSEC=false
SSL_VERIFY=trueThen restart the container:
docker restart npmplusWrap-up & what’s next
NPMplus is now connected to CrowdSec. The familiar Nginx Proxy Manager web interface is available at https://<HOSTNAME>:81. Log in with the ACME_EMAIL configured in your compose file and the initial password you saved earlier.
The basic setup is complete - NPMplus is running, CrowdSec is monitoring logs and blocking suspicious IPs, and the AppSec component is analyzing traffic at the application layer.
For a quick reference of the most useful CrowdSec CLI commands, I’ve put together a cheatsheet.
In my setup there are additional log processors and bouncers on other systems communicating with the LAPI. How I integrated those - and how to connect your LAPI to the Cloud Console - is coming in a follow-up post.