commit a700a08c5b40f3ceec7e7eabf04be5151426b2b1 Author: Christian Kampka Date: Sun Mar 10 15:42:56 2024 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b2be92b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +result diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6920b3a --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..137fbb2 --- /dev/null +++ b/README.md @@ -0,0 +1,132 @@ +# Crowdsec for NixOS + +This repository contains a [Nix flake](https://nixos.wiki/wiki/Flakes) for running [Crowdsec](https://www.crowdsec.net/) on NixOS. + +CrowdSec is a security tool designed to protect servers, services, and applications by analyzing user behavior and network traffic to detect and block potential attacks. It operates similarly to Fail2Ban but with a few key differences: + +CrowdSec leverages the power of its community by sharing information about attacks among users. When one user detects a new threat, the details are shared across the network, allowing others to protect themselves against this threat, effectively creating a collective intelligence about emerging threats. + +In simple terms, think of CrowdSec as a neighborhood watch program for the internet, where everyone contributes to and benefits from a shared pool of intelligence about potential threats. + +## Usage + +### Crowdsec engine + +To setup the [security engine](https://docs.crowdsec.net/docs/getting_started/security_engine_intro/), import the module and activate the service. + +```nix +{ + inputs = { + crowdsec = { + url = "github:kampka/nix-flake-crowdsec"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = flakes @ { + self, + nixpkgs, + crowdsec, + ... + }: { + nixosConfiguration. = nixpkgs.lib.nixosSystem { + # ... + modules = [ + # ... + crowdsec.nixosModules.crowdsec + + ({ pkgs, lib, ... }: { + services.crowdsec = { + enable = true; + enrollKeyFile = "/path/to/enroll-key"; + settings = { + api.server = { + listen_uri = "127.0.0.1:8080"; + }; + }; + }; + }) + ]; + }; + }; +} +``` + +In case you are setting up a central security engine, adjust the `listen_uri` to be reachable by your bouncers. + +To enroll your crowdsec engine into the central API, you need to obtain an enrollment key from the central [app dashboard](https://app.crowdsec.net/). +Enrolling your engine will give it access to community or commercial blocklist and decisions, depending on your plan. +Enrollment is optional, if you do not want to enroll your engine and just at on your own logs / events, simply omit the `enrollKeyFile` from the settings. + +For additional configuration options, please consult the (Crowdsec documentation)[https://docs.crowdsec.net/docs/configuration/crowdsec_configuration/]. + + +### Crowdsec firewall bouncer + +This flake ships the Crowdsec [firewall bouncer](https://docs.crowdsec.net/docs/getting_started/security_engine_intro/). +It will block traffic from blacklisted IPs on the firewall level. + +At the time of writing, only `iptables` support has proper defaults and testing. +If you are using `nftables` (`networking.nftables.enable = true`), you need to supply bouncer configuration yourself (PRs welcome). +Please consult the [bouncer documentation](https://docs.crowdsec.net/u/bouncers/firewall/#nftables-specific-directives) for directions. + + +```nix +{ + inputs = { + crowdsec = { + url = "github:kampka/nix-flake-crowdsec"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = flakes @ { + self, + nixpkgs, + crowdsec, + ... + }: { + nixosConfiguration. = nixpkgs.lib.nixosSystem { + # ... + modules = [ + # ... + crowdsec.nixosModules.crowdsec-firewall-bouncer; + + ({ pkgs, lib, ... }: { + nixpkgs.overlays = [crowdsec.overlays.default]; + services.crowdsec-firewall-bouncer = { + enable = true; + settings = { + api_key = ""; + api_url = "http://localhost:8080"; + }; + }; + }) + ]; + }; + }; +} +``` + +In order to connect to your security engine, you need to [add your bouncer](https://docs.crowdsec.net/docs/cscli/cscli_bouncers_add/) to the security engine. +You can either use a pre-generated key or have the security engine generate one for you. +Depending on your security requirements and secrets management, this process is scriptable through an `ExecStartPre` script of the engine, eg. + +```nix +{ + services.crowdsec = { + ExecStartPre = let + script = pkgs.writeScriptBin "register-bouncer" '' + #!${pkgs.runtimeShell} + set -eu + set -o pipefail + + if ! cscli bouncers list | grep -q "my-bouncer"; then + cscli bouncers add "my-bouncer" --key "" + fi + ''; + in ["${script}/bin/register-bouncer"]; + }; +} + +``` diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..fddec77 --- /dev/null +++ b/flake.lock @@ -0,0 +1,58 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "id": "flake-utils", + "type": "indirect" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1697915759, + "narHash": "sha256-WyMj5jGcecD+KC8gEs+wFth1J1wjisZf8kVZH13f1Zo=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "51d906d2341c9e866e48c2efcaac0f2d70bfd43e", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..91f6295 --- /dev/null +++ b/flake.nix @@ -0,0 +1,27 @@ +{ + description = "A Aggregate prometheus exporters into a single endpoint"; + + outputs = { + self, + nixpkgs, + flake-utils, + }: let + systems = flake-utils.lib.eachDefaultSystem (system: let + pkgs = import nixpkgs {inherit system;}; + + bouncer-firewall = pkgs.callPackage ./packages/bouncer-firewall {}; + in { + formatter = pkgs.alejandra; + packages."crowdsec-firewall-bouncer" = bouncer-firewall; + }); + in (systems + // { + nixosModules = { + crowdsec = import ./modules/crowdsec; + crowdsec-firewall-bouncer = import ./modules/crowdsec-firewall-bouncer; + }; + overlays.default = final: prev: { + crowdsec-firewall-bouncer = systems.packages.${final.system}.crowdsec-firewall-bouncer; + }; + }); +} diff --git a/modules/crowdsec-firewall-bouncer/default.nix b/modules/crowdsec-firewall-bouncer/default.nix new file mode 100644 index 0000000..6ea2674 --- /dev/null +++ b/modules/crowdsec-firewall-bouncer/default.nix @@ -0,0 +1,101 @@ +{ + config, + pkgs, + lib, + ... +}: let + cfg = config.services.crowdsec-firewall-bouncer; + format = pkgs.formats.yaml {}; + configFile = format.generate "crowdsec.yaml" cfg.settings; + + pkg = cfg.package; + + backend = + if config.networking.nftables.enable + then "nftables" + else "iptables"; + + defaultSettings = with lib; { + log_mode = "stdout"; + + mode = mkDefault backend; + ipset_type = mkDefault "nethash"; + update_frequency = mkDefault "10s"; + deny_action = mkDefault "DROP"; + blacklists_ipv4 = mkDefault "crowdsec-blacklists"; + blacklists_ipv6 = mkDefault "crowdsec6-blacklists"; + iptables_chains = mkDefault ["INPUT"]; + }; +in { + options.services.crowdsec-firewall-bouncer = with lib; { + enable = mkEnableOption "CrowSec Firewall Bouncer"; + package = mkPackageOption pkgs "crowdsec-firewall-bouncer" {}; + settings = mkOption { + description = mdDoc '' + Settings for CrowdSec Firewall Bouncer. Refer to for details. + ''; + type = format.type; + default = {}; + }; + }; + config = lib.mkIf (cfg.enable) { + services.crowdsec-firewall-bouncer.settings = defaultSettings; + + systemd.packages = [pkg]; + systemd.services = { + crowdsec-firewall-bouncer = { + description = "Crowdsec Firewall Bouncer"; + + path = [pkg pkgs.ipset pkgs.iptables pkgs.nftables]; + + wantedBy = ["multi-user.target"]; + partOf = ["firewall.service"]; + + serviceConfig = with lib; { + Type = "notify"; + Restart = "on-failure"; + RestartSec = 10; + + LimitNOFILE = mkDefault 65536; + + MemoryDenyWriteExecute = mkDefault true; + + CapabilityBoundingSet = mkDefault ["CAP_NET_ADMIN" "CAP_NET_RAW"]; + + NoNewPrivileges = mkDefault true; + LockPersonality = mkDefault true; + RemoveIPC = mkDefault true; + + ProtectSystem = mkDefault "strict"; + ProtectHome = mkDefault true; + + PrivateTmp = mkDefault true; + PrivateDevices = mkDefault true; + ProtectHostname = mkDefault true; + ProtectKernelTunables = mkDefault true; + ProtectKernelModules = mkDefault true; + ProtectControlGroups = mkDefault true; + + ProtectProc = mkDefault "invisible"; + ProcSubset = mkDefault "pid"; + + RestrictNamespaces = mkDefault true; + RestrictRealtime = mkDefault true; + RestrictSUIDSGID = mkDefault true; + + SystemCallFilter = mkDefault ["@system-service" "@network-io"]; + SystemCallArchitectures = ["native"]; + SystemCallErrorNumber = mkDefault "EPERM"; + + ExecPaths = ["/nix/store"]; + NoExecPaths = ["/"]; + + ExecStartPost = "${pkgs.coreutils}/bin/sleep 0.2"; + + ExecStart = "${pkg}/bin/cs-firewall-bouncer -c ${configFile}"; + ExecStartPre = ["${pkg}/bin/cs-firewall-bouncer -t -c ${configFile}"]; + }; + }; + }; + }; +} diff --git a/modules/crowdsec/default.nix b/modules/crowdsec/default.nix new file mode 100644 index 0000000..17dd829 --- /dev/null +++ b/modules/crowdsec/default.nix @@ -0,0 +1,264 @@ +{ + config, + pkgs, + lib, + ... +}: let + cfg = config.services.crowdsec; + format = pkgs.formats.yaml {}; + configFile = format.generate "crowdsec.yaml" cfg.settings; + + pkg = cfg.package.overrideAttrs (old: { + ldflags = + (old.ldflags or []) + ++ [ + "-X github.com/crowdsecurity/go-cs-lib/version.Version=v${old.version}" + ]; + patches = + (old.patches or []) + ++ [ + ( + pkgs.fetchpatch + { + url = "https://patch-diff.githubusercontent.com/raw/crowdsecurity/crowdsec/pull/2868.patch"; + hash = "sha256-RSfLhNZ3JVvHoW/BNca9Hs4lpjcDtE1vsBDjJeaHqvc="; + } + ) + ]; + }); + + defaultPatterns = lib.mapAttrs (name: value: lib.mkDefault "${pkg}/share/crowdsec/config/patterns/${name}") (builtins.readDir "${pkg}/share/crowdsec/config/patterns"); + + patternsDir = pkgs.runCommandNoCC "crowdsec-patterns" {} '' + mkdir -p $out + ${lib.concatStringsSep "\n" (lib.attrValues (lib.mapAttrs ( + k: v: '' + ln -sf ${v} $out/${k} + '' + ) + cfg.patterns))} + ''; + + consoleSettings = { + share_manual_decisions = false; + share_custom = true; + share_tainted = true; + share_context = false; + }; + + defaultSettings = with lib; { + common = { + daemonize = mkForce false; + log_media = mkForce "stdout"; + }; + config_paths = { + config_dir = mkDefault "/var/lib/crowdsec/config"; + data_dir = mkDefault dataDir; + hub_dir = mkDefault hubDir; + index_path = mkDefault "${hubDir}/.index.json"; + simulation_path = mkDefault "${pkg}/share/crowdsec/config/simulation.yaml"; + pattern_dir = mkDefault patternsDir; + }; + db_config = { + type = mkDefault "sqlite"; + db_path = mkDefault "${dataDir}/crowdsec.db"; + use_wal = true; + }; + crowdsec_service = { + enable = mkDefault true; + }; + api = { + client = { + credentials_path = mkDefault "${stateDir}/local_api_credentials.yaml"; + }; + server = { + enable = mkDefault (cfg.enrollKeyFile != null); + listen_uri = mkDefault "127.0.0.1:8080"; + + console_path = mkDefault "${stateDir}/console.yaml"; + profiles_path = mkDefault "${pkg}/share/crowdsec/config/profiles.yaml"; + + online_client.credentials_path = mkDefault "${stateDir}/online_api_credentials.yaml"; + }; + }; + }; + + user = "crowdsec"; + group = "crowdsec"; + stateDir = "/var/lib/crowdsec"; + dataDir = "${stateDir}/data"; + hubDir = "${stateDir}/hub"; +in { + options.services.crowdsec = with lib; { + enable = mkEnableOption "CrowSec Security Engine"; + package = mkPackageOption pkgs "crowdsec" {}; + name = mkOption { + type = types.str; + description = mdDoc '' + Name of the machine when registering it at the central or loal api. + ''; + default = config.networking.hostName; + }; + enrollKeyFile = mkOption { + description = mdDoc '' + The file containing the enrollment key used to enroll the engine at the central api console. + See for details. + ''; + type = types.nullOr types.path; + default = null; + }; + patterns = mkOption { + description = mdDoc '' + A set of pattern files for parsing logs, in the form "type" to file containing the corresponding GROK patterns. + All default patterns are automatically included. + See . + ''; + type = types.attrsOf types.pathInStore; + default = {}; + example = lib.literalExpression '' + { ssh = ./patterns/ssh;} + ''; + }; + settings = mkOption { + description = mdDoc '' + Settings for MediaMTX. Refer to the defaults at + . + ''; + type = format.type; + default = {}; + }; + }; + config = let + cscli = pkgs.writeScriptBin "cscli" '' + #!${pkgs.runtimeShell} + set -eu + set -o pipefail + + exec ${pkg}/bin/cscli -c=${configFile} "''${@}" + ''; + in + lib.mkIf (cfg.enable) { + services.crowdsec.settings = defaultSettings; + services.crowdsec.patterns = defaultPatterns; + + environment = { + systemPackages = [cscli]; + }; + + systemd.packages = [pkg]; + systemd.timers.crowdsec-update-hub = { + description = "Update the crowdsec hub index"; + wantedBy = ["timers.target"]; + timerConfig = { + OnCalendar = "daily"; + Persistent = "yes"; + Unit = "crowdsec-update-hub.service"; + }; + }; + systemd.services = let + sudo_doas = + if config.security.doas.enable == true + then "${pkgs.doas}/bin/doas" + else "${pkgs.sudo}/bin/sudo"; + in { + crowdsec-update-hub = { + description = "Update the crowdsec hub index"; + path = [cscli]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${sudo_doas} -u crowdsec ${cscli}/bin/cscli --error hub upgrade"; + ExecStartPost = " systemctl restart crowdsec.service"; + }; + }; + + crowdsec = { + description = "CrowdSec is a free, modern & collaborative behavior detection engine, coupled with a global IP reputation network."; + + path = [cscli]; + + wantedBy = ["multi-user.target"]; + serviceConfig = with lib; { + User = "crowdsec"; + Group = "crowdsec"; + Restart = "on-failure"; + + LimitNOFILE = mkDefault 65536; + + CapabilityBoundingSet = mkDefault []; + + NoNewPrivileges = mkDefault true; + LockPersonality = mkDefault true; + RemoveIPC = mkDefault true; + + ReadWritePaths = [stateDir]; + ProtectSystem = mkDefault "strict"; + + PrivateUsers = mkDefault true; + ProtectHome = mkDefault true; + PrivateTmp = mkDefault true; + + PrivateDevices = mkDefault true; + ProtectHostname = mkDefault true; + ProtectKernelTunables = mkDefault true; + ProtectKernelModules = mkDefault true; + ProtectControlGroups = mkDefault true; + + ProtectProc = mkDefault "invisible"; + ProcSubset = mkDefault "pid"; + + RestrictNamespaces = mkDefault true; + RestrictRealtime = mkDefault true; + RestrictSUIDSGID = mkDefault true; + + SystemCallFilter = mkDefault ["@system-service" "@network-io"]; + SystemCallArchitectures = ["native"]; + SystemCallErrorNumber = mkDefault "EPERM"; + + ExecPaths = ["/nix/store"]; + NoExecPaths = ["/"]; + + ExecStart = "${pkg}/bin/crowdsec -c ${configFile}"; + ExecStartPre = let + script = pkgs.writeScriptBin "crowdsec-setup" '' + #!${pkgs.runtimeShell} + set -eu + set -o pipefail + + if [ ! -s "${cfg.settings.api.client.credentials_path}" ]; then + cscli machine add "${cfg.name}" --auto + fi + + ${lib.optionalString cfg.settings.api.server.enable '' + if ! grep -q password "${cfg.settings.api.server.online_client.credentials_path}" ]; then + cscli capi register + fi + + cscli hub update + + ${lib.optionalString (cfg.enrollKeyFile != null) '' + if [ ! -e "${cfg.settings.api.server.console_path}" ]; then + cscli console enroll "$(cat ${cfg.enrollKeyFile})" --name ${cfg.name} + fi + ''} + ''} + ''; + in ["${script}/bin/crowdsec-setup"]; + }; + }; + }; + systemd.tmpfiles.rules = [ + "d '${stateDir}' 0750 ${user} ${group} - -" + "d '${dataDir}' 0750 ${user} ${group} - -" + "d '${hubDir}' 0750 ${user} ${group} - -" + "f '${cfg.settings.api.server.online_client.credentials_path}' 0750 ${user} ${group} - -" + "f '${cfg.settings.config_paths.index_path}' 0750 ${user} ${group} - -" + ]; + users.users.${user} = lib.mapAttrs (name: lib.mkDefault) { + description = "Cowdsec service user"; + isSystemUser = true; + inherit group; + }; + + users.groups.${group} = lib.mapAttrs (name: lib.mkDefault) {}; + }; +} diff --git a/packages/bouncer-firewall/default.nix b/packages/bouncer-firewall/default.nix new file mode 100644 index 0000000..23cdc0e --- /dev/null +++ b/packages/bouncer-firewall/default.nix @@ -0,0 +1,28 @@ +{ + lib, + buildGoModule, + fetchFromGitHub, +}: +buildGoModule rec { + pname = "cs-firewall-bouncer"; + version = "0.0.28"; + + src = fetchFromGitHub { + owner = "crowdsecurity"; + repo = pname; + rev = "v${version}"; + hash = "sha256-Y1pCupCtYkOD6vKpcmM8nPlsGbO0kYhc3PC9YjJHeMw="; + }; + + vendorHash = "sha256-BA7OHvqIRck3LVgtx7z8qhgueaJ6DOMU8clvWKUCdqE="; + + meta = with lib; { + homepage = "https://crowdsec.net/"; + changelog = "https://github.com/crowdsecurity/${pname}/releases/tag/v${version}"; + description = "Crowdsec bouncer for firewalls."; + longDescription = '' + crowdsec-firewall-bouncer will fetch new and old decisions from a CrowdSec API to add them in a blocklist used by supported firewalls. + ''; + license = licenses.mit; + }; +}