Skip to content

Instantly share code, notes, and snippets.

@Sly777
Created December 28, 2025 18:20
Show Gist options
  • Select an option

  • Save Sly777/0b612907800773445431d6d6ba045b12 to your computer and use it in GitHub Desktop.

Select an option

Save Sly777/0b612907800773445431d6d6ba045b12 to your computer and use it in GitHub Desktop.
Deploy CrowdSec Agent and prepare bouncer to Debian LXC Containers
#!/bin/bash
# =============================================================================
# Deploy CrowdSec Agent to Debian LXC Containers
# Registers with central LAPI on CrowdSec container (CT 172)
# Usage: ./deploy-crowdsec.sh <CT_ID> [--dry-run]
# ./deploy-crowdsec.sh all [--dry-run]
# =============================================================================
# =============================================================================
# CONFIGURATION - Edit these variables
# =============================================================================
CROWDSEC_LAPI_CT="0" # CrowdSec LAPI container ID
CROWDSEC_LAPI_IP="0.0.0.0" # CrowdSec LAPI IP address
CROWDSEC_LAPI_PORT="8080" # CrowdSec LAPI port
CROWDSEC_LAPI_URL="http://${CROWDSEC_LAPI_IP}:${CROWDSEC_LAPI_PORT}"
LOG_DIR="/var/log/crowdsec-deploy"
DATE=$(date +%Y%m%d-%H%M%S)
# Dry run mode
DRY_RUN=false
# Colors for terminal output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
NC='\033[0m'
# =============================================================================
# PARSE COMMAND LINE ARGUMENTS
# =============================================================================
show_usage() {
echo "Usage: $0 <CT_ID|all> [--dry-run]"
echo ""
echo "Arguments:"
echo " CT_ID Container ID to deploy CrowdSec to"
echo " all Deploy to all Debian containers"
echo ""
echo "Options:"
echo " --dry-run Show what would be done without making changes"
echo ""
echo "Examples:"
echo " $0 112 # Deploy CrowdSec to CT 112"
echo " $0 112 --dry-run # Preview deployment for CT 112"
echo " $0 all # Deploy to all containers"
echo " $0 all --dry-run # Preview deployment for all containers"
}
if [ $# -lt 1 ]; then
echo -e "${RED}Error: Container ID required${NC}"
echo ""
show_usage
exit 1
fi
TARGET=""
for arg in "$@"; do
case "$arg" in
--dry-run)
DRY_RUN=true
;;
--help|-h)
show_usage
exit 0
;;
*)
if [ -z "$TARGET" ]; then
TARGET="$arg"
else
echo -e "${RED}Error: Unknown argument '$arg'${NC}"
show_usage
exit 1
fi
;;
esac
done
# Validate target
if [ "$TARGET" != "all" ]; then
if ! [[ "$TARGET" =~ ^[0-9]+$ ]]; then
echo -e "${RED}Error: Container ID must be numeric or 'all'${NC}"
exit 1
fi
if ! pct list | awk '{print $1}' | grep -q "^${TARGET}$"; then
echo -e "${RED}Error: Container $TARGET does not exist${NC}"
echo ""
echo "Available containers:"
pct list
exit 1
fi
if [ "$TARGET" == "$CROWDSEC_LAPI_CT" ]; then
echo -e "${RED}Error: Cannot deploy agent to LAPI container (CT $CROWDSEC_LAPI_CT)${NC}"
exit 1
fi
fi
# =============================================================================
# LOGGING SETUP
# =============================================================================
mkdir -p "$LOG_DIR"
LOG_FILE="${LOG_DIR}/crowdsec-deploy-$(date +%Y%m%d).log"
log() {
local level="$1"
local message="$2"
local timestamp="[$(date '+%Y-%m-%d %H:%M:%S')]"
local prefix=""
if [ "$DRY_RUN" = true ] && [ "$level" != "INFO" ] && [ "$level" != "STEP" ]; then
prefix="[DRY-RUN] "
fi
echo "${timestamp} [${level}] ${prefix}${message}" >> "$LOG_FILE"
case "$level" in
"INFO") echo -e "${timestamp} ${GREEN}[${level}]${NC} ${prefix}${message}" ;;
"WARN") echo -e "${timestamp} ${YELLOW}[${level}]${NC} ${prefix}${message}" ;;
"ERROR") echo -e "${timestamp} ${RED}[${level}]${NC} ${prefix}${message}" ;;
"STEP") echo -e "${timestamp} ${CYAN}[${level}]${NC} ${prefix}${message}" ;;
"SUCCESS") echo -e "${timestamp} ${GREEN}[${level}]${NC} ${prefix}${message}" ;;
"DRY-RUN") echo -e "${timestamp} ${MAGENTA}[${level}]${NC} ${message}" ;;
*) echo -e "${timestamp} [${level}] ${prefix}${message}" ;;
esac
}
log_section() {
local title="$1"
echo "" >> "$LOG_FILE"
echo "========================================" >> "$LOG_FILE"
echo "$title" >> "$LOG_FILE"
echo "========================================" >> "$LOG_FILE"
echo ""
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}${title}${NC}"
echo -e "${BLUE}========================================${NC}"
}
# =============================================================================
# HELPER FUNCTIONS - FIXED CHECKS
# =============================================================================
# Check if CrowdSec is already installed in container
crowdsec_installed() {
local ct_id=$1
pct exec "$ct_id" -- test -f /usr/bin/cscli 2>/dev/null
}
# Check if machine is already registered on LAPI [3][5]
# Returns: 0 if registered, 1 if not
machine_registered() {
local machine_name=$1
# Get list of machines and check for exact match
local machines
machines=$(pct exec "$CROWDSEC_LAPI_CT" -- cscli machines list -o raw 2>/dev/null)
# Raw output format: name,ipAddress,lastUpdate,isValidated,version,authType
# Check if machine name appears at start of any line
echo "$machines" | grep -q "^${machine_name},"
return $?
}
# Check if machine is validated on LAPI [5][7]
# Returns: 0 if validated, 1 if not
machine_validated() {
local machine_name=$1
# Get the machine's line and check isValidated field (4th column)
local machine_line
machine_line=$(pct exec "$CROWDSEC_LAPI_CT" -- cscli machines list -o raw 2>/dev/null | grep "^${machine_name},")
if [ -z "$machine_line" ]; then
return 1 # Machine not found
fi
# Extract isValidated field (4th column, comma-separated)
local is_validated
is_validated=$(echo "$machine_line" | cut -d',' -f4)
if [ "$is_validated" = "true" ]; then
return 0
else
return 1
fi
}
# Check if bouncer already exists on LAPI [2][4]
# Returns: 0 if exists, 1 if not
bouncer_exists() {
local bouncer_name=$1
# Get list of bouncers and check for exact match
local bouncers
bouncers=$(pct exec "$CROWDSEC_LAPI_CT" -- cscli bouncers list -o raw 2>/dev/null)
# Raw output format: name,ipAddress,isValid,lastPull,type,version,authType
# Check if bouncer name appears at start of any line
echo "$bouncers" | grep -q "^${bouncer_name},"
return $?
}
# Get container name (lowercase)
get_container_name() {
local ct_id=$1
pct list | awk -v id="$ct_id" '$1==id {print tolower($3)}'
}
# Disable local LAPI using sed
disable_local_lapi() {
local ct_id=$1
local config_file="/etc/crowdsec/config.yaml"
if [ "$DRY_RUN" = true ]; then
log "DRY-RUN" "Would modify $config_file to set api.server.enable: false"
return 0
fi
# Check if 'enable:' exists under api.server section
if pct exec "$ct_id" -- grep -A10 "^api:" "$config_file" 2>/dev/null | grep -A5 "server:" | grep -q "enable:"; then
# Update existing enable value to false
pct exec "$ct_id" -- sed -i '/^api:/,/^[^ ]/{
/server:/,/^ [^ ]/{
s/\(enable:\).*/\1 false/
}
}' "$config_file"
else
# Add enable: false after server: line within api section
pct exec "$ct_id" -- sed -i '/^ server:/a\ enable: false' "$config_file"
fi
}
# =============================================================================
# MAIN SCRIPT START
# =============================================================================
if [ "$DRY_RUN" = true ]; then
log_section "CrowdSec Deployment [DRY-RUN MODE]"
log "WARN" "DRY-RUN: No changes will be made"
else
log_section "CrowdSec Agent Deployment"
fi
log "INFO" "Date: $DATE"
log "INFO" "Target: $TARGET"
log "INFO" "LAPI URL: $CROWDSEC_LAPI_URL"
log "INFO" "Log file: $LOG_FILE"
logger -t crowdsec-deploy "Starting CrowdSec deployment (target: $TARGET, dry-run: $DRY_RUN)"
# =============================================================================
# PRE-FLIGHT CHECKS
# =============================================================================
log "STEP" "Pre-flight checks"
LAPI_STATUS=$(pct status "$CROWDSEC_LAPI_CT" 2>/dev/null | awk '{print $2}')
if [ "$LAPI_STATUS" != "running" ]; then
log "ERROR" "CrowdSec LAPI container (CT $CROWDSEC_LAPI_CT) is not running!"
exit 1
fi
log "SUCCESS" "LAPI container (CT $CROWDSEC_LAPI_CT) is running"
if ! pct exec "$CROWDSEC_LAPI_CT" -- cscli machines list > /dev/null 2>&1; then
log "ERROR" "Cannot access CrowdSec LAPI on CT $CROWDSEC_LAPI_CT"
exit 1
fi
log "SUCCESS" "LAPI is accessible"
# Show current LAPI state
log "INFO" "Current machines on LAPI:"
pct exec "$CROWDSEC_LAPI_CT" -- cscli machines list 2>/dev/null | head -10 | while read line; do
log "INFO" " $line"
done
log "INFO" "Current bouncers on LAPI:"
pct exec "$CROWDSEC_LAPI_CT" -- cscli bouncers list 2>/dev/null | head -10 | while read line; do
log "INFO" " $line"
done
# =============================================================================
# BUILD CONTAINER LIST
# =============================================================================
if [ "$TARGET" == "all" ]; then
containers=$(pct list | tail -n +2 | awk '{print $1}' | grep -v "^${CROWDSEC_LAPI_CT}$")
else
containers="$TARGET"
fi
# Counters
TOTAL=0
INSTALLED=0
SKIPPED=0
ERRORS=0
# =============================================================================
# MAIN DEPLOYMENT LOOP
# =============================================================================
for CT_ID in $containers; do
TOTAL=$((TOTAL + 1))
CT_STATUS=$(pct status "$CT_ID" | awk '{print $2}')
CT_NAME=$(get_container_name "$CT_ID")
MACHINE_NAME="$CT_NAME"
BOUNCER_NAME="${CT_NAME}-bouncer"
log_section "CT $CT_ID ($CT_NAME)"
log "INFO" "Status: $CT_STATUS"
log "INFO" "Machine name: $MACHINE_NAME"
log "INFO" "Bouncer name: $BOUNCER_NAME"
# Skip stopped containers
if [ "$CT_STATUS" != "running" ]; then
log "WARN" "Container not running, skipping"
SKIPPED=$((SKIPPED + 1))
continue
fi
# Skip non-Debian containers
if ! pct exec "$CT_ID" -- test -f /usr/bin/dpkg 2>/dev/null; then
log "WARN" "Not Debian-based, skipping"
SKIPPED=$((SKIPPED + 1))
continue
fi
# =========================================================================
# STEP 1: Install CrowdSec if not present
# =========================================================================
log "STEP" "Step 1: Checking CrowdSec installation"
if crowdsec_installed "$CT_ID"; then
log "INFO" "CrowdSec already installed, skipping installation"
else
log "INFO" "Installing CrowdSec..."
if [ "$DRY_RUN" = true ]; then
log "DRY-RUN" "Would run: curl -s https://install.crowdsec.net | bash"
log "DRY-RUN" "Would run: apt-get install -y crowdsec nftables crowdsec-firewall-bouncer-nftables"
else
# Add CrowdSec repository
if ! pct exec "$CT_ID" -- bash -c "curl -s https://install.crowdsec.net | bash" 2>&1; then
log "ERROR" "Failed to add CrowdSec repository"
ERRORS=$((ERRORS + 1))
continue
fi
# Update apt
pct exec "$CT_ID" -- apt-get update -qq 2>&1
# Install packages
if ! pct exec "$CT_ID" -- apt-get install -y -qq crowdsec nftables crowdsec-firewall-bouncer-nftables 2>&1; then
log "ERROR" "Failed to install CrowdSec packages"
ERRORS=$((ERRORS + 1))
continue
fi
fi
log "SUCCESS" "CrowdSec installed"
fi
# =========================================================================
# STEP 2: Register machine with LAPI (WITH CHECK)
# =========================================================================
log "STEP" "Step 2: Checking machine registration"
if [ "$DRY_RUN" = true ]; then
log "DRY-RUN" "Would check if machine '$MACHINE_NAME' is registered"
log "DRY-RUN" "Would run: cscli lapi register --machine $MACHINE_NAME --url $CROWDSEC_LAPI_URL"
else
if machine_registered "$MACHINE_NAME"; then
log "INFO" "Machine '$MACHINE_NAME' already registered, skipping registration"
else
log "INFO" "Registering machine '$MACHINE_NAME'..."
if ! pct exec "$CT_ID" -- cscli lapi register --machine "$MACHINE_NAME" --url "$CROWDSEC_LAPI_URL" 2>&1; then
log "ERROR" "Failed to register machine"
ERRORS=$((ERRORS + 1))
continue
fi
log "SUCCESS" "Machine registered"
fi
fi
# =========================================================================
# STEP 3: Disable local LAPI (set api.server.enable: false)
# =========================================================================
log "STEP" "Step 3: Disabling local LAPI server"
if [ "$DRY_RUN" = true ]; then
log "DRY-RUN" "Would backup config.yaml"
log "DRY-RUN" "Would set api.server.enable: false"
else
# Backup config
pct exec "$CT_ID" -- cp /etc/crowdsec/config.yaml "/etc/crowdsec/config.yaml.backup.$DATE" 2>/dev/null
# Check current state
CURRENT_ENABLE=$(pct exec "$CT_ID" -- grep -A10 "^api:" /etc/crowdsec/config.yaml 2>/dev/null | grep -A5 "server:" | grep "enable:" | head -1 | awk '{print $2}')
if [ "$CURRENT_ENABLE" == "false" ]; then
log "INFO" "Local LAPI already disabled, skipping"
else
log "INFO" "Updating config.yaml to disable local LAPI..."
disable_local_lapi "$CT_ID"
log "SUCCESS" "Local LAPI disabled"
fi
fi
# =========================================================================
# STEP 4: Validate machine on LAPI container (WITH CHECK)
# =========================================================================
log "STEP" "Step 4: Checking machine validation"
if [ "$DRY_RUN" = true ]; then
log "DRY-RUN" "Would check if machine '$MACHINE_NAME' is validated"
log "DRY-RUN" "Would run on LAPI: cscli machines validate $MACHINE_NAME"
else
if machine_validated "$MACHINE_NAME"; then
log "INFO" "Machine '$MACHINE_NAME' already validated, skipping validation"
else
log "INFO" "Validating machine '$MACHINE_NAME'..."
if ! pct exec "$CROWDSEC_LAPI_CT" -- cscli machines validate "$MACHINE_NAME" 2>&1; then
log "ERROR" "Failed to validate machine"
ERRORS=$((ERRORS + 1))
continue
fi
log "SUCCESS" "Machine validated"
fi
fi
# =========================================================================
# STEP 5: Restart CrowdSec on target container
# =========================================================================
log "STEP" "Step 5: Restarting CrowdSec service"
if [ "$DRY_RUN" = true ]; then
log "DRY-RUN" "Would restart crowdsec service"
else
if ! pct exec "$CT_ID" -- systemctl restart crowdsec 2>&1; then
log "ERROR" "Failed to restart CrowdSec"
ERRORS=$((ERRORS + 1))
continue
fi
sleep 2
if pct exec "$CT_ID" -- systemctl is-active crowdsec > /dev/null 2>&1; then
log "SUCCESS" "CrowdSec service running"
else
log "ERROR" "CrowdSec service failed to start"
ERRORS=$((ERRORS + 1))
continue
fi
fi
# =========================================================================
# STEP 6: Create bouncer on LAPI (WITH CHECK)
# =========================================================================
log "STEP" "Step 6: Checking firewall bouncer"
BOUNCER_ALREADY_EXISTS=false
if [ "$DRY_RUN" = true ]; then
log "DRY-RUN" "Would check if bouncer '$BOUNCER_NAME' exists"
log "DRY-RUN" "Would run on LAPI: cscli bouncers add $BOUNCER_NAME"
else
if bouncer_exists "$BOUNCER_NAME"; then
log "INFO" "Bouncer '$BOUNCER_NAME' already exists, skipping creation"
log "WARN" "Cannot retrieve existing API key - if bouncer not working:"
log "INFO" " 1. Delete: pct exec $CROWDSEC_LAPI_CT -- cscli bouncers delete $BOUNCER_NAME"
log "INFO" " 2. Re-run this script"
BOUNCER_ALREADY_EXISTS=true
else
log "INFO" "Creating bouncer '$BOUNCER_NAME'..."
# Create bouncer and capture API key
API_KEY=$(pct exec "$CROWDSEC_LAPI_CT" -- cscli bouncers add "$BOUNCER_NAME" -o raw 2>/dev/null | tail -1)
if [ -z "$API_KEY" ]; then
log "ERROR" "Failed to create bouncer or retrieve API key"
ERRORS=$((ERRORS + 1))
continue
fi
log "SUCCESS" "Bouncer created"
log "INFO" "API Key: $API_KEY"
fi
fi
# =========================================================================
# STEP 7: Configure bouncer on target container (only if newly created)
# =========================================================================
if [ "$BOUNCER_ALREADY_EXISTS" = false ]; then
log "STEP" "Step 7: Configuring firewall bouncer"
BOUNCER_CONFIG="/etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml"
if [ "$DRY_RUN" = true ]; then
log "DRY-RUN" "Would update $BOUNCER_CONFIG with:"
log "DRY-RUN" " api_url: ${CROWDSEC_LAPI_URL}/"
log "DRY-RUN" " api_key: <generated-key>"
else
# Backup existing config
pct exec "$CT_ID" -- cp "$BOUNCER_CONFIG" "${BOUNCER_CONFIG}.backup.$DATE" 2>/dev/null
# Update api_url and api_key using sed
pct exec "$CT_ID" -- sed -i "s|^api_url:.*|api_url: ${CROWDSEC_LAPI_URL}/|" "$BOUNCER_CONFIG"
pct exec "$CT_ID" -- sed -i "s|^api_key:.*|api_key: ${API_KEY}|" "$BOUNCER_CONFIG"
log "SUCCESS" "Bouncer configured"
fi
# =====================================================================
# STEP 8: Restart bouncer service
# =====================================================================
log "STEP" "Step 8: Restarting firewall bouncer"
if [ "$DRY_RUN" = true ]; then
log "DRY-RUN" "Would restart crowdsec-firewall-bouncer service"
else
if ! pct exec "$CT_ID" -- systemctl restart crowdsec-firewall-bouncer 2>&1; then
log "ERROR" "Failed to restart firewall bouncer"
ERRORS=$((ERRORS + 1))
continue
fi
sleep 2
if pct exec "$CT_ID" -- systemctl is-active crowdsec-firewall-bouncer > /dev/null 2>&1; then
log "SUCCESS" "Firewall bouncer running"
else
log "ERROR" "Firewall bouncer failed to start"
ERRORS=$((ERRORS + 1))
continue
fi
fi
else
log "INFO" "Skipping bouncer configuration (already exists)"
fi
# =========================================================================
# VERIFICATION
# =========================================================================
log "STEP" "Verification"
if [ "$DRY_RUN" = true ]; then
log "DRY-RUN" "Would verify CrowdSec and bouncer services are active"
INSTALLED=$((INSTALLED + 1))
else
CS_STATUS=$(pct exec "$CT_ID" -- systemctl is-active crowdsec 2>/dev/null)
FB_STATUS=$(pct exec "$CT_ID" -- systemctl is-active crowdsec-firewall-bouncer 2>/dev/null)
if [ "$CS_STATUS" == "active" ] && [ "$FB_STATUS" == "active" ]; then
log "SUCCESS" "✓ CrowdSec agent: active"
log "SUCCESS" "✓ Firewall bouncer: active"
log "SUCCESS" "CT $CT_ID ($CT_NAME) fully configured!"
INSTALLED=$((INSTALLED + 1))
else
log "ERROR" "Services not running properly (crowdsec: $CS_STATUS, bouncer: $FB_STATUS)"
ERRORS=$((ERRORS + 1))
fi
fi
done
# =============================================================================
# DEPLOYMENT SUMMARY
# =============================================================================
log_section "Deployment Summary"
if [ "$DRY_RUN" = true ]; then
log "WARN" "DRY-RUN MODE - No changes were made"
fi
log "INFO" "Total containers: $TOTAL"
if [ "$DRY_RUN" = true ]; then
log "INFO" "Would setup: $INSTALLED"
else
log "INFO" "Successfully setup: $INSTALLED"
fi
log "INFO" "Skipped: $SKIPPED"
if [ "$ERRORS" -gt 0 ]; then
log "ERROR" "Errors: $ERRORS"
else
log "SUCCESS" "Errors: $ERRORS"
fi
log "INFO" ""
log "INFO" "LAPI Container: CT $CROWDSEC_LAPI_CT ($CROWDSEC_LAPI_IP)"
log "INFO" "Log saved to: $LOG_FILE"
log "INFO" ""
log "INFO" "Verify on LAPI container:"
log "INFO" " pct exec $CROWDSEC_LAPI_CT -- cscli machines list"
log "INFO" " pct exec $CROWDSEC_LAPI_CT -- cscli bouncers list"
logger -t crowdsec-deploy "Completed: $INSTALLED installed, $SKIPPED skipped, $ERRORS errors (dry-run: $DRY_RUN)"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment