# CrowdSec - Collaborative Intrusion Prevention System # # This module provides simplified boolean feature toggles for common use cases # and can use either a custom implementation or the native NixOS module. # # Module structure: # - default.nix: Core CrowdSec engine and detection features # - bouncers/: Response modules (firewall, haproxy, python) # - integrations/: External system integrations (auditd, console) { config, pkgs, lib, options, ... }: let appName = "crowdsec"; cfg = config.infrastructure.${appName}; # ========================================================================== # Version Detection (must not depend on cfg to avoid recursion) # ========================================================================== # Check if the native services.crowdsec module exists (NixOS 25.11+) hasNativeCrowdsecModule = options ? services && options.services ? crowdsec; # The native module in NixOS 25.11 has multiple bugs that make it unusable: # - #445342: Missing sensible defaults, API server disabled by default # - #446764: Console enrollment broken # - #459224: Cannot enable local API # - Missing hub.postoverflows, hub.scenarios, hub.parsers options # - Null coercion errors in systemd service generation # # We mark the native module as unstable until these are fixed. # Users can override with implementation = "native" to test. nativeModuleIsStable = false; # State directory for CrowdSec stateDir = "/var/lib/crowdsec"; # Helper to generate YAML format yamlFormat = pkgs.formats.yaml {}; in { # ========================================================================== # Import Sub-Modules # ========================================================================== imports = [ # Bouncers - Response mechanisms ./bouncers/firewall.nix ./bouncers/haproxy.nix ./bouncers/python.nix # Integrations - External system connections ./integrations/auditd.nix ./integrations/console.nix ]; # ========================================================================== # Options # ========================================================================== options.infrastructure.${appName} = { enable = lib.mkEnableOption '' CrowdSec - Collaborative Intrusion Prevention System. CrowdSec is an open-source security automation tool that detects and blocks malicious behavior by analyzing logs and sharing threat intelligence with the community. This module provides simplified boolean feature toggles for common use cases and can use either a custom implementation or the native NixOS module. [NIS2 COMPLIANCE] Article 21(2)(b) - Incident Handling: CrowdSec provides automated threat detection and response capabilities, helping organizations meet requirements for detecting, analyzing, and responding to cybersecurity incidents. Article 21(2)(d) - Network Security: Acts as an Intrusion Detection/Prevention System (IDS/IPS), a core requirement for protecting network infrastructure. ''; implementation = lib.mkOption { type = lib.types.enum [ "auto" "native" "custom" ]; description = '' Which implementation to use for CrowdSec. - "auto": Automatically select based on NixOS version and module stability. Currently defaults to "custom" because the native module has bugs. - "native": Force use of NixOS's native services.crowdsec module. Requires NixOS 25.11+. May have bugs - use for testing only. - "custom": Use the custom implementation that manages its own systemd service. Works on all NixOS versions with the crowdsec package. The native module in NixOS 25.11 has several known issues: - #445342: Missing sensible defaults - #446764: Console enrollment broken - #459224: Cannot enable local API When these are fixed, "auto" will switch to using the native module. ''; default = "auto"; example = "custom"; }; package = lib.mkOption { type = lib.types.package; description = "CrowdSec package to use."; default = pkgs.crowdsec; defaultText = lib.literalExpression "pkgs.crowdsec"; }; logLevel = lib.mkOption { type = lib.types.enum [ "trace" "debug" "info" "warning" "error" "fatal" ]; description = '' Log level for CrowdSec. [NIS2 COMPLIANCE] Article 21(2)(g) - Security Monitoring: Appropriate logging level enables proper security event monitoring and incident investigation. ''; default = "info"; example = "debug"; }; # ========================================================================== # API Configuration # ========================================================================== api = { listenAddr = lib.mkOption { type = lib.types.str; description = '' Address for the CrowdSec Local API (LAPI) to listen on. Use "127.0.0.1" for local-only access or "0.0.0.0" for network access. ''; default = "127.0.0.1"; example = "0.0.0.0"; }; listenPort = lib.mkOption { type = lib.types.port; description = "Port for the CrowdSec Local API (LAPI) to listen on."; default = 8080; example = 8080; }; openFirewall = lib.mkOption { type = lib.types.bool; description = '' Whether to open the firewall port for the CrowdSec API. Only needed if bouncers from other machines need to connect. ''; default = false; }; }; # ========================================================================== # Detection Features (Simple Boolean Options) # ========================================================================== features = { sshProtection = lib.mkOption { type = lib.types.bool; description = '' Enable SSH brute-force detection and prevention. Monitors SSH authentication logs to detect and block IP addresses attempting password guessing or credential stuffing attacks. [NIS2 COMPLIANCE] Article 21(2)(i) - Human Resources Security: Protects authentication systems and helps prevent unauthorized access attempts. Article 21(2)(j) - Access Control: Provides automated protection against credential-based attacks on administrative interfaces. ''; default = true; }; nginxProtection = lib.mkOption { type = lib.types.bool; description = '' Enable nginx/web server attack detection. Monitors nginx access and error logs to detect web-based attacks including SQL injection, XSS, path traversal, and more. [NIS2 COMPLIANCE] Article 21(2)(d) - Network Security: Provides web application firewall (WAF) capabilities to protect public-facing services. Article 21(2)(e) - Supply Chain Security: Helps protect web services that may be part of the digital supply chain. ''; default = false; }; nginxLogPaths = lib.mkOption { type = lib.types.listOf lib.types.str; description = "Paths to nginx log files to monitor."; default = [ "/var/log/nginx/*.log" ]; example = [ "/var/log/nginx/access.log" "/var/log/nginx/error.log" ]; }; systemProtection = lib.mkOption { type = lib.types.bool; description = '' Enable system/kernel-level threat detection. Monitors kernel and system logs for suspicious activity including privilege escalation attempts and system abuse. [NIS2 COMPLIANCE] Article 21(2)(a) - Risk Analysis: Provides continuous monitoring to identify and respond to system-level threats. Article 21(2)(g) - Security Monitoring: Implements comprehensive security monitoring across the system infrastructure. ''; default = false; }; communityBlocklists = lib.mkOption { type = lib.types.bool; description = '' Enable community-contributed IP blocklists. When enrolled in the CrowdSec Console, your instance can receive curated blocklists of known malicious IPs from the community. [NIS2 COMPLIANCE] Article 21(2)(d) - Network Security: Leverages collective threat intelligence to proactively block known attackers. Article 14 - Information Sharing: Participates in cybersecurity information sharing to improve collective defense. ''; default = true; }; }; # ========================================================================== # Hub Configuration (Parsers, Scenarios, Collections) # ========================================================================== hub = { collections = lib.mkOption { type = lib.types.listOf lib.types.str; description = '' Additional CrowdSec Hub collections to install. Collections bundle related parsers and scenarios together. Browse available collections at: https://hub.crowdsec.net/ ''; default = []; example = [ "crowdsecurity/apache2" "crowdsecurity/postfix" ]; }; scenarios = lib.mkOption { type = lib.types.listOf lib.types.str; description = '' Additional CrowdSec Hub scenarios to install. Scenarios define detection rules for specific attack patterns. ''; default = []; example = [ "crowdsecurity/http-bf-wordpress_bf" ]; }; parsers = lib.mkOption { type = lib.types.listOf lib.types.str; description = '' Additional CrowdSec Hub parsers to install. Parsers extract structured data from log files. ''; default = []; example = [ "crowdsecurity/docker-logs" ]; }; postoverflows = lib.mkOption { type = lib.types.listOf lib.types.str; description = "Additional post-overflow parsers to install."; default = []; example = [ "crowdsecurity/cdn-whitelist" ]; }; }; # ========================================================================== # Custom Acquisitions # ========================================================================== acquisitions = lib.mkOption { type = lib.types.listOf lib.types.attrs; description = '' Additional log sources for CrowdSec to monitor. Each acquisition defines a log source (file, journalctl, etc.) and the parser type to use. [NIS2 COMPLIANCE] Article 21(2)(g) - Security Monitoring: Enables comprehensive log collection and monitoring across all systems. ''; default = []; example = lib.literalExpression '' [ { source = "journalctl"; journalctl_filter = [ "_SYSTEMD_UNIT=postgresql.service" ]; labels.type = "syslog"; } { filenames = [ "/var/log/myapp/*.log" ]; labels.type = "syslog"; } ] ''; }; # ========================================================================== # Pass-through Configuration # ========================================================================== extraSettings = lib.mkOption { type = lib.types.attrsOf lib.types.anything; description = '' Extra settings merged into the CrowdSec configuration. For native implementation: merged into services.crowdsec.settings. For custom implementation: merged into the generated config.yaml. ''; default = {}; }; extraLocalConfig = lib.mkOption { type = lib.types.attrsOf lib.types.anything; description = '' Extra settings merged into the local configuration. For native implementation: merged into services.crowdsec.localConfig. For custom implementation: not currently used. ''; default = {}; }; }; # ========================================================================== # Configuration # ========================================================================== config = lib.mkIf cfg.enable ( let # ======================================================================== # All cfg-dependent values MUST be defined inside this let block # to avoid infinite recursion during module evaluation # ======================================================================== # Determine which implementation to use useNativeImplementation = if cfg.implementation == "native" then true else if cfg.implementation == "custom" then false else if cfg.implementation == "auto" then hasNativeCrowdsecModule && nativeModuleIsStable else false; # Build acquisitions list based on enabled features acquisitions = lib.flatten [ # SSH acquisition (journalctl-based) (lib.optional cfg.features.sshProtection { source = "journalctl"; journalctl_filter = [ "_SYSTEMD_UNIT=sshd.service" ]; labels.type = "syslog"; }) # Nginx acquisition (log file-based) (lib.optional cfg.features.nginxProtection { filenames = cfg.features.nginxLogPaths; labels.type = "nginx"; }) # System/kernel logs acquisition (lib.optional cfg.features.systemProtection { source = "journalctl"; journalctl_filter = [ "_TRANSPORT=kernel" ]; labels.type = "syslog"; }) # Custom acquisitions from user cfg.acquisitions ]; # Build hub collections list based on enabled features hubCollections = lib.flatten [ (lib.optional cfg.features.sshProtection "crowdsecurity/sshd") (lib.optional cfg.features.nginxProtection "crowdsecurity/nginx") (lib.optional cfg.features.systemProtection "crowdsecurity/linux") cfg.hub.collections ]; # ======================================================================== # Custom Implementation: Configuration Files # ======================================================================== # Generate acquisitions file as multi-document YAML # CrowdSec expects each acquisition as a separate YAML document (separated by ---) # We use yamlFormat.generate for each acquisition and concatenate them acquisitionsFile = pkgs.writeText "acquisitions.yaml" ( lib.concatMapStringsSep "\n---\n" (acq: builtins.readFile (yamlFormat.generate "acq.yaml" acq) ) acquisitions ); # Generate simulation file (CrowdSec requires this) simulationFile = yamlFormat.generate "simulation.yaml" { simulation = false; exclusions = []; }; # Generate main config file (compatible with CrowdSec 1.7.x) configFile = yamlFormat.generate "config.yaml" { common = { daemonize = false; log_media = "stdout"; log_level = cfg.logLevel; }; config_paths = { config_dir = "${stateDir}/config"; data_dir = "${stateDir}/data"; hub_dir = "${stateDir}/hub"; simulation_path = "${stateDir}/config/simulation.yaml"; }; crowdsec_service = { acquisition_path = "${stateDir}/config/acquisitions.yaml"; parser_routines = 1; }; cscli = { output = "human"; }; api = { client = { insecure_skip_verify = false; credentials_path = "${stateDir}/config/local_api_credentials.yaml"; }; server = { enable = true; listen_uri = "${cfg.api.listenAddr}:${toString cfg.api.listenPort}"; profiles_path = "${stateDir}/config/profiles.yaml"; online_client = { credentials_path = "${stateDir}/config/online_api_credentials.yaml"; }; }; }; db_config = { type = "sqlite"; db_path = "${stateDir}/data/crowdsec.db"; use_wal = true; }; }; # Generate profiles file (CrowdSec expects multi-document YAML format) # Use bouncer.banDuration if available, otherwise default to 4h banDuration = cfg.bouncer.banDuration or "4h"; profilesFile = pkgs.writeText "profiles.yaml" '' name: default_ip_remediation filters: - Alert.Remediation == true && Alert.GetScope() == "Ip" decisions: - type: ban duration: ${banDuration} on_success: break ''; # Initialization script - sets up CrowdSec on first run initScript = pkgs.writeShellScript "crowdsec-init" '' set -e export PATH="${lib.makeBinPath [ cfg.package pkgs.coreutils pkgs.gnugrep pkgs.nettools pkgs.findutils ]}:$PATH" STATE_DIR="${stateDir}" CONFIG_DIR="$STATE_DIR/config" DATA_DIR="$STATE_DIR/data" HUB_DIR="$STATE_DIR/hub" PACKAGE="${cfg.package}" # Create directories mkdir -p "$CONFIG_DIR" "$DATA_DIR" "$HUB_DIR" # Copy configuration files cp -f ${configFile} "$CONFIG_DIR/config.yaml" cp -f ${profilesFile} "$CONFIG_DIR/profiles.yaml" cp -f ${acquisitionsFile} "$CONFIG_DIR/acquisitions.yaml" cp -f ${simulationFile} "$CONFIG_DIR/simulation.yaml" # Debug: Show acquisitions file content echo "Generated acquisitions.yaml:" cat "$CONFIG_DIR/acquisitions.yaml" echo "" # Copy patterns directory from package (required for parser grok patterns) echo "Looking for patterns directory..." # Try common locations PATTERNS_FOUND=0 for PATTERNS_PATH in \ "$PACKAGE/share/crowdsec/config/patterns" \ "$PACKAGE/share/crowdsec/patterns" \ "$PACKAGE/etc/crowdsec/patterns" \ ; do if [ -d "$PATTERNS_PATH" ]; then echo "Found patterns at: $PATTERNS_PATH" rm -rf "$CONFIG_DIR/patterns" cp -r "$PATTERNS_PATH" "$CONFIG_DIR/patterns" PATTERNS_FOUND=1 break fi done # If not found in common locations, search the entire package if [ "$PATTERNS_FOUND" = "0" ]; then echo "Searching for patterns directory in package..." PATTERNS_PATH=$(find "$PACKAGE" -type d -name "patterns" 2>/dev/null | head -1) if [ -n "$PATTERNS_PATH" ]; then echo "Found patterns at: $PATTERNS_PATH" rm -rf "$CONFIG_DIR/patterns" cp -r "$PATTERNS_PATH" "$CONFIG_DIR/patterns" PATTERNS_FOUND=1 fi fi if [ "$PATTERNS_FOUND" = "0" ]; then echo "WARNING: Could not find patterns directory!" echo "Package contents:" ls -la "$PACKAGE/" || true ls -la "$PACKAGE/share/" || true ls -la "$PACKAGE/share/crowdsec/" 2>/dev/null || true fi # Initialize database if it doesn't exist if [ ! -f "$DATA_DIR/crowdsec.db" ]; then echo "Initializing CrowdSec database..." touch "$CONFIG_DIR/local_api_credentials.yaml" touch "$CONFIG_DIR/online_api_credentials.yaml" chmod 640 "$CONFIG_DIR/local_api_credentials.yaml" chmod 640 "$CONFIG_DIR/online_api_credentials.yaml" fi # Generate machine ID if it doesn't exist if [ ! -f "$CONFIG_DIR/local_api_credentials.yaml" ] || [ ! -s "$CONFIG_DIR/local_api_credentials.yaml" ]; then echo "Registering local machine..." cscli -c "$CONFIG_DIR/config.yaml" machines add "$(hostname)" --auto --force || true fi # Update hub index echo "Updating hub index..." cscli -c "$CONFIG_DIR/config.yaml" hub update || true # Set correct ownership chown -R crowdsec:crowdsec "$STATE_DIR" ''; # Hub installation script (runs after service is started) hubInstallScript = pkgs.writeShellScript "crowdsec-hub-install" '' set -e export PATH="${lib.makeBinPath [ cfg.package pkgs.coreutils pkgs.gnugrep ]}:$PATH" CONFIG_DIR="${stateDir}/config" # Wait for API to be ready for i in $(seq 1 30); do if cscli -c "$CONFIG_DIR/config.yaml" hub list >/dev/null 2>&1; then break fi sleep 1 done # Install collections ${lib.concatMapStringsSep "\n" (c: '' if ! cscli -c "$CONFIG_DIR/config.yaml" collections list 2>/dev/null | grep -q "${c}"; then cscli -c "$CONFIG_DIR/config.yaml" collections install ${c} || true fi '') hubCollections} # Install additional scenarios ${lib.concatMapStringsSep "\n" (s: '' if ! cscli -c "$CONFIG_DIR/config.yaml" scenarios list 2>/dev/null | grep -q "${s}"; then cscli -c "$CONFIG_DIR/config.yaml" scenarios install ${s} || true fi '') cfg.hub.scenarios} # Install additional parsers ${lib.concatMapStringsSep "\n" (p: '' if ! cscli -c "$CONFIG_DIR/config.yaml" parsers list 2>/dev/null | grep -q "${p}"; then cscli -c "$CONFIG_DIR/config.yaml" parsers install ${p} || true fi '') cfg.hub.parsers} ''; in lib.mkMerge [ # ========================================================================== # Common Configuration (both implementations) # ========================================================================== { # Assertions assertions = [ { assertion = acquisitions != []; message = '' CrowdSec requires at least one acquisition source. Enable at least one of: - infrastructure.crowdsec.features.sshProtection = true - infrastructure.crowdsec.features.nginxProtection = true - infrastructure.crowdsec.features.systemProtection = true Or add custom acquisitions via infrastructure.crowdsec.acquisitions ''; } { assertion = cfg.implementation != "native" || hasNativeCrowdsecModule; message = '' CrowdSec native implementation requires NixOS 25.11 or later. Either: 1. Upgrade to NixOS 25.11+ 2. Set infrastructure.crowdsec.implementation = "custom" 3. Set infrastructure.crowdsec.implementation = "auto" (recommended) ''; } ]; # Open firewall for LAPI if configured networking.firewall.allowedTCPPorts = lib.mkIf cfg.api.openFirewall [ cfg.api.listenPort ]; # Install useful CLI tools environment.systemPackages = [ cfg.package # Includes cscli ]; } # ========================================================================== # Custom Implementation # ========================================================================== (lib.mkIf (!useNativeImplementation) { # Create crowdsec user and group users.users.crowdsec = { isSystemUser = true; group = "crowdsec"; home = stateDir; description = "CrowdSec daemon user"; }; users.groups.crowdsec = {}; # Ensure data directories exist and create config symlink for cscli systemd.tmpfiles.rules = [ "d ${stateDir} 0755 crowdsec crowdsec - -" "d ${stateDir}/config 0755 crowdsec crowdsec - -" "d ${stateDir}/data 0755 crowdsec crowdsec - -" "d ${stateDir}/hub 0755 crowdsec crowdsec - -" # Create /etc/crowdsec directory and symlink for cscli default config path "L+ /etc/crowdsec/config.yaml - - - - ${stateDir}/config/config.yaml" ]; # Main CrowdSec service systemd.services.crowdsec = { description = "CrowdSec Security Engine"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" "local-fs.target" ]; serviceConfig = { Type = "simple"; User = "crowdsec"; Group = "crowdsec"; ExecStartPre = [ "+${initScript}" # Run as root for permissions ]; ExecStart = "${cfg.package}/bin/crowdsec -c ${stateDir}/config/config.yaml"; ExecStartPost = "${hubInstallScript}"; Restart = "always"; RestartSec = "10s"; # Security hardening ProtectSystem = "strict"; ProtectHome = true; PrivateTmp = true; NoNewPrivileges = true; ReadWritePaths = [ stateDir ]; # Allow journal access for systemd log sources SupplementaryGroups = lib.optional (cfg.features.sshProtection || cfg.features.systemProtection) "systemd-journal"; }; }; }) # ========================================================================== # Native Implementation (NixOS 25.11+) # ========================================================================== (lib.mkIf (useNativeImplementation && hasNativeCrowdsecModule) { # Workarounds for native module bugs systemd.tmpfiles.rules = [ # WORKAROUND #445342: Create state directory # WORKAROUND #446764: Create online_api_credentials.yaml "f /var/lib/crowdsec/online_api_credentials.yaml 0640 crowdsec crowdsec - -" ]; services.crowdsec = { enable = true; package = cfg.package; # Hub items to install (only collections - other options may not exist) hub = { collections = hubCollections; }; # Local configuration (acquisitions) localConfig = { inherit acquisitions; } // cfg.extraLocalConfig; # Main settings settings = lib.mkMerge [ { # WORKAROUND: BUG #445342 - Enable API server by default general.api.server.enable = true; } # Console enrollment (if configured) # Note: console options are defined in integrations/console.nix (lib.mkIf (cfg.console.enrollKeyFile != null) { console.tokenFile = cfg.console.enrollKeyFile; }) # User's extra settings cfg.extraSettings ]; }; }) ] ); }