{ config, pkgs, lib, ... }: let format = pkgs.formats.yaml { }; rootDir = "/var/lib/crowdsec"; stateDir = "${rootDir}/state"; confDir = "/etc/crowdsec/"; hubDir = "${stateDir}/hub/"; notificationsDir = "${confDir}/notifications/"; pluginDir = "${confDir}/plugins/"; parsersDir = "${confDir}/parsers/"; localPostOverflowsDir = "${confDir}/postoverflows/"; localPostOverflowsS01WhitelistDir = "${localPostOverflowsDir}/s01-whitelist/"; localScenariosDir = "${confDir}/scenarios/"; localParsersS00RawDir = "${parsersDir}/s00-raw/"; localParsersS01ParseDir = "${parsersDir}/s01-parse/"; localParsersS02EnrichDir = "${parsersDir}/s02-enrich/"; localContextsDir = "${confDir}/contexts/"; in { options.services.crowdsec = with lib; { enable = mkEnableOption "CrowdSec Security Engine"; package = mkPackageOption pkgs "crowdsec" { }; autoUpdateService = mkEnableOption "Auto Hub Update"; openFirewall = mkEnableOption "opening the ports in the firewall"; user = mkOption { type = types.str; description = "The user to run crowdsec as"; default = "crowdsec"; }; group = mkOption { type = types.str; description = "The group to run crowdsec as"; default = "crowdsec"; }; name = mkOption { type = types.str; description = '' Name of the machine when registering it at the central or local api. ''; default = config.networking.hostName; defaultText = lib.literalExpression "config.networking.hostName"; }; localConfig = mkOption { type = types.submodule { options = { acquisitions = mkOption { type = types.listOf format.type; default = [ ]; description = '' 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"; }; } ]; }; scenarios = mkOption { type = types.listOf format.type; default = [ ]; description = '' A list of scenarios specifications. See for details. ''; example = [ { type = "leaky"; name = "crowdsecurity/myservice-bf"; description = "Detect myservice bruteforce"; filter = "evt.Meta.log_type == 'myservice_failed_auth'"; leakspeed = "10s"; capacity = 5; groupby = "evt.Meta.source_ip"; } ]; }; parsers = mkOption { type = types.submodule { options = { s00Raw = mkOption { type = types.listOf format.type; default = [ ]; description = '' A list of stage s00-raw specifications. Most of the time, those are already included in the hub, but are presented here anyway. See for details. ''; }; s01Parse = mkOption { type = types.listOf format.type; default = [ ]; description = '' A list of stage s01-parse specifications. See for details. ''; example = [ { filter = "1=1"; debug = true; onsuccess = "next_stage"; name = "example/custom-service-logs"; description = "Parsing custom service logs"; grok = { pattern = "^%{DATA:some_data}$"; apply_on = "message"; }; statics = [ { parsed = "is_my_custom_service"; value = "yes"; } ]; } ]; }; s02Enrich = mkOption { type = types.listOf format.type; default = [ ]; description = '' A list of stage s02-enrich specifications. Inside this list, you can specify Parser Whitelists. See for details. ''; example = [ { name = "myips/whitelist"; description = "Whitelist parse events from my IPs"; whitelist = { reason = "My IP ranges"; ip = [ "1.2.3.4" ]; cidr = [ "1.2.3.0/24" ]; }; } ]; }; }; }; default = { }; }; postOverflows = mkOption { type = types.submodule { options = { s01Whitelist = mkOption { type = types.listOf format.type; default = [ ]; description = '' A list of stage s01-whitelist specifications. Inside this list, you can specify Postoverflows Whitelists. See for details. ''; example = [ { name = "postoverflows/whitelist_my_dns_domain"; description = "Whitelist my reverse DNS"; whitelist = { reason = "Don't ban me"; expression = [ "evt.Enriched.reverse_dns endsWith '.local.'" ]; }; } ]; }; }; }; default = { }; }; contexts = mkOption { type = types.listOf format.type; description = '' A list of additional contexts to specify. See for details. ''; example = [ { context = { target_uri = [ "evt.Meta.http_path" ]; user_agent = [ "evt.Meta.http_user_agent" ]; method = [ "evt.Meta.http_verb" ]; status = [ "evt.Meta.http_status" ]; }; } ]; default = [ ]; }; notifications = mkOption { type = types.listOf format.type; description = '' A list of notifications to enable and use in your profiles. Note that for now, only the plugins shipped by default with CrowdSec are supported. See for details. ''; example = [ { type = "http"; name = "default_http_notification"; log_level = "info"; format = '' {{.|toJson}} ''; url = "https://example.com/hook"; method = "POST"; } ]; default = [ ]; }; profiles = mkOption { type = types.listOf format.type; description = '' A list of profiles to enable. See for more details. ''; example = [ { name = "default_ip_remediation"; filters = [ "Alert.Remediation == true && Alert.GetScope() == 'Ip'" ]; decisions = [ { type = "ban"; duration = "4h"; } ]; on_success = "break"; } { name = "default_range_remediation"; filters = [ "Alert.Remediation == true && Alert.GetScope() == 'Range'" ]; decisions = [ { type = "ban"; duration = "4h"; } ]; on_success = "break"; } ]; default = [ { name = "default_ip_remediation"; filters = [ "Alert.Remediation == true && Alert.GetScope() == 'Ip'" ]; decisions = [ { type = "ban"; duration = "4h"; } ]; on_success = "break"; } { name = "default_range_remediation"; filters = [ "Alert.Remediation == true && Alert.GetScope() == 'Range'" ]; decisions = [ { type = "ban"; duration = "4h"; } ]; on_success = "break"; } ]; }; patterns = mkOption { type = types.listOf types.package; default = [ ]; example = lib.literalExpression '' [ (pkgs.writeTextDir "custom_service_logs" (builtins.readFile ./custom_service_logs)) ] ''; }; }; }; default = { }; }; hub = mkOption { type = types.submodule { options = { collections = mkOption { type = types.listOf types.str; default = [ ]; description = "List of hub collections to install"; example = [ "crowdsecurity/linux" ]; }; scenarios = mkOption { type = types.listOf types.str; default = [ ]; description = "List of hub scenarios to install"; example = [ "crowdsecurity/ssh-bf" ]; }; parsers = mkOption { type = types.listOf types.str; default = [ ]; description = "List of hub parsers to install"; example = [ "crowdsecurity/sshd-logs" ]; }; postOverflows = mkOption { type = types.listOf types.str; default = [ ]; description = "List of hub postoverflows to install"; example = [ "crowdsecurity/auditd-nix-wrappers-whitelist-process" ]; }; appSecConfigs = mkOption { type = types.listOf types.str; default = [ ]; description = "List of hub appsec configurations to install"; example = [ "crowdsecurity/appsec-default" ]; }; appSecRules = mkOption { type = types.listOf types.str; default = [ ]; description = "List of hub appsec rules to install"; example = [ "crowdsecurity/base-config" ]; }; }; }; default = { }; description = '' Hub collections, parsers, AppSec rules, etc. ''; }; settings = mkOption { type = types.submodule { options = { general = mkOption { description = '' Settings for the main CrowdSec configuration file. Refer to the defaults at . ''; type = format.type; default = { }; }; simulation = mkOption { type = format.type; default = { simulation = false; }; description = '' Attributes inside the simulation.yaml file. ''; }; lapi = mkOption { type = types.submodule { options = { credentialsFile = mkOption { type = types.nullOr types.path; example = "/run/crowdsec/lapi.yaml"; description = '' The LAPI credential file to use. ''; default = null; }; }; }; description = '' LAPI Configuration attributes ''; default = { }; }; capi = mkOption { type = types.submodule { options = { credentialsFile = mkOption { type = types.nullOr types.path; example = "/run/crowdsec/capi.yaml"; description = '' The CAPI credential file to use. ''; default = null; }; }; }; description = '' CAPI Configuration attributes ''; default = { }; }; console = mkOption { type = types.submodule { options = { tokenFile = mkOption { type = types.nullOr types.path; example = "/run/crowdsec/console_token.yaml"; description = '' The Console Token file to use. ''; default = null; }; configuration = mkOption { type = format.type; default = { share_manual_decisions = false; share_custom = false; share_tainted = false; share_context = false; }; description = '' Attributes inside the console.yaml file. ''; }; }; }; description = '' Console Configuration attributes ''; default = { }; }; }; }; }; }; config = let cfg = config.services.crowdsec; configFile = format.generate "crowdsec.yaml" cfg.settings.general; simulationFile = format.generate "simulation.yaml" cfg.settings.simulation; consoleFile = format.generate "console.yaml" cfg.settings.console.configuration; patternsDir = pkgs.buildPackages.symlinkJoin { name = "crowdsec-patterns"; paths = [ cfg.localConfig.patterns "${lib.attrsets.getOutput "out" cfg.package}/share/crowdsec/config/patterns/" ]; }; cscli = pkgs.writeShellScriptBin "cscli" '' set -euo pipefail # cscli needs crowdsec on it's path in order to be able to run `cscli explain` export PATH="$PATH:${lib.makeBinPath [ cfg.package ]}" sudo=exec if [ "$USER" != "${cfg.user}" ]; then sudo='exec /run/wrappers/bin/sudo -u ${cfg.user}' fi $sudo ${lib.getExe' cfg.package "cscli"} -c=${configFile} "$@" ''; localScenariosMap = (map (format.generate "scenario.yaml") cfg.localConfig.scenarios); localParsersS00RawMap = ( map (format.generate "parsers-s00-raw.yaml") cfg.localConfig.parsers.s00Raw ); localParsersS01ParseMap = ( map (format.generate "parsers-s01-parse.yaml") cfg.localConfig.parsers.s01Parse ); localParsersS02EnrichMap = ( map (format.generate "parsers-s02-enrich.yaml") cfg.localConfig.parsers.s02Enrich ); localPostOverflowsS01WhitelistMap = ( map (format.generate "postoverflows-s01-whitelist.yaml") cfg.localConfig.postOverflows.s01Whitelist ); localContextsMap = (map (format.generate "context.yaml") cfg.localConfig.contexts); localNotificationsMap = (map (format.generate "notification.yaml") cfg.localConfig.notifications); localProfilesFile = pkgs.writeText "local_profiles.yaml" '' --- ${lib.strings.concatMapStringsSep "\n---\n" builtins.toJSON cfg.localConfig.profiles} --- ''; localAcquisisionFile = pkgs.writeText "local_acquisisions.yaml" '' --- ${lib.strings.concatMapStringsSep "\n---\n" builtins.toJSON cfg.localConfig.acquisitions} --- ''; scriptArray = [ "set -euo pipefail" "${lib.getExe cscli} hub update" ] ++ lib.optionals (cfg.hub.collections != [ ]) [ "${lib.getExe cscli} collections install ${ lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.collections }" ] ++ lib.optionals (cfg.hub.scenarios != [ ]) [ "${lib.getExe cscli} scenarios install ${ lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.scenarios }" ] ++ lib.optionals (cfg.hub.parsers != [ ]) [ "${lib.getExe cscli} parsers install ${ lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.parsers }" ] ++ lib.optionals (cfg.hub.postOverflows != [ ]) [ "${lib.getExe cscli} postoverflows install ${ lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.postOverflows }" ] ++ lib.optionals (cfg.hub.appSecConfigs != [ ]) [ "${lib.getExe cscli} appsec-configs install ${ lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.appSecConfigs }" ] ++ lib.optionals (cfg.hub.appSecRules != [ ]) [ "${lib.getExe cscli} appsec-rules install ${ lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.appSecRules }" ] ++ lib.optionals (cfg.settings.general.api.server.enable) [ '' if [ ! -s "${cfg.settings.general.api.client.credentials_path}" ]; then ${lib.getExe cscli} machine add "${cfg.name}" --auto fi '' ] ++ lib.optionals (cfg.settings.capi.credentialsFile != null) [ '' if ! grep -q password "${cfg.settings.capi.credentialsFile}" ]; then ${lib.getExe cscli} capi register fi '' ] ++ lib.optionals (cfg.settings.console.tokenFile != null) [ '' if [ ! -e "${cfg.settings.console.tokenFile}" ]; then ${lib.getExe cscli} console enroll "$(cat ${cfg.settings.console.tokenFile})" --name ${cfg.name} fi '' ]; setupScript = pkgs.writeShellScriptBin "crowdsec-setup" ( lib.strings.concatStringsSep "\n" scriptArray ); in lib.mkIf (cfg.enable) { warnings = [ ] ++ lib.optionals (cfg.localConfig.profiles == [ ]) [ "By not specifying profiles in services.crowdsec.localConfig.profiles, CrowdSec will not react to any alert by default." ] ++ lib.optionals (cfg.localConfig.acquisitions == [ ]) [ "By not specifying acquisitions in services.crowdsec.localConfig.acquisitions, CrowdSec will not look for any data source." ]; services.crowdsec.settings.general = with lib; { common = { daemonize = false; log_media = "stdout"; }; config_paths = { config_dir = confDir; data_dir = stateDir; simulation_path = simulationFile; hub_dir = hubDir; index_path = lib.strings.normalizePath "${stateDir}/hub/.index.json"; notification_dir = notificationsDir; plugin_dir = pluginDir; pattern_dir = patternsDir; }; db_config = { type = mkDefault "sqlite"; db_path = mkDefault (lib.strings.normalizePath "${stateDir}/crowdsec.db"); use_wal = mkDefault true; }; crowdsec_service = { enable = mkDefault true; acquisition_path = mkDefault localAcquisisionFile; }; api = { client = { credentials_path = cfg.settings.lapi.credentialsFile; }; server = { enable = mkDefault false; listen_uri = mkDefault "127.0.0.1:8080"; console_path = mkDefault consoleFile; profiles_path = mkDefault localProfilesFile; #online_client = mkDefault { # sharing = mkDefault true; # pull = mkDefault { # community = mkDefault true; # blocklists = mkDefault true; # }; # credentials_path = cfg.settings.capi.credentialsFile; #}; }; }; prometheus = { enabled = mkDefault true; level = mkDefault "full"; listen_addr = mkDefault "127.0.0.1"; listen_port = mkDefault 6060; }; cscli = { hub_branch = "v${cfg.package.version}"; }; }; environment = { systemPackages = [ cscli ]; }; systemd.packages = [ cfg.package ]; systemd.timers.crowdsec-update-hub = lib.mkIf (cfg.autoUpdateService) { description = "Update the crowdsec hub index"; wantedBy = [ "timers.target" ]; timerConfig = { OnCalendar = "daily"; Persistent = "yes"; Unit = "crowdsec-update-hub.service"; }; }; systemd.services = { crowdsec-update-hub = lib.mkIf (cfg.autoUpdateService) { description = "Update the crowdsec hub index"; serviceConfig = { Type = "oneshot"; User = cfg.user; Group = cfg.group; LimitNOFILE = 65536; NoNewPrivileges = true; LockPersonality = true; RemoveIPC = true; ReadWritePaths = [ rootDir confDir ]; ProtectSystem = "strict"; PrivateUsers = true; ProtectHome = true; PrivateTmp = true; PrivateDevices = true; ProtectHostname = true; UMask = "0077"; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; ProtectProc = "invisible"; SystemCallFilter = [ " " # This is needed to clear the SystemCallFilter existing definitions "~@reboot" "~@swap" "~@obsolete" "~@mount" "~@module" "~@debug" "~@cpu-emulation" "~@clock" "~@raw-io" "~@privileged" "~@resources" ]; CapabilityBoundingSet = [ " " # Reset all capabilities to an empty set ]; RestrictAddressFamilies = [ " " # This is needed to clear the RestrictAddressFamilies existing definitions "none" # Remove all addresses families "AF_UNIX" "AF_INET" "AF_INET6" ]; DevicePolicy = "closed"; ProtectKernelLogs = true; SystemCallArchitectures = "native"; RestrictNamespaces = true; RestrictRealtime = true; RestrictSUIDSGID = true; ExecStart = "${lib.getExe cscli} --error hub update"; ExecStartPost = "systemctl reload crowdsec.service"; }; }; crowdsec = { description = "CrowdSec is a free, modern & collaborative behavior detection engine, coupled with a global IP reputation network."; wantedBy = [ "multi-user.target" ]; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; path = lib.mkForce [ ]; environment = { LC_ALL = "C"; LANG = "C"; }; serviceConfig = { User = cfg.user; Group = cfg.group; Type = "simple"; RestartSec = 60; LimitNOFILE = 65536; NoNewPrivileges = true; LockPersonality = true; RemoveIPC = true; ReadWritePaths = [ rootDir confDir ]; ProtectSystem = "strict"; PrivateUsers = true; ProtectHome = true; PrivateTmp = true; PrivateDevices = true; ProtectHostname = true; ProtectClock = true; UMask = "0077"; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; ProtectProc = "invisible"; SystemCallFilter = [ " " # This is needed to clear the SystemCallFilter existing definitions "~@reboot" "~@swap" "~@obsolete" "~@mount" "~@module" "~@debug" "~@cpu-emulation" "~@clock" "~@raw-io" "~@privileged" "~@resources" ]; CapabilityBoundingSet = [ " " # Reset all capabilities to an empty set "CAP_SYSLOG" # Add capability to read syslog ]; RestrictAddressFamilies = [ " " # This is needed to clear the RestrictAddressFamilies existing definitions "none" # Remove all addresses families "AF_UNIX" "AF_INET" "AF_INET6" ]; DevicePolicy = "closed"; ProtectKernelLogs = true; SystemCallArchitectures = "native"; RestrictNamespaces = true; RestrictRealtime = true; RestrictSUIDSGID = true; ExecReload = [ " " # This is needed to clear the ExecReload definitions from upstream ]; ExecStart = [ " " # This is needed to clear the ExecStart definitions from upstream "${lib.getExe' cfg.package "crowdsec"} -c ${configFile} -info" ]; ExecStartPre = [ " " # This is needed to clear the ExecStartPre definitions from upstream "${lib.getExe setupScript}" "${lib.getExe' cfg.package "crowdsec"} -c ${configFile} -t -error" ]; }; }; }; systemd.tmpfiles.settings = { "10-crowdsec" = builtins.listToAttrs ( map (dirName: { inherit cfg; name = lib.strings.normalizePath dirName; value = { d = { user = cfg.user; group = cfg.group; mode = "0750"; }; }; }) [ stateDir hubDir confDir localScenariosDir localPostOverflowsDir localPostOverflowsS01WhitelistDir parsersDir localParsersS00RawDir localParsersS01ParseDir localParsersS02EnrichDir localContextsDir notificationsDir pluginDir ] ) // builtins.listToAttrs ( map (scenarioFile: { inherit cfg; name = lib.strings.normalizePath "${localScenariosDir}/${builtins.unsafeDiscardStringContext (builtins.baseNameOf scenarioFile)}"; value = { link = { type = "L+"; argument = "${scenarioFile}"; }; }; }) localScenariosMap ) // builtins.listToAttrs ( map (parser: { inherit cfg; name = lib.strings.normalizePath "${localParsersS00RawDir}/${builtins.unsafeDiscardStringContext (builtins.baseNameOf parser)}"; value = { link = { type = "L+"; argument = "${parser}"; }; }; }) localParsersS00RawMap ) // builtins.listToAttrs ( map (parser: { inherit cfg; name = lib.strings.normalizePath "${localParsersS01ParseDir}/${builtins.unsafeDiscardStringContext (builtins.baseNameOf parser)}"; value = { link = { type = "L+"; argument = "${parser}"; }; }; }) localParsersS01ParseMap ) // builtins.listToAttrs ( map (parser: { inherit cfg; name = lib.strings.normalizePath "${localParsersS02EnrichDir}/${builtins.unsafeDiscardStringContext (builtins.baseNameOf parser)}"; value = { link = { type = "L+"; argument = "${parser}"; }; }; }) localParsersS02EnrichMap ) // builtins.listToAttrs ( map (postoverflow: { inherit cfg; name = lib.strings.normalizePath "${localPostOverflowsS01WhitelistDir}/${builtins.unsafeDiscardStringContext (builtins.baseNameOf postoverflow)}"; value = { link = { type = "L+"; argument = "${postoverflow}"; }; }; }) localPostOverflowsS01WhitelistMap ) // builtins.listToAttrs ( map (context: { inherit cfg; name = lib.strings.normalizePath "${localContextsDir}/${builtins.unsafeDiscardStringContext (builtins.baseNameOf context)}"; value = { link = { type = "L+"; argument = "${context}"; }; }; }) localContextsMap ) // builtins.listToAttrs ( map (notification: { inherit cfg; name = lib.strings.normalizePath "${notificationsDir}/${builtins.unsafeDiscardStringContext (builtins.baseNameOf notification)}"; value = { link = { type = "L+"; argument = "${notification}"; }; }; }) localNotificationsMap ); }; users.users.${cfg.user} = { name = cfg.user; description = lib.mkDefault "CrowdSec service user"; isSystemUser = true; group = cfg.group; extraGroups = [ "systemd-journal" ]; }; users.groups.${cfg.group} = lib.mapAttrs (name: lib.mkDefault) { }; networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ 6060 8080 ]; }; meta = { maintainers = with lib.maintainers; [ m0ustach3 jk ]; }; }