70 KiB
tags
| tags | |||
|---|---|---|---|
|
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: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 & 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
| Variable | Purpose | How 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 email | Any valid email |
| `BOOTSTRAP_ADMIN_PASSWORD` | First superadmin password | Strong 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):- Create a free account at https://www.maxmind.com/en/geolite2/signup.
- Download GeoLite2 City (
.mmdbformat). - Place the file in this directory as
GeoLite2-City.mmdb, or setGEOIP_DB_PATHin.envto 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 ```docker compose ps
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:- Log in (see above) and add your upstream application as a site.
- Choose detection mode (log only) or blocking mode (reject attacks).
- 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)
Updating
``` docker compose pull # fetch the latest images (or your pinned WAF_IMAGE_TAG) docker compose up -d # recreate changed containers ```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.
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-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’sheader_uporforward_authdirectives.
If you love the ease of Nginx Proxy Manager for advanced routing, SSL client certificates, and authentik header manipulation, you don't actually have to abandon it. Because SOCFortress is a dedicated WAF, it is highly common to chain them together:
Internet ➔ SOCFortress WAF (Filters bad traffic) ➔ Nginx Proxy Manager (Handles Authentik, headers, & routing) ➔ Your Apps
By placing SOCFortress at the edge, it strips out SQL injections, XSS, and bot traffic before it ever hits NPM. NPM can then securely parse your custom headers and pass them back to your applications exactly as you have it configured right now.
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-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
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 port443if 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: