Illustrative WIUNIX Academy studio with Yuna, laptop and a DEMO Nginx/CrowdSec teaching diagram.

WIUNIX ACADEMY

CrowdSec and Nginx: trustworthy logs, private LAPI and reversible IP bans

Build an isolated CrowdSec1.8.1 and Lua bouncer1.2.3 lab; verify real HTTP blocking, unban, expiry, spoofed-header handling and fail-open API outages.

Intermediate55 min readUpdated
Make this guide yours

YOUR SYSTEM. YOUR STEPS.

Where are you working?

Choose your OS and version. The prerequisites, commands and troubleshooting follow your environment.

Debian12Reviewed against official documentation ·

USE YOUR OWN VALUES

Configure this tutorial

Set the example values for your environment; related explanations and copy panels update together. Never enter passwords, private keys or tokens. Values stay in this tab and are not sent to the server or share links. Check actual DNS, certificates and network policy before running commands.

Different IP addresses, domains or ports? Open the settings below, change the values and select “Apply to tutorial”, then copy the updated commands.

Set values for my environment7 editable values

THE OUTCOME

What will you have at the end?

Verify a complete local decision-to-HTTP enforcement path.

01Nginx127.0.0.1:18080 → access log
02Engine → LAPI/srv/academy-crowdsec · 127.0.0.1:18081
03Lua bounceracademy_nginx · ban/allow 127.0.0.2
Supported operating systems and versions

Documented OS paths; isolated fixture evidence and production acceptance are distinct.

Before you begin

  • Fresh amd64 VM, sudo and compatible distro Lua module.
  • Independent recovery console; no production traffic.

Make detection and enforcement observable

Build a loopback-only CrowdSec/Nginx lab under /srv/academy-crowdsec. Security Engine1.8.1 reads standard Nginx logs, a local SQLite-backed API stores decisions, and Lua Nginx bouncer1.2.3 checks the request source. This is a safe starting point for Linux operators before a separately planned production rollout, not a recipe to paste over a live web server.

Allow about55 minutes after prerequisites. You will distinguish parsed logs from scenario alerts and from actual HTTP403 enforcement, then verify unban, expiry, spoofed headers and API outage behavior. Installing the engine alone does not protect Nginx. IP reputation is defense in depth alongside authentication, patching and application rate limits.

Edit public values, Apply, then copy

Open the collapsed settings, edit public values, press Apply, then copy. Pending or invalid changes disable copy buttons. Never put an API key, password, private key or log containing personal data into this form. The bouncer key is generated inside a protected process and written straight to a private file.

Settings and custom progress use sessionStorage for this tab/language/article/environment; default progress uses localStorage. Reset restores reviewed defaults. Changing text does not execute a test or reserve a port. 127.0.0.2 must remain a loopback source distinct from the trusted management CIDR 127.0.0.1/32; the preflight validates that additional relationship.

Use an empty amd64 VM and matching Lua packages

Use a fresh sudo-enabled x86_64 VM, not a production host. The release archive contains Linux amd64 executables. Ubuntu26.10 is the reviewed current stable default;26.04/24.04 LTS and Debian13/12 are documented package paths. Do not replace a distribution Nginx binary with nginx.org packages while retaining a module built for another ABI. OpenResty has built-in Lua but uses different paths and packaging; it is outside this exact recipe.

Review package candidates, enough disk space and port availability. Package installation may start the distribution default Nginx on this disposable VM; our child Nginx reads only the dedicated configuration and listens at127.0.0.1:18080. The private LAPI uses127.0.0.1:18081. No firewall bouncer, host port exposure, public TLS, Console enrollment, CAPI registration, CAPTCHA or AppSec WAF is enabled.

01

LET’S BUILD

Install distribution Nginx and Lua dependencies

Run only on the empty lab VM. Inspect the apt candidates before accepting installation; stop if the Lua module has no matching Nginx dependency. No package candidate is a promise of support by CrowdSec.

bash

Dedicated disposable VM · terminal

set -eu
. /etc/os-release
test "$ID" = 'debian'
test "$VERSION_CODENAME" = 'bookworm'
sudo apt-get update
apt-cache policy nginx libnginx-mod-http-lua lua-cjson luarocks
sudo apt-get install --no-install-recommends nginx libnginx-mod-http-lua lua5.1 lua-cjson luarocks python3 python3-yaml curl ca-certificates nano
nginx -v
dpkg-query -W nginx libnginx-mod-http-lua lua-cjson luarocks python3
How do I know it worked?

Nginx, the distribution Lua module, Python/YAML and LuaRocks are installed; actual versions are printed.

02

LET’S BUILD

Check architecture, ports and management separation

Keep 127.0.0.2 in127/8; the source is only meaningful on this VM. Use 127.0.0.1/32 only for the actual local management address, not a whole visitor network.

bash

Dedicated disposable VM · terminal

set -eu
test "$(uname -m)" = x86_64
sudo python3 - <<'PY'
import socket,ipaddress
assert ipaddress.ip_address('127.0.0.2') in ipaddress.ip_network('127.0.0.0/8'), 'Test source must be loopback'
assert ipaddress.ip_address('127.0.0.2') not in ipaddress.ip_network('127.0.0.1/32'), 'Test source is allowlisted'
for port in (18080,18081,18082):
    with socket.socket() as s: s.bind(('127.0.0.1',port))
print('amd64, separate free ports and test source: OK')
PY
How do I know it worked?

The check prints OK without opening a persistent listener; busy ports or an allowlisted test source stop it.

03

LET’S BUILD

Reserve one private root without overwriting files

The parent must already be root-controlled. Existing roots, symlinks and writable parent directories are refused. Do not rerun this stage against an existing lab; choose a new dedicated root after reviewing the settings.

bash

Dedicated disposable VM · terminal

sudo python3 - <<'PY'
import os,stat,grp
from pathlib import Path
r=Path('/srv/academy-crowdsec')
assert r.is_absolute() and str(r) not in ('/','/srv','/opt','/etc','/tmp')
for p in reversed(r.parents):
    m=p.lstat()
    assert stat.S_ISDIR(m.st_mode) and m.st_uid==0 and not m.st_mode & 0o022, 'Unsafe parent'
r.mkdir(mode=0o750)
os.chown(r,0,grp.getgrnam('www-data').gr_gid)
for name in ('downloads','config','data','hub','logs','run','www','notifications','plugins'):
    p=r/name;p.mkdir(mode=0o700 if name in ('config','data') else 0o755)
(r/'www'/'index.html').write_text('academy-ok\n',encoding='utf-8')
for name in ('engine.yaml','acquis.yaml','profiles.yaml','simulation.yaml','nginx.conf'):
    fd=os.open(r/'config'/name,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600);os.close(fd)
print('Exclusive lab root and empty editor files created')
PY
How do I know it worked?

A new root and empty editor files exist; a second run fails rather than clobbering data.

Explore all tutorials ↗