Files
Compose-Files/Backups/Miker/Bookstack/web-application-firewall-waf.md
2026-07-20 09:23:17 -04:00

70 KiB
Raw Permalink Blame History

tags
tags
Documentation
Bookstack
Notes

Web Application Firewall (WAF)

SOCFortess

SOCFortress Docs

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

Deploy the WAF, get automatic TLS, block live attacks, and tune a false positive — in about 10 minutes:

Watch the demo on YouTube


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) │ └─────────────────────┘

<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
### 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

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.
  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

``` docker compose up -d ```
Wait until all containers report healthy (typically 3060 seconds):
docker compose ps
### 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

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

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

``` 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

``` 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

- 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

- **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 Caddys 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.
<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 Caddys 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:

InternetSOCFortress 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

<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

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