CrowdSec & OpenBSD PF Perimeter Security Setup
1. Overview & Architectural Concept
This document outlines the distributed intrusion detection and perimeter enforcement setup across the local infrastructure (drheck.dev).
[ Public Internet (WAN) ]
│
▼
┌─────────────────────────────────┐
│ danknasty (OpenBSD Router) │
│ ─────────────────────────────── │
│ • OpenBSD PF Kernel Firewall │
│ • Kernel SSH Rate Limiter │
│ • Syslog SSH Forwarder (UDP 514)│
│ • CrowdSec PF Firewall Bouncer │
└────────────────┬────────────────┘
│ Drops banned IPs at WAN edge
│ Stream bans via IPv6 LAN ([fd77:...:c70d]:8080)
▼
┌─────────────────────────────────┐
│ ul (Arch Linux Server) │
│ ─────────────────────────────── │
│ • Dedicated IPv6: ...:c70d/64 │
│ • Nginx Web Reverse Proxy │
│ • Remote Syslog Ingestion (514) │
│ • CrowdSec LAPI Decision Engine │
│ • 53-Hour Ban Policy Engine │
│ • Public Active Enriched JSON │
│ • Append-Only NDJSON Stream │
└─────────────────────────────────┘
Design Philosophy & Conventions
- Dedicated Service IPv6 Address (
ul):- Per local network convention (
20-wired.network), CrowdSec LAPI listens on a dedicated IPv6 ULA:fd77:7777:5010:0:1337:f1ac:d00d:c70don port8080(LAPI) and UDP port514(Syslog Ingestion).
- Per local network convention (
- Offload All Heavy Detection to Powerful Hardware (
ul):- All heavy log analysis, regex pattern matching, SQLite/LAPI database operations, and scenario evaluations run on the Arch Linux server (
ul). - Nginx Web Probes & Direct IP Hits: Parses Nginx access logs to detect scanner bots (e.g.,
.envharvesting, WordPress probes, webshell attempts, open proxy CONNECT attempts, and direct IP connection attempts). - Router SSH Probes, Invalid Users & Root Attempts: Receives OpenBSD SSH authentication logs (
/var/log/authlog) forwarded over UDP IPv6 syslog fromdanknastyto detect SSH brute-force attempts, instant invalid username bans, and instant root attempt bans. - 53-Hour Ban Policy: Issues a global 53-hour ban for any detected web probe, direct IP scan, or SSH brute-force/invalid user/root attempt.
- All heavy log analysis, regex pattern matching, SQLite/LAPI database operations, and scenario evaluations run on the Arch Linux server (
- Perimeter Enforcement at Gateway (
danknasty):- The OpenBSD router (
danknasty) runs only the lightweight CrowdSec PF Bouncer (crowdsec_firewall_bouncer) and nativesyslogd. - Banned IPs are pulled from
uland injected into OpenBSD PF's kernel<crowdsec-blacklists>table. Banned packets are dropped at the network interface before entering the local network.
- The OpenBSD router (
- Zero-Overhead Edge SSH Protection (
danknasty):- OpenBSD PF tracks TCP connection rates directly in kernel state space (
max-src-conn-rate 3/30). - Any bot opening more than 3 SSH connections in 30 seconds is added to the OpenBSD PF
<bruteforce>table and dropped with 0% CPU and ~0 MB RAM usage.
- OpenBSD PF tracks TCP connection rates directly in kernel state space (
- Rich Context & OSINT Enriched Export (
ul):- Every banned IP is automatically enriched with Reverse DNS (PTR), Country, Region, City, Coordinates (Lat/Lon), Timezone, ISP, Organization, and Autonomous System (ASN) details.
- Active Array File:
/var/log/crowdsec-bans-public.json(chmod 644). - Append-Only NDJSON Stream:
/var/log/crowdsec-bans.json(chmod 644).
2. Implementation & Configuration Details
A. Central Decision Engine on ul (Arch Linux)
-
Interface Configuration (
/etc/systemd/network/20-wired.network):# crowdsec Address=fd77:7777:5010::1337:f1ac:d00d:c70d/64 -
Listen URI (
/etc/crowdsec/config.yaml): Bound to dedicated IPv6 ULA[fd77:7777:5010:0:1337:f1ac:d00d:c70d]:8080:api: server: listen_uri: "[fd77:7777:5010:0:1337:f1ac:d00d:c70d]:8080" -
Nginx Log Acquisition (
/etc/crowdsec/acquis.d/nginx.yaml):filenames: - /var/log/nginx/*.log - /var/log/nginx/access.log labels: type: nginx -
Router SSH Syslog Ingestion (
/etc/crowdsec/acquis.d/danknasty-ssh.yaml):source: syslog listen_addr: "fd77:7777:5010:0:1337:f1ac:d00d:c70d" listen_port: 514 labels: type: syslog -
Custom Honeypot Scenario (
/etc/crowdsec/scenarios/honeypot-endpoints.yaml):type: leaky name: custom/honeypot-endpoints description: "Instant trigger on bot/scanner endpoints" filter: | evt.Parsed.request in ["/.env", "/cgi-bin/authLogin.cgi", "/IndoSec.php", "/alfa.php", "/luxe.php", "/BDKR28WP.php"] || evt.Parsed.request matches "(?i)^/(wp-admin|wp-login|wp-includes|\\.env|cgi-bin|alfa\\.php|1\\.php|222\\.php|8\\.php|shell\\.php)" capacity: 1 leakspeed: 1h groupby: evt.Meta.source_ip blackhole: 1m labels: service: nginx type: honeypot_scan remediation: true -
Custom SSH Invalid User Scenario (
/etc/crowdsec/scenarios/ssh-invalid-user.yaml): Instant 53-hour ban for any attempt to log in using an invalid username:type: leaky name: custom/ssh-invalid-user description: "Instant 53h ban for any SSH login attempt using an invalid user" filter: | evt.Meta.log_type == "ssh_failed-auth" && ( evt.Parsed.sshd_invalid_user != "" || evt.Meta.target_user != "" ) capacity: 1 leakspeed: 1h groupby: evt.Meta.source_ip blackhole: 1m labels: service: ssh type: bruteforce remediation: true -
Custom SSH Root Attempt Scenario (
/etc/crowdsec/scenarios/ssh-root-login-attempt.yaml): Instant 53-hour ban for any attempt targeting therootaccount:type: leaky name: custom/ssh-root-login-attempt description: "Instant 53h ban for any SSH login attempt targeting user root" filter: | evt.Meta.log_type == "ssh_failed-auth" && ( evt.Meta.target_user == "root" || evt.Parsed.sshd_invalid_user == "root" || evt.Parsed.sshd_user == "root" ) capacity: 1 leakspeed: 1h groupby: evt.Meta.source_ip blackhole: 1m labels: service: ssh type: bruteforce remediation: true -
Custom Direct IP Access Scenario (
/etc/crowdsec/scenarios/direct-ip-access.yaml): Instant 53-hour ban for any HTTP/HTTPS connection made directly by IP address or invalid host header:type: leaky name: custom/direct-ip-access description: "Instant 53h ban for any HTTP/HTTPS connection made directly to IP address or non-domain host" filter: | evt.Meta.log_type == "http_access-log" && ( evt.Meta.http_status in ["444", "400", "403"] || evt.Parsed.status in ["444", "400", "403"] || evt.Parsed.target_fqdn matches "^([0-9]{1,3}\\.){3}[0-9]{1,3}$" || evt.Parsed.http_host matches "^([0-9]{1,3}\\.){3}[0-9]{1,3}$" ) capacity: 1 leakspeed: 1h groupby: evt.Meta.source_ip blackhole: 1m labels: service: nginx type: direct_ip_scan remediation: true -
53-Hour Ban Policy (
/etc/crowdsec/profiles.yaml):name: default_ip_remediation filters: - Alert.Remediation == true && Alert.GetScope() == "Ip" decisions: - type: ban duration: 53h notifications: - file_default on_success: break --- name: default_range_remediation filters: - Alert.Remediation == true && Alert.GetScope() == "Range" decisions: - type: ban duration: 53h notifications: - file_default on_success: break -
File Notification Plugin (
/etc/crowdsec/notifications/file.yaml): Appends real-time events upon trigger to/var/log/crowdsec-bans.json:type: file name: file_default log_level: info group_wait: 0s group_threshold: 1 format: | {{range . -}} {{$alert := . -}} {{range .Decisions -}} {"timestamp":"{{$alert.CreatedAt}}","ip":"{{.Value}}","action":"{{.Action}}","duration":"{{.Duration}}","reason":"{{.Scenario}}","country":"{{$alert.Source.Cn}}","as_name":"{{$alert.Source.AsName}}"} {{end -}} {{end -}} log_path: "/var/log/crowdsec-bans.json" rotate: enabled: false -
Host Firewall Rules (
/etc/nftables.conf):ip6 saddr fd77:7777:5010::/64 ip6 daddr fd77:7777:5010:0:1337:f1ac:d00d:c70d tcp dport 8080 accept comment "Allow CrowdSec LAPI from local IPv6 LAN" ip6 saddr fd77:7777:5010::/64 ip6 daddr fd77:7777:5010:0:1337:f1ac:d00d:c70d udp dport 514 accept comment "Allow CrowdSec Syslog Ingestion from local IPv6 LAN" -
Automated OSINT Exporter (
/usr/local/bin/crowdsec-export-bans): Runs every 60 seconds (crowdsec-export-bans.timer) to update/var/log/crowdsec-bans-public.json(active JSON array) and append newly discovered ban events with full GeoIP/ASN/rDNS metadata to/var/log/crowdsec-bans.json(chmod 644).
B. Perimeter Enforcement & Syslog Forwarding on danknasty (OpenBSD 7.9 Router)
-
Syslog Forwarding Configuration (
/etc/syslog.conf):auth.info;authpriv.info @[fd77:7777:5010:0:1337:f1ac:d00d:c70d] -
Bouncer Configuration (
/etc/crowdsec-firewall-bouncer/crowdsec-firewall-bouncer.yaml):mode: pf retry_initial_connect: true update_frequency: 10s api_url: http://[fd77:7777:5010:0:1337:f1ac:d00d:c70d]:8080/ api_key: <GENERATED_API_KEY> deny_action: DROP blacklists_ipv4: crowdsec-blacklists blacklists_ipv6: crowdsec6-blacklists pf: anchor_name: "" -
Router Firewall Rules (
/etc/pf.conf):# CrowdSec Blacklist Tables table <crowdsec-blacklists> persist table <crowdsec6-blacklists> persist block drop quick from <crowdsec-blacklists> block drop quick from <crowdsec6-blacklists> # Table for SSH Brute-force Auto-Blocking table <bruteforce> persist block drop quick from <bruteforce> # SSH rules with stateful connection tracking pass in quick on $ext proto tcp to self port ssh \ flags S/SA keep state \ (max-src-conn 10, max-src-conn-rate 3/30, overload <bruteforce> flush global)
3. Interaction, Operations & Data Format
A. Enriched Public JSON Data Schema
The exported JSON files (/var/log/crowdsec-bans-public.json and /var/log/crowdsec-bans.json) contain rich OSINT, network, and threat context for each banned IP:
{
"decision_id": 4,
"alert_id": 4,
"ip": "119.30.119.3",
"ip_version": "IPv4",
"origin": "crowdsec",
"reason": "crowdsecurity/netgear_rce",
"action": "ban",
"duration": "52h59m18s",
"country": "Pakistan",
"country_code": "PK",
"region": "Punjab",
"city": "Sialkot",
"latitude": 32.426,
"longitude": 74.3644,
"timezone": "Asia/Karachi",
"isp": "Mobilink GSM, Pakistan Mobile Communication Ltd.",
"org": "Linkdotnet Telecom Limited",
"as_name": "AS58470 IX Peering for Mobilink and Link Direct International.",
"rdns": "static-host119-30-119-3.link.net.pk",
"target_uri": "/setup.cgi?next_file=netgear.cfg&todo=syscmd&cmd=rm+-rf+/tmp/*...",
"user_agent": "-",
"method": "GET",
"status_code": "301",
"created_at": "2026-08-05T02:44:21Z"
}
B. Viewing Active System State
1. View Active Bans on ul (CrowdSec LAPI)
sudo cscli decisions list
2. View Active Enriched JSON Array
cat /var/log/crowdsec-bans-public.json
3. Tail the Append-Only Ban Stream (NDJSON)
tail -f /var/log/crowdsec-bans.json
4. View Active Bouncers & Connection Status
sudo cscli bouncers list
5. View Detection Metrics & Statistics
Shows lines read via Nginx logs and remote router syslog:
sudo cscli metrics
6. View Active Kernel Ban Tables on OpenBSD Router (danknasty)
# View CrowdSec streamed bans:
SSH_AUTH_SOCK=/home/luke/.local/ssh_auth_sock/ssh_auth_sock ssh danknasty "doas pfctl -t crowdsec-blacklists -T show"
# View OpenBSD SSH kernel brute-force bans:
SSH_AUTH_SOCK=/home/luke/.local/ssh_auth_sock/ssh_auth_sock ssh danknasty "doas pfctl -t bruteforce -T show"
C. Manual Interactivity & Ban Management
1. Manually Ban an IP for 53 Hours
sudo cscli decisions add --ip <IP_ADDRESS> --duration 53h --reason "Manual administrative ban"
2. Remove a Ban (Unban an IP)
sudo cscli decisions delete --ip <IP_ADDRESS>
3. Force Immediate Synchronization & OSINT Enrichment
sudo /usr/local/bin/crowdsec-export-bans
4. Inspect Recent Detection Alerts
sudo cscli alerts list
sudo cscli alerts inspect <ALERT_ID>
5. Flush Banned IP Tables on the Router (Emergency Reset)
SSH_AUTH_SOCK=/home/luke/.local/ssh_auth_sock/ssh_auth_sock ssh danknasty "doas pfctl -t crowdsec-blacklists -T flush"
D. Whitelisting Local Trusted Networks
To prevent accidental self-bans (e.g. during administrative testing or API debugging), create /etc/crowdsec/parsers/s02-override/00-whitelist.yaml on ul:
name: custom/whitelist
description: "Whitelist trusted local LAN IPs"
filter: "evt.Parsed.static_source_ip != ''"
whitelist:
reason: "Trusted internal IPs"
ip:
- "127.0.0.1"
- "10.0.0.3"
- "10.0.0.1"
cidr:
- "10.0.0.0/24"
- "fd77:7777:5010::/64"
After modifying whitelists or scenarios, reload CrowdSec:
sudo systemctl reload crowdsec