Architecture
From sw-nginx subrequest to LAPI verdict.
Bastion runs four bouncers fed by a single local CrowdSec LAPI. The enforcement path lives inside Plesk's own nginx, hooked via a version-pinned auth_request module that survives every sw-nginx upgrade.
visitor
│
▼
┌──────────────────────────────────────────────────────────┐
│ sw-nginx (Plesk) │
│ bastion server-block + auth_request dynamic module │
└──────────┬───────────────────────────────────────────────┘
│ internal subrequest
▼
┌──────────────────────────────────────────────────────────┐
│ dedicated PHP-FPM pool (private socket) │
│ verify.php ──► local CrowdSec LAPI │
└──────────┬───────────────────────────────────────────────┘
│
┌───────┼─────────────┬───────────────┐
▼ ▼ ▼ ▼
204 401 captcha 403 ban AppSec verdict
pass challenge return (SQLi / XSS / CVE)
01
Request hits sw-nginx
Plesk's bundled nginx receives the request. The bastion server-block is included globally via the custom Plesk vhost template, survives plesk repair web.
02
auth_request module
A pre-compiled bastion_ngx_http_auth_request_module.so (ABI-pinned to sw-nginx 1.28.x / 1.30.x) issues an internal subrequest to /__bastion__/verify.
03
Dedicated PHP-FPM pool
Private pool on its own Unix socket, isolated from the Plesk panel PHP tree so plesk repair never wipes it. ~0.5 ms PHP startup, no disk I/O on the hot path.
04
verify.php → LAPI
PHP reads the X-CS-ACTION header populated by chained nginx geo + map lookups fed from the cached LAPI decision file. 204 = pass, 401 = redirect to captcha, 403 = ban.
05
Decision enforced
Either the request continues to the site, gets a captcha challenge, or returns 403, all before reaching Apache, PHP-FPM, or the application stack.
Four bouncers, one LAPI
Each layer covers what the layer above can't see.
Bastion talks only to the local CrowdSec LAPI. The CrowdSec daemon handles CAPI sync independently, Bastion never reaches the community API directly. This keeps blast radius small and the local engine authoritative.
- appsec-bouncer (L7 WAF)
- Inline inspection of SQLi, XSS, path traversal, and CVE virtual-patches. Runs in the same auth_request chain, verdict in ~200 ms before the request touches the backend.
- php-bouncer (captcha L7)
- Serves the hCaptcha challenge page through bastion-php-fpm. HMAC cookie validation means returning humans pass without solving twice.
- firewall-bouncer (L4)
- Kernel-level drop for banned IPs across every port, SSH, SMTP, IMAP, FTP, anything that doesn't go through nginx.
- custom-bouncer (nginx fast path)
- Rebuilds the $cs_action lookup map every few minutes via systemd timer. Lets nginx resolve a ban without calling PHP, fast path for known-bad IPs.
HTTP/HTTPS, all vhosts
Per-vhost via server-block include
iptables / nftables / ipset
Plesk-managed nginx config tree
Differentiator
Auto-recompile on every sw-nginx upgrade.
The official cs-nginx-bouncer breaks Plesk because sw-nginx ships custom module ABI. Bastion ships an APT DPkg hook that detects every sw-nginx upgrade, swaps the matching .so, and reloads nginx, without operator intervention.
Pre-Install-Pkgs
If sw-nginx is in the upgrade batch, stash bastion_auth_request.conf so the new nginx binary boots clean.
sw-nginx installed
Plesk replaces the running nginx binary, module ABI is now mismatched.
Post-Invoke
Detect nginx -v, install shipped .so for that version, restore the load config, run nginx -t, reload.
Fallback
If no shipped .so exists, recompile from auth_request_src/ against the new headers. On failure, the load config stays stashed, nginx still boots.
Plesk-repair-proof
bastion-server-block.conf is included via the Plesk custom vhost template, plesk repair web and httpdmng --reconfigure-* preserve it.
Standalone PHP-FPM pool
The captcha pool lives outside the Plesk panel PHP tree on a private Unix socket, plesk repair php never wipes it. The pool reuses the Plesk-provided PHP runtime.
Local-first decisions
All bouncers read from the same LAPI on localhost:8080. CAPI enrichment happens inside the CrowdSec daemon, never on the request path.