{ config, pkgs, lib, ... }: let cfg = config.services.crowdsec; format = pkgs.formats.yaml {}; configFile = format.generate "crowdsec.yaml" cfg.settings; pkg = cfg.package; defaultPatterns = [pkg.patterns]; patternsDir = pkgs.symlinkJoin { name = "crowdsec-merged-patterns"; paths = [cfg.patterns];}; defaultSettings = with lib; { common = { daemonize = mkForce false; log_media = mkForce "stdout"; }; config_paths = { config_dir = mkDefault "/var/lib/crowdsec/config"; data_dir = mkDefault dataDir; hub_dir = mkDefault hubDir; index_path = mkDefault "${hubDir}/.index.json"; simulation_path = mkDefault "${pkg}/share/crowdsec/config/simulation.yaml"; pattern_dir = mkDefault patternsDir; }; db_config = { type = mkDefault "sqlite"; db_path = mkDefault "${dataDir}/crowdsec.db"; use_wal = true; }; crowdsec_service = { enable = mkDefault true; acquisition_dir = let yamlFiles = map (format.generate "acquisition.yaml") cfg.acquisitions; dir = pkgs.runCommand "crowdsec-acquisitions" {} '' mkdir -p $out cp ${lib.concatStringsSep " " yamlFiles} $out ''; in mkDefault dir; }; api = { client = { credentials_path = mkDefault "${stateDir}/local_api_credentials.yaml"; }; server = { enable = mkDefault (cfg.enrollKeyFile != null); listen_uri = mkDefault "127.0.0.1:8080"; console_path = mkDefault "${stateDir}/console.yaml"; profiles_path = mkDefault "${pkg}/share/crowdsec/config/profiles.yaml"; online_client.credentials_path = mkDefault "${stateDir}/online_api_credentials.yaml"; }; }; prometheus = { enabled = mkDefault true; level = mkDefault "full"; listen_addr = mkDefault "127.0.0.1"; listen_port = mkDefault 6060; }; }; user = "crowdsec"; group = "crowdsec"; stateDir = "/var/lib/crowdsec"; dataDir = "${stateDir}/data"; hubDir = "${stateDir}/hub"; in { options.services.crowdsec = with lib; { enable = mkEnableOption "CrowSec Security Engine"; package = mkOption { description = "The crowdsec package to use in this module"; type = types.package; default = pkgs.callPackage ../../packages/crowdsec {}; }; name = mkOption { type = types.str; description = mdDoc '' Name of the machine when registering it at the central or local api. ''; default = config.networking.hostName; }; enrollKeyFile = mkOption { description = mdDoc '' The file containing the enrollment key used to enroll the engine at the central api console. See for details. ''; type = types.nullOr types.path; default = null; }; acquisitions = mkOption { type = with types; listOf format.type; default = {}; description = mdDoc '' A list of acquisition specifications, which define the data sources you want to be parsed. See for details. ''; example = [ { source = "journalctl"; journalctl_filter = ["_SYSTEMD_UNIT=sshd.service"]; labels.type = "syslog"; } ]; }; patterns = mkOption { description = mdDoc '' A list of pattern derrivations for parsing logs, in the form "type" to file containing the corresponding GROK patterns. Files in the derriviatons will be merged into one and must only contains files in the root of the derivation. All default patterns are automatically included. See . ''; type = types.listOf types.package; #types.attrsOf types.pathInStore; default = []; example = lib.literalExpression '' [ (pkgs.writeTextDir "ssh" (builtins.readFile ./ssh)) ] ''; }; settings = mkOption { description = mdDoc '' Settings for Crowdsec. Refer to the defaults at . ''; type = format.type; default = {}; }; allowLocalJournalAccess = mkOption { description = mkDoc '' Allow acquisitions from local systemd-journald. For details, see . ''; type = types.bool; default = false; }; }; config = let cscli = pkgs.writeScriptBin "cscli" '' #!${pkgs.runtimeShell} set -eu set -o pipefail # cscli needs crowdsec on it's path in order to be able to run `cscli explain` export PATH=$PATH:${lib.makeBinPath [pkg]} exec ${pkg}/bin/cscli -c=${configFile} "''${@}" ''; in lib.mkIf (cfg.enable) { services.crowdsec.settings = defaultSettings; services.crowdsec.patterns = defaultPatterns; environment = { systemPackages = [cscli]; }; systemd.packages = [pkg]; systemd.timers.crowdsec-update-hub = { description = "Update the crowdsec hub index"; wantedBy = ["timers.target"]; timerConfig = { OnCalendar = "daily"; Persistent = "yes"; Unit = "crowdsec-update-hub.service"; }; }; systemd.services = let sudo_doas = if config.security.doas.enable == true then "${pkgs.doas}/bin/doas" else "${pkgs.sudo}/bin/sudo"; in { crowdsec-update-hub = { description = "Update the crowdsec hub index"; path = [cscli]; serviceConfig = { Type = "oneshot"; ExecStart = "${sudo_doas} -u crowdsec ${cscli}/bin/cscli --error hub upgrade"; ExecStartPost = " systemctl restart crowdsec.service"; }; }; crowdsec = { description = "CrowdSec is a free, modern & collaborative behavior detection engine, coupled with a global IP reputation network."; path = [cscli]; wantedBy = ["multi-user.target"]; after = ["network-online.target"]; wants = ["network-online.target"]; serviceConfig = with lib; { User = "crowdsec"; Group = "crowdsec"; Restart = "on-failure"; LimitNOFILE = mkDefault 65536; CapabilityBoundingSet = mkDefault []; NoNewPrivileges = mkDefault true; LockPersonality = mkDefault true; RemoveIPC = mkDefault true; ReadWritePaths = [stateDir]; ProtectSystem = mkDefault "strict"; PrivateUsers = mkDefault true; ProtectHome = mkDefault true; PrivateTmp = mkDefault true; PrivateDevices = mkDefault true; ProtectHostname = mkDefault true; ProtectKernelTunables = mkDefault true; ProtectKernelModules = mkDefault true; ProtectControlGroups = mkDefault true; ProtectProc = mkDefault "invisible"; ProcSubset = mkIf (!cfg.allowLocalJournalAccess) (mkDefault "pid"); RestrictNamespaces = mkDefault true; RestrictRealtime = mkDefault true; RestrictSUIDSGID = mkDefault true; SystemCallFilter = mkDefault ["@system-service" "@network-io"]; SystemCallArchitectures = ["native"]; SystemCallErrorNumber = mkDefault "EPERM"; ExecPaths = ["/nix/store"]; NoExecPaths = ["/"]; ExecStart = "${pkg}/bin/crowdsec -c ${configFile}"; ExecStartPre = let script = pkgs.writeScriptBin "crowdsec-setup" '' #!${pkgs.runtimeShell} set -eu set -o pipefail if [ ! -s "${cfg.settings.api.client.credentials_path}" ]; then cscli machine add "${cfg.name}" --auto fi ${lib.optionalString cfg.settings.api.server.enable '' if ! grep -q password "${cfg.settings.api.server.online_client.credentials_path}" ]; then cscli capi register fi cscli hub update ${lib.optionalString (cfg.enrollKeyFile != null) '' if [ ! -e "${cfg.settings.api.server.console_path}" ]; then cscli console enroll "$(cat ${cfg.enrollKeyFile})" --name ${cfg.name} fi ''} ''} ''; in ["${script}/bin/crowdsec-setup"]; }; }; }; systemd.tmpfiles.rules = [ "d '${stateDir}' 0750 ${user} ${group} - -" "d '${dataDir}' 0750 ${user} ${group} - -" "d '${hubDir}' 0750 ${user} ${group} - -" "f '${cfg.settings.api.server.online_client.credentials_path}' 0750 ${user} ${group} - -" "f '${cfg.settings.config_paths.index_path}' 0750 ${user} ${group} - -" ]; users.users.${user} = { name = lib.mkDefault user; description = lib.mkDefault "Crowdsec service user"; isSystemUser = lib.mkDefault true; group = lib.mkDefault group; extraGroups = lib.mkIf cfg.allowLocalJournalAccess ["systemd-journal"]; }; users.groups.${group} = lib.mapAttrs (name: lib.mkDefault) {}; }; }