diff --git a/modules/nixos/services/crowdsec/default.nix b/modules/nixos/services/crowdsec/default.nix index acf8aaa..8d4d7e6 100755 --- a/modules/nixos/services/crowdsec/default.nix +++ b/modules/nixos/services/crowdsec/default.nix @@ -13,34 +13,20 @@ let ntfyServer = "https://ntfy.mjallen.dev"; ntfyTopic = "crowdsec"; - # CrowdSec HTTP notification plugin config — written to - # /etc/crowdsec/notifications/ntfy.yaml at runtime. Credentials are - # injected via EnvironmentFile so the plugin can reference them with - # {{env "NTFY_USER"}} / {{env "NTFY_PASSWORD"}} in the URL. - ntfyPluginConfig = pkgs.writeText "crowdsec-ntfy.yaml" '' - type: http - name: ntfy_plugin - log_level: info - format: | - {{range . -}} - CrowdSec blocked: {{.Scenario}} - Source IP: {{.Source.Value}} - Country: {{.Source.Cn}} - Decisions: {{.Decisions | len}} - {{range .Decisions -}} - Action: {{.Type}} for {{.Duration}} - {{end}} - {{- end}} - url: ${ntfyServer}/${ntfyTopic} - method: POST - headers: - Title: "CrowdSec: {{(index . 0).Scenario}}" - Priority: "high" - Tags: "rotating_light,shield" - Authorization: "Basic {{b64enc (print (env "NTFY_USER") ":" (env "NTFY_PASSWORD"))}}" - skip_tls_verify: false - timeout: 10s - ''; + # Build the notification-http plugin binary from the crowdsec source. + # The nixpkgs crowdsec package omits all notification plugin binaries; + # we build just the http one we need. + crowdsecHttpPlugin = pkgs.buildGoModule { + pname = "crowdsec-notification-http"; + inherit (pkgs.crowdsec) version src; + vendorHash = pkgs.crowdsec.vendorHash or null; + subPackages = [ "cmd/notification-http" ]; + ldflags = [ + "-s" + "-w" + ]; + meta.description = "CrowdSec HTTP notification plugin"; + }; crowdsecConfig = lib.${namespace}.mkModule { inherit config name; @@ -144,17 +130,26 @@ let ]; }; settings = { - general.api = { - server = { - enable = true; - listen_uri = "${cfg.listenAddress}:${toString cfg.port}"; + general = { + api = { + server = { + enable = true; + listen_uri = "${cfg.listenAddress}:${toString cfg.port}"; + }; + client = { + credentials_path = lib.mkForce "${cfg.configDir}/crowdsec/client.yaml"; + }; }; - client = { - credentials_path = lib.mkForce "${cfg.configDir}/crowdsec/client.yaml"; + # plugin_config must be in settings.general so it ends up in the + # NixOS-generated crowdsec.yaml that the daemon reads via -c. + plugin_config = lib.mkIf cfg.ntfy.enable { + user = "crowdsec"; + group = "crowdsec"; }; }; capi.credentialsFile = lib.mkDefault "${cfg.configDir}/crowdsec/capi.yaml"; }; + }; crowdsec-firewall-bouncer = { @@ -175,7 +170,12 @@ let # but /var/lib/crowdsec already exists as a real dir. Disabling DynamicUser on # those two services lets them use the real crowdsec user/group instead, which is # consistent with how crowdsec.service itself runs. - systemd.services.crowdsec.serviceConfig.DynamicUser = lib.mkForce false; + systemd.services.crowdsec.serviceConfig = lib.mkMerge [ + { DynamicUser = lib.mkForce false; } + (lib.mkIf (cfg.ntfy.enable && cfg.ntfy.envFile != "") { + EnvironmentFile = [ cfg.ntfy.envFile ]; + }) + ]; systemd.services.crowdsec-firewall-bouncer.serviceConfig.DynamicUser = lib.mkForce false; systemd.services.crowdsec-firewall-bouncer-register.serviceConfig.DynamicUser = lib.mkForce false; @@ -220,12 +220,11 @@ let # crowdsec-firewall-bouncer-register calls cscli without -c, so cscli # looks for /etc/crowdsec/config.yaml. The upstream crowdsec.service uses - # a nix store path via -c and never creates that file. Expose the config - # at /etc/crowdsec/config.yaml by extracting the store path from the - # crowdsec service's ExecStart list at NixOS eval time. + # a nix store path via -c and never creates that file. Expose the full + # NixOS-generated config (which includes plugin_config via + # settings.general.plugin_config) at the well-known path. environment.etc."crowdsec/config.yaml" = let - # ExecStart is [ " " "/crowdsec -c -info" ] execStart = builtins.elemAt config.systemd.services.crowdsec.serviceConfig.ExecStart 1; configPath = builtins.head (builtins.match ".* -c ([^ ]+) .*" execStart); in @@ -240,16 +239,27 @@ let # ntfy notifications via the CrowdSec HTTP notification plugin # --------------------------------------------------------------------------- - # Drop the plugin config YAML into /etc/crowdsec/notifications/. - # CrowdSec scans this directory on startup and registers any plugin - # config files it finds. - environment.etc."crowdsec/notifications/ntfy.yaml" = lib.mkIf cfg.ntfy.enable { - source = ntfyPluginConfig; - mode = "0440"; + # Place the notification-http binary at the path the NixOS crowdsec module + # hardcodes for plugin_dir (/etc/crowdsec/plugins/). CrowdSec matches + # plugins by their filename — it expects "notification-http" for type=http. + environment.etc."crowdsec/plugins/notification-http" = lib.mkIf cfg.ntfy.enable { + source = "${crowdsecHttpPlugin}/bin/notification-http"; + mode = "0550"; user = "crowdsec"; group = "crowdsec"; }; + # The ntfy plugin config YAML (with credentials baked in) is managed as a + # SOPS template in sops.nix — it renders to /run/secrets/rendered/crowdsec/ + # notifications/ntfy.yaml at runtime. We use a tmpfiles symlink to expose + # it at the path CrowdSec scans, since environment.etc can't reference + # /run paths as source. + systemd.tmpfiles.rules = lib.mkIf cfg.ntfy.enable [ + "L /etc/crowdsec/notifications/ntfy.yaml - - - - ${ + config.sops.templates."crowdsec/notifications/ntfy.yaml".path + }" + ]; + # CrowdSec profiles.yaml: route every alert to the ntfy plugin. # This replaces the default "do nothing" profile. environment.etc."crowdsec/profiles.yaml" = lib.mkIf cfg.ntfy.enable { @@ -279,13 +289,6 @@ let group = "crowdsec"; }; - # Inject NTFY_USER and NTFY_PASSWORD into the crowdsec service so the - # HTTP plugin template can reference them. The plugin config uses - # {{env "NTFY_BASIC_AUTH"}} — a pre-encoded "user:pass" base64 string - # for the Authorization: Basic header — computed in ExecStartPre. - systemd.services.crowdsec.serviceConfig.EnvironmentFile = lib.mkIf cfg.ntfy.enable [ - cfg.ntfy.envFile - ]; }; }; in diff --git a/systems/x86_64-linux/jallen-nas/sops.nix b/systems/x86_64-linux/jallen-nas/sops.nix index e2f55dd..3b25bb8 100755 --- a/systems/x86_64-linux/jallen-nas/sops.nix +++ b/systems/x86_64-linux/jallen-nas/sops.nix @@ -259,10 +259,10 @@ in "jallen-nas/ntfy/auth-users" = { sopsFile = defaultSops; }; + "jallen-nas/ntfy/user" = { sopsFile = defaultSops; mode = "0440"; - owner = "grafana"; group = "keys"; restartUnits = [ "grafana.service" @@ -273,7 +273,6 @@ in "jallen-nas/ntfy/password" = { sopsFile = defaultSops; mode = "0440"; - owner = "grafana"; group = "keys"; restartUnits = [ "grafana.service" @@ -357,7 +356,8 @@ in NTFY_USER=${config.sops.placeholder."jallen-nas/ntfy/user"} NTFY_PASSWORD=${config.sops.placeholder."jallen-nas/ntfy/password"} ''; - mode = "0600"; + mode = "0640"; + group = "keys"; restartUnits = [ "crowdsec.service" "upsmon.service" @@ -366,6 +366,33 @@ in ]; }; + # CrowdSec HTTP notification plugin config with credentials baked in. + # The plugin process spawned by crowdsec/cscli reads this file directly. + # Credentials are embedded in the URL using HTTP basic auth so no + # base64 encoding or env var injection is needed. + "crowdsec/notifications/ntfy.yaml" = { + content = '' + type: http + name: ntfy_plugin + log_level: info + format: "{{range . -}}CrowdSec blocked: {{.Scenario}}\nSource IP: {{.Source.Value}}\nCountry: {{.Source.Cn}}\nDecisions: {{.Decisions | len}}{{range .Decisions}}\nAction: {{.Type}} for {{.Duration}}{{end}}\n{{end}}" + url: https://${config.sops.placeholder."jallen-nas/ntfy/user"}:${ + config.sops.placeholder."jallen-nas/ntfy/password" + }@ntfy.mjallen.dev/crowdsec + method: POST + headers: + Title: "CrowdSec: {{(index . 0).Scenario}}" + Priority: "high" + Tags: "rotating_light,shield" + skip_tls_verify: false + timeout: 10s + ''; + mode = "0440"; + owner = "crowdsec"; + group = "crowdsec"; + restartUnits = [ "crowdsec.service" ]; + }; + "paperless.env" = { content = '' PAPERLESS_ADMIN_USER = "mjallen" diff --git a/systems/x86_64-linux/jallen-nas/users.nix b/systems/x86_64-linux/jallen-nas/users.nix index 771eec0..47b6d47 100755 --- a/systems/x86_64-linux/jallen-nas/users.nix +++ b/systems/x86_64-linux/jallen-nas/users.nix @@ -57,8 +57,16 @@ in prometheus = { extraGroups = [ "keys" ]; }; + + # crowdsec needs to read the ntfy.env SOPS template for notifications. + crowdsec = { + isSystemUser = true; + group = "crowdsec"; + extraGroups = [ "keys" ]; + }; }; groups.nextcloud-exporter = { }; + groups.crowdsec = { }; }; }