From 98abe1317af9bfeedd37c319fedd869a84c3e20c Mon Sep 17 00:00:00 2001 From: M0ustach3 <37956764+M0ustach3@users.noreply.github.com> Date: Thu, 6 Mar 2025 16:28:39 +0100 Subject: [PATCH 01/13] maintainers: add m0ustach3 --- maintainers/maintainer-list.nix | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/maintainers/maintainer-list.nix b/maintainers/maintainer-list.nix index 7b4ce772050a7..b12084071a30f 100644 --- a/maintainers/maintainer-list.nix +++ b/maintainers/maintainer-list.nix @@ -14091,6 +14091,11 @@ github = "m00wl"; githubId = 46034439; }; + m0ustach3 = { + name = "M0ustach3"; + github = "M0ustach3"; + githubId = 37956764; + }; m1cr0man = { email = "lucas+nix@m1cr0man.com"; github = "m1cr0man"; From 6574dc5b6c643af8c1501f809f3a2b7b1005c2bb Mon Sep 17 00:00:00 2001 From: M0ustach3 <37956764+M0ustach3@users.noreply.github.com> Date: Thu, 6 Mar 2025 16:29:38 +0100 Subject: [PATCH 02/13] nixos/crowdsec: init module --- nixos/modules/services/security/crowdsec.nix | 841 +++++++++++++++++++ 1 file changed, 841 insertions(+) create mode 100644 nixos/modules/services/security/crowdsec.nix diff --git a/nixos/modules/services/security/crowdsec.nix b/nixos/modules/services/security/crowdsec.nix new file mode 100644 index 0000000000000..e57b8b0fb718f --- /dev/null +++ b/nixos/modules/services/security/crowdsec.nix @@ -0,0 +1,841 @@ +{ + 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 "CrowSec Security Engine"; + + package = mkPackageOption pkgs "crowdsec" { }; + + autoUpdateService = mkEnableOption "Auto Hub Update"; + + 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 = "${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 '.asnieres.rev.numericable.fr.'" + ]; + }; + } + ]; + }; + }; + }; + 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 = "http://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 = [ "LePresidente/authelia-bf" ]; + }; + + parsers = mkOption { + type = types.listOf types.str; + default = [ ]; + description = ''List of hub parsers to install''; + example = [ "LePresidente/adguardhome-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 = { + credentials = mkOption { + type = types.nullOr format.type; + default = null; + example = { + url = "http://localhost:8080"; + login = "login"; + password = "password"; + ca_cert_path = "example"; + key_path = "example"; + cert_path = "example"; + }; + description = '' + Attributes inside the local_api_credentials file. + ''; + }; + credentialsFile = mkOption { + type = types.nullOr types.path; + example = "/run/crowdsec/lapi.yaml"; + description = '' + The credential file to use. This is preferred instead of putting secrets in the Nix store. + ''; + default = null; + }; + }; + }; + description = '' + LAPI Configuration attributes + ''; + default = { }; + }; + capi = mkOption { + type = types.submodule { + options = { + credentials = mkOption { + type = types.nullOr format.type; + default = null; + example = { + url = "https://api.crowdsec.net/"; + login = "abcdefghijklmnopqrstuvwxyz"; + password = "abcdefghijklmnopqrstuvwxyz"; + }; + description = '' + Attributes inside the central_api_credentials.yaml file. + ''; + }; + credentialsFile = mkOption { + type = types.nullOr types.path; + example = "/run/crowdsec/capi.yaml"; + description = '' + The credential file to use. This is preferred instead of putting secrets in the Nix store. + ''; + default = null; + }; + }; + }; + description = '' + CAPI Configuration attributes + ''; + default = { }; + }; + console = mkOption { + type = types.submodule { + options = { + token = mkOption { + type = types.nullOr types.str; + default = null; + example = "abcde"; + description = '' + The console token to enroll to the web console. + ''; + }; + tokenFile = mkOption { + type = types.nullOr types.path; + example = "/run/crowdsec/console_token.yaml"; + description = '' + The credential file to use. This is preferred instead of putting secrets in the Nix store. + ''; + 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; + lapiFile = + if cfg.settings.lapi.credentialsFile != null then + cfg.settings.lapi.credentialsFile + else + format.generate "local_api_credentials.yaml" cfg.settings.lapi.credentials; + capiFile = + if cfg.settings.capi.credentialsFile != null then + cfg.settings.capi.credentialsFile + else + ( + if cfg.settings.capi.credentials != null then + format.generate "central_api_credentials.yaml" cfg.settings.capi.credentials + else + null + ); + + tokenFile = + if cfg.settings.console.tokenFile != null then + cfg.settings.console.tokenFile + else + ( + if cfg.settings.console.token != null then + pkgs.writeText "console_token.txt" cfg.settings.console.token + else + null + ); + 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 "patterns" pkg) + ]; + }; + + pkg = cfg.package; + + 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} "''${@}" + ''; + + 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" (filename: builtins.readFile filename) ( + map (prof: format.generate "profile.yaml" prof) cfg.localConfig.profiles + ) + ); + localAcquisisionFile = pkgs.writeText "local_acquisisions.yaml" ( + lib.strings.concatMapStringsSep "\n---\n" (filename: builtins.readFile filename) ( + map (prof: format.generate "acquisition.yaml" prof) cfg.localConfig.acquisitions + ) + ); + + in + lib.mkIf (cfg.enable) { + + assertions = [ + { + assertion = lib.trivial.xor (cfg.settings.lapi.credentials != null) ( + cfg.settings.lapi.credentialsFile != null + ); + message = "Please specify either cfg.settings.lapi.credentials, or cfg.settings.lapi.credentialsFile, not more, not less."; + } + ]; + + warnings = + [ ] + ++ ( + if cfg.localConfig.profiles == [ ] then + [ + "By not specifying profiles in cfg.localConfig.profiles, Crowdsec will not react to any alert by default" + ] + else + [ ] + ) + ++ ( + if cfg.localConfig.acquisitions == [ ] then + [ + "By not specifying acquisitions in cfg.localConfig.acquisitions, Crowdsec will not look for any data source" + ] + else + [ ] + ); + + services.crowdsec.settings.general = with lib; { + common = { + daemonize = mkDefault false; + log_media = mkDefault "stdout"; + }; + config_paths = { + config_dir = mkDefault confDir; + data_dir = mkDefault stateDir; + simulation_path = mkDefault simulationFile; + hub_dir = mkDefault hubDir; + index_path = mkDefault (lib.strings.normalizePath "${stateDir}/hub/.index.json"); + notification_dir = mkDefault notificationsDir; + plugin_dir = mkDefault pluginDir; + pattern_dir = mkDefault 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 = mkDefault lapiFile; + }; + 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 = mkDefault capiFile; + }; + }; + }; + 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 = [ 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 = lib.mkIf (cfg.autoUpdateService) { + description = "Update the crowdsec hub index"; + path = [ cscli ]; + serviceConfig = with lib; { + Type = "oneshot"; + User = cfg.user; + Group = cfg.group; + LimitNOFILE = mkDefault 65536; + CapabilityBoundingSet = mkDefault [ ]; + NoNewPrivileges = mkDefault true; + LockPersonality = mkDefault true; + RemoveIPC = mkDefault true; + ReadWritePaths = [ + rootDir + confDir + ]; + 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"; + RestrictNamespaces = mkDefault true; + RestrictRealtime = mkDefault true; + RestrictSUIDSGID = mkDefault true; + ExecPaths = [ "/nix/store" ]; + NoExecPaths = [ "/" ]; + ExecStart = "${cscli}/bin/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."; + path = [ cscli ]; + wantedBy = [ "multi-user.target" ]; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + serviceConfig = with lib; { + User = cfg.user; + Group = cfg.group; + Restart = "on-failure"; + + LimitNOFILE = mkDefault 65536; + CapabilityBoundingSet = mkDefault [ ]; + NoNewPrivileges = mkDefault true; + LockPersonality = mkDefault true; + RemoveIPC = mkDefault true; + ReadWritePaths = [ + rootDir + confDir + ]; + 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"; + RestrictNamespaces = mkDefault true; + RestrictRealtime = mkDefault true; + RestrictSUIDSGID = mkDefault true; + ExecPaths = [ "/nix/store" ]; + NoExecPaths = [ "/" ]; + + ExecStart = "${pkg}/bin/crowdsec -c ${configFile}"; + ExecStartPre = + let + script = pkgs.writeScriptBin "crowdsec-setup" '' + #!${pkgs.runtimeShell} + set -eu + set -o pipefail + + cscli hub update + + ${lib.optionalString (cfg.hub.collections != [ ]) '' + cscli collections install ${builtins.concatStringsSep " " cfg.hub.collections} + ''} + + ${lib.optionalString (cfg.hub.scenarios != [ ]) '' + cscli scenarios install ${builtins.concatStringsSep " " cfg.hub.scenarios} + ''} + + ${lib.optionalString (cfg.hub.parsers != [ ]) '' + cscli parsers install ${builtins.concatStringsSep " " cfg.hub.parsers} + ''} + + ${lib.optionalString (cfg.hub.postOverflows != [ ]) '' + cscli postoverflows install ${builtins.concatStringsSep " " cfg.hub.postOverflows} + ''} + + ${lib.optionalString (cfg.hub.appSecConfigs != [ ]) '' + cscli appsec-configs install ${builtins.concatStringsSep " " cfg.hub.appSecConfigs} + ''} + + ${lib.optionalString (cfg.hub.appSecRules != [ ]) '' + cscli appsec-rules install ${builtins.concatStringsSep " " cfg.hub.appSecRules} + ''} + + ${lib.optionalString cfg.settings.general.api.server.enable '' + if [ ! -s "${cfg.settings.general.api.client.credentials_path}" ]; then + cscli machine add "${cfg.name}" --auto + fi + ''} + + ${lib.optionalString (capiFile != null) '' + if ! grep -q password "${capiFile}" ]; then + cscli capi register + fi + ${lib.optionalString (tokenFile != null) '' + if [ ! -e "${tokenFile}" ]; then + cscli console enroll "$(cat ${tokenFile})" --name ${cfg.name} + fi + ''} + ''} + ''; + in + [ "${script}/bin/crowdsec-setup" ]; + }; + }; + }; + + systemd.tmpfiles.rules = ( + [ + "d ${stateDir} 0750 ${cfg.user} ${cfg.group} - -" + "d ${hubDir} 0750 ${cfg.user} ${cfg.group} - -" + "d ${confDir} 0750 ${cfg.user} ${cfg.group} - -" + "d ${localScenariosDir} 0750 ${cfg.user} ${cfg.group} - -" + "d ${localPostOverflowsDir} 0750 ${cfg.user} ${cfg.group} - -" + "d ${localPostOverflowsS01WhitelistDir} 0750 ${cfg.user} ${cfg.group} - -" + "d ${parsersDir} 0750 ${cfg.user} ${cfg.group} - -" + "d ${localParsersS00RawDir} 0750 ${cfg.user} ${cfg.group} - -" + "d ${localParsersS01ParseDir} 0750 ${cfg.user} ${cfg.group} - -" + "d ${localParsersS02EnrichDir} 0750 ${cfg.user} ${cfg.group} - -" + "d ${localContextsDir} 0750 ${cfg.user} ${cfg.group} - -" + "d ${notificationsDir} 0750 ${cfg.user} ${cfg.group} - -" + "d ${pluginDir} 0750 ${cfg.user} ${cfg.group} - -" + ] + ++ (map ( + file: "L+ ${localScenariosDir}/${builtins.baseNameOf file} - - - - ${file}" + ) localScenariosMap) + ++ (map ( + file: "L+ ${localParsersS00RawDir}/${builtins.baseNameOf file} - - - - ${file}" + ) localParsersS00RawMap) + ++ (map ( + file: "L+ ${localParsersS01ParseDir}/${builtins.baseNameOf file} - - - - ${file}" + ) localParsersS01ParseMap) + ++ (map ( + file: "L+ ${localParsersS02EnrichDir}/${builtins.baseNameOf file} - - - - ${file}" + ) localParsersS02EnrichMap) + ++ (map ( + file: "L+ ${localPostOverflowsS01WhitelistDir}/${builtins.baseNameOf file} - - - - ${file}" + ) localPostOverflowsS01WhitelistMap) + ++ (map ( + file: "L+ ${localContextsDir}/${builtins.baseNameOf file} - - - - ${file}" + ) localContextsMap) + ++ (map ( + file: "L+ ${notificationsDir}/${builtins.baseNameOf file} - - - - ${file}" + ) localNotificationsMap) + ); + + users.users.${cfg.user} = { + name = lib.mkDefault cfg.user; + description = lib.mkDefault "Crowdsec service user"; + isSystemUser = lib.mkDefault true; + group = lib.mkDefault cfg.group; + extraGroups = [ "systemd-journal" ]; + }; + + users.groups.${cfg.group} = lib.mapAttrs (name: lib.mkDefault) { }; + }; + + meta = { + maintainers = with lib.maintainers; [ m0ustach3 ]; + buildDocsInSandbox = true; + }; +} From 7564866e9f1e10b388d504f577867da8a41395dc Mon Sep 17 00:00:00 2001 From: M0ustach3 <37956764+M0ustach3@users.noreply.github.com> Date: Fri, 7 Mar 2025 14:29:35 +0100 Subject: [PATCH 03/13] nixos/crowdsec: fix typos and enhanced setup script generation --- nixos/modules/services/security/crowdsec.nix | 245 ++++++++++--------- 1 file changed, 129 insertions(+), 116 deletions(-) diff --git a/nixos/modules/services/security/crowdsec.nix b/nixos/modules/services/security/crowdsec.nix index e57b8b0fb718f..b63a1c8d8d14a 100644 --- a/nixos/modules/services/security/crowdsec.nix +++ b/nixos/modules/services/security/crowdsec.nix @@ -27,7 +27,7 @@ in { options.services.crowdsec = with lib; { - enable = mkEnableOption "CrowSec Security Engine"; + enable = mkEnableOption "CrowdSec Security Engine"; package = mkPackageOption pkgs "crowdsec" { }; @@ -78,7 +78,7 @@ in type = types.listOf format.type; default = [ ]; description = '' - A list of scenarios specifications + A list of scenarios specifications. See for details. ''; example = [ @@ -171,7 +171,7 @@ in whitelist = { reason = "Don't ban me"; expression = [ - "evt.Enriched.reverse_dns endsWith '.asnieres.rev.numericable.fr.'" + "evt.Enriched.reverse_dns endsWith '.local.'" ]; }; } @@ -201,7 +201,7 @@ in 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. + 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 = [ { @@ -297,42 +297,42 @@ in collections = mkOption { type = types.listOf types.str; default = [ ]; - description = ''List of hub collections to install''; + 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 = [ "LePresidente/authelia-bf" ]; + 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 = [ "LePresidente/adguardhome-logs" ]; + 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''; + 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''; + 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''; + description = "List of hub appsec rules to install"; example = [ "crowdsecurity/base-config" ]; }; }; @@ -348,7 +348,7 @@ in options = { general = mkOption { description = '' - Settings for the main Crowdsec configuration file. Refer to the defaults at + Settings for the main CrowdSec configuration file. Refer to the defaults at . ''; type = format.type; @@ -379,14 +379,14 @@ in cert_path = "example"; }; description = '' - Attributes inside the local_api_credentials file. + Attributes inside the local_api_credentials file. This is not the most secure way to define settings, as this is put in the Nix store. Use of lapi.credentialsFile is preferred. ''; }; credentialsFile = mkOption { type = types.nullOr types.path; example = "/run/crowdsec/lapi.yaml"; description = '' - The credential file to use. This is preferred instead of putting secrets in the Nix store. + The credential file to use. This is strongly preferred instead of putting secrets in the Nix store. ''; default = null; }; @@ -409,14 +409,14 @@ in password = "abcdefghijklmnopqrstuvwxyz"; }; description = '' - Attributes inside the central_api_credentials.yaml file. + Attributes inside the central_api_credentials.yaml file. This is not the most secure way to define settings, as this is put in the Nix store. Use of capi.credentialsFile is preferred. ''; }; credentialsFile = mkOption { type = types.nullOr types.path; example = "/run/crowdsec/capi.yaml"; description = '' - The credential file to use. This is preferred instead of putting secrets in the Nix store. + The credential file to use. This is strongly preferred instead of putting secrets in the Nix store. ''; default = null; }; @@ -435,14 +435,14 @@ in default = null; example = "abcde"; description = '' - The console token to enroll to the web console. + The console token to enroll to the web console. This is not the most secure way to define settings, as this is put in the Nix store. Use of console.tokenFile is preferred. ''; }; tokenFile = mkOption { type = types.nullOr types.path; example = "/run/crowdsec/console_token.yaml"; description = '' - The credential file to use. This is preferred instead of putting secrets in the Nix store. + The credential file to use. This is strongly preferred instead of putting secrets in the Nix store. ''; default = null; }; @@ -519,7 +519,7 @@ in # 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} "''${@}" + exec ${lib.getExe' pkg "cscli"} -c=${configFile} "''${@}" ''; localScenariosMap = (map (format.generate "scenario.yaml") cfg.localConfig.scenarios); @@ -556,7 +556,7 @@ in assertion = lib.trivial.xor (cfg.settings.lapi.credentials != null) ( cfg.settings.lapi.credentialsFile != null ); - message = "Please specify either cfg.settings.lapi.credentials, or cfg.settings.lapi.credentialsFile, not more, not less."; + message = "Please specify either services.crowdsec.settings.lapi.credentialsFile or services.crowdsec.settings.lapi.credentials, not more, not less."; } ]; @@ -565,7 +565,7 @@ in ++ ( if cfg.localConfig.profiles == [ ] then [ - "By not specifying profiles in cfg.localConfig.profiles, Crowdsec will not react to any alert by default" + "By not specifying profiles in services.crowdsec.localConfig.profiles, CrowdSec will not react to any alert by default" ] else [ ] @@ -573,7 +573,7 @@ in ++ ( if cfg.localConfig.acquisitions == [ ] then [ - "By not specifying acquisitions in cfg.localConfig.acquisitions, Crowdsec will not look for any data source" + "By not specifying acquisitions in services.crowdsec.localConfig.acquisitions, CrowdSec will not look for any data source" ] else [ ] @@ -640,7 +640,7 @@ in }; systemd.packages = [ pkg ]; - systemd.timers.crowdsec-update-hub = { + systemd.timers.crowdsec-update-hub = lib.mkIf (cfg.autoUpdateService) { description = "Update the crowdsec hub index"; wantedBy = [ "timers.target" ]; timerConfig = { @@ -657,36 +657,35 @@ in { crowdsec-update-hub = lib.mkIf (cfg.autoUpdateService) { description = "Update the crowdsec hub index"; - path = [ cscli ]; - serviceConfig = with lib; { + serviceConfig = { Type = "oneshot"; User = cfg.user; Group = cfg.group; - LimitNOFILE = mkDefault 65536; - CapabilityBoundingSet = mkDefault [ ]; - NoNewPrivileges = mkDefault true; - LockPersonality = mkDefault true; - RemoveIPC = mkDefault true; + LimitNOFILE = lib.mkDefault 65536; + CapabilityBoundingSet = lib.mkDefault [ ]; + NoNewPrivileges = lib.mkDefault true; + LockPersonality = lib.mkDefault true; + RemoveIPC = lib.mkDefault true; ReadWritePaths = [ rootDir confDir ]; - 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"; - RestrictNamespaces = mkDefault true; - RestrictRealtime = mkDefault true; - RestrictSUIDSGID = mkDefault true; + ProtectSystem = lib.mkDefault "strict"; + PrivateUsers = lib.mkDefault true; + ProtectHome = lib.mkDefault true; + PrivateTmp = lib.mkDefault true; + PrivateDevices = lib.mkDefault true; + ProtectHostname = lib.mkDefault true; + ProtectKernelTunables = lib.mkDefault true; + ProtectKernelModules = lib.mkDefault true; + ProtectControlGroups = lib.mkDefault true; + ProtectProc = lib.mkDefault "invisible"; + RestrictNamespaces = lib.mkDefault true; + RestrictRealtime = lib.mkDefault true; + RestrictSUIDSGID = lib.mkDefault true; ExecPaths = [ "/nix/store" ]; NoExecPaths = [ "/" ]; - ExecStart = "${cscli}/bin/cscli --error hub update"; + ExecStart = "${lib.getExe cscli} --error hub update"; ExecStartPost = "systemctl reload crowdsec.service"; }; }; @@ -697,89 +696,100 @@ in wantedBy = [ "multi-user.target" ]; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; - serviceConfig = with lib; { + serviceConfig = { User = cfg.user; Group = cfg.group; Restart = "on-failure"; - LimitNOFILE = mkDefault 65536; - CapabilityBoundingSet = mkDefault [ ]; - NoNewPrivileges = mkDefault true; - LockPersonality = mkDefault true; - RemoveIPC = mkDefault true; + LimitNOFILE = lib.mkDefault 65536; + CapabilityBoundingSet = lib.mkDefault [ ]; + NoNewPrivileges = lib.mkDefault true; + LockPersonality = lib.mkDefault true; + RemoveIPC = lib.mkDefault true; ReadWritePaths = [ rootDir confDir ]; - 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"; - RestrictNamespaces = mkDefault true; - RestrictRealtime = mkDefault true; - RestrictSUIDSGID = mkDefault true; + ProtectSystem = lib.mkDefault "strict"; + PrivateUsers = lib.mkDefault true; + ProtectHome = lib.mkDefault true; + PrivateTmp = lib.mkDefault true; + PrivateDevices = lib.mkDefault true; + ProtectHostname = lib.mkDefault true; + ProtectKernelTunables = lib.mkDefault true; + ProtectKernelModules = lib.mkDefault true; + ProtectControlGroups = lib.mkDefault true; + ProtectProc = lib.mkDefault "invisible"; + RestrictNamespaces = lib.mkDefault true; + RestrictRealtime = lib.mkDefault true; + RestrictSUIDSGID = lib.mkDefault true; ExecPaths = [ "/nix/store" ]; NoExecPaths = [ "/" ]; - ExecStart = "${pkg}/bin/crowdsec -c ${configFile}"; + ExecStart = "${lib.getExe' pkg "crowdsec"} -c ${configFile}"; ExecStartPre = let - script = pkgs.writeScriptBin "crowdsec-setup" '' - #!${pkgs.runtimeShell} - set -eu - set -o pipefail - - cscli hub update - - ${lib.optionalString (cfg.hub.collections != [ ]) '' - cscli collections install ${builtins.concatStringsSep " " cfg.hub.collections} - ''} - - ${lib.optionalString (cfg.hub.scenarios != [ ]) '' - cscli scenarios install ${builtins.concatStringsSep " " cfg.hub.scenarios} - ''} - - ${lib.optionalString (cfg.hub.parsers != [ ]) '' - cscli parsers install ${builtins.concatStringsSep " " cfg.hub.parsers} - ''} - - ${lib.optionalString (cfg.hub.postOverflows != [ ]) '' - cscli postoverflows install ${builtins.concatStringsSep " " cfg.hub.postOverflows} - ''} - - ${lib.optionalString (cfg.hub.appSecConfigs != [ ]) '' - cscli appsec-configs install ${builtins.concatStringsSep " " cfg.hub.appSecConfigs} - ''} - - ${lib.optionalString (cfg.hub.appSecRules != [ ]) '' - cscli appsec-rules install ${builtins.concatStringsSep " " cfg.hub.appSecRules} - ''} - - ${lib.optionalString cfg.settings.general.api.server.enable '' - if [ ! -s "${cfg.settings.general.api.client.credentials_path}" ]; then - cscli machine add "${cfg.name}" --auto - fi - ''} - - ${lib.optionalString (capiFile != null) '' - if ! grep -q password "${capiFile}" ]; then - cscli capi register - fi - ${lib.optionalString (tokenFile != null) '' - if [ ! -e "${tokenFile}" ]; then - cscli console enroll "$(cat ${tokenFile})" --name ${cfg.name} - fi - ''} - ''} - ''; + scriptArray = + [ + "#!${pkgs.runtimeShell}" + "set -euxo pipefail" + "cscli hub update" + ] + ++ lib.optionals (cfg.hub.collections != [ ]) [ + "cscli collections install ${ + lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.collections + }" + ] + ++ lib.optionals (cfg.hub.scenarios != [ ]) [ + "cscli scenarios install ${ + lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.scenarios + }" + ] + ++ lib.optionals (cfg.hub.parsers != [ ]) [ + "cscli parsers install ${ + lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.parsers + }" + ] + ++ lib.optionals (cfg.hub.postOverflows != [ ]) [ + "cscli postoverflows install ${ + lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.postOverflows + }" + ] + ++ lib.optionals (cfg.hub.appSecConfigs != [ ]) [ + "cscli appsec-configs install ${ + lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.appSecConfigs + }" + ] + ++ lib.optionals (cfg.hub.appSecRules != [ ]) [ + "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 + cscli machine add "${cfg.name}" --auto + fi + '' + ] + ++ lib.optionals (capiFile != null) [ + '' + if ! grep -q password "${capiFile}" ]; then + cscli capi register + fi + '' + ] + ++ lib.optionals (tokenFile != null) [ + '' + if [ ! -e "${tokenFile}" ]; then + cscli console enroll "$(cat ${tokenFile})" --name ${cfg.name} + fi + '' + ]; + + script = pkgs.writeScriptBin "crowdsec-setup" (lib.strings.concatStringsSep "\n" scriptArray); in - [ "${script}/bin/crowdsec-setup" ]; + [ "${lib.getExe script}" ]; }; }; }; @@ -825,7 +835,7 @@ in users.users.${cfg.user} = { name = lib.mkDefault cfg.user; - description = lib.mkDefault "Crowdsec service user"; + description = lib.mkDefault "CrowdSec service user"; isSystemUser = lib.mkDefault true; group = lib.mkDefault cfg.group; extraGroups = [ "systemd-journal" ]; @@ -835,7 +845,10 @@ in }; meta = { - maintainers = with lib.maintainers; [ m0ustach3 ]; + maintainers = with lib.maintainers; [ + m0ustach3 + jk + ]; buildDocsInSandbox = true; }; } From 2a1708c553215c29e7af921934d3a48b24984c0b Mon Sep 17 00:00:00 2001 From: M0ustach3 <37956764+M0ustach3@users.noreply.github.com> Date: Sat, 8 Mar 2025 14:59:23 +0100 Subject: [PATCH 04/13] nixos/crowdsec: removed sudo dependencies and reformatted file --- nixos/modules/services/security/crowdsec.nix | 275 +++++++++---------- 1 file changed, 135 insertions(+), 140 deletions(-) diff --git a/nixos/modules/services/security/crowdsec.nix b/nixos/modules/services/security/crowdsec.nix index b63a1c8d8d14a..071252984f3b8 100644 --- a/nixos/modules/services/security/crowdsec.nix +++ b/nixos/modules/services/security/crowdsec.nix @@ -649,150 +649,145 @@ in 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 = lib.mkIf (cfg.autoUpdateService) { - description = "Update the crowdsec hub index"; - serviceConfig = { - Type = "oneshot"; - User = cfg.user; - Group = cfg.group; - LimitNOFILE = lib.mkDefault 65536; - CapabilityBoundingSet = lib.mkDefault [ ]; - NoNewPrivileges = lib.mkDefault true; - LockPersonality = lib.mkDefault true; - RemoveIPC = lib.mkDefault true; - ReadWritePaths = [ - rootDir - confDir - ]; - ProtectSystem = lib.mkDefault "strict"; - PrivateUsers = lib.mkDefault true; - ProtectHome = lib.mkDefault true; - PrivateTmp = lib.mkDefault true; - PrivateDevices = lib.mkDefault true; - ProtectHostname = lib.mkDefault true; - ProtectKernelTunables = lib.mkDefault true; - ProtectKernelModules = lib.mkDefault true; - ProtectControlGroups = lib.mkDefault true; - ProtectProc = lib.mkDefault "invisible"; - RestrictNamespaces = lib.mkDefault true; - RestrictRealtime = lib.mkDefault true; - RestrictSUIDSGID = lib.mkDefault true; - ExecPaths = [ "/nix/store" ]; - NoExecPaths = [ "/" ]; - ExecStart = "${lib.getExe cscli} --error hub update"; - ExecStartPost = "systemctl reload crowdsec.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 = lib.mkDefault 65536; + CapabilityBoundingSet = lib.mkDefault [ ]; + NoNewPrivileges = lib.mkDefault true; + LockPersonality = lib.mkDefault true; + RemoveIPC = lib.mkDefault true; + ReadWritePaths = [ + rootDir + confDir + ]; + ProtectSystem = lib.mkDefault "strict"; + PrivateUsers = lib.mkDefault true; + ProtectHome = lib.mkDefault true; + PrivateTmp = lib.mkDefault true; + PrivateDevices = lib.mkDefault true; + ProtectHostname = lib.mkDefault true; + ProtectKernelTunables = lib.mkDefault true; + ProtectKernelModules = lib.mkDefault true; + ProtectControlGroups = lib.mkDefault true; + ProtectProc = lib.mkDefault "invisible"; + RestrictNamespaces = lib.mkDefault true; + RestrictRealtime = lib.mkDefault true; + RestrictSUIDSGID = lib.mkDefault true; + ExecPaths = [ "/nix/store" ]; + NoExecPaths = [ "/" ]; + 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."; - path = [ cscli ]; - wantedBy = [ "multi-user.target" ]; - after = [ "network-online.target" ]; - wants = [ "network-online.target" ]; - serviceConfig = { - User = cfg.user; - Group = cfg.group; - Restart = "on-failure"; - - LimitNOFILE = lib.mkDefault 65536; - CapabilityBoundingSet = lib.mkDefault [ ]; - NoNewPrivileges = lib.mkDefault true; - LockPersonality = lib.mkDefault true; - RemoveIPC = lib.mkDefault true; - ReadWritePaths = [ - rootDir - confDir - ]; - ProtectSystem = lib.mkDefault "strict"; - PrivateUsers = lib.mkDefault true; - ProtectHome = lib.mkDefault true; - PrivateTmp = lib.mkDefault true; - PrivateDevices = lib.mkDefault true; - ProtectHostname = lib.mkDefault true; - ProtectKernelTunables = lib.mkDefault true; - ProtectKernelModules = lib.mkDefault true; - ProtectControlGroups = lib.mkDefault true; - ProtectProc = lib.mkDefault "invisible"; - RestrictNamespaces = lib.mkDefault true; - RestrictRealtime = lib.mkDefault true; - RestrictSUIDSGID = lib.mkDefault true; - ExecPaths = [ "/nix/store" ]; - NoExecPaths = [ "/" ]; - - ExecStart = "${lib.getExe' pkg "crowdsec"} -c ${configFile}"; - ExecStartPre = - let - scriptArray = - [ - "#!${pkgs.runtimeShell}" - "set -euxo pipefail" - "cscli hub update" - ] - ++ lib.optionals (cfg.hub.collections != [ ]) [ - "cscli collections install ${ - lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.collections - }" - ] - ++ lib.optionals (cfg.hub.scenarios != [ ]) [ - "cscli scenarios install ${ - lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.scenarios - }" - ] - ++ lib.optionals (cfg.hub.parsers != [ ]) [ - "cscli parsers install ${ - lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.parsers - }" - ] - ++ lib.optionals (cfg.hub.postOverflows != [ ]) [ - "cscli postoverflows install ${ - lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.postOverflows - }" - ] - ++ lib.optionals (cfg.hub.appSecConfigs != [ ]) [ - "cscli appsec-configs install ${ - lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.appSecConfigs - }" - ] - ++ lib.optionals (cfg.hub.appSecRules != [ ]) [ - "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 - cscli machine add "${cfg.name}" --auto - fi - '' - ] - ++ lib.optionals (capiFile != null) [ - '' - if ! grep -q password "${capiFile}" ]; then - cscli capi register - fi - '' - ] - ++ lib.optionals (tokenFile != null) [ - '' - if [ ! -e "${tokenFile}" ]; then - cscli console enroll "$(cat ${tokenFile})" --name ${cfg.name} - fi - '' - ]; - - script = pkgs.writeScriptBin "crowdsec-setup" (lib.strings.concatStringsSep "\n" scriptArray); - in - [ "${lib.getExe script}" ]; - }; + 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 = { + User = cfg.user; + Group = cfg.group; + Restart = "on-failure"; + + LimitNOFILE = lib.mkDefault 65536; + CapabilityBoundingSet = lib.mkDefault [ ]; + NoNewPrivileges = lib.mkDefault true; + LockPersonality = lib.mkDefault true; + RemoveIPC = lib.mkDefault true; + ReadWritePaths = [ + rootDir + confDir + ]; + ProtectSystem = lib.mkDefault "strict"; + PrivateUsers = lib.mkDefault true; + ProtectHome = lib.mkDefault true; + PrivateTmp = lib.mkDefault true; + PrivateDevices = lib.mkDefault true; + ProtectHostname = lib.mkDefault true; + ProtectKernelTunables = lib.mkDefault true; + ProtectKernelModules = lib.mkDefault true; + ProtectControlGroups = lib.mkDefault true; + ProtectProc = lib.mkDefault "invisible"; + RestrictNamespaces = lib.mkDefault true; + RestrictRealtime = lib.mkDefault true; + RestrictSUIDSGID = lib.mkDefault true; + ExecPaths = [ "/nix/store" ]; + NoExecPaths = [ "/" ]; + + ExecStart = "${lib.getExe' pkg "crowdsec"} -c ${configFile}"; + ExecStartPre = + let + scriptArray = + [ + "#!${pkgs.runtimeShell}" + "set -euxo pipefail" + "cscli hub update" + ] + ++ lib.optionals (cfg.hub.collections != [ ]) [ + "cscli collections install ${ + lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.collections + }" + ] + ++ lib.optionals (cfg.hub.scenarios != [ ]) [ + "cscli scenarios install ${ + lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.scenarios + }" + ] + ++ lib.optionals (cfg.hub.parsers != [ ]) [ + "cscli parsers install ${ + lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.parsers + }" + ] + ++ lib.optionals (cfg.hub.postOverflows != [ ]) [ + "cscli postoverflows install ${ + lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.postOverflows + }" + ] + ++ lib.optionals (cfg.hub.appSecConfigs != [ ]) [ + "cscli appsec-configs install ${ + lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.appSecConfigs + }" + ] + ++ lib.optionals (cfg.hub.appSecRules != [ ]) [ + "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 + cscli machine add "${cfg.name}" --auto + fi + '' + ] + ++ lib.optionals (capiFile != null) [ + '' + if ! grep -q password "${capiFile}" ]; then + cscli capi register + fi + '' + ] + ++ lib.optionals (tokenFile != null) [ + '' + if [ ! -e "${tokenFile}" ]; then + cscli console enroll "$(cat ${tokenFile})" --name ${cfg.name} + fi + '' + ]; + + script = pkgs.writeScriptBin "crowdsec-setup" (lib.strings.concatStringsSep "\n" scriptArray); + in + [ "${lib.getExe script}" ]; }; }; + }; systemd.tmpfiles.rules = ( [ From a9fe339b20dfb73d8b3dc4f80aec0a457a0066b1 Mon Sep 17 00:00:00 2001 From: M0ustach3 <37956764+M0ustach3@users.noreply.github.com> Date: Sat, 8 Mar 2025 15:26:25 +0100 Subject: [PATCH 05/13] nixos/crowdsec: removed path modification for service units --- nixos/modules/services/security/crowdsec.nix | 21 ++++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/nixos/modules/services/security/crowdsec.nix b/nixos/modules/services/security/crowdsec.nix index 071252984f3b8..a2a5199d5ce07 100644 --- a/nixos/modules/services/security/crowdsec.nix +++ b/nixos/modules/services/security/crowdsec.nix @@ -687,7 +687,6 @@ in 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" ]; @@ -728,56 +727,56 @@ in [ "#!${pkgs.runtimeShell}" "set -euxo pipefail" - "cscli hub update" + "${lib.getExe cscli} hub update" ] ++ lib.optionals (cfg.hub.collections != [ ]) [ - "cscli collections install ${ + "${lib.getExe cscli} collections install ${ lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.collections }" ] ++ lib.optionals (cfg.hub.scenarios != [ ]) [ - "cscli scenarios install ${ + "${lib.getExe cscli} scenarios install ${ lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.scenarios }" ] ++ lib.optionals (cfg.hub.parsers != [ ]) [ - "cscli parsers install ${ + "${lib.getExe cscli} parsers install ${ lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.parsers }" ] ++ lib.optionals (cfg.hub.postOverflows != [ ]) [ - "cscli postoverflows install ${ + "${lib.getExe cscli} postoverflows install ${ lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.postOverflows }" ] ++ lib.optionals (cfg.hub.appSecConfigs != [ ]) [ - "cscli appsec-configs install ${ + "${lib.getExe cscli} appsec-configs install ${ lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.appSecConfigs }" ] ++ lib.optionals (cfg.hub.appSecRules != [ ]) [ - "cscli appsec-rules install ${ + "${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 - cscli machine add "${cfg.name}" --auto + ${lib.getExe cscli} machine add "${cfg.name}" --auto fi '' ] ++ lib.optionals (capiFile != null) [ '' if ! grep -q password "${capiFile}" ]; then - cscli capi register + ${lib.getExe cscli} capi register fi '' ] ++ lib.optionals (tokenFile != null) [ '' if [ ! -e "${tokenFile}" ]; then - cscli console enroll "$(cat ${tokenFile})" --name ${cfg.name} + ${lib.getExe cscli} console enroll "$(cat ${tokenFile})" --name ${cfg.name} fi '' ]; From 61461cf5559a2f8a23abf005adf10754b22a70cf Mon Sep 17 00:00:00 2001 From: M0ustach3 <37956764+M0ustach3@users.noreply.github.com> Date: Sat, 8 Mar 2025 16:49:40 +0100 Subject: [PATCH 06/13] nixos/crowdsec: added upstream systemd options --- nixos/modules/services/security/crowdsec.nix | 24 ++++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/nixos/modules/services/security/crowdsec.nix b/nixos/modules/services/security/crowdsec.nix index a2a5199d5ce07..aa5f7128308ba 100644 --- a/nixos/modules/services/security/crowdsec.nix +++ b/nixos/modules/services/security/crowdsec.nix @@ -690,11 +690,14 @@ in wantedBy = [ "multi-user.target" ]; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; + environment = { + LC_ALL = "C"; + LANG = "C"; + }; serviceConfig = { User = cfg.user; Group = cfg.group; - Restart = "on-failure"; - + RestartSec = 60; LimitNOFILE = lib.mkDefault 65536; CapabilityBoundingSet = lib.mkDefault [ ]; NoNewPrivileges = lib.mkDefault true; @@ -719,8 +722,15 @@ in RestrictSUIDSGID = lib.mkDefault true; ExecPaths = [ "/nix/store" ]; NoExecPaths = [ "/" ]; - - ExecStart = "${lib.getExe' pkg "crowdsec"} -c ${configFile}"; + ExecReload = lib.mkForce [ + " " # This is needed to clear the ExecReload definitions from upstream + "${lib.getExe' pkg "crowdsec"} -c ${configFile} -t -error" + "${lib.getExe' pkgs.util-linux "kill"} -HUP $MAINPID" + ]; + ExecStart = [ + " " # This is needed to clear the ExecStart definitions from upstream + "${lib.getExe' pkg "crowdsec"} -c ${configFile}" + ]; ExecStartPre = let scriptArray = @@ -783,7 +793,11 @@ in script = pkgs.writeScriptBin "crowdsec-setup" (lib.strings.concatStringsSep "\n" scriptArray); in - [ "${lib.getExe script}" ]; + [ + " " # This is needed to clear the ExecStartPre definitions from upstream + "${lib.getExe script}" + "${lib.getExe' pkg "crowdsec"} -c ${configFile} -t -error" + ]; }; }; }; From c4ad52bf3d52af2d1dab1fe3f9980809f4c88d14 Mon Sep 17 00:00:00 2001 From: M0ustach3 <37956764+M0ustach3@users.noreply.github.com> Date: Sat, 8 Mar 2025 16:57:18 +0100 Subject: [PATCH 07/13] nixos/crowdsec: moved to writeShellScriptBin and fixed sudo permissions for cscli wrapper --- nixos/modules/services/security/crowdsec.nix | 32 +++++++++----------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/nixos/modules/services/security/crowdsec.nix b/nixos/modules/services/security/crowdsec.nix index aa5f7128308ba..e71b5f97f1df9 100644 --- a/nixos/modules/services/security/crowdsec.nix +++ b/nixos/modules/services/security/crowdsec.nix @@ -505,21 +505,19 @@ in name = "crowdsec-patterns"; paths = [ cfg.localConfig.patterns - (lib.attrsets.getOutput "patterns" pkg) + (lib.attrsets.getOutput "patterns" cfg.package) ]; }; - pkg = cfg.package; - - cscli = pkgs.writeScriptBin "cscli" '' - #!${pkgs.runtimeShell} - set -eu - set -o pipefail - + 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 [ pkg ]} - - exec ${lib.getExe' pkg "cscli"} -c=${configFile} "''${@}" + 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); @@ -639,7 +637,8 @@ in systemPackages = [ cscli ]; }; - systemd.packages = [ pkg ]; + systemd.packages = [ cfg.package ]; + systemd.timers.crowdsec-update-hub = lib.mkIf (cfg.autoUpdateService) { description = "Update the crowdsec hub index"; wantedBy = [ "timers.target" ]; @@ -724,18 +723,17 @@ in NoExecPaths = [ "/" ]; ExecReload = lib.mkForce [ " " # This is needed to clear the ExecReload definitions from upstream - "${lib.getExe' pkg "crowdsec"} -c ${configFile} -t -error" + "${lib.getExe' cfg.package "crowdsec"} -c ${configFile} -t -error" "${lib.getExe' pkgs.util-linux "kill"} -HUP $MAINPID" ]; ExecStart = [ " " # This is needed to clear the ExecStart definitions from upstream - "${lib.getExe' pkg "crowdsec"} -c ${configFile}" + "${lib.getExe' cfg.package "crowdsec"} -c ${configFile}" ]; ExecStartPre = let scriptArray = [ - "#!${pkgs.runtimeShell}" "set -euxo pipefail" "${lib.getExe cscli} hub update" ] @@ -791,12 +789,12 @@ in '' ]; - script = pkgs.writeScriptBin "crowdsec-setup" (lib.strings.concatStringsSep "\n" scriptArray); + script = pkgs.writeShellScriptBin "crowdsec-setup" (lib.strings.concatStringsSep "\n" scriptArray); in [ " " # This is needed to clear the ExecStartPre definitions from upstream "${lib.getExe script}" - "${lib.getExe' pkg "crowdsec"} -c ${configFile} -t -error" + "${lib.getExe' cfg.package "crowdsec"} -c ${configFile} -t -error" ]; }; }; From d5057df8f9f73740c51814c50e69c680edc7c0ac Mon Sep 17 00:00:00 2001 From: M0ustach3 <37956764+M0ustach3@users.noreply.github.com> Date: Sat, 8 Mar 2025 17:12:28 +0100 Subject: [PATCH 08/13] nixos/crowdsec: reduced number of derivation by using toJSON --- nixos/modules/services/security/crowdsec.nix | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/nixos/modules/services/security/crowdsec.nix b/nixos/modules/services/security/crowdsec.nix index e71b5f97f1df9..a0857054f52a5 100644 --- a/nixos/modules/services/security/crowdsec.nix +++ b/nixos/modules/services/security/crowdsec.nix @@ -535,16 +535,16 @@ in ); 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" (filename: builtins.readFile filename) ( - map (prof: format.generate "profile.yaml" prof) cfg.localConfig.profiles - ) - ); - localAcquisisionFile = pkgs.writeText "local_acquisisions.yaml" ( - lib.strings.concatMapStringsSep "\n---\n" (filename: builtins.readFile filename) ( - map (prof: format.generate "acquisition.yaml" prof) cfg.localConfig.acquisitions - ) - ); + 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} + --- + ''; in lib.mkIf (cfg.enable) { @@ -734,7 +734,7 @@ in let scriptArray = [ - "set -euxo pipefail" + "set -euo pipefail" "${lib.getExe cscli} hub update" ] ++ lib.optionals (cfg.hub.collections != [ ]) [ From 70ddc1b40910da73957fe58862d3d63049b76b71 Mon Sep 17 00:00:00 2001 From: M0ustach3 <37956764+M0ustach3@users.noreply.github.com> Date: Sat, 8 Mar 2025 17:55:10 +0100 Subject: [PATCH 09/13] nixos/crowdsec: added default patterns from package output --- nixos/modules/services/security/crowdsec.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixos/modules/services/security/crowdsec.nix b/nixos/modules/services/security/crowdsec.nix index a0857054f52a5..1528cef54c870 100644 --- a/nixos/modules/services/security/crowdsec.nix +++ b/nixos/modules/services/security/crowdsec.nix @@ -505,7 +505,7 @@ in name = "crowdsec-patterns"; paths = [ cfg.localConfig.patterns - (lib.attrsets.getOutput "patterns" cfg.package) + "${lib.attrsets.getOutput "out" cfg.package}/share/crowdsec/config/patterns/" ]; }; From 355de6570cba9e67b8761b98747a84e7a2034d14 Mon Sep 17 00:00:00 2001 From: M0ustach3 <37956764+M0ustach3@users.noreply.github.com> Date: Sat, 8 Mar 2025 18:03:44 +0100 Subject: [PATCH 10/13] nixos/crowdsec: add openFirewall option --- nixos/modules/services/security/crowdsec.nix | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/nixos/modules/services/security/crowdsec.nix b/nixos/modules/services/security/crowdsec.nix index 1528cef54c870..faa284cff5c4e 100644 --- a/nixos/modules/services/security/crowdsec.nix +++ b/nixos/modules/services/security/crowdsec.nix @@ -33,6 +33,8 @@ in 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"; @@ -848,6 +850,11 @@ in }; users.groups.${cfg.group} = lib.mapAttrs (name: lib.mkDefault) { }; + + networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ + 6060 + 8080 + ]; }; meta = { From d4be53323071d5123d6918cfb88ce88ff07fa90f Mon Sep 17 00:00:00 2001 From: M0ustach3 <37956764+M0ustach3@users.noreply.github.com> Date: Sun, 9 Mar 2025 17:33:29 +0100 Subject: [PATCH 11/13] nixos/crowdsec: hardened systemd unit, moved to lib.optionals and removed useless mkDefault --- nixos/modules/services/security/crowdsec.nix | 355 +++++++++++-------- 1 file changed, 202 insertions(+), 153 deletions(-) diff --git a/nixos/modules/services/security/crowdsec.nix b/nixos/modules/services/security/crowdsec.nix index faa284cff5c4e..15f5aacb51125 100644 --- a/nixos/modules/services/security/crowdsec.nix +++ b/nixos/modules/services/security/crowdsec.nix @@ -53,7 +53,7 @@ in Name of the machine when registering it at the central or local api. ''; default = config.networking.hostName; - defaultText = "${config.networking.hostName}"; + defaultText = lib.literalExpression "config.networking.hostName"; }; localConfig = mkOption { @@ -102,14 +102,16 @@ in 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. + 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. + A list of stage s01-parse specifications. + See for details. ''; example = [ { @@ -135,7 +137,8 @@ in 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. + A list of stage s02-enrich specifications. Inside this list, you can specify Parser Whitelists. + See for details. ''; example = [ { @@ -164,7 +167,8 @@ in 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. + A list of stage s01-whitelist specifications. Inside this list, you can specify Postoverflows Whitelists. + See for details. ''; example = [ { @@ -186,7 +190,8 @@ in contexts = mkOption { type = types.listOf format.type; description = '' - A list of additional contexts to specify. See for details. + A list of additional contexts to specify. + See for details. ''; example = [ { @@ -203,7 +208,8 @@ in 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. + 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 = [ { @@ -213,7 +219,7 @@ in format = '' {{.|toJson}} ''; - url = "http://example.com/hook"; + url = "https://example.com/hook"; method = "POST"; } ]; @@ -222,7 +228,8 @@ in profiles = mkOption { type = types.listOf format.type; description = '' - A list of profiles to enable. See for more details. + A list of profiles to enable. + See for more details. ''; example = [ { @@ -350,8 +357,8 @@ in options = { general = mkOption { description = '' - Settings for the main CrowdSec configuration file. Refer to the defaults at - . + Settings for the main CrowdSec configuration file. + Refer to the defaults at . ''; type = format.type; default = { }; @@ -381,7 +388,8 @@ in cert_path = "example"; }; description = '' - Attributes inside the local_api_credentials file. This is not the most secure way to define settings, as this is put in the Nix store. Use of lapi.credentialsFile is preferred. + Attributes inside the local_api_credentials file. + This is not the most secure way to define settings, as this is put in the Nix store. Use of lapi.credentialsFile is preferred. ''; }; credentialsFile = mkOption { @@ -512,7 +520,7 @@ in }; cscli = pkgs.writeShellScriptBin "cscli" '' - set -euo pipefail + set -euox 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 @@ -548,6 +556,67 @@ in --- ''; + scriptArray = + [ + "set -euox 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 (capiFile != null) [ + '' + if ! grep -q password "${capiFile}" ]; then + ${lib.getExe cscli} capi register + fi + '' + ] + ++ lib.optionals (tokenFile != null) [ + '' + if [ ! -e "${tokenFile}" ]; then + ${lib.getExe cscli} console enroll "$(cat ${tokenFile})" --name ${cfg.name} + fi + '' + ]; + + setupScript = pkgs.writeShellScriptBin "crowdsec-setup" ( + lib.strings.concatStringsSep "\n" scriptArray + ); + in lib.mkIf (cfg.enable) { @@ -562,37 +631,27 @@ in warnings = [ ] - ++ ( - if cfg.localConfig.profiles == [ ] then - [ - "By not specifying profiles in services.crowdsec.localConfig.profiles, CrowdSec will not react to any alert by default" - ] - else - [ ] - ) - ++ ( - if cfg.localConfig.acquisitions == [ ] then - [ - "By not specifying acquisitions in services.crowdsec.localConfig.acquisitions, CrowdSec will not look for any data source" - ] - else - [ ] - ); + ++ 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 = mkDefault false; - log_media = mkDefault "stdout"; + daemonize = false; + log_media = "stdout"; }; config_paths = { - config_dir = mkDefault confDir; - data_dir = mkDefault stateDir; - simulation_path = mkDefault simulationFile; - hub_dir = mkDefault hubDir; - index_path = mkDefault (lib.strings.normalizePath "${stateDir}/hub/.index.json"); - notification_dir = mkDefault notificationsDir; - plugin_dir = mkDefault pluginDir; - pattern_dir = mkDefault patternsDir; + 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"; @@ -657,30 +716,55 @@ in Type = "oneshot"; User = cfg.user; Group = cfg.group; - LimitNOFILE = lib.mkDefault 65536; - CapabilityBoundingSet = lib.mkDefault [ ]; - NoNewPrivileges = lib.mkDefault true; - LockPersonality = lib.mkDefault true; - RemoveIPC = lib.mkDefault true; + LimitNOFILE = 65536; + NoNewPrivileges = true; + LockPersonality = true; + RemoveIPC = true; ReadWritePaths = [ rootDir confDir ]; - ProtectSystem = lib.mkDefault "strict"; - PrivateUsers = lib.mkDefault true; - ProtectHome = lib.mkDefault true; - PrivateTmp = lib.mkDefault true; - PrivateDevices = lib.mkDefault true; - ProtectHostname = lib.mkDefault true; - ProtectKernelTunables = lib.mkDefault true; - ProtectKernelModules = lib.mkDefault true; - ProtectControlGroups = lib.mkDefault true; - ProtectProc = lib.mkDefault "invisible"; - RestrictNamespaces = lib.mkDefault true; - RestrictRealtime = lib.mkDefault true; - RestrictSUIDSGID = lib.mkDefault true; - ExecPaths = [ "/nix/store" ]; - NoExecPaths = [ "/" ]; + 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"; }; @@ -691,6 +775,7 @@ in wantedBy = [ "multi-user.target" ]; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; + path = lib.mkForce [ ]; environment = { LC_ALL = "C"; LANG = "C"; @@ -698,106 +783,71 @@ in serviceConfig = { User = cfg.user; Group = cfg.group; + Type = "simple"; RestartSec = 60; - LimitNOFILE = lib.mkDefault 65536; - CapabilityBoundingSet = lib.mkDefault [ ]; - NoNewPrivileges = lib.mkDefault true; - LockPersonality = lib.mkDefault true; - RemoveIPC = lib.mkDefault true; + LimitNOFILE = 65536; + NoNewPrivileges = true; + LockPersonality = true; + RemoveIPC = true; ReadWritePaths = [ rootDir confDir ]; - ProtectSystem = lib.mkDefault "strict"; - PrivateUsers = lib.mkDefault true; - ProtectHome = lib.mkDefault true; - PrivateTmp = lib.mkDefault true; - PrivateDevices = lib.mkDefault true; - ProtectHostname = lib.mkDefault true; - ProtectKernelTunables = lib.mkDefault true; - ProtectKernelModules = lib.mkDefault true; - ProtectControlGroups = lib.mkDefault true; - ProtectProc = lib.mkDefault "invisible"; - RestrictNamespaces = lib.mkDefault true; - RestrictRealtime = lib.mkDefault true; - RestrictSUIDSGID = lib.mkDefault true; - ExecPaths = [ "/nix/store" ]; - NoExecPaths = [ "/" ]; - ExecReload = lib.mkForce [ + 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 - "${lib.getExe' cfg.package "crowdsec"} -c ${configFile} -t -error" - "${lib.getExe' pkgs.util-linux "kill"} -HUP $MAINPID" ]; ExecStart = [ " " # This is needed to clear the ExecStart definitions from upstream - "${lib.getExe' cfg.package "crowdsec"} -c ${configFile}" + "${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" ]; - ExecStartPre = - let - 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 (capiFile != null) [ - '' - if ! grep -q password "${capiFile}" ]; then - ${lib.getExe cscli} capi register - fi - '' - ] - ++ lib.optionals (tokenFile != null) [ - '' - if [ ! -e "${tokenFile}" ]; then - ${lib.getExe cscli} console enroll "$(cat ${tokenFile})" --name ${cfg.name} - fi - '' - ]; - - script = pkgs.writeShellScriptBin "crowdsec-setup" (lib.strings.concatStringsSep "\n" scriptArray); - in - [ - " " # This is needed to clear the ExecStartPre definitions from upstream - "${lib.getExe script}" - "${lib.getExe' cfg.package "crowdsec"} -c ${configFile} -t -error" - ]; }; }; }; @@ -842,10 +892,10 @@ in ); users.users.${cfg.user} = { - name = lib.mkDefault cfg.user; + name = cfg.user; description = lib.mkDefault "CrowdSec service user"; - isSystemUser = lib.mkDefault true; - group = lib.mkDefault cfg.group; + isSystemUser = true; + group = cfg.group; extraGroups = [ "systemd-journal" ]; }; @@ -862,6 +912,5 @@ in m0ustach3 jk ]; - buildDocsInSandbox = true; }; } From f1ce17d4a2e7b3b690ef10ba36b37ffcccaaef45 Mon Sep 17 00:00:00 2001 From: M0ustach3 <37956764+M0ustach3@users.noreply.github.com> Date: Sun, 9 Mar 2025 18:47:35 +0100 Subject: [PATCH 12/13] nixos/crowdsec: removed credentials option --- nixos/modules/services/security/crowdsec.nix | 95 +++----------------- 1 file changed, 12 insertions(+), 83 deletions(-) diff --git a/nixos/modules/services/security/crowdsec.nix b/nixos/modules/services/security/crowdsec.nix index 15f5aacb51125..d67864e6b1292 100644 --- a/nixos/modules/services/security/crowdsec.nix +++ b/nixos/modules/services/security/crowdsec.nix @@ -376,27 +376,11 @@ in lapi = mkOption { type = types.submodule { options = { - credentials = mkOption { - type = types.nullOr format.type; - default = null; - example = { - url = "http://localhost:8080"; - login = "login"; - password = "password"; - ca_cert_path = "example"; - key_path = "example"; - cert_path = "example"; - }; - description = '' - Attributes inside the local_api_credentials file. - This is not the most secure way to define settings, as this is put in the Nix store. Use of lapi.credentialsFile is preferred. - ''; - }; credentialsFile = mkOption { type = types.nullOr types.path; example = "/run/crowdsec/lapi.yaml"; description = '' - The credential file to use. This is strongly preferred instead of putting secrets in the Nix store. + The LAPI credential file to use. ''; default = null; }; @@ -410,23 +394,11 @@ in capi = mkOption { type = types.submodule { options = { - credentials = mkOption { - type = types.nullOr format.type; - default = null; - example = { - url = "https://api.crowdsec.net/"; - login = "abcdefghijklmnopqrstuvwxyz"; - password = "abcdefghijklmnopqrstuvwxyz"; - }; - description = '' - Attributes inside the central_api_credentials.yaml file. This is not the most secure way to define settings, as this is put in the Nix store. Use of capi.credentialsFile is preferred. - ''; - }; credentialsFile = mkOption { type = types.nullOr types.path; example = "/run/crowdsec/capi.yaml"; description = '' - The credential file to use. This is strongly preferred instead of putting secrets in the Nix store. + The CAPI credential file to use. ''; default = null; }; @@ -440,19 +412,11 @@ in console = mkOption { type = types.submodule { options = { - token = mkOption { - type = types.nullOr types.str; - default = null; - example = "abcde"; - description = '' - The console token to enroll to the web console. This is not the most secure way to define settings, as this is put in the Nix store. Use of console.tokenFile is preferred. - ''; - }; tokenFile = mkOption { type = types.nullOr types.path; example = "/run/crowdsec/console_token.yaml"; description = '' - The credential file to use. This is strongly preferred instead of putting secrets in the Nix store. + The Console Token file to use. ''; default = null; }; @@ -483,32 +447,6 @@ in let cfg = config.services.crowdsec; configFile = format.generate "crowdsec.yaml" cfg.settings.general; - lapiFile = - if cfg.settings.lapi.credentialsFile != null then - cfg.settings.lapi.credentialsFile - else - format.generate "local_api_credentials.yaml" cfg.settings.lapi.credentials; - capiFile = - if cfg.settings.capi.credentialsFile != null then - cfg.settings.capi.credentialsFile - else - ( - if cfg.settings.capi.credentials != null then - format.generate "central_api_credentials.yaml" cfg.settings.capi.credentials - else - null - ); - - tokenFile = - if cfg.settings.console.tokenFile != null then - cfg.settings.console.tokenFile - else - ( - if cfg.settings.console.token != null then - pkgs.writeText "console_token.txt" cfg.settings.console.token - else - null - ); simulationFile = format.generate "simulation.yaml" cfg.settings.simulation; consoleFile = format.generate "console.yaml" cfg.settings.console.configuration; patternsDir = pkgs.buildPackages.symlinkJoin { @@ -520,7 +458,7 @@ in }; cscli = pkgs.writeShellScriptBin "cscli" '' - set -euox pipefail + 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 @@ -558,7 +496,7 @@ in scriptArray = [ - "set -euox pipefail" + "set -euo pipefail" "${lib.getExe cscli} hub update" ] ++ lib.optionals (cfg.hub.collections != [ ]) [ @@ -598,17 +536,17 @@ in fi '' ] - ++ lib.optionals (capiFile != null) [ + ++ lib.optionals (cfg.settings.capi.credentialsFile != null) [ '' - if ! grep -q password "${capiFile}" ]; then + if ! grep -q password "${cfg.settings.capi.credentialsFile}" ]; then ${lib.getExe cscli} capi register fi '' ] - ++ lib.optionals (tokenFile != null) [ + ++ lib.optionals (cfg.settings.console.tokenFile != null) [ '' - if [ ! -e "${tokenFile}" ]; then - ${lib.getExe cscli} console enroll "$(cat ${tokenFile})" --name ${cfg.name} + if [ ! -e "${cfg.settings.console.tokenFile}" ]; then + ${lib.getExe cscli} console enroll "$(cat ${cfg.settings.console.tokenFile})" --name ${cfg.name} fi '' ]; @@ -620,15 +558,6 @@ in in lib.mkIf (cfg.enable) { - assertions = [ - { - assertion = lib.trivial.xor (cfg.settings.lapi.credentials != null) ( - cfg.settings.lapi.credentialsFile != null - ); - message = "Please specify either services.crowdsec.settings.lapi.credentialsFile or services.crowdsec.settings.lapi.credentials, not more, not less."; - } - ]; - warnings = [ ] ++ lib.optionals (cfg.localConfig.profiles == [ ]) [ @@ -664,7 +593,7 @@ in }; api = { client = { - credentials_path = mkDefault lapiFile; + credentials_path = cfg.settings.lapi.credentialsFile; }; server = { enable = mkDefault false; @@ -679,7 +608,7 @@ in community = mkDefault true; blocklists = mkDefault true; }; - credentials_path = mkDefault capiFile; + credentials_path = cfg.settings.capi.credentialsFile; }; }; }; From f3e14e4cc453e2cf48de6e0515d4d35286d1f6f2 Mon Sep 17 00:00:00 2001 From: M0ustach3 <37956764+M0ustach3@users.noreply.github.com> Date: Sun, 9 Mar 2025 20:05:29 +0100 Subject: [PATCH 13/13] nixos/crowdsec: moved to tmpfiles settings --- nixos/modules/services/security/crowdsec.nix | 154 ++++++++++++++----- 1 file changed, 116 insertions(+), 38 deletions(-) diff --git a/nixos/modules/services/security/crowdsec.nix b/nixos/modules/services/security/crowdsec.nix index d67864e6b1292..fd9f702f6dd41 100644 --- a/nixos/modules/services/security/crowdsec.nix +++ b/nixos/modules/services/security/crowdsec.nix @@ -781,44 +781,122 @@ in }; }; - systemd.tmpfiles.rules = ( - [ - "d ${stateDir} 0750 ${cfg.user} ${cfg.group} - -" - "d ${hubDir} 0750 ${cfg.user} ${cfg.group} - -" - "d ${confDir} 0750 ${cfg.user} ${cfg.group} - -" - "d ${localScenariosDir} 0750 ${cfg.user} ${cfg.group} - -" - "d ${localPostOverflowsDir} 0750 ${cfg.user} ${cfg.group} - -" - "d ${localPostOverflowsS01WhitelistDir} 0750 ${cfg.user} ${cfg.group} - -" - "d ${parsersDir} 0750 ${cfg.user} ${cfg.group} - -" - "d ${localParsersS00RawDir} 0750 ${cfg.user} ${cfg.group} - -" - "d ${localParsersS01ParseDir} 0750 ${cfg.user} ${cfg.group} - -" - "d ${localParsersS02EnrichDir} 0750 ${cfg.user} ${cfg.group} - -" - "d ${localContextsDir} 0750 ${cfg.user} ${cfg.group} - -" - "d ${notificationsDir} 0750 ${cfg.user} ${cfg.group} - -" - "d ${pluginDir} 0750 ${cfg.user} ${cfg.group} - -" - ] - ++ (map ( - file: "L+ ${localScenariosDir}/${builtins.baseNameOf file} - - - - ${file}" - ) localScenariosMap) - ++ (map ( - file: "L+ ${localParsersS00RawDir}/${builtins.baseNameOf file} - - - - ${file}" - ) localParsersS00RawMap) - ++ (map ( - file: "L+ ${localParsersS01ParseDir}/${builtins.baseNameOf file} - - - - ${file}" - ) localParsersS01ParseMap) - ++ (map ( - file: "L+ ${localParsersS02EnrichDir}/${builtins.baseNameOf file} - - - - ${file}" - ) localParsersS02EnrichMap) - ++ (map ( - file: "L+ ${localPostOverflowsS01WhitelistDir}/${builtins.baseNameOf file} - - - - ${file}" - ) localPostOverflowsS01WhitelistMap) - ++ (map ( - file: "L+ ${localContextsDir}/${builtins.baseNameOf file} - - - - ${file}" - ) localContextsMap) - ++ (map ( - file: "L+ ${notificationsDir}/${builtins.baseNameOf file} - - - - ${file}" - ) localNotificationsMap) - ); + 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;