# CrowdSec Python Bouncer Module # Provides API key registration for pycrowdsec and python-capi-sdk integration { config, pkgs, lib, ... }: let appName = "crowdsec"; cfg = config.infrastructure.${appName}; stateDir = "/var/lib/crowdsec"; in { # ========================================================================== # Options # ========================================================================== options.infrastructure.${appName}.python = { enable = lib.mkOption { type = lib.types.bool; description = '' Enable Python bouncer registration for use with pycrowdsec. This registers a bouncer with CrowdSec and stores the API key in a configurable location so Python applications can use the pycrowdsec library to check IPs against CrowdSec decisions. Python applications should use the StreamClient or QueryClient from pycrowdsec to query decisions: ```python from pycrowdsec.client import StreamClient client = StreamClient( api_key=open("/run/crowdsec-python-bouncer/api_key").read().strip(), lapi_url="http://127.0.0.1:8080/" ) client.run() action = client.get_action_for("1.2.3.4") # Returns "ban", "captcha", etc. ``` For Flask/Django integration, configure the middleware to read the API key from the configured apiKeyFile path. [NIS2 COMPLIANCE] Article 21(2)(d) - Network Security: Provides application-layer protection for Python web applications through CrowdSec integration. ''; default = false; }; bouncerName = lib.mkOption { type = lib.types.str; description = "Name to register the Python bouncer with in CrowdSec."; default = "python-bouncer"; example = "my-flask-app-bouncer"; }; apiKeyFile = lib.mkOption { type = lib.types.str; description = '' Path where the bouncer API key will be stored. This file will be readable by the configured group (default: root). Python applications need read access to this file to authenticate with the CrowdSec Local API. ''; default = "/run/crowdsec-python-bouncer/api_key"; example = "/run/secrets/crowdsec-python-api-key"; }; apiKeyFileGroup = lib.mkOption { type = lib.types.str; description = '' Group that should have read access to the API key file. Set this to match the group your Python application runs as. For example, if your Flask app runs as user "flask" in group "flask", set this to "flask". ''; default = "root"; example = "www-data"; }; enableCapi = lib.mkOption { type = lib.types.bool; description = '' Enable Central API (CAPI) credentials for signal sharing with python-capi-sdk. When enabled, this generates machine credentials that can be used with the python-capi-sdk to send attack signals to CrowdSec's central infrastructure and receive community blocklists. The credentials are stored in the capiCredentialsFile location. Example usage with python-capi-sdk: ```python from cscapi.client import CAPIClient, CAPIClientConfig from cscapi.sql_storage import SQLStorage import yaml # Load credentials generated by this module with open("/run/crowdsec-python-bouncer/capi_credentials.yaml") as f: creds = yaml.safe_load(f) client = CAPIClient( storage=SQLStorage(connection_string="sqlite:///signals.db"), config=CAPIClientConfig(scenarios=["crowdsecurity/ssh-bf"]) ) ``` [NIS2 COMPLIANCE] Article 14 - Information Sharing: Enables participation in threat intelligence sharing through the CrowdSec community network. ''; default = false; }; capiCredentialsFile = lib.mkOption { type = lib.types.str; description = '' Path where CAPI credentials will be stored for python-capi-sdk. This file contains machine_id and password for authenticating with CrowdSec's Central API. ''; default = "/run/crowdsec-python-bouncer/capi_credentials.yaml"; example = "/run/secrets/crowdsec-capi-credentials.yaml"; }; capiScenarios = lib.mkOption { type = lib.types.listOf lib.types.str; description = '' Scenarios that your Python application will report signals for. These should match the attack patterns your application detects. Common scenarios include: - crowdsecurity/ssh-bf (SSH brute force) - crowdsecurity/http-bf (HTTP brute force) - crowdsecurity/http-crawl-non_statics (Web crawling) ''; default = []; example = [ "crowdsecurity/http-bf" "crowdsecurity/http-crawl-non_statics" ]; }; }; # ========================================================================== # Configuration # ========================================================================== config = lib.mkIf (cfg.enable && cfg.python.enable) ( let # Get the directory from the apiKeyFile path pythonBouncerDir = builtins.dirOf cfg.python.apiKeyFile; pythonCapiDir = builtins.dirOf cfg.python.capiCredentialsFile; # Python bouncer registration script pythonBouncerRegisterScript = pkgs.writeShellScript "crowdsec-python-bouncer-register" '' set -e export PATH="${lib.makeBinPath [ cfg.package pkgs.coreutils pkgs.gnugrep pkgs.gnused ]}:$PATH" CONFIG_DIR="${stateDir}/config" KEY_FILE="${cfg.python.apiKeyFile}" KEY_DIR="${pythonBouncerDir}" BOUNCER_NAME="${cfg.python.bouncerName}" KEY_GROUP="${cfg.python.apiKeyFileGroup}" # Create directory if it doesn't exist mkdir -p "$KEY_DIR" # Wait for CrowdSec API to be ready for i in $(seq 1 60); do if cscli -c "$CONFIG_DIR/config.yaml" bouncers list >/dev/null 2>&1; then break fi sleep 1 done # Check if bouncer already registered if ! cscli -c "$CONFIG_DIR/config.yaml" bouncers list 2>/dev/null | grep -q "$BOUNCER_NAME"; then # Register new bouncer and save key KEY=$(cscli -c "$CONFIG_DIR/config.yaml" bouncers add "$BOUNCER_NAME" -o raw 2>/dev/null || echo "") if [ -n "$KEY" ]; then echo "$KEY" > "$KEY_FILE" # Set permissions: owner read/write, group read chmod 640 "$KEY_FILE" chown root:"$KEY_GROUP" "$KEY_FILE" echo "Python bouncer '$BOUNCER_NAME' registered successfully" echo "API key stored at: $KEY_FILE" fi else echo "Python bouncer '$BOUNCER_NAME' already registered" fi # Create a helper config file for Python applications LAPI_URL="http://${cfg.api.listenAddr}:${toString cfg.api.listenPort}/" cat > "$KEY_DIR/config.yaml" << EOF # CrowdSec Python Bouncer Configuration # Generated by NixOS infrastructure.crowdsec module # # Usage with pycrowdsec: # from pycrowdsec.client import StreamClient # import yaml # # with open('${pythonBouncerDir}/config.yaml') as f: # config = yaml.safe_load(f) # # client = StreamClient( # api_key=open(config['api_key_file']).read().strip(), # lapi_url=config['lapi_url'] # ) # client.run() lapi_url: "$LAPI_URL" api_key_file: "$KEY_FILE" bouncer_name: "$BOUNCER_NAME" EOF chmod 644 "$KEY_DIR/config.yaml" chown root:"$KEY_GROUP" "$KEY_DIR/config.yaml" ''; # Python CAPI credentials script (for python-capi-sdk signal sharing) pythonCapiRegisterScript = pkgs.writeShellScript "crowdsec-python-capi-register" '' set -e export PATH="${lib.makeBinPath [ cfg.package pkgs.coreutils pkgs.gnugrep pkgs.gnused pkgs.openssl ]}:$PATH" CONFIG_DIR="${stateDir}/config" CAPI_FILE="${cfg.python.capiCredentialsFile}" CAPI_DIR="${pythonCapiDir}" KEY_GROUP="${cfg.python.apiKeyFileGroup}" # Create directory if it doesn't exist mkdir -p "$CAPI_DIR" # Generate unique machine ID based on hostname and a random component MACHINE_ID="python-$(hostname)-$(openssl rand -hex 4)" # Generate a secure password MACHINE_PASSWORD=$(openssl rand -base64 32) # Check if credentials already exist if [ -f "$CAPI_FILE" ]; then echo "CAPI credentials already exist at $CAPI_FILE" else # Create credentials file for python-capi-sdk cat > "$CAPI_FILE" << EOF # CrowdSec Central API Credentials for python-capi-sdk # Generated by NixOS infrastructure.crowdsec module # # Usage with python-capi-sdk: # from cscapi.client import CAPIClient, CAPIClientConfig # from cscapi.sql_storage import SQLStorage # from cscapi.utils import generate_machine_id_from_key # import yaml # # with open('${cfg.python.capiCredentialsFile}') as f: # creds = yaml.safe_load(f) # # client = CAPIClient( # storage=SQLStorage(connection_string="sqlite:///signals.db"), # config=CAPIClientConfig( # scenarios=${builtins.toJSON cfg.python.capiScenarios} # ) # ) # # # Note: Machine enrollment with CrowdSec CAPI requires manual approval # # Contact CrowdSec for signal sharing partnership details machine_id: "$MACHINE_ID" password: "$MACHINE_PASSWORD" scenarios: ${builtins.toJSON cfg.python.capiScenarios} capi_url: "https://api.crowdsec.net/" # Local API connection (for reading decisions) lapi_url: "http://${cfg.api.listenAddr}:${toString cfg.api.listenPort}/" EOF chmod 640 "$CAPI_FILE" chown root:"$KEY_GROUP" "$CAPI_FILE" echo "CAPI credentials generated at $CAPI_FILE" echo "" echo "NOTE: To share signals with CrowdSec CAPI, you need to:" echo "1. Contact CrowdSec for signal sharing partnership enrollment" echo "2. Use the machine_id from this file when enrolling" echo "3. Update your application to use the python-capi-sdk" fi ''; in { # Python bouncer registration service (oneshot - registers bouncer and stores API key) systemd.services.crowdsec-python-bouncer = { description = "CrowdSec Python Bouncer Registration"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" "crowdsec.service" ]; requires = [ "crowdsec.service" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; ExecStart = "${pythonBouncerRegisterScript}"; }; }; # Python CAPI credentials service (oneshot - generates CAPI credentials for signal sharing) systemd.services.crowdsec-python-capi = lib.mkIf cfg.python.enableCapi { description = "CrowdSec Python CAPI Credentials Generation"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" "crowdsec.service" "crowdsec-python-bouncer.service" ]; requires = [ "crowdsec.service" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; ExecStart = "${pythonCapiRegisterScript}"; }; }; } ); }