Files
k8scd/nix-infra-machine/app_modules/_unstable/crowdsec/bouncers/python.nix
T
2026-03-15 18:31:53 +01:00

314 lines
12 KiB
Nix

# 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}";
};
};
}
);
}