# services/crowdsec.nix — Vendors the crowdsec module rewrite from # https://github.com/NixOS/nixpkgs/pull/446307 (TornaxO7's branch) until # it lands upstream. The upstream module in nixpkgs at the pinned revision # is broken for first-time bootstrap (no auto cscli machines add, DynamicUser # state ownership wedges). # # When PR #446307 merges to nixpkgs unstable: # 1. Bump flake.lock past the merge commit # 2. Delete ../modules/crowdsec/ and the disabledModules + imports lines below # 3. The settings/option API is the same as the PR's, so config below is forward-compatible # # Before first deploy, create /var/secrets/ntfy-url with your topic URL: # echo 'https://ntfy.sh/nordhammer-' | sudo tee /var/secrets/ntfy-url # sudo chmod 640 /var/secrets/ntfy-url { 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 { disabledModules = [ "services/security/crowdsec.nix" "services/security/crowdsec-firewall-bouncer.nix" ]; imports = [ ../modules/crowdsec/crowdsec.nix ../modules/crowdsec/crowdsec-firewall-bouncer.nix ]; config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") { services.crowdsec = { enable = true; name = "fredos-mediaserver"; hub.collections = [ "crowdsecurity/linux" # sshd + linux LPE "crowdsecurity/nginx" # nginx parser "crowdsecurity/base-http-scenarios" # generic HTTP attacks "crowdsecurity/http-cve" # known-CVE fingerprints "crowdsecurity/whitelist-good-actors" # don't ban legit crawlers ]; # Allow the agent to read nginx logs (it runs as DynamicUser). readOnlyPaths = [ "/var/log/nginx" ]; settings = { # config.yaml — main agent + LAPI configuration config.api.server.listen_uri = "127.0.0.1:8081"; # 8080 is qBit # Log sources to ingest 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 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 default profiles 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"; } ]; }; }; # Firewall bouncer enforces decisions via nftables; auto-registers with LAPI services.crowdsec-firewall-bouncer = { enable = true; registerBouncer.enable = true; }; }; }