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.
Make this guide yoursYOUR SYSTEM. YOUR STEPS.
Where are you working?
Choose your OS and version. The prerequisites, commands and troubleshooting follow your environment.
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.
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.
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.
Dedicated disposable VM · terminal
set -eu
. /etc/os-release
test "$ID" = 'ubuntu'
test "$VERSION_CODENAME" = 'stonking'
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
Nginx, the distribution Lua module, Python/YAML and LuaRocks are installed; actual versions are printed.
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.
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
The check prints OK without opening a persistent listener; busy ports or an allowlisted test source stop it.
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.
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
A new root and empty editor files exist; a second run fails rather than clobbering data.