---
tags:
  - Documentation
  - Bookstack
  - Notes
---
# Web Application Firewall (WAF)

# SOCFortess

# SOCFortress Docs

# WAF Management Platform

<div class="markdown-heading" dir="auto" id="bkmrk-">[<svg aria-hidden="true" class="octicon octicon-link" data-component="Octicon" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>](https://github.com/socfortress/waf-platform-public/blob/main/README.md#waf-management-platform)</div>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

<div class="markdown-heading" dir="auto" id="bkmrk--2">[<svg aria-hidden="true" class="octicon octicon-link" data-component="Octicon" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>](https://github.com/socfortress/waf-platform-public/blob/main/README.md#-watch-the-demo)</div>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

<div class="markdown-heading" dir="auto" id="bkmrk--6">[<svg aria-hidden="true" class="octicon octicon-link" data-component="Octicon" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>](https://github.com/socfortress/waf-platform-public/blob/main/README.md#architecture)</div>```
        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) │
  └─────────────────────┘

```

<div class="snippet-clipboard-content notranslate position-relative overflow-auto" id="bkmrk--8"><div class="zeroclipboard-container"><svg aria-hidden="true" class="octicon octicon-copy js-clipboard-copy-icon" data-component="Octicon" data-view-component="true" height="16" version="1.1" viewbox="0 0 16 16" width="16"></svg></div></div>---

## Features

<div class="markdown-heading" dir="auto" id="bkmrk--10">[<svg aria-hidden="true" class="octicon octicon-link" data-component="Octicon" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>](https://github.com/socfortress/waf-platform-public/blob/main/README.md#features)</div>- **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 &amp; 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

<div class="markdown-heading" dir="auto" id="bkmrk--13">[<svg aria-hidden="true" class="octicon octicon-link" data-component="Octicon" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>](https://github.com/socfortress/waf-platform-public/blob/main/README.md#quick-start)</div>### Prerequisites

<div class="markdown-heading" dir="auto" id="bkmrk--15">[<svg aria-hidden="true" class="octicon octicon-link" data-component="Octicon" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>](https://github.com/socfortress/waf-platform-public/blob/main/README.md#prerequisites)</div>- 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

<div class="markdown-heading" dir="auto" id="bkmrk--17">[<svg aria-hidden="true" class="octicon octicon-link" data-component="Octicon" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>](https://github.com/socfortress/waf-platform-public/blob/main/README.md#1-get-this-repo)</div>```
git clone https://github.com/socfortress/waf-platform-public.git waf-platform
cd waf-platform
cp .env.example .env
```

<div class="highlight highlight-source-shell notranslate position-relative overflow-auto" dir="auto" id="bkmrk--19"><div class="zeroclipboard-container"><svg aria-hidden="true" class="octicon octicon-copy js-clipboard-copy-icon" data-component="Octicon" data-view-component="true" height="16" version="1.1" viewbox="0 0 16 16" width="16"></svg></div></div>### 2. Configure `.env`

<div class="markdown-heading" dir="auto" id="bkmrk--20">[<svg aria-hidden="true" class="octicon octicon-link" data-component="Octicon" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>](https://github.com/socfortress/waf-platform-public/blob/main/README.md#2-configure-env)</div>Edit `.env` and replace every `CHANGE_ME` value:

<table id="bkmrk-variable-purpose-how"><thead><tr><th>Variable</th><th>Purpose</th><th>How to generate</th></tr></thead><tbody><tr><td>`POSTGRES_PASSWORD`</td><td>Database password</td><td>`openssl rand -hex 32`</td></tr><tr><td>`SECRET_KEY`</td><td>JWT signing key</td><td>`python3 -c "import secrets; print(secrets.token_hex(32))"`</td></tr><tr><td>`TOTP_ENCRYPTION_KEY`</td><td>TOTP secret encryption</td><td>`python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"`</td></tr><tr><td>`BOOTSTRAP_ADMIN_EMAIL`</td><td>First superadmin email</td><td>Any valid email</td></tr><tr><td>`BOOTSTRAP_ADMIN_PASSWORD`</td><td>First superadmin password</td><td>Strong password — **change after first login**</td></tr><tr><td>`ALLOWED_ORIGINS`</td><td>CORS origins</td><td>`https://localhost:8443` for the default local deploy</td></tr></tbody></table>

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

<div class="markdown-heading" dir="auto" id="bkmrk--22">[<svg aria-hidden="true" class="octicon octicon-link" data-component="Octicon" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>](https://github.com/socfortress/waf-platform-public/blob/main/README.md#3-geoip-setup)</div>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

<div class="markdown-heading" dir="auto" id="bkmrk--24">[<svg aria-hidden="true" class="octicon octicon-link" data-component="Octicon" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>](https://github.com/socfortress/waf-platform-public/blob/main/README.md#4-start-the-stack)</div>```
docker compose up -d
```

<div class="highlight highlight-source-shell notranslate position-relative overflow-auto" dir="auto" id="bkmrk--26"><div class="zeroclipboard-container"><svg aria-hidden="true" class="octicon octicon-copy js-clipboard-copy-icon" data-component="Octicon" data-view-component="true" height="16" version="1.1" viewbox="0 0 16 16" width="16"></svg></div></div>Wait until all containers report healthy (typically 30–60 seconds):

```
docker compose ps
```

<div class="highlight highlight-source-shell notranslate position-relative overflow-auto" dir="auto" id="bkmrk--27"><div class="zeroclipboard-container"><svg aria-hidden="true" class="octicon octicon-copy js-clipboard-copy-icon" data-component="Octicon" data-view-component="true" height="16" version="1.1" viewbox="0 0 16 16" width="16"></svg></div></div>### 5. First login

<div class="markdown-heading" dir="auto" id="bkmrk--28">[<svg aria-hidden="true" class="octicon octicon-link" data-component="Octicon" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>](https://github.com/socfortress/waf-platform-public/blob/main/README.md#5-first-login)</div>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

<div class="markdown-heading" dir="auto" id="bkmrk--31">[<svg aria-hidden="true" class="octicon octicon-link" data-component="Octicon" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>](https://github.com/socfortress/waf-platform-public/blob/main/README.md#protecting-your-app)</div>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

<div class="markdown-heading" dir="auto" id="bkmrk--33">[<svg aria-hidden="true" class="octicon octicon-link" data-component="Octicon" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>](https://github.com/socfortress/waf-platform-public/blob/main/README.md#optional-verify-against-the-bundled-test-upstream)</div>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)
```

<div class="highlight highlight-source-shell notranslate position-relative overflow-auto" dir="auto" id="bkmrk--35"><div class="zeroclipboard-container"><svg aria-hidden="true" class="octicon octicon-copy js-clipboard-copy-icon" data-component="Octicon" data-view-component="true" height="16" version="1.1" viewbox="0 0 16 16" width="16"></svg></div></div>A benign request returns `200`; the SQLi probe returns `403` once the site is in blocking mode.

---

## Updating

<div class="markdown-heading" dir="auto" id="bkmrk--37">[<svg aria-hidden="true" class="octicon octicon-link" data-component="Octicon" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>](https://github.com/socfortress/waf-platform-public/blob/main/README.md#updating)</div>```
docker compose pull        # fetch the latest images (or your pinned WAF_IMAGE_TAG)
docker compose up -d        # recreate changed containers
```

<div class="highlight highlight-source-shell notranslate position-relative overflow-auto" dir="auto" id="bkmrk--39"><div class="zeroclipboard-container"><svg aria-hidden="true" class="octicon octicon-copy js-clipboard-copy-icon" data-component="Octicon" data-view-component="true" height="16" version="1.1" viewbox="0 0 16 16" width="16"></svg></div></div>Your data (Postgres, Redis, rules, certs) lives in named Docker volumes and survives updates.

---

## Managing the stack

<div class="markdown-heading" dir="auto" id="bkmrk--41">[<svg aria-hidden="true" class="octicon octicon-link" data-component="Octicon" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>](https://github.com/socfortress/waf-platform-public/blob/main/README.md#managing-the-stack)</div>```
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
```

<div class="highlight highlight-source-shell notranslate position-relative overflow-auto" dir="auto" id="bkmrk--43"><div class="zeroclipboard-container"><svg aria-hidden="true" class="octicon octicon-copy js-clipboard-copy-icon" data-component="Octicon" data-view-component="true" height="16" version="1.1" viewbox="0 0 16 16" width="16"></svg></div></div>---

## Security notes

<div class="markdown-heading" dir="auto" id="bkmrk--45">[<svg aria-hidden="true" class="octicon octicon-link" data-component="Octicon" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>](https://github.com/socfortress/waf-platform-public/blob/main/README.md#security-notes)</div>- 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 &amp; licensing

<div class="markdown-heading" dir="auto" id="bkmrk--48">[<svg aria-hidden="true" class="octicon octicon-link" data-component="Octicon" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>](https://github.com/socfortress/waf-platform-public/blob/main/README.md#support--licensing)</div>- **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.** <span class="citation-9">Instead, the SOCFortress WAF platform is built on top of </span>**<span class="citation-9">Caddy</span>**<span class="citation-9"> as its reverse proxy engine, combined with </span>**<span class="citation-9">Coraza</span>**<span class="citation-9 citation-end-9"> (a Go-based WAF engine) running the OWASP Core Rule Set (CRS).</span> Because it uses Caddy instead of Nginx, any native Nginx syntax or snippet you used to paste into NPM will not work here.

<div _ngcontent-ng-c405365209="" chatcontentdimensions="" class="chat-history-scroll-container lm" id="bkmrk-socfortress---medium"><div _ngcontent-ng-c405365209="" class="conversation-container message-actions-hover-boundary ng-star-inserted"><div _ngcontent-ng-c3598951857=""><div _ngcontent-ng-c3060770435="" class="response-container response-container-with-gpi ng-tns-c3060770435-20 no-background" data-hveid="1" jslog="173900;track:impression,attention"><div _ngcontent-ng-c3060770435="" class="presented-response-container ng-tns-c3060770435-20" data-hveid="2" jslog="283401;track:impression,attention"><div _ngcontent-ng-c3060770435="" class="response-container-content ng-tns-c3060770435-20 has-thoughts"><div _ngcontent-ng-c3598951857="" class="response-content ng-tns-c3060770435-20"><div _ngcontent-ng-c1922256434="" class="container"><div _ngcontent-ng-c2066222059="" aria-busy="false" aria-live="off" class="markdown markdown-main-panel enable-luminous-fast-follows enable-updated-hr-color stronger tutor-markdown-rendering" dir="ltr" inline-copy-host=""><div _ngcontent-ng-c2854609078="" class="source-inline-chip-container luminous-sources hide-from-message-actions ng-star-inserted"><div _ngcontent-ng-c2854609078="" class="source-label-container gds-body-s ng-star-inserted" dir="auto"><span class="source-title">SOCFortress - Medium</span></div></div></div></div></div></div></div></div></div></div></div>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.

<div _ngcontent-ng-c405365209="" chatcontentdimensions="" class="chat-history-scroll-container lm" id="bkmrk-socfortress-spins-up"><div _ngcontent-ng-c405365209="" class="conversation-container message-actions-hover-boundary ng-star-inserted"><div _ngcontent-ng-c3598951857=""><div _ngcontent-ng-c3060770435="" class="response-container response-container-with-gpi ng-tns-c3060770435-20 no-background" data-hveid="1" jslog="173900;track:impression,attention"><div _ngcontent-ng-c3060770435="" class="presented-response-container ng-tns-c3060770435-20" data-hveid="2" jslog="283401;track:impression,attention"><div _ngcontent-ng-c3060770435="" class="response-container-content ng-tns-c3060770435-20 has-thoughts"><div _ngcontent-ng-c3598951857="" class="response-content ng-tns-c3060770435-20"><div _ngcontent-ng-c1922256434="" class="container"><div _ngcontent-ng-c2066222059="" aria-busy="false" aria-live="off" class="markdown markdown-main-panel enable-luminous-fast-follows enable-updated-hr-color stronger tutor-markdown-rendering" dir="ltr" inline-copy-host="">- <span class="citation-8 citation-end-8">SOCFortress spins up via Docker Compose.</span> You will need to locate the volume or directory where the WAF platform stores its generated Caddy configuration.
    
    <div _ngcontent-ng-c2854609078="" class="source-inline-chip-container luminous-sources hide-from-message-actions ng-star-inserted"><div _ngcontent-ng-c2854609078="" class="source-label-container gds-body-s ng-star-inserted" dir="auto"><span class="source-title">SOCFortress - Medium</span></div></div>
- Instead of Nginx directives (`proxy_set_header`), you will use Caddy’s `header_up` or `forward_auth` directives.

</div></div></div></div></div></div></div></div></div>#### 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`

<span class="citation-7 citation-end-7">By placing SOCFortress at the edge, it strips out SQL injections, XSS, and bot traffic before it ever hits NPM.</span> NPM can then securely parse your custom headers and pass them back to your applications exactly as you have it configured right now.

<div _ngcontent-ng-c405365209="" chatcontentdimensions="" class="chat-history-scroll-container lm" id="bkmrk-socfortress---medium-1"><div _ngcontent-ng-c405365209="" class="conversation-container message-actions-hover-boundary ng-star-inserted"><div _ngcontent-ng-c3598951857=""><div _ngcontent-ng-c3060770435="" class="response-container response-container-with-gpi ng-tns-c3060770435-20 no-background" data-hveid="1" jslog="173900;track:impression,attention"><div _ngcontent-ng-c3060770435="" class="presented-response-container ng-tns-c3060770435-20" data-hveid="2" jslog="283401;track:impression,attention"><div _ngcontent-ng-c3060770435="" class="response-container-content ng-tns-c3060770435-20 has-thoughts"><div _ngcontent-ng-c3598951857="" class="response-content ng-tns-c3060770435-20"><div _ngcontent-ng-c1922256434="" class="container"><div _ngcontent-ng-c2066222059="" aria-busy="false" aria-live="off" class="markdown markdown-main-panel enable-luminous-fast-follows enable-updated-hr-color stronger tutor-markdown-rendering" dir="ltr" inline-copy-host=""><div _ngcontent-ng-c2854609078="" class="source-inline-chip-container luminous-sources hide-from-message-actions ng-star-inserted"><div _ngcontent-ng-c2854609078="" class="source-label-container gds-body-s ng-star-inserted" dir="auto"><span class="source-title">SOCFortress - Medium</span></div></div></div></div></div></div></div></div></div></div></div>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?

<div _ngcontent-ng-c405365209="" chatcontentdimensions="" class="chat-history-scroll-container lm" id="bkmrk-"><div _ngcontent-ng-c405365209="" class="conversation-container message-actions-hover-boundary ng-star-inserted" id="bkmrk--1"><div _ngcontent-ng-c3598951857=""><div _ngcontent-ng-c3060770435="" class="response-container response-container-with-gpi ng-tns-c3060770435-20 no-background" data-hveid="1" jslog="173900;track:impression,attention"><div _ngcontent-ng-c3060770435="" class="presented-response-container ng-tns-c3060770435-20" data-hveid="2" jslog="283401;track:impression,attention"><div _ngcontent-ng-c3060770435="" class="response-container-content ng-tns-c3060770435-20 has-thoughts"><div _ngcontent-ng-c3598951857="" class="response-content ng-tns-c3060770435-20"><div _ngcontent-ng-c1922256434="" class="container"><div _ngcontent-ng-c2066222059="" aria-busy="false" aria-live="off" class="markdown markdown-main-panel enable-luminous-fast-follows enable-updated-hr-color stronger tutor-markdown-rendering" dir="ltr" id="bkmrk--2" inline-copy-host=""></div></div><div _ngcontent-ng-c3598951857="" class="response-footer complete has-thoughts gap"><div _ngcontent-ng-c3537663079="" jslog="268370;track:generic_click,impression">  
</div></div></div></div></div><div _ngcontent-ng-c3060770435="" class="response-container-footer ng-tns-c3060770435-20 ng-star-inserted"><div _ngcontent-ng-c356251628="" class="actions-container-v2 ng-tns-c356251628-21"><div _ngcontent-ng-c356251628="" class="buttons-container-v2 ng-tns-c356251628-21 ng-star-inserted"><button aria-label="Good response" aria-pressed="false" class="mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-badge mat-unthemed mat-badge-overlap mat-badge-above mat-badge-after mat-badge-small mat-badge-hidden ng-star-inserted"></button><button aria-label="Bad response" aria-pressed="false" class="mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-badge mat-unthemed mat-badge-overlap mat-badge-above mat-badge-after mat-badge-small mat-badge-hidden ng-star-inserted"></button><button aria-label="Copy" class="mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-badge mat-unthemed mat-badge-overlap mat-badge-above mat-badge-after mat-badge-small mat-badge-hidden ng-star-inserted"></button><div _ngcontent-ng-c356251628="" class="ng-tns-c356251628-21 ng-star-inserted"><div _ngcontent-ng-c356251628="" class="menu-button-wrapper ng-tns-c356251628-21 ng-star-inserted"><div _ngcontent-ng-c356251628="" class="more-menu-button-container ng-tns-c356251628-21"><button aria-label="Show more options" class="mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-badge mat-unthemed mat-badge-overlap mat-badge-above mat-badge-after mat-badge-small mat-badge-hidden ng-star-inserted"></button>  
</div></div></div><div _ngcontent-ng-c356251628="" class="spacer ng-tns-c356251628-21 ng-star-inserted">  
</div></div></div></div></div></div><div _ngcontent-ng-c405365209="" class="restart-chat-button-scroll-placeholder ng-star-inserted">  
</div></div><div _ngcontent-ng-c405365209="" class="conversation-container message-actions-hover-boundary ng-star-inserted"><div _ngcontent-ng-c3824630338="" class="user-query-container"><div _ngcontent-ng-c3824630338="" class="file-preview-container ng-star-inserted">  
</div><div _ngcontent-ng-c3824630338="" class="query-content ng-star-inserted verticle-align-for-single-line-text" data-hveid="0" data-ved="0CAAQ3ucQahcKEwixkur_vK-VAxUAAAAAHQAAAAAQVA" decode-data-ved="1" jslog="275422;track:impression,attention"><div _ngcontent-ng-c3824630338="" aria-level="2" class="query-text gds-body-l" dir="ltr" role="heading">  
</div></div></div></div></div>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

<div _ngcontent-ng-c405365209="" chatcontentdimensions="" class="chat-history-scroll-container lm" id="bkmrk-1-configure-npm-to-a"><div _ngcontent-ng-c405365209="" class="conversation-container message-actions-hover-boundary ng-star-inserted"><div _ngcontent-ng-c3598951857=""><div _ngcontent-ng-c3060770435="" class="response-container response-container-with-gpi ng-tns-c3060770435-22 no-background" data-hveid="1" jslog="173900;track:impression,attention"><div _ngcontent-ng-c3060770435="" class="presented-response-container ng-tns-c3060770435-22" data-hveid="2" jslog="283401;track:impression,attention"><div _ngcontent-ng-c3060770435="" class="response-container-content ng-tns-c3060770435-22"><div _ngcontent-ng-c3598951857="" class="response-content ng-tns-c3060770435-22"><div _ngcontent-ng-c1922256434="" class="container"><div _ngcontent-ng-c2066222059="" aria-busy="false" aria-live="off" class="markdown markdown-main-panel enable-luminous-fast-follows enable-updated-hr-color stronger tutor-markdown-rendering" dir="ltr" inline-copy-host=""><div class="attachment-container unknown"><div class="sequence-container" data-hveid="3" jslog="308913;track:impression,attention"><div class="sequence-event ng-star-inserted"><div class="sequence-event-marker-container hide-from-message-actions" hide-from-message-actions=""><div class="sequence-event-marker gds-body-s ng-star-inserted">1</div><div class="sequence-event-line ng-star-inserted">  
</div></div><div class="sequence-event-content"><div class="hide-from-message-actions" hide-from-message-actions=""><div class="sequence-event-title gds-emphasized-body-l">Configure NPM to accept HTTP traffic</div><div class="sequence-event-subtitle gds-extended-caption ng-star-inserted">Nginx Proxy Manager Side</div></div><div class="sequence-event-description gds-body-l">  
</div></div></div></div></div></div></div></div></div></div></div></div></div></div>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.

<div _ngcontent-ng-c405365209="" chatcontentdimensions="" class="chat-history-scroll-container lm" id="bkmrk-2-establish-docker-n"><div _ngcontent-ng-c405365209="" class="conversation-container message-actions-hover-boundary ng-star-inserted"><div _ngcontent-ng-c3598951857=""><div _ngcontent-ng-c3060770435="" class="response-container response-container-with-gpi ng-tns-c3060770435-22 no-background" data-hveid="1" jslog="173900;track:impression,attention"><div _ngcontent-ng-c3060770435="" class="presented-response-container ng-tns-c3060770435-22" data-hveid="2" jslog="283401;track:impression,attention"><div _ngcontent-ng-c3060770435="" class="response-container-content ng-tns-c3060770435-22"><div _ngcontent-ng-c3598951857="" class="response-content ng-tns-c3060770435-22"><div _ngcontent-ng-c1922256434="" class="container"><div _ngcontent-ng-c2066222059="" aria-busy="false" aria-live="off" class="markdown markdown-main-panel enable-luminous-fast-follows enable-updated-hr-color stronger tutor-markdown-rendering" dir="ltr" inline-copy-host=""><div class="attachment-container unknown"><div class="sequence-container" data-hveid="3" jslog="308913;track:impression,attention"><div class="sequence-event ng-star-inserted"><div class="sequence-event-content"><div class="sequence-event-description gds-body-l">  
</div></div></div><div class="sequence-event ng-star-inserted"><div class="sequence-event-marker-container hide-from-message-actions" hide-from-message-actions=""><div class="sequence-event-marker gds-body-s ng-star-inserted">2</div><div class="sequence-event-line ng-star-inserted">  
</div></div><div class="sequence-event-content"><div class="hide-from-message-actions" hide-from-message-actions=""><div class="sequence-event-title gds-emphasized-body-l">Establish Docker network communication</div><div class="sequence-event-subtitle gds-extended-caption ng-star-inserted">Networking Side</div></div><div class="sequence-event-description gds-body-l">  
</div></div></div></div></div></div></div></div></div></div></div></div></div></div>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:

<div _ngcontent-ng-c1958104536="" class="code-block ng-tns-c1958104536-26 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation" data-hveid="4" id="bkmrk-bash" jslog="223238;track:impression,attention;BardVeMetadataKey:[["r_30b702b296852146","c_4a90e1c3e176c650",null,"rc_c6288839219dd582",null,null,"en",null,1,null,null,1,0]]"><div _ngcontent-ng-c1958104536="" class="formatted-code-block-internal-container ng-tns-c1958104536-26"><div _ngcontent-ng-c1958104536="" class="animated-opacity ng-tns-c1958104536-26"><div _ngcontent-ng-c1958104536="" class="code-block-decoration header-formatted gds-emphasized-body-m ng-tns-c1958104536-26 ng-star-inserted"><span class="ng-tns-c1958104536-26">Bash</span><div _ngcontent-ng-c1958104536="" class="buttons ng-tns-c1958104536-26 ng-star-inserted"><button aria-label="Download code" class="mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-badge mat-unthemed mat-badge-overlap mat-badge-above mat-badge-after mat-badge-small mat-badge-hidden ng-star-inserted"></button><button aria-label="Copy code" class="mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-badge mat-unthemed mat-badge-overlap mat-badge-above mat-badge-after mat-badge-small mat-badge-hidden ng-star-inserted"></button>  
</div></div></div></div></div>```
docker network connect your_shared_network_name socfortress-waf-container

```

<div _ngcontent-ng-c405365209="" chatcontentdimensions="" class="chat-history-scroll-container lm" id="bkmrk-3-point-the-public-d"><div _ngcontent-ng-c405365209="" class="conversation-container message-actions-hover-boundary ng-star-inserted"><div _ngcontent-ng-c3598951857=""><div _ngcontent-ng-c3060770435="" class="response-container response-container-with-gpi ng-tns-c3060770435-22 no-background" data-hveid="1" jslog="173900;track:impression,attention"><div _ngcontent-ng-c3060770435="" class="presented-response-container ng-tns-c3060770435-22" data-hveid="2" jslog="283401;track:impression,attention"><div _ngcontent-ng-c3060770435="" class="response-container-content ng-tns-c3060770435-22"><div _ngcontent-ng-c3598951857="" class="response-content ng-tns-c3060770435-22"><div _ngcontent-ng-c1922256434="" class="container"><div _ngcontent-ng-c2066222059="" aria-busy="false" aria-live="off" class="markdown markdown-main-panel enable-luminous-fast-follows enable-updated-hr-color stronger tutor-markdown-rendering" dir="ltr" inline-copy-host=""><div class="attachment-container unknown"><div class="sequence-container" data-hveid="3" jslog="308913;track:impression,attention"><div class="sequence-event ng-star-inserted"><div class="sequence-event-content"><div class="sequence-event-description gds-body-l">  
</div></div></div><div class="sequence-event ng-star-inserted"><div class="sequence-event-marker-container hide-from-message-actions" hide-from-message-actions=""><div class="sequence-event-marker gds-body-s ng-star-inserted">3</div><div class="sequence-event-line ng-star-inserted">  
</div></div><div class="sequence-event-content"><div class="hide-from-message-actions" hide-from-message-actions=""><div class="sequence-event-title gds-emphasized-body-l">Point the public DNS to SOCFortress</div><div class="sequence-event-subtitle gds-extended-caption ng-star-inserted">DNS Side</div></div><div class="sequence-event-description gds-body-l">  
</div></div></div></div></div></div></div></div></div></div></div></div></div></div>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.

<div _ngcontent-ng-c405365209="" chatcontentdimensions="" class="chat-history-scroll-container lm" id="bkmrk-4-add-the-applicatio"><div _ngcontent-ng-c405365209="" class="conversation-container message-actions-hover-boundary ng-star-inserted"><div _ngcontent-ng-c3598951857=""><div _ngcontent-ng-c3060770435="" class="response-container response-container-with-gpi ng-tns-c3060770435-22 no-background" data-hveid="1" jslog="173900;track:impression,attention"><div _ngcontent-ng-c3060770435="" class="presented-response-container ng-tns-c3060770435-22" data-hveid="2" jslog="283401;track:impression,attention"><div _ngcontent-ng-c3060770435="" class="response-container-content ng-tns-c3060770435-22"><div _ngcontent-ng-c3598951857="" class="response-content ng-tns-c3060770435-22"><div _ngcontent-ng-c1922256434="" class="container"><div _ngcontent-ng-c2066222059="" aria-busy="false" aria-live="off" class="markdown markdown-main-panel enable-luminous-fast-follows enable-updated-hr-color stronger tutor-markdown-rendering" dir="ltr" inline-copy-host=""><div class="attachment-container unknown"><div class="sequence-container" data-hveid="3" jslog="308913;track:impression,attention"><div class="sequence-event ng-star-inserted"><div class="sequence-event-content"><div class="sequence-event-description gds-body-l">  
</div></div></div><div class="sequence-event ng-star-inserted"><div class="sequence-event-marker-container hide-from-message-actions" hide-from-message-actions=""><div class="sequence-event-marker gds-body-s ng-star-inserted">4</div></div><div class="sequence-event-content"><div class="hide-from-message-actions" hide-from-message-actions=""><div class="sequence-event-title gds-emphasized-body-l">Add the application in SOCFortress WAF</div><div class="sequence-event-subtitle gds-extended-caption ng-star-inserted">SOCFortress Side</div></div><div class="sequence-event-description gds-body-l">  
</div></div></div></div></div></div></div></div></div></div></div></div></div></div>Open the SOCFortress UI and create a new protected application asset:

<div _ngcontent-ng-c405365209="" chatcontentdimensions="" class="chat-history-scroll-container lm" id="bkmrk-domain%2Fhost%3A-your-pu"><div _ngcontent-ng-c405365209="" class="conversation-container message-actions-hover-boundary ng-star-inserted"><div _ngcontent-ng-c3598951857=""><div _ngcontent-ng-c3060770435="" class="response-container response-container-with-gpi ng-tns-c3060770435-22 no-background" data-hveid="1" jslog="173900;track:impression,attention"><div _ngcontent-ng-c3060770435="" class="presented-response-container ng-tns-c3060770435-22" data-hveid="2" jslog="283401;track:impression,attention"><div _ngcontent-ng-c3060770435="" class="response-container-content ng-tns-c3060770435-22"><div _ngcontent-ng-c3598951857="" class="response-content ng-tns-c3060770435-22"><div _ngcontent-ng-c1922256434="" class="container"><div _ngcontent-ng-c2066222059="" aria-busy="false" aria-live="off" class="markdown markdown-main-panel enable-luminous-fast-follows enable-updated-hr-color stronger tutor-markdown-rendering" dir="ltr" inline-copy-host=""><div class="attachment-container unknown"><div class="sequence-container" data-hveid="3" jslog="308913;track:impression,attention"><div class="sequence-event ng-star-inserted"><div class="sequence-event-content"><div class="sequence-event-description gds-body-l">- **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.

</div></div></div></div></div></div></div></div></div></div></div></div></div></div>### 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.

<div _ngcontent-ng-c405365209="" chatcontentdimensions="" class="chat-history-scroll-container lm" id="bkmrk--3"><div _ngcontent-ng-c405365209="" class="conversation-container message-actions-hover-boundary ng-star-inserted" id="bkmrk--4"><div _ngcontent-ng-c3598951857=""><div _ngcontent-ng-c3060770435="" class="response-container response-container-with-gpi ng-tns-c3060770435-22 no-background" data-hveid="1" jslog="173900;track:impression,attention"><div _ngcontent-ng-c3060770435="" class="presented-response-container ng-tns-c3060770435-22" data-hveid="2" jslog="283401;track:impression,attention"><div _ngcontent-ng-c3060770435="" class="response-container-content ng-tns-c3060770435-22"><div _ngcontent-ng-c3598951857="" class="response-content ng-tns-c3060770435-22"><div _ngcontent-ng-c1922256434="" class="container"><div _ngcontent-ng-c2066222059="" aria-busy="false" aria-live="off" class="markdown markdown-main-panel enable-luminous-fast-follows enable-updated-hr-color stronger tutor-markdown-rendering" dir="ltr" id="bkmrk--5" inline-copy-host=""></div></div><div _ngcontent-ng-c3598951857="" class="response-footer contains-extensions-response complete gap"><div _ngcontent-ng-c3537663079="" jslog="268370;track:generic_click,impression">  
</div></div></div></div></div><div _ngcontent-ng-c3060770435="" class="response-container-footer ng-tns-c3060770435-22 ng-star-inserted"><div _ngcontent-ng-c356251628="" class="actions-container-v2 ng-tns-c356251628-23"><div _ngcontent-ng-c356251628="" class="buttons-container-v2 ng-tns-c356251628-23 ng-star-inserted"><button aria-label="Good response" aria-pressed="false" class="mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-badge mat-unthemed mat-badge-overlap mat-badge-above mat-badge-after mat-badge-small mat-badge-hidden ng-star-inserted"></button><button aria-label="Bad response" aria-pressed="false" class="mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-badge mat-unthemed mat-badge-overlap mat-badge-above mat-badge-after mat-badge-small mat-badge-hidden ng-star-inserted"></button><button aria-label="Copy" class="mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-badge mat-unthemed mat-badge-overlap mat-badge-above mat-badge-after mat-badge-small mat-badge-hidden ng-star-inserted"></button><div _ngcontent-ng-c356251628="" class="ng-tns-c356251628-23 ng-star-inserted"><div _ngcontent-ng-c356251628="" class="menu-button-wrapper ng-tns-c356251628-23 ng-star-inserted"><div _ngcontent-ng-c356251628="" class="more-menu-button-container ng-tns-c356251628-23"><button aria-label="Show more options" class="mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-badge mat-unthemed mat-badge-overlap mat-badge-above mat-badge-after mat-badge-small mat-badge-hidden ng-star-inserted"></button>  
</div></div></div><div _ngcontent-ng-c356251628="" class="spacer ng-tns-c356251628-23 ng-star-inserted">  
</div></div></div></div></div></div><div _ngcontent-ng-c405365209="" class="restart-chat-button-scroll-placeholder ng-star-inserted">  
</div></div><div _ngcontent-ng-c405365209="" class="conversation-container message-actions-hover-boundary ng-star-inserted"><div _ngcontent-ng-c3824630338="" class="user-query-container"><div _ngcontent-ng-c3824630338="" class="file-preview-container ng-star-inserted">  
</div><div _ngcontent-ng-c3824630338="" class="query-content ng-star-inserted" data-hveid="0" data-ved="0CAAQ3ucQahcKEwixkur_vK-VAxUAAAAAHQAAAAAQVw" decode-data-ved="1" jslog="275422;track:impression,attention"><div _ngcontent-ng-c3824630338="" aria-level="2" class="query-text gds-body-l" dir="ltr" role="heading">  
</div></div></div></div></div>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:

<div _ngcontent-ng-c405365209="" chatcontentdimensions="" class="chat-history-scroll-container lm" id="bkmrk-listens-on-public-po"><div _ngcontent-ng-c405365209="" class="conversation-container message-actions-hover-boundary ng-star-inserted"><div _ngcontent-ng-c3598951857=""><div _ngcontent-ng-c3060770435="" class="response-container response-container-with-gpi ng-tns-c3060770435-24 no-background response-container-has-multiple-responses" data-hveid="1" jslog="173900;track:impression,attention"><div _ngcontent-ng-c3060770435="" class="presented-response-container ng-tns-c3060770435-24" data-hveid="2" jslog="283401;track:impression,attention"><div _ngcontent-ng-c3060770435="" class="response-container-content ng-tns-c3060770435-24"><div _ngcontent-ng-c3598951857="" class="response-content ng-tns-c3060770435-24"><div _ngcontent-ng-c1922256434="" class="container"><div _ngcontent-ng-c2066222059="" aria-busy="false" aria-live="polite" class="markdown markdown-main-panel enable-luminous-fast-follows enable-updated-hr-color stronger tutor-markdown-rendering" dir="ltr" inline-copy-host="">- 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.

</div></div></div></div></div></div></div></div></div>### What Nginx Proxy Manager Does:

<div _ngcontent-ng-c405365209="" chatcontentdimensions="" class="chat-history-scroll-container lm" id="bkmrk-listens-internally-o"><div _ngcontent-ng-c405365209="" class="conversation-container message-actions-hover-boundary ng-star-inserted"><div _ngcontent-ng-c3598951857=""><div _ngcontent-ng-c3060770435="" class="response-container response-container-with-gpi ng-tns-c3060770435-24 no-background response-container-has-multiple-responses" data-hveid="1" jslog="173900;track:impression,attention"><div _ngcontent-ng-c3060770435="" class="presented-response-container ng-tns-c3060770435-24" data-hveid="2" jslog="283401;track:impression,attention"><div _ngcontent-ng-c3060770435="" class="response-container-content ng-tns-c3060770435-24"><div _ngcontent-ng-c3598951857="" class="response-content ng-tns-c3060770435-24"><div _ngcontent-ng-c1922256434="" class="container"><div _ngcontent-ng-c2066222059="" aria-busy="false" aria-live="polite" class="markdown markdown-main-panel enable-luminous-fast-follows enable-updated-hr-color stronger tutor-markdown-rendering" dir="ltr" inline-copy-host="">- 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.

</div></div></div></div></div></div></div></div></div>### 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.

<div _ngcontent-ng-c1958104536="" class="code-block ng-tns-c1958104536-32 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation" data-hveid="3" id="bkmrk-yaml" jslog="223238;track:impression,attention;BardVeMetadataKey:[["r_d832160a9ba7de36","c_26bc2a2bba230e76",null,"rc_c711e786aee4e246",null,null,"en",null,1,null,null,1,0]]"><div _ngcontent-ng-c1958104536="" class="formatted-code-block-internal-container ng-tns-c1958104536-32"><div _ngcontent-ng-c1958104536="" class="animated-opacity ng-tns-c1958104536-32"><div _ngcontent-ng-c1958104536="" class="code-block-decoration header-formatted gds-emphasized-body-m ng-tns-c1958104536-32 ng-star-inserted"><span class="ng-tns-c1958104536-32">YAML</span><div _ngcontent-ng-c1958104536="" class="buttons ng-tns-c1958104536-32 ng-star-inserted"><button aria-label="Download code" class="mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-badge mat-unthemed mat-badge-overlap mat-badge-above mat-badge-after mat-badge-small mat-badge-hidden ng-star-inserted"></button><button aria-label="Copy code" class="mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-badge mat-unthemed mat-badge-overlap mat-badge-above mat-badge-after mat-badge-small mat-badge-hidden ng-star-inserted"></button></div></div></div></div></div>```
  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

```

<div _ngcontent-ng-c1958104536="" class="code-block ng-tns-c1958104536-32 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation" data-hveid="3" id="bkmrk--7" jslog="223238;track:impression,attention;BardVeMetadataKey:[["r_d832160a9ba7de36","c_26bc2a2bba230e76",null,"rc_c711e786aee4e246",null,null,"en",null,1,null,null,1,0]]"><div _ngcontent-ng-c1958104536="" class="formatted-code-block-internal-container ng-tns-c1958104536-32"><div _ngcontent-ng-c1958104536="" class="animated-opacity ng-tns-c1958104536-32"></div></div></div>## 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.

<div _ngcontent-ng-c405365209="" chatcontentdimensions="" class="chat-history-scroll-container lm" id="bkmrk--9"><div _ngcontent-ng-c405365209="" class="conversation-container message-actions-hover-boundary ng-star-inserted" id="bkmrk--10"><div _ngcontent-ng-c3598951857=""><div _ngcontent-ng-c3060770435="" class="response-container response-container-with-gpi ng-tns-c3060770435-24 no-background response-container-has-multiple-responses" data-hveid="1" jslog="173900;track:impression,attention"><div _ngcontent-ng-c3060770435="" class="response-container-footer ng-tns-c3060770435-24 ng-star-inserted"><div _ngcontent-ng-c356251628="" class="actions-container-v2 ng-tns-c356251628-25"><div _ngcontent-ng-c356251628="" class="buttons-container-v2 ng-tns-c356251628-25 ng-star-inserted"><button aria-label="Good response" aria-pressed="false" class="mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-badge mat-unthemed mat-badge-overlap mat-badge-above mat-badge-after mat-badge-small mat-badge-hidden ng-star-inserted"></button><button aria-label="Bad response" aria-pressed="false" class="mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-badge mat-unthemed mat-badge-overlap mat-badge-above mat-badge-after mat-badge-small mat-badge-hidden ng-star-inserted"></button><div _ngcontent-ng-c356251628="" class="ng-tns-c356251628-25 ng-star-inserted"><button aria-label="Redo" class="mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-badge mat-unthemed mat-badge-overlap mat-badge-above mat-badge-after mat-badge-small mat-badge-hidden ng-star-inserted"></button>  
</div><button aria-label="Copy" class="mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-badge mat-unthemed mat-badge-overlap mat-badge-above mat-badge-after mat-badge-small mat-badge-hidden ng-star-inserted"></button><div _ngcontent-ng-c356251628="" class="ng-tns-c356251628-25 ng-star-inserted"><div _ngcontent-ng-c356251628="" class="menu-button-wrapper ng-tns-c356251628-25 ng-star-inserted"><div _ngcontent-ng-c356251628="" class="more-menu-button-container ng-tns-c356251628-25"><button aria-label="Show more options" class="mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-badge mat-unthemed mat-badge-overlap mat-badge-above mat-badge-after mat-badge-small mat-badge-hidden ng-star-inserted"></button>  
</div></div></div><div _ngcontent-ng-c356251628="" class="spacer ng-tns-c356251628-25 ng-star-inserted">  
</div></div></div></div></div></div><div _ngcontent-ng-c405365209="" class="restart-chat-button-scroll-placeholder ng-star-inserted">  
</div></div><div _ngcontent-ng-c405365209="" class="autosuggest-scrim">  
</div></div><fieldset class="input-area-container ng-trigger ng-trigger-inputAreaAnimation" id="bkmrk-%C2%A0-4"></fieldset>

# 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:<port>. 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:

```