CRITICAL INFRA
Loading critical CVEs…
ALL EXPLOITED
Loading…

Case study: OPNsense + Caddy + CrowdSec, correlated decisions

A client exposed web apps through OPNsense and got non-stop scanning and brute-force attempts. We integrated Caddy as a reverse proxy and CrowdSec to read logs from TWO sources — Caddy and the pf firewall — correlate them and automatically block hostile sources. Here is the real configuration.

The requirement: correlated firewall + app defense

The attacks came on two planes: at the network level (port scans, floods of rejected connections) and at the web app level (path scanning, login brute-force, CVE probes). A firewall alone sees only the network; a WAF alone sees only HTTP. We wanted a single brain that sees BOTH and decides.

In short: the same attacker is seen by both the firewall and the web app; we block it once, for everything — automatically.

Architecture

The internet enters through OPNsense (pf firewall), which sends web traffic to Caddy (reverse proxy with automatic HTTPS) and from there to the apps. CrowdSec reads the Caddy logs and the pf logs in parallel, applies scenarios, makes decisions, and a bouncer enforces them.

Internetatacuri OPNsensefirewall (pf) Caddyreverse proxy + HTTPS aplicatii web CrowdSecciteste loguri -> decizii loguri Caddy loguri pf bouncer: block

Caddy: reverse proxy with automatic HTTPS + logs

Caddy does HTTPS automatically (Let's Encrypt) and logs in JSON — a format CrowdSec parses easily.

# Caddyfile
{
    order crowdsec first
    crowdsec {
        api_url http://127.0.0.1:8080
        api_key {env.CROWDSEC_BOUNCER_KEY}
    }
}

app.example.com {
    crowdsec                       # app-layer bouncer (403 for bad IPs)
    reverse_proxy 10.0.0.20:8080
    log {
        output file /var/log/caddy/access.log
        format json
    }
}

CrowdSec: reads TWO sources (Caddy + pf)

This is the key: in acquis.yaml you give CrowdSec both log sources. So it sees both HTTP (from Caddy) and the network (from the OPNsense firewall).

# /etc/crowdsec/acquis.yaml
---
filenames:
  - /var/log/caddy/access.log
labels:
  type: caddy
---
filenames:
  - /var/log/filter/latest.log     # OPNsense firewall logs (filterlog)
labels:
  type: opnsense
---
source: journalctl
journalctl_filter:
  - "_SYSTEMD_UNIT=sshd.service"
labels:
  type: syslog

Scenarios: HTTP + firewall

You install the collections that recognize attacks from each source — path scanning, brute-force, CVE probes (from Caddy) and scans/floods (from the firewall).

cscli collections install crowdsecurity/caddy crowdsecurity/base-http-scenarios crowdsecurity/http-cve
cscli parsers install crowdsecurity/opnsense-logs   # parses filterlog
cscli collections list
cscli hub update && cscli hub upgrade

Bouncer: block at the firewall + 403 at Caddy

Two enforcement levels: the firewall bouncer puts the bad IP into a pf/nftables set (stops it before it reaches Caddy), and the Caddy bouncer returns 403 at the app layer. Together they cover everything.

# 1. firewall bouncer (on OPNsense / Linux) -> block in pf/nftables
apt -y install crowdsec-firewall-bouncer-nftables
cscli bouncers add opnsense-fw
# 2. Caddy bouncer (403 at HTTP layer) -> the key in the Caddyfile
cscli bouncers add caddy-bouncer
cscli bouncers list

Decisions and operations

It correlates everything into one set of decisions, plus community reputation (IPs reported across the whole CrowdSec network).

cscli metrics                    # what it reads from each source
cscli decisions list             # IPs blocked now (from Caddy OR pf)
cscli alerts list --since 24h
cscli console enroll <KEY>       # dashboard + CTI blocklists

Result

An attacker who scans ports and then tries login brute-force is caught by both sources and blocked once, at the firewall, before touching the app again. False positives are reduced by tuning, everything is visible in the CrowdSec console, and community reputation stops many attacks before they start. We design it, tune it and bring it into 24/7 monitoring.

Let's discuss your project →