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)

HTTP/HTTPS, all vhosts

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)

Per-vhost via server-block include

Serves the hCaptcha challenge page through bastion-php-fpm. HMAC cookie validation means returning humans pass without solving twice.
firewall-bouncer (L4)

iptables / nftables / ipset

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)

Plesk-managed nginx config tree

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.

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.