router: make CrowdSec opt-in (crowdsec.enable, default off)

Not every site wants the ban engine (hub sync needs internet at
activation, and it is one more moving part on a small box). Gate
crowdsec.nix on a new `crowdsec.enable` option like `omada.enable`.

gw-cnx-1 keeps it on; the VM test drops its mkForce overrides.
This commit is contained in:
2026-09-21 07:20:17 +00:00
parent e1e18dd9f3
commit 596ac1f4bb
6 changed files with 51 additions and 45 deletions
+2 -2
View File
@@ -28,7 +28,7 @@ mgmt-only trust model, SSH exposure and the Wi-Fi bridge ports. Run it with
| DHCP | Kea, one subnet per VLAN | | DHCP | Kea, one subnet per VLAN |
| DNS | Blocky (blocklist resolver), metrics on :4000 scraped by control | | DNS | Blocky (blocklist resolver), metrics on :4000 scraped by control |
| IPv6 | DHCPv6-PD on ppp0, /64 per VLAN via SLAAC | | IPv6 | DHCPv6-PD on ppp0, /64 per VLAN via SLAAC |
| Bans | CrowdSec + nftables bouncer (sshd log parsing) | | Bans | Optional per site (`crowdsec.enable`): CrowdSec + nftables bouncer (sshd log parsing) |
| Omada | Optional per site: TP-Link Omada controller as a podman container | | Omada | Optional per site: TP-Link Omada controller as a podman container |
| Wi-Fi | Optional: hostapd on the router's radios; each SSID (`wifi.networks`) is an untagged access port of its VLAN, passphrases via vars prompts — see `modules/clan/router/README.md` | | Wi-Fi | Optional: hostapd on the router's radios; each SSID (`wifi.networks`) is an untagged access port of its VLAN, passphrases via vars prompts — see `modules/clan/router/README.md` |
| Proxy | Optional: Caddy reverse proxy for internal services under `*.<site><n>.cnx.network` with a real Let's Encrypt wildcard (DNS-01 against ns1) | | Proxy | Optional: Caddy reverse proxy for internal services under `*.<site><n>.cnx.network` with a real Let's Encrypt wildcard (DNS-01 against ns1) |
@@ -71,7 +71,7 @@ Trust model: mgmt → everything; other VLANs → router DNS/DHCP + internet onl
installer: `ls -l /dev/disk/by-id/`). installer: `ls -l /dev/disk/by-id/`).
2. Add the machine to `inventory.machines` in `clan.nix`, to the `router` 2. Add the machine to `inventory.machines` in `clan.nix`, to the `router`
instance in `inventory.nix` (`roles.default.machines.gw-<city>-<n>.settings`: `site`, instance in `inventory.nix` (`roles.default.machines.gw-<city>-<n>.settings`: `site`,
`siteId` (next free number), port names, VLANs, `omada.enable`; keep the `siteId` (next free number), port names, VLANs, `omada.enable`, `crowdsec.enable`; keep the
`mgmt`/`lan` VLANs), and to the machine list in `modules/mesh-hosts.nix`. `mgmt`/`lan` VLANs), and to the machine list in `modules/mesh-hosts.nix`.
Do **not** add it to `modules/hosts.nix` (dynamic PPPoE IP; clan connects Do **not** add it to `modules/hosts.nix` (dynamic PPPoE IP; clan connects
over the mesh). over the mesh).
+2
View File
@@ -77,6 +77,8 @@ in
}; };
# This site runs the Omada controller for its APs/switches. # This site runs the Omada controller for its APs/switches.
omada.enable = true; omada.enable = true;
# sshd ban engine (was unconditional before the option existed).
crowdsec.enable = true;
# Internal reverse proxy: real wildcard cert *.cnx1.cnx.network; Blocky # Internal reverse proxy: real wildcard cert *.cnx1.cnx.network; Blocky
# resolves the names to the router's LAN address for mgmt+lan clients. # resolves the names to the router's LAN address for mgmt+lan clients.
+5 -4
View File
@@ -3,10 +3,11 @@
Turns a machine with several NICs into a site gateway: PPPoE WAN (ISP Turns a machine with several NICs into a site gateway: PPPoE WAN (ISP
credentials via vars prompts), a VLAN-filtering bridge over the LAN ports with credentials via vars prompts), a VLAN-filtering bridge over the LAN ports with
one L3 interface per VLAN, Kea DHCP and Blocky DNS per VLAN, nftables one L3 interface per VLAN, Kea DHCP and Blocky DNS per VLAN, nftables
firewall/NAT, DHCPv6-PD, CrowdSec, an iperf3 server and a WAN speed-test firewall/NAT, DHCPv6-PD, an iperf3 server and a WAN speed-test timer.
timer. Optional: a Wi-Fi access point on the router's own radios (hostapd), Optional: a Wi-Fi access point on the router's own radios (hostapd), CrowdSec
the TP-Link Omada controller (podman) and an internal Caddy reverse proxy with the nftables bouncer (sshd log parsing), the TP-Link Omada controller
with a real wildcard certificate (ACME DNS-01). (podman) and an internal Caddy reverse proxy with a real wildcard certificate
(ACME DNS-01).
Addressing convention: a site owns `10.<siteId>.0.0/16`; VLAN `<id>` defaults Addressing convention: a site owns `10.<siteId>.0.0/16`; VLAN `<id>` defaults
to `10.<siteId>.<id>.0/24`, router at `.1`, DHCP pool `.100-.199`. The `mgmt` to `10.<siteId>.<id>.0/24`, router at `.1`, DHCP pool `.100-.199`. The `mgmt`
+5 -2
View File
@@ -1,12 +1,14 @@
# CrowdSec security engine + nftables bouncer: parses sshd auth attempts from # CrowdSec security engine + nftables bouncer: parses sshd auth attempts from
# the journal and bans offending source IPs at the firewall. Log-based (no # the journal and bans offending source IPs at the firewall. Log-based (no
# inline DPI) so it costs the N300 next to nothing. # inline DPI) so it costs the N300 next to nothing. Opt-in per site
# (`crowdsec.enable`): the hub sync needs internet at activation time.
{ settings }: { settings }:
{ ... }: { lib, ... }:
let let
cfg = settings; cfg = settings;
in in
{ {
config = lib.mkIf cfg.crowdsec.enable {
services.crowdsec = { services.crowdsec = {
enable = true; enable = true;
autoUpdateService = true; autoUpdateService = true;
@@ -43,4 +45,5 @@ in
registerBouncer.enable = true; registerBouncer.enable = true;
settings.mode = "nftables"; settings.mode = "nftables";
}; };
};
} }
+2
View File
@@ -285,6 +285,8 @@ in
omada.enable = lib.mkEnableOption "TP-Link Omada SDN controller (podman container)"; omada.enable = lib.mkEnableOption "TP-Link Omada SDN controller (podman container)";
crowdsec.enable = lib.mkEnableOption "CrowdSec (sshd log parsing) with the nftables bouncer";
proxy = { proxy = {
enable = lib.mkEnableOption "internal reverse proxy (Caddy, wildcard cert via DNS-01)"; enable = lib.mkEnableOption "internal reverse proxy (Caddy, wildcard cert via DNS-01)";
+2 -4
View File
@@ -131,13 +131,11 @@ in
services.openssh.enable = true; services.openssh.enable = true;
# The sandbox has no internet: serve the blocklist from a local file # The sandbox has no internet: serve the blocklist from a local file
# instead of GitHub, and skip CrowdSec, whose hub sync needs the network # instead of GitHub. (CrowdSec, whose hub sync needs the network too,
# (it is not what this test exercises). # is opt-in and stays off.)
services.blocky.settings.blocking.denylists.ads = lib.mkForce [ services.blocky.settings.blocking.denylists.ads = lib.mkForce [
(toString (pkgs.writeText "ads.hosts" "0.0.0.0 ads.example.com\n")) (toString (pkgs.writeText "ads.hosts" "0.0.0.0 ads.example.com\n"))
]; ];
services.crowdsec.enable = lib.mkForce false;
services.crowdsec-firewall-bouncer.enable = lib.mkForce false;
# Two simulated radios: wlan0 is the AP (settings above), wlan1 plays a # Two simulated radios: wlan0 is the AP (settings above), wlan1 plays a
# wireless client. It lives in its own network namespace, like the # wireless client. It lives in its own network namespace, like the