--- tags: - Documentation - Bookstack - Notes --- # Web Application Firewall (WAF) # SOCFortess # SOCFortress Docs # WAF Management Platform
[](https://github.com/socfortress/waf-platform-public/blob/main/README.md#waf-management-platform)
Self-hosted Web Application Firewall with a modern admin UI. Powered by **Caddy + Coraza** (OWASP Core Rule Set v4) as the WAF engine, a **FastAPI** management API, and a **React** dashboard. This repository contains everything you need to **run** the platform from prebuilt container images — no source build required. ## 📺 Watch the demo
[](https://github.com/socfortress/waf-platform-public/blob/main/README.md#-watch-the-demo)
Deploy the WAF, get automatic TLS, block live attacks, and tune a false positive — in about 10 minutes: [![Watch the demo on YouTube](https://camo.githubusercontent.com/e252a2d7b4fd9ab6ea64ec33e7a5aa5d60623d081c9740023f5a10fe5a944c01/68747470733a2f2f696d672e796f75747562652e636f6d2f76692f5a766a66505850335833302f6d617872657364656661756c742e6a7067)](https://youtu.be/ZvjfPXP3X30) --- ## Architecture
[](https://github.com/socfortress/waf-platform-public/blob/main/README.md#architecture)
``` Internet │ ▼ ┌─────────────────────┐ ┌──────────────────────────┐ │ Caddy + Coraza │ ───► │ Your protected upstream │ │ WAF engine (80/443) │ │ app(s) │ └─────────────────────┘ └──────────────────────────┘ ▲ (Admin API 2019, container-internal only) │ ┌─────────────────────┐ ┌────────────┐ ┌─────────┐ │ FastAPI Admin API │ ◄─►│ PostgreSQL │ │ Redis │ │ (8000, internal) │ └────────────┘ └─────────┘ └─────────────────────┘ ▲ │ ┌─────────────────────┐ │ React Admin UI │ ← you log in here: https://localhost:8443 │ (Nginx, 8443 HTTPS) │ └─────────────────────┘ ```
--- ## Features
[](https://github.com/socfortress/waf-platform-public/blob/main/README.md#features)
- **Caddy + Coraza WAF** with the OWASP Core Rule Set v4, in detection or blocking mode per site. - **Site management** — front any number of upstream apps behind the WAF. - **CRS & custom rules** — tune the ruleset, add custom rules, manage false-positive exclusions from the UI. - **Authentication** with TOTP 2FA and role-based access control. - **Log viewer** — searchable, PostgreSQL-backed request/blocking logs with GeoIP enrichment. - **Alerting** — email notifications on configurable conditions. --- ## Quick start
[](https://github.com/socfortress/waf-platform-public/blob/main/README.md#quick-start)
### Prerequisites
[](https://github.com/socfortress/waf-platform-public/blob/main/README.md#prerequisites)
- Docker ≥ 24 and Docker Compose ≥ 2.20 - 2 GB RAM minimum - Ports **80**, **443**, and **8443** available on the host - A free **MaxMind GeoLite2** license key (see [GeoIP setup](https://github.com/socfortress/waf-platform-public/blob/main/README.md#geoip-setup)) ### 1. Get this repo
[](https://github.com/socfortress/waf-platform-public/blob/main/README.md#1-get-this-repo)
``` git clone https://github.com/socfortress/waf-platform-public.git waf-platform cd waf-platform cp .env.example .env ```
### 2. Configure `.env`
[](https://github.com/socfortress/waf-platform-public/blob/main/README.md#2-configure-env)
Edit `.env` and replace every `CHANGE_ME` value:
VariablePurposeHow to generate
`POSTGRES_PASSWORD`Database password`openssl rand -hex 32`
`SECRET_KEY`JWT signing key`python3 -c "import secrets; print(secrets.token_hex(32))"`
`TOTP_ENCRYPTION_KEY`TOTP secret encryption`python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"`
`BOOTSTRAP_ADMIN_EMAIL`First superadmin emailAny valid email
`BOOTSTRAP_ADMIN_PASSWORD`First superadmin passwordStrong password — **change after first login**
`ALLOWED_ORIGINS`CORS origins`https://localhost:8443` for the default local deploy
Optional — email alerts: `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASSWORD`, `SMTP_FROM`. Optional — pin a version: set `WAF_IMAGE_TAG` to a release tag (e.g. `v1.0.0`) instead of `latest`. ### 3. GeoIP setup
[](https://github.com/socfortress/waf-platform-public/blob/main/README.md#3-geoip-setup)
The log viewer enriches client IPs with country/city data using MaxMind's GeoLite2 database. MaxMind's license does not allow us to redistribute it, so you supply your own (it's free): 1. Create a free account at [https://www.maxmind.com/en/geolite2/signup](https://www.maxmind.com/en/geolite2/signup). 2. Download **GeoLite2 City** (`.mmdb` format). 3. Place the file in this directory as `GeoLite2-City.mmdb`, **or** set `GEOIP_DB_PATH` in `.env` to its full path. > The stack will start without it, but GeoIP enrichment in logs will be disabled. ### 4. Start the stack
[](https://github.com/socfortress/waf-platform-public/blob/main/README.md#4-start-the-stack)
``` docker compose up -d ```
Wait until all containers report healthy (typically 30–60 seconds): ``` docker compose ps ```
### 5. First login
[](https://github.com/socfortress/waf-platform-public/blob/main/README.md#5-first-login)
1. Open **[https://localhost:8443](https://localhost:8443/)** (the UI uses a self-signed certificate on first boot — accept the browser warning, or upload your own cert in the UI). 2. Log in with the `BOOTSTRAP_ADMIN_EMAIL` / `BOOTSTRAP_ADMIN_PASSWORD` from `.env`. 3. **Change your password immediately** via Users → Edit. 4. Optionally enroll TOTP under Settings → Security. --- ## Protecting your app
[](https://github.com/socfortress/waf-platform-public/blob/main/README.md#protecting-your-app)
A fresh install has **no sites configured** — Caddy listens on 80/443 but does not proxy anything yet (a bare request to `http://localhost/` returns an empty `200`). You configure protection from the admin UI: 1. Log in (see above) and add your upstream application as a **site**. 2. Choose **detection** mode (log only) or **blocking** mode (reject attacks). 3. The WAF then proxies and protects that site. ### Optional: verify against the bundled test upstream
[](https://github.com/socfortress/waf-platform-public/blob/main/README.md#optional-verify-against-the-bundled-test-upstream)
The stack includes a throwaway `http-echo` container you can use as a target. Add a test site in **blocking** mode pointing at `http-echo:5678`, then: ``` curl http://localhost/ # → upstream-ok curl -s -o /dev/null -w "%{http_code}\n" "http://localhost/?id=1+OR+1%3D1" # → 403 (SQLi blocked) ```
A benign request returns `200`; the SQLi probe returns `403` once the site is in blocking mode. --- ## Updating
[](https://github.com/socfortress/waf-platform-public/blob/main/README.md#updating)
``` docker compose pull # fetch the latest images (or your pinned WAF_IMAGE_TAG) docker compose up -d # recreate changed containers ```
Your data (Postgres, Redis, rules, certs) lives in named Docker volumes and survives updates. --- ## Managing the stack
[](https://github.com/socfortress/waf-platform-public/blob/main/README.md#managing-the-stack)
``` docker compose ps # status docker compose logs -f # follow logs docker compose down # stop (volumes preserved) docker compose down -v # stop AND delete all data volumes — destructive ```
--- ## Security notes
[](https://github.com/socfortress/waf-platform-public/blob/main/README.md#security-notes)
- Passwords are bcrypt-hashed; TOTP secrets are encrypted at rest. - Secrets are read from environment variables only — never hardcoded. Keep `.env` private. - The Caddy Admin API (port 2019) is bound inside the Docker network only; it is not exposed to the host. - CORS is restricted to `ALLOWED_ORIGINS` — no wildcard. - Auth endpoints are rate-limited; refresh tokens rotate on every use. - For production, place the UI behind your own TLS termination / trusted certificate rather than the self-signed default. --- ## Support & licensing
[](https://github.com/socfortress/waf-platform-public/blob/main/README.md#support--licensing)
- **License:** see [LICENSE](https://github.com/socfortress/waf-platform-public/blob/main/LICENSE). - **Issues / questions:** open an issue on this repository. - Commercial support is available from [SOCFortress](https://www.socfortress.co/). # EDGE WAF - SOCFortress and NPM I have installed SOCFortress WAF to test and compare to my current proxy which is Nginx Proxy Manager. The one area of concern that I have and cannot locate in SOCFortress is the ability to add Custom Configuration for authentik for headers. In Nginx Proxy manager, I can add this information in Custom Nginx Configuration section. Does SOCFortress allow this and if so where? The short answer is **no, not directly in the UI** the way you are used to with Nginx Proxy Manager (NPM). The primary reason for this is a difference in the underlying architecture: **SOCFortress WAF does not use Nginx.** Instead, the SOCFortress WAF platform is built on top of **Caddy** as its reverse proxy engine, combined with **Coraza** (a Go-based WAF engine) running the OWASP Core Rule Set (CRS). Because it uses Caddy instead of Nginx, any native Nginx syntax or snippet you used to paste into NPM will not work here.
SOCFortress - Medium
To achieve what you are trying to do for your authentik integration (forwarding headers, establishing proxy providers, or setting up forward auth), you have to approach it differently. ### How to manage this in SOCFortress WAF Because the SOCFortress WAF UI focuses entirely on the security layer (toggling rules, managing exclusions, and GeoIP blocking) rather than deep proxy customizations, you have two primary options: #### 1. Use Caddy’s Native File System (The Backend Route) If you need to inject custom proxy headers or set up an upstream forward-auth middleware for authentik, you cannot use the UI wrapper. You must modify the underlying **Caddyfile** configuration directly.
- SOCFortress spins up via Docker Compose. You will need to locate the volume or directory where the WAF platform stores its generated Caddy configuration.
SOCFortress - Medium
- Instead of Nginx directives (`proxy_set_header`), you will use Caddy’s `header_up` or `forward_auth` directives.
#### 2. The Recommended Topology: Layer Both Tools If you love the ease of Nginx Proxy Manager for advanced routing, SSL client certificates, and authentik header manipulation, you don't actually have to abandon it. Because SOCFortress is a dedicated WAF, it is highly common to chain them together: `Internet` ➔ `SOCFortress WAF (Filters bad traffic)` ➔ `Nginx Proxy Manager (Handles Authentik, headers, & routing)` ➔ `Your Apps` By placing SOCFortress at the edge, it strips out SQL injections, XSS, and bot traffic before it ever hits NPM. NPM can then securely parse your custom headers and pass them back to your applications exactly as you have it configured right now.
SOCFortress - Medium
Are you attempting to use authentik via standard proxy headers (like `X-Authentik-Username`) or are you trying to implement a full Forward Auth middleware block?
I like this approach, how would you go about setting this up Setting up this multi-layer structure is highly effective because it gives you the best of both worlds: enterprise-grade threat filtering at the front door, and easy web routing at the back. To achieve this seamlessly, the most critical shift is moving **SSL termination** to the SOCFortress WAF layer. Because a WAF needs to inspect the actual HTTP payload for malicious activity (like SQL injections or cross-site scripting), it *must* hold your public SSL certificates. Here is exactly how to sequence this setup: ## The Blueprint
1
Configure NPM to accept HTTP traffic
Nginx Proxy Manager Side
Keep your internal host configurations inside NPM exactly as they are right now, including your custom headers for authentik. However, **disable SSL** inside NPM for these hosts. NPM will now listen for unencrypted HTTP traffic from the internal network, specifically from the SOCFortress container.
2
Establish Docker network communication
Networking Side
Ensure the SOCFortress WAF container can reach your NPM container. If they are on the same machine, the cleanest method is to join them to a shared custom Docker network:
Bash
``` docker network connect your_shared_network_name socfortress-waf-container ```
3
Point the public DNS to SOCFortress
DNS Side
Update your firewall, port forwarding, or public DNS records so that ports **80** and **443** point directly to the host machine running SOCFortress WAF, rather than Nginx Proxy Manager.
4
Add the application in SOCFortress WAF
SOCFortress Side
Open the SOCFortress UI and create a new protected application asset:
- **Domain/Host:** Your public domain (e.g., `app.domain.com`). - **Upstream/Backend URL:** Point this directly to your NPM container. Use NPM's container name or its IP on the shared network (e.g., `http://nginx-proxy-manager:80` or `[http://172.20.0.5:80](http://172.20.0.5:80)`). - **Cert Mode:** Enable Let's Encrypt or upload your custom certificates here. SOCFortress (via Caddy) will handle the HTTPS handshake with the internet.
### Understanding the Traffic Flow Once active, a user request flows through your network using this lifecycle: ``` [User Browser] │ ▼ (HTTPS - TLS Encrypted) [SOCFortress WAF] <-- Decrypts traffic & runs Coraza OWASP CRS rules │ ▼ (HTTP - Internal Network) [Nginx Proxy Manager] <-- Matches domain, executes your Custom Authentik Config, injects headers │ ▼ (HTTP/HTTPS) [Your Application Backend] ``` > ⚠️ **Important Note on Client IPs:** Because NPM now sits behind SOCFortress, NPM will think *every* request is coming from the SOCFortress WAF IP. To ensure your authentik logs show the real user's location, make sure you have `Real IP` or `X-Forwarded-For` headers enabled in your NPM advanced config to pass through the original client IP.
So the only site entry in SOCFortress is pointing to my NPM and the NPM hold all my site forwarding minus the SSL certs, correct? Yes, you have it **100% correct**. You are essentially reducing SOCFortress down to a smart "security shield" at your front door, while letting NPM continue to act as the traffic controller that knows exactly which application gets what headers. To visualize how clean this makes your configuration, here is how the responsibilities are split: ### What SOCFortress Does:
- Listens on public ports `80` and `443`. - Handles the **SSL/TLS Certificates** for your domains. - Inspects incoming requests for malicious payloads (WAF). - Has **one job** for routing: Forward *all* clean traffic for your domains directly to NPM's HTTP port.
### What Nginx Proxy Manager Does:
- Listens internally on port `80` (no SSL configured inside NPM). - Looks at the incoming domain name (e.g., `app1.yourdomain.com`). - Executes your **Custom Nginx Configuration** for authentik (injecting user headers, checking auth states). - Forwards the final, authenticated request to your actual backend application container.
### A Quick Tip for a Smooth Cutover When you remove the SSL certificates from NPM, Nginx will expect plain HTTP traffic. In the SOCFortress asset configuration, make sure your upstream/backend destination is explicitly set to use **`http://`** and not `https://` when pointing to NPM. If you use a common Docker network, you can just point SOCFortress to `http://nginx-proxy-manager-container-name:80` and it will map perfectly. To handle a setup where **SOCFortress (Caddy+Coraza)** sits at the edge and **Nginx Proxy Manager (NPM)** sits behind it, you have to break the port binding conflict on the host machine. Only **one** service can bind to ports `80` and `443` on your host's network interface. Since SOCFortress is your WAF edge, it gets the privilege of claiming those host ports. ## 1. The Strategy 1. **SOCFortress (`caddy-waf`)** binds to the host's `80:80` and `443:443`. It catches all incoming public internet traffic first. 2. **Nginx Proxy Manager (`nginx-proxy-manager`)** removes its public host port bindings for `80` and `443`. 3. **Internal Routing:** Because both services share the `waf-internal` Docker network, the Caddy WAF can route traffic directly to NPM using NPM's container name (`nginx-proxy-manager`) on internal ports `80` and `443`. ## 2. Updated Docker Compose Here is how your `app` (NPM) and `caddy-waf` services should look in the file. Notice that NPM's ports `80` and `443` have been removed from the host mapping, but port `81` is kept so you can still log into the NPM Admin UI from your local network.
YAML
``` app: image: 'jc21/nginx-proxy-manager:latest' container_name: nginx-proxy-manager restart: unless-stopped env_file: .env environment: - TZ=${TZ} - DB_MYSQL_HOST=db - DB_MYSQL_PORT=3306 - DB_MYSQL_USER=${MYSQL_USER} - DB_MYSQL_PASSWORD=${MYSQL_PASSWORD} - DB_MYSQL_NAME=${MYSQL_DATABASE} ports: - '81:81' # Kept so you can access the NPM dashboard on host-ip:81 # '80:80' and '443:443' REMOVED here. They are reachable internally via the network. volumes: - ./data/npm:/data - ./data/letsencrypt:/etc/letsencrypt depends_on: - db networks: - proxy - proxydb - waf-internal healthcheck: test: ["CMD", "curl", "-f", "http://localhost:81/api/status"] interval: 20s timeout: 5s retries: 5 start_period: 40s caddy-waf: image: ghcr.io/socfortress/waf-caddy:${WAF_IMAGE_TAG:-latest} container_name: caddy-waf ports: - "80:80" # Claims the host ports - "443:443" # Claims the host ports extra_hosts: - "host.docker.internal:host-gateway" volumes: - coraza-rules:/etc/coraza/rules - caddy-config:/etc/caddy - coraza-custom:/etc/coraza/custom - crs-data:/etc/coraza/crs-rules - coraza-logs:/var/log/coraza networks: - waf-internal depends_on: http-echo: condition: service_started healthcheck: test: ["CMD", "wget", "-q", "-O-", "http://localhost:2019/config/"] interval: 30s timeout: 10s retries: 3 start_period: 20s deploy: resources: limits: cpus: "1.0" memory: 512M restart: unless-stopped ```
## 3. How to Route Traffic in the WAF When you configure your backend pools or proxy routes inside the SOCFortress Admin UI, you will point the upstream traffic to NPM. Instead of an IP address, use Docker's internal DNS resolver: - **Upstream Target:** `http://nginx-proxy-manager:80` (or port `443` if passing through SSL) From there, NPM will receive the clean, inspected traffic and can handle the final routing to your other services (like Gitea, Mailcow, or homepage) living on your `proxy` network.
# Docker Compose ``` --- services: db: image: 'jc21/mariadb-aria:latest' container_name: npm-db restart: unless-stopped env_file: .env environment: - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} - MYSQL_DATABASE=${MYSQL_DATABASE} - MYSQL_USER=${MYSQL_USER} - MYSQL_PASSWORD=${MYSQL_PASSWORD} - MARIADB_AUTO_UPGRADE=1 volumes: - ./data/mysql:/var/lib/mysql networks: - proxydb healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u${MYSQL_USER}", "-p${MYSQL_PASSWORD}"] interval: 20s timeout: 5s retries: 5 start_period: 30s app: image: 'jc21/nginx-proxy-manager:latest' container_name: nginx-proxy-manager restart: unless-stopped env_file: .env environment: - TZ=${TZ} - DB_MYSQL_HOST=db - DB_MYSQL_PORT=3306 - DB_MYSQL_USER=${MYSQL_USER} - DB_MYSQL_PASSWORD=${MYSQL_PASSWORD} - DB_MYSQL_NAME=${MYSQL_DATABASE} ports: - '81:81' # Kept so you can access the NPM dashboard on host-ip:81 # '80:80' and '443:443' REMOVED here. They are reachable internally via the network. volumes: - ./data/npm:/data - ./data/letsencrypt:/etc/letsencrypt depends_on: - db networks: - proxy - proxydb - waf-internal healthcheck: test: ["CMD", "curl", "-f", "http://localhost:81/api/status"] interval: 20s timeout: 5s retries: 5 start_period: 40s # --------------------------------------------------------------------------- # Caddy + Coraza: WAF engine and reverse proxy # --------------------------------------------------------------------------- caddy-waf: image: ghcr.io/socfortress/waf-caddy:${WAF_IMAGE_TAG:-latest} container_name: caddy-waf ports: - "80:80" - "443:443" # Lets the WAF proxy to apps running on the Docker host (e.g. an nginx # upstream) via http://host.docker.internal:. See demo/README.md. extra_hosts: - "host.docker.internal:host-gateway" volumes: - coraza-rules:/etc/coraza/rules - caddy-config:/etc/caddy - coraza-custom:/etc/coraza/custom - crs-data:/etc/coraza/crs-rules # live CRS dir (seeded by admin-api) - coraza-logs:/var/log/coraza networks: - waf-internal depends_on: http-echo: condition: service_started healthcheck: test: ["CMD", "wget", "-q", "-O-", "http://localhost:2019/config/"] interval: 30s timeout: 10s retries: 3 start_period: 20s deploy: resources: limits: cpus: "1.0" memory: 512M restart: unless-stopped # --------------------------------------------------------------------------- # Dummy upstream — replace with your real protected app(s) # --------------------------------------------------------------------------- http-echo: image: hashicorp/http-echo:0.2.3 container_name: http-echo command: ["-text=upstream-ok"] user: "65534:65534" networks: - waf-internal healthcheck: test: ["NONE"] deploy: resources: limits: cpus: "0.25" memory: 64M restart: unless-stopped # --------------------------------------------------------------------------- # FastAPI Admin API # --------------------------------------------------------------------------- admin-api: image: ghcr.io/socfortress/waf-admin-api:${WAF_IMAGE_TAG:-latest} container_name: admin-api env_file: .env volumes: - tls-certs:/certs # shared TLS cert volume (see admin-ui) # GeoLite2 DB is user-supplied — MaxMind licensing forbids redistribution. # Point GEOIP_DB_PATH at your downloaded GeoLite2-City.mmdb (see README). - ./GeoLite2-City/GeoLite2-City.mmdb}:/etc/geoip-bundle/GeoLite2-City.mmdb:ro - geoip-data:/etc/geoip - coraza-rules:/etc/coraza/rules - caddy-config:/etc/caddy - coraza-custom:/etc/coraza/custom - crs-data:/etc/coraza/crs-rules # live CRS dir (seeded from image bundle) - coraza-logs:/var/log/coraza networks: - waf-internal depends_on: postgres: condition: service_healthy redis: condition: service_healthy healthcheck: test: ["CMD", "wget", "-q", "-O-", "http://localhost:8000/health"] interval: 30s timeout: 10s retries: 3 start_period: 30s deploy: resources: limits: cpus: "1.0" memory: 512M restart: unless-stopped # --------------------------------------------------------------------------- # React Admin UI (served by Nginx, HTTPS on 8443) # --------------------------------------------------------------------------- admin-ui: image: ghcr.io/socfortress/waf-admin-ui:${WAF_IMAGE_TAG:-latest} container_name: admin-ui ports: - "8443:8080" volumes: - tls-certs:/etc/nginx/certs # shared with admin-api for cert upload + reload networks: - waf-internal depends_on: admin-api: condition: service_healthy healthcheck: test: ["CMD", "wget", "-q", "--no-check-certificate", "-O-", "https://localhost:8080/"] interval: 30s timeout: 10s retries: 3 start_period: 30s deploy: resources: limits: cpus: "0.5" memory: 128M restart: unless-stopped # --------------------------------------------------------------------------- # PostgreSQL 16 # --------------------------------------------------------------------------- postgres: image: postgres:16.3-alpine container_name: postgres env_file: .env volumes: - postgres-data:/var/lib/postgresql/data networks: - waf-internal healthcheck: test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] interval: 10s timeout: 5s retries: 5 start_period: 10s deploy: resources: limits: cpus: "1.0" memory: 512M restart: unless-stopped # --------------------------------------------------------------------------- # Redis 7 # --------------------------------------------------------------------------- redis: image: redis:7.2.5-alpine container_name: redis user: redis command: ["redis-server", "--appendonly", "yes"] volumes: - redis-data:/data networks: - waf-internal healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 start_period: 5s deploy: resources: limits: cpus: "0.5" memory: 256M restart: unless-stopped # ============================================================================= # Networks # ============================================================================= networks: proxy: external: true proxydb: external: true waf-internal: driver: bridge # ============================================================================= # Named volumes # ============================================================================= volumes: coraza-rules: coraza-custom: coraza-logs: caddy-config: crs-data: geoip-data: postgres-data: redis-data: tls-certs: ```