Skip to content
Back to Blog

NPMplus & CrowdSec: More Than Just a Reverse Proxy

NPM was fine, but bare. With NPMplus and CrowdSec you get a full Layer 7 protection stack - including community blocklists and AppSec.

Tobi Tobi 7 min read
Open combination lock on a computer keyboard

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.yaml

This 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:

compose.yaml
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: {}
🔴 Caution

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.

ℹ Note

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:

npmplus.yaml
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: openappsec
ℹ Note

The current version of this file can also be found in the NPMplus repo: npmplus.yaml.

Now you can start the stack:

docker compose up -d
❗ Important

After the first start, check the logs to get the initial password:

docker logs npmplus

Setting 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 raw

The API key needs to be added to /opt/npmplus/crowdsec/crowdsec.conf and ENABLED needs to be set to true:

crowdsec.conf
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=true

Then restart the container:

docker restart npmplus

Wrap-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.