Skip to content

WAF security model

Access control

  • Login: single sign-on (OIDC) for the control plane GUI and API.
  • Teams: a team claim in the user's token decides which apps they see and change.
  • Global admin: an admin role in the token. Admins see everything and manage global IP lists.
  • Per-app roles: viewer (read only), operator, owner. Changing an app's policy (WAF mode, geo/ASN, bots, IP lists for that app) needs owner.
  • Every API call checks the user's access server-side; hiding buttons in the GUI is not the control. Team changes apply at the user's next login.
  • Audit: every change is recorded with the user, time and a before/after diff (GUI → Audit log).

Network exposure

Surface Who can reach it
Ports 80/443 on the VIP The internet, via the edge firewall's NAT
HAProxy Runtime API Only the control plane (node firewall rule)
HAProxy peers, CrowdSec LAPI Only the other WAF nodes
BGP, BFD Only the configured BGP peers
Control plane GUI Internal network only
Origins Only the WAF nodes (enforced by the network firewall)

Request hardening

  • The WAF decides the app. The app label on logs and policies comes only from the WAF's own host-to-app routing, never from a client header.
  • Strip before set. Every WAF-owned header (X-Client-*, X-WAF-*, X-Bot-*, X-Request-ID, X-Origin-Auth, X-TLS-JA4) and every client-supplied forwarding header (X-Forwarded-For, Forwarded, X-Real-IP, X-Forwarded-Host) is deleted on the way in, then set by the WAF if the app enables it. Forged values never reach an origin.
  • Origin secret: an optional per-app X-Origin-Auth header lets the origin reject anything that didn't come through the WAF.
  • Config validation: every rendered HAProxy config is checked with haproxy -c before it goes live; a failed deploy leaves the previous config running.
  • Control-plane data is never templated. All strings from the control plane are marked unsafe for Ansible templating, so free-text fields such as a robots.txt section or a header value can't execute template code on the deploy runner.

Secrets

Secret Where it lives
Database password, CrowdSec keys, CI token, SSO client secret, DNS provider token, optional BGP password Ansible vault in the WAF repository (encrypted)
Vault password, SSH deploy key, registry credentials, GeoLite2 licence key CI/CD masked variables
Origin secrets, TLS private keys Control plane database, encrypted

Secrets are never logged: secret-handling tasks run with no_log, and deploy logs posted back to the GUI exclude them. Rotation steps are in the WAF repository's runbook.

Failure behaviour

Failure Behaviour
Coraza (OWASP rules) unavailable Per app: fail open (default: request allowed and logged as spoa_error) or fail closed (503)
CrowdSec unavailable Fail open: requests are not checked against CrowdSec
Geo/ASN/bot maps missing or empty Fail open: those checks don't match anything
Vendor bot list download fails Previous data is kept; the nightly job goes red
Control plane down The WAF keeps serving with its last config; instant changes queue until it's back
WAF node unhealthy The watcher withdraws the VIP; with two nodes, traffic moves to the other

Known limitations

  • The "challenge" action is enforced as a block; a JavaScript challenge page is not built yet.
  • Behaviour-based bot scoring is not built yet; bot detection covers known bots and crawlers.
  • Logs and metrics stay on the nodes until they are shipped to the central log/metrics stack.
  • DDoS mitigation and caching are out of scope.