diff --git a/flake.lock b/flake.lock index d761f4f..d86b361 100644 --- a/flake.lock +++ b/flake.lock @@ -190,6 +190,22 @@ "type": "github" } }, + "nixpkgs-crowdsec": { + "locked": { + "lastModified": 1738085579, + "narHash": "sha256-7mLjMrOiiIi0vI7BJwbEipYQzwA7JF/NWHP+LM4q5S8=", + "owner": "tlater", + "repo": "nixpkgs", + "rev": "426a7afc9a6ecfdac544bda4022acef31e36df34", + "type": "github" + }, + "original": { + "owner": "tlater", + "ref": "tlater/fix-crowdsec", + "repo": "nixpkgs", + "type": "github" + } + }, "nixpkgs-unstable": { "locked": { "lastModified": 1737192615, @@ -310,6 +326,7 @@ "disko": "disko", "foundryvtt": "foundryvtt", "nixpkgs": "nixpkgs_2", + "nixpkgs-crowdsec": "nixpkgs-crowdsec", "nixpkgs-unstable": "nixpkgs-unstable", "sonnenshift": "sonnenshift", "sops-nix": "sops-nix", diff --git a/flake.nix b/flake.nix index 2253566..444d4b8 100644 --- a/flake.nix +++ b/flake.nix @@ -26,6 +26,8 @@ url = "git+ssh://git@github.com/sonnenshift/battery-manager"; inputs.nixpkgs.follows = "nixpkgs"; }; + + nixpkgs-crowdsec.url = "github:tlater/nixpkgs/tlater/fix-crowdsec"; }; outputs = @@ -98,7 +100,10 @@ # Garbage collection root # ########################### - packages.${system}.default = vm.config.system.build.vm; + packages.${system} = { + default = vm.config.system.build.vm; + crowdsec = pkgs.callPackage "${inputs.nixpkgs-crowdsec}/pkgs/by-name/cr/crowdsec/package.nix" { }; + }; ################### # Utility scripts # diff --git a/modules/crowdsec.nix b/modules/crowdsec.nix new file mode 100644 index 0000000..1f3ffea --- /dev/null +++ b/modules/crowdsec.nix @@ -0,0 +1,333 @@ +{ + flake-inputs, + pkgs, + lib, + config, + ... +}: +let + cfg = config.services.crowdsec; + settingsFormat = pkgs.formats.yaml { }; + + crowdsec = flake-inputs.self.packages.${pkgs.system}.crowdsec; + + hub = pkgs.fetchFromGitHub { + owner = "crowdsecurity"; + repo = "hub"; + rev = "7a3b4753f4577257c0cbeb8f8f90c7f17d2ae008"; + hash = "sha256-HB4jHyhiO8gjBkLmpo6bDbwhfm5m5nAtNlKhDkZjt2I="; + }; + + cscli = pkgs.writeShellScriptBin "cscli" '' + export PATH="$PATH:${crowdsec}/bin/" + + cd ${cfg.stateDirectory} + sudo=exec + if [ "$USER" != "crowdsec" ]; then + sudo='exec /run/wrappers/bin/sudo -u crowdsec' + fi + + $sudo ${crowdsec}/bin/cscli "$@" + ''; +in +{ + options.services.crowdsec = + let + inherit (lib.types) + nullOr + listOf + package + path + str + ; + in + { + enable = lib.mkEnableOption "crowdsec"; + + package = lib.mkOption { + type = package; + default = crowdsec; + }; + + stateDirectory = lib.mkOption { + type = path; + readOnly = true; + + description = '' + The state directory of the crowdsec instance. Cannot be + changed, but is exposed for downstream use. + ''; + }; + + settings = lib.mkOption { + inherit (settingsFormat) type; + default = { }; + + description = '' + The crowdsec configuration. Refer to + <https://docs.crowdsec.net/docs/next/configuration/crowdsec_configuration/> + for details on supported values. + ''; + }; + + parserWhitelist = lib.mkOption { + type = listOf str; + default = [ ]; + description = '' + Set of IP addresses to add to a parser-based whitelist. + + Addresses can be specified either as plain IP addresses or + in CIDR notation. + ''; + }; + + centralApiCredentials = lib.mkOption { + type = nullOr path; + default = null; + + description = '' + The API key to access crowdsec's central API - this is + required to access any of the shared blocklists. + + Use of this feature is optional, entering no API key (the + default) turns all sharing or receiving of blocked IPs off. + + Note that adding the API key by itself does not enable + sharing of blocked IPs with the central API. This limits the + types of blocklists this instance can access. + + To also turn sharing blocked IPs on, set + `api.server.online_client.sharing = true;`. + ''; + }; + + ctiApiKey = lib.mkOption { + type = nullOr path; + default = null; + + description = '' + The API key for crowdsec's CTI offering. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + # Set up default settings; anything that *shouldn't* be changed is + # set to the default priority so that users need to use + # `lib.mkForce`. + services.crowdsec = { + stateDirectory = "/var/lib/crowdsec"; + + settings = { + common = { + daemonize = true; + # The default logs to files, which isn't the preferred way + # on NixOS + log_media = "stdout"; + }; + + config_paths = { + config_dir = "${cfg.stateDirectory}/config/"; + data_dir = "${cfg.stateDirectory}/data/"; + # This "config" file is intended to be written to using the + # cscli tool, so you can temporarily make it so rules don't + # do anything but log what they *would* do for + # experimentation. + simulation_path = "${cfg.stateDirectory}/config/simulation.yaml"; + + pattern_dir = lib.mkDefault "${cfg.package}/share/crowdsec/config/patterns"; + + hub_dir = hub; + index_path = "${hub}/.index.json"; + + # Integrations aren't supported for now + notification_dir = lib.mkDefault "/var/empty/"; + plugin_dir = lib.mkDefault "/var/empty/"; + }; + + crowdsec_service.acquisition_path = lib.mkDefault "${cfg.package}/share/crowdsec/config/acquis.yaml"; + + cscli = { + prometheus_uri = lib.mkDefault "127.0.0.1:6060"; + }; + + db_config = { + type = lib.mkDefault "sqlite"; + db_path = lib.mkDefault "${cfg.stateDirectory}/data/crowdsec.db"; + use_wal = lib.mkDefault true; + flush = { + max_items = lib.mkDefault 5000; + max_age = lib.mkDefault "7d"; + }; + }; + + api = { + cti = { + enabled = cfg.ctiApiKey != null; + key = cfg.ctiApiKey; + }; + client.credentials_path = "${cfg.stateDirectory}/local_credentials.yaml"; + server = { + listen_uri = lib.mkDefault "127.0.0.1:8080"; + profiles_path = lib.mkDefault "${cfg.package}/share/crowdsec/config/profiles.yaml"; + console_path = lib.mkDefault "${cfg.package}/share/crowdsec/config/console.yaml"; + + online_client = { + # By default, we don't let crowdsec phone home, since + # this is usually within NixOS users' concerns. + # + # TODO: Enable when this option becomes available + # (1.6.4, current nixpkgs-unstable) + # sharing = lib.mkDefault false; + credentials_path = cfg.centralApiCredentials; + }; + }; + }; + + # We enable prometheus by default, since cscli relies on it + # for metrics + prometheus = { + enabled = lib.mkDefault true; + level = lib.mkDefault "full"; + listen_addr = lib.mkDefault "127.0.0.1"; + listen_port = lib.mkDefault 6060; + }; + }; + }; + + systemd.packages = [ + cfg.package + ]; + + environment = { + systemPackages = [ + # TODO(tlater): Figure out a way to get completions to work + cscli + ]; + etc."crowdsec/config.yaml".source = settingsFormat.generate "crowdsec-settings.yaml" cfg.settings; + }; + + systemd = { + tmpfiles.settings."10-crowdsec" = lib.mkIf (cfg.parserWhitelist != [ ]) { + "${cfg.stateDirectory}".d = { + user = "crowdsec"; + group = "crowdsec"; + mode = "0700"; + }; + + "${cfg.stateDirectory}/config".d = { + user = "crowdsec"; + group = "crowdsec"; + mode = "0700"; + }; + + "${cfg.stateDirectory}/config/parsers".d = { + user = "crowdsec"; + group = "crowdsec"; + mode = "0700"; + }; + + "${cfg.stateDirectory}/config/parsers/s02-enrich".d = { + user = "crowdsec"; + group = "crowdsec"; + mode = "0700"; + }; + + "${cfg.stateDirectory}/config/parsers/s02-enrich/nixos-whitelist.yaml" = { + "L+".argument = + (settingsFormat.generate "crowdsec-nixos-whitelist.yaml" { + name = "nixos/parser-whitelist"; + description = "Parser whitelist generated by the crowdsec NixOS module"; + whitelist = { + reason = "Filtered by NixOS whitelist"; + ip = lib.lists.filter (ip: !(lib.hasInfix "/" ip)) cfg.parserWhitelist; + cidr = lib.lists.filter (ip: lib.hasInfix "/" ip) cfg.parserWhitelist; + }; + }).outPath; + }; + }; + + services = { + crowdsec-setup = { + description = "Crowdsec database and config preparation"; + + script = '' + mkdir -p '${cfg.stateDirectory}/'{config,} + + if [ ! -e '${cfg.settings.config_paths.simulation_path}' ]; then + cp '${cfg.package}/share/crowdsec/config/simulation.yaml' '${cfg.settings.config_paths.simulation_path}' + fi + + if [ ! -e '${cfg.settings.api.client.credentials_path}' ]; then + ${cfg.package}/bin/cscli machines add --auto --file '${cfg.settings.api.client.credentials_path}' + fi + ''; + + serviceConfig = { + User = "crowdsec"; + Group = "crowdsec"; + StateDirectory = "crowdsec"; + + Type = "oneshot"; + RemainAfterExit = true; + }; + }; + + # Note that the service basics are already defined upstream + crowdsec = { + after = [ "crowdsec-setup.service" ]; + bindsTo = [ "crowdsec-setup.service" ]; + + serviceConfig = { + User = "crowdsec"; + Group = "crowdsec"; + SupplementaryGroups = [ "systemd-journal" ]; + + StateDirectory = "crowdsec"; + + # PrivateTmp = true; + # PrivateUsers = true; + # ProtectHome = true; + # CapabilityBoundingSet = [ ]; + # LockPersonality = true; + # PrivateDevices = true; + # ProtectHostname = true; + # ProtectKernelTunables = true; + # ProtectKernelModules = true; + # ProtectControlGroups = true; + + # NoNewPrivileges = true; + # RestrictSUIDSGID = true; + + # ProtectProc = "invisible"; + # ProcSubset = "pid"; # Needed for journal access + + # RestrictNamespaces = true; + # RestrictRealtime = true; + + # SystemCallFilter = [ + # "@system-service" + # "@network-io" + # ]; + # SystemCallArchitectures = [ "native" ]; + # SystemCallErrorNumber = "EPERM"; + + # ExecPaths = [ "/nix/store" ]; + # NoExecPaths = [ "/" ]; + }; + }; + }; + }; + + users = { + users.crowdsec = { + isSystemUser = true; + home = cfg.stateDirectory; + group = "crowdsec"; + }; + groups = { + crowdsec = { }; + }; + }; + }; +} diff --git a/modules/default.nix b/modules/default.nix index e1db4cc..977539a 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -1 +1,6 @@ -{ imports = [ ./nginxExtensions.nix ]; } +{ + imports = [ + ./crowdsec.nix + ./nginxExtensions.nix + ]; +}