From 7ec61469176be1ef630221922afe2fc68c752faa Mon Sep 17 00:00:00 2001 From: ediblerope Date: Fri, 24 Apr 2026 22:30:16 +0100 Subject: [PATCH] crowdsec: add community IDS/IPS with ntfy push alerts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enables the CrowdSec agent with sshd/nginx/http-cve hub collections, acquires logs from nginx, sshd, and Authelia journald, and wires the firewall bouncer to enforce bans via nftables. Alerts are POSTed to a self-chosen ntfy.sh topic (URL read from /var/secrets/ntfy-url, falls back to a placeholder so the repo stays eval-clean without the secret). Module is self-contained — remove the file + import to uninstall; state lives under /var/lib/crowdsec. Co-Authored-By: Claude Opus 4.7 --- common.nix | 1 + services/crowdsec.nix | 101 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 services/crowdsec.nix diff --git a/common.nix b/common.nix index 32fc978..52c6470 100644 --- a/common.nix +++ b/common.nix @@ -34,6 +34,7 @@ ./services/arr-interconnect.nix ./services/adguard.nix ./services/router.nix + ./services/crowdsec.nix ]; ### Make build time quicker diff --git a/services/crowdsec.nix b/services/crowdsec.nix new file mode 100644 index 0000000..786ff60 --- /dev/null +++ b/services/crowdsec.nix @@ -0,0 +1,101 @@ +# services/crowdsec.nix — Community-driven IDS/IPS for the mediaserver. +# +# Removes cleanly: delete this file + its import from common.nix, then +# on the server: `sudo rm -rf /var/lib/crowdsec /etc/crowdsec` after a +# final rebuild. +# +# Before first deploy, create /var/secrets/ntfy-url with your ntfy topic URL: +# echo 'https://ntfy.sh/nordhammer-' | sudo tee /var/secrets/ntfy-url +# sudo chmod 640 /var/secrets/ntfy-url +# Then subscribe to the same URL in the ntfy Android/iOS app. +{ config, lib, ... }: +let + ntfyUrlFile = "/var/secrets/ntfy-url"; + ntfyUrl = + if builtins.pathExists ntfyUrlFile + then lib.removeSuffix "\n" (builtins.readFile ntfyUrlFile) + else "https://ntfy.sh/CHANGE-ME-CREATE-VAR-SECRETS-NTFY-URL"; +in +{ + config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") { + + services.crowdsec = { + enable = true; + + # Hub collections — parsers + scenarios, pulled from the community hub. + hub.collections = [ + "crowdsecurity/linux" # sshd + linux privilege escalation + "crowdsecurity/nginx" # nginx log parser + "crowdsecurity/base-http-scenarios" # generic HTTP attack patterns + "crowdsecurity/http-cve" # known-CVE fingerprints + "crowdsecurity/whitelist-good-actors" # don't ban legit crawlers + ]; + + localConfig = { + # Log sources to ingest. Labels drive which parsers apply. + acquisitions = [ + { + source = "file"; + filenames = [ "/var/log/nginx/access.log" ]; + labels.type = "nginx"; + } + { + source = "journalctl"; + journalctl_filter = [ "_SYSTEMD_UNIT=sshd.service" ]; + labels.type = "syslog"; + } + { + source = "journalctl"; + journalctl_filter = [ "_SYSTEMD_UNIT=authelia-main.service" ]; + labels.type = "syslog"; + } + ]; + + # Push phone notifications via ntfy.sh. + notifications = [ + { + name = "ntfy_http"; + type = "http"; + log_level = "info"; + url = ntfyUrl; + method = "POST"; + headers = { + Title = "CrowdSec alert"; + Priority = "high"; + Tags = "rotating_light"; + }; + format = '' + {{range . -}} + {{.Scenario}} from {{.Source.IP}} ({{.Source.Cn}}) — {{len .Decisions}} decision(s) taken + {{end -}} + ''; + } + ]; + + # Override the default profile to attach the ntfy notifier. + profiles = [ + { + name = "default_ip_remediation"; + filters = [ "Alert.Remediation == true && Alert.GetScope() == 'Ip'" ]; + decisions = [{ type = "ban"; duration = "4h"; }]; + notifications = [ "ntfy_http" ]; + on_success = "break"; + } + { + name = "default_range_remediation"; + filters = [ "Alert.Remediation == true && Alert.GetScope() == 'Range'" ]; + decisions = [{ type = "ban"; duration = "4h"; }]; + notifications = [ "ntfy_http" ]; + on_success = "break"; + } + ]; + }; + }; + + # Enforce CrowdSec decisions at the firewall level via nftables. + services.crowdsec-firewall-bouncer = { + enable = true; + registerBouncer.enable = true; + }; + }; +}