diff --git a/docs/src/gateways.md b/docs/src/gateways.md index d3c2e86..d060a4e 100644 --- a/docs/src/gateways.md +++ b/docs/src/gateways.md @@ -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 | | DNS | Blocky (blocklist resolver), metrics on :4000 scraped by control | | 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 | | 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 `*..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/`). 2. Add the machine to `inventory.machines` in `clan.nix`, to the `router` instance in `inventory.nix` (`roles.default.machines.gw--.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`. Do **not** add it to `modules/hosts.nix` (dynamic PPPoE IP; clan connects over the mesh). diff --git a/inventory.nix b/inventory.nix index 2899d27..a274d12 100644 --- a/inventory.nix +++ b/inventory.nix @@ -77,6 +77,8 @@ in }; # This site runs the Omada controller for its APs/switches. 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 # resolves the names to the router's LAN address for mgmt+lan clients. diff --git a/modules/clan/router/README.md b/modules/clan/router/README.md index d47c14d..0dfcabc 100644 --- a/modules/clan/router/README.md +++ b/modules/clan/router/README.md @@ -3,10 +3,11 @@ 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 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 -timer. Optional: a Wi-Fi access point on the router's own radios (hostapd), -the TP-Link Omada controller (podman) and an internal Caddy reverse proxy -with a real wildcard certificate (ACME DNS-01). +firewall/NAT, DHCPv6-PD, an iperf3 server and a WAN speed-test timer. +Optional: a Wi-Fi access point on the router's own radios (hostapd), CrowdSec +with the nftables bouncer (sshd log parsing), the TP-Link Omada controller +(podman) and an internal Caddy reverse proxy with a real wildcard certificate +(ACME DNS-01). Addressing convention: a site owns `10..0.0/16`; VLAN `` defaults to `10...0/24`, router at `.1`, DHCP pool `.100-.199`. The `mgmt` diff --git a/modules/clan/router/crowdsec.nix b/modules/clan/router/crowdsec.nix index b7128e5..a745e26 100644 --- a/modules/clan/router/crowdsec.nix +++ b/modules/clan/router/crowdsec.nix @@ -1,46 +1,49 @@ # CrowdSec security engine + nftables bouncer: parses sshd auth attempts from # 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 }: -{ ... }: +{ lib, ... }: let cfg = settings; in { - services.crowdsec = { - enable = true; - autoUpdateService = true; - hub.collections = [ - "crowdsecurity/linux" - "crowdsecurity/sshd" - ]; - localConfig = { - acquisitions = [ - { - source = "journalctl"; - journalctl_filter = [ "_SYSTEMD_UNIT=sshd.service" ]; - labels.type = "syslog"; - } - ]; - # Never ban the ZeroTier mesh — it is the only admin path to these - # boxes (no public SSH), so a false positive would lock us out. - # Parser-stage whitelist: mesh events are dropped before any scenario. - parsers.s02Enrich = [ - { - name = "cnx/mesh-whitelist"; - description = "Whitelist the ZeroTier management mesh"; - whitelist = { - reason = "ZeroTier mesh is the admin path"; - cidr = [ cfg.mesh.subnet ]; - }; - } + config = lib.mkIf cfg.crowdsec.enable { + services.crowdsec = { + enable = true; + autoUpdateService = true; + hub.collections = [ + "crowdsecurity/linux" + "crowdsecurity/sshd" ]; + localConfig = { + acquisitions = [ + { + source = "journalctl"; + journalctl_filter = [ "_SYSTEMD_UNIT=sshd.service" ]; + labels.type = "syslog"; + } + ]; + # Never ban the ZeroTier mesh — it is the only admin path to these + # boxes (no public SSH), so a false positive would lock us out. + # Parser-stage whitelist: mesh events are dropped before any scenario. + parsers.s02Enrich = [ + { + name = "cnx/mesh-whitelist"; + description = "Whitelist the ZeroTier management mesh"; + whitelist = { + reason = "ZeroTier mesh is the admin path"; + cidr = [ cfg.mesh.subnet ]; + }; + } + ]; + }; + }; + + services.crowdsec-firewall-bouncer = { + enable = true; + registerBouncer.enable = true; + settings.mode = "nftables"; }; }; - - services.crowdsec-firewall-bouncer = { - enable = true; - registerBouncer.enable = true; - settings.mode = "nftables"; - }; } diff --git a/modules/clan/router/interface.nix b/modules/clan/router/interface.nix index 94ccfa0..15d2293 100644 --- a/modules/clan/router/interface.nix +++ b/modules/clan/router/interface.nix @@ -285,6 +285,8 @@ in omada.enable = lib.mkEnableOption "TP-Link Omada SDN controller (podman container)"; + crowdsec.enable = lib.mkEnableOption "CrowdSec (sshd log parsing) with the nftables bouncer"; + proxy = { enable = lib.mkEnableOption "internal reverse proxy (Caddy, wildcard cert via DNS-01)"; diff --git a/modules/clan/router/tests/vm/default.nix b/modules/clan/router/tests/vm/default.nix index ce00aef..0509ffb 100644 --- a/modules/clan/router/tests/vm/default.nix +++ b/modules/clan/router/tests/vm/default.nix @@ -131,13 +131,11 @@ in services.openssh.enable = true; # The sandbox has no internet: serve the blocklist from a local file - # instead of GitHub, and skip CrowdSec, whose hub sync needs the network - # (it is not what this test exercises). + # instead of GitHub. (CrowdSec, whose hub sync needs the network too, + # is opt-in and stays off.) services.blocky.settings.blocking.denylists.ads = lib.mkForce [ (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 # wireless client. It lives in its own network namespace, like the