#!/bin/bash
# services/crowdsec.sh — CrowdSec intrusion prevention (fail2ban successor).
# Part of the modular post-install system (sourced by setup.sh).
#
# Can also be run standalone on any machine:
#   sudo bash crowdsec.sh
# (Docker must already be installed when run standalone)
#
# CrowdSec is a SYSTEM install (apt repo + agent), NOT a docker-compose service:
#   • Installs the CrowdSec agent and the iptables firewall bouncer (enforces bans).
#   • Installs detection collections for SSH, Linux, Caddy and base HTTP scenarios.
#   • Reads Caddy's JSON access logs (/var/log/caddy/*.log) to spot attacks.
#   • Optionally pushes ban alerts to an ntfy topic.
#   • Adds community IP reputation + optional geo-enrichment on top.
#
# There is no ~/docker/crowdsec compose; we only create a docs-only folder there
# with a README pointing at the real config under /etc/crowdsec.

# ── Standalone bootstrap ──────────────────────────────────────────────────────
# Detected when the script is executed directly rather than sourced by setup.sh.
# Sets up helpers and globals, then defers execution until after the function
# definition at the bottom of this file.
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    [[ "$(id -u)" == "0" ]] || { echo "Run with sudo: sudo bash $0"; exit 1; }

    _SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    _COMMON="$_SELF_DIR/../lib/common.sh"

    if [[ -f "$_COMMON" ]]; then
        # Full repo present — use the real helpers (picks up ~/docker/.config too)
        # shellcheck source=../lib/common.sh
        source "$_COMMON"
    else
        # One-off copy — inline minimal stubs so the script works without the repo
        log_info()    { echo -e "\033[0;34m[INFO]\033[0m $*"; }
        log_success() { echo -e "\033[0;32m[OK]\033[0m $*"; }
        log_warning() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
        log_error()   { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }

        ensure_docker_dir_ownership() {
            chown -R "$ACTUAL_USER:$ACTUAL_USER" "$@" 2>/dev/null || true
        }

        # Match common.sh's eval-based pattern so local vars in install_* are set correctly
        prompt_text() {
            local _q="$1" _def="$2" _var="$3" _r
            [[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
            read -r -p "  $_q " _r
            eval "$_var='${_r:-$_def}'"
        }

        prompt_yn() {
            local _q="$1" _def="$2" _var="$3" _r
            [[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
            read -r -p "  $_q " _r
            eval "$_var='${_r:-$_def}'"
        }

        write_readme() {
            local _dir="$1"; shift
            mkdir -p "$_dir"
            cat > "$_dir/README.md"
        }
    fi

    # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR
    # ($HOME under sudo is /root, not the real user's home)
    ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}"
    ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "${HOME:-/root}")"
    DOCKER_DIR="${DOCKER_DIR:-$ACTUAL_HOME/docker}"
    DRY_RUN="${DRY_RUN:-false}"
    UNATTENDED="${UNATTENDED:-false}"
    SITE_TZ="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}"
    SITE_DOMAIN="${SITE_DOMAIN:-example.com}"
    SITE_CADDY_NET="${SITE_CADDY_NET:-caddy_net}"
    CADDY_REMOTE_HOST="${CADDY_REMOTE_HOST:-}"

    register_service() { :; }   # no-op — no wizard to register into
    _RUN_STANDALONE=1
fi
# ─────────────────────────────────────────────────────────────────────────────

register_service crowdsec homelab "Intrusion prevention: bans + geo + IP reputation (CrowdSec)"

install_crowdsec() {
    log_info "Installing CrowdSec intrusion prevention..."

    local DOCS_DIR="$DOCKER_DIR/crowdsec"

    echo ""
    echo "┌─────────────────────────────────────────────────────────────────┐"
    echo "│ CROWDSEC - Intrusion Prevention (fail2ban successor)            │"
    echo "│ Bans malicious IPs + geo-blocking + community IP reputation     │"
    echo "│ Protects SSH, Caddy, and other services                         │"
    echo "└─────────────────────────────────────────────────────────────────┘"
    echo ""

    # ── DRY-RUN: describe the plan and bail before touching anything real ────
    if [ "$DRY_RUN" = true ]; then
        echo "[DRY-RUN] Would install the CrowdSec agent (curl https://install.crowdsec.net | sh; apt install crowdsec)"
        echo "[DRY-RUN] Would install the firewall bouncer (crowdsec-firewall-bouncer-iptables)"
        echo "[DRY-RUN] Would ensure /var/log/caddy exists for log acquisition"
        echo "[DRY-RUN] Would install collections: sshd, linux, caddy, base-http-scenarios"
        echo "[DRY-RUN] Would write Caddy acquisition /etc/crowdsec/acquis.d/caddy.yaml"
        echo "[DRY-RUN] Would install crowdsecurity/asterisk + write an acquisition if asterisk is installed"
        echo "[DRY-RUN] Would optionally wire ntfy ban alerts into the default profile"
        echo "[DRY-RUN] Would optionally register with a remote/central LAPI and disable the local one"
        echo "[DRY-RUN] Would enable + restart crowdsec and crowdsec-firewall-bouncer"
        echo "[DRY-RUN] Would write $DOCS_DIR/README.md (docs-only folder)"
        return 0
    fi

    # ── Existing install? Offer update-in-place instead of a full reconfigure ─
    # Everything below (ASN exemptions, geo-allowlist, ntfy, remote LAPI) is
    # additive/idempotent at the file level, but there was no gate at all
    # here before — every rerun re-asked all four optional questions
    # unconditionally, which reads as "reconfigure" rather than "update" even
    # though nothing already-configured was actually being destroyed.
    local _REMOTE_LAPI_PENDING=""
    local ASK_OPTIONAL=true
    if command -v cscli &> /dev/null; then
        echo "  CrowdSec is already installed."
        local CS_MODE=""
        prompt_reinstall_mode CS_MODE 2>/dev/null || {
            # prompt_reinstall_mode isn't defined in the standalone stub — fall
            # back to a plain yes/no when run outside the full repo.
            local _r=""
            prompt_yn "  Reconfigure the optional settings below (ASN exemptions, geo-allowlist, ntfy, remote LAPI)? (y/n):" "n" _r
            [ "$_r" = "y" ] || [ "$_r" = "Y" ] && CS_MODE="fresh" || CS_MODE="update"
        }
        case "$CS_MODE" in
            update)
                ASK_OPTIONAL=false
                log_info "Refreshing agent/collections/acquisitions only — ASN exemptions,"
                log_info "geo-allowlist, ntfy alerts, and remote-LAPI settings are left exactly as"
                log_info "they are. Choose 'fresh' instead to revisit any of those."
                ;;
            cancel)
                log_info "Leaving the existing CrowdSec install as-is."
                return 0
                ;;
            fresh)
                log_info "Proceeding with a full reconfigure — every optional prompt below runs again."
                ;;
        esac
    fi

    # ── 1. Install the CrowdSec agent ────────────────────────────────────────
    if command -v cscli &> /dev/null; then
        echo "  ✓ CrowdSec is already installed"
    else
        echo "  Adding CrowdSec repository and installing agent..."
        if curl -s https://install.crowdsec.net | sudo sh && sudo apt install -y crowdsec; then
            echo "  ✓ CrowdSec installed successfully"
        else
            echo "  ⚠ Failed to install CrowdSec"
            echo "  See https://docs.crowdsec.net/ for manual installation"
        fi
    fi

    # ── 2. Firewall bouncer (enforces bans via iptables/nftables) ────────────
    echo "  Installing firewall bouncer..."
    sudo apt install -y crowdsec-firewall-bouncer-iptables 2>/dev/null || \
        echo "  ⚠ Could not install firewall bouncer automatically"

    # ── 3. Create log directory for Caddy ────────────────────────────────────
    if [ ! -d "/var/log/caddy" ]; then
        sudo mkdir -p /var/log/caddy
        sudo chmod 755 /var/log/caddy
        echo "  ✓ Created /var/log/caddy directory"
    fi

    # ── 4. Detection collections: SSH, Caddy HTTP scenarios, base http ───────
    echo "  Installing CrowdSec collections (sshd, caddy, base-http)..."
    sudo cscli collections install crowdsecurity/sshd crowdsecurity/linux crowdsecurity/caddy crowdsecurity/base-http-scenarios 2>/dev/null || \
        echo "  ⚠ Some collections may already be installed"

    # ── 5. Tell CrowdSec to read Caddy's JSON access logs ────────────────────
    local ACQUIS_FILE="/etc/crowdsec/acquis.d/caddy.yaml"
    if [ ! -f "$ACQUIS_FILE" ]; then
        echo "  Creating Caddy log acquisition for CrowdSec..."
        sudo mkdir -p /etc/crowdsec/acquis.d
        local ACQUIS_CONTENT='filenames:
  - /var/log/caddy/*.log
  - /var/log/caddy/*-access.log
labels:
  type: caddy'
        if echo "$ACQUIS_CONTENT" | sudo tee "$ACQUIS_FILE" > /dev/null; then
            echo "  ✓ Created Caddy acquisition ($ACQUIS_FILE)"
        else
            echo "  ⚠ Failed to create acquisition - create it manually"
        fi
    else
        echo "  ✓ Caddy acquisition already exists"
    fi

    # ── 5b. SIP brute-force/enumeration protection, if Asterisk is installed.
    # services/asterisk.sh patches Asterisk to log security events — auth
    # failures, registration scanning — to $EA_DIR/logs/full, which is what
    # the acquisition below tails. Both directories are probed: a box set up
    # before the droplet edition was merged back into `asterisk` still runs
    # out of ~/docker/asterisk-digital-ocean. The logging patch used to be
    # droplet-only; it now applies to every install, so a home/LAN box gets
    # SIP protection here too.
    local ASTERISK_LOG_DIR=""
    local _ea_candidate
    for _ea_candidate in "$DOCKER_DIR/asterisk-digital-ocean" "$DOCKER_DIR/asterisk"; do
        [ -d "$_ea_candidate/logs" ] && { ASTERISK_LOG_DIR="$_ea_candidate/logs"; break; }
    done
    if [ -n "$ASTERISK_LOG_DIR" ]; then
        echo "  Detected Asterisk at ${ASTERISK_LOG_DIR%/logs} — installing SIP brute-force/enumeration protection..."
        sudo cscli collections install crowdsecurity/asterisk 2>/dev/null || \
            echo "  ⚠ crowdsecurity/asterisk collection may already be installed"

        # Filename kept as-is so a droplet that already has this acquisition
        # isn't given a second one pointing at the same log.
        local ASTERISK_ACQUIS="/etc/crowdsec/acquis.d/asterisk-digital-ocean.yaml"
        if [ ! -f "$ASTERISK_ACQUIS" ]; then
            local ASTERISK_ACQUIS_CONTENT="filenames:
  - $ASTERISK_LOG_DIR/full
  - $ASTERISK_LOG_DIR/full.*
labels:
  type: asterisk"
            if echo "$ASTERISK_ACQUIS_CONTENT" | sudo tee "$ASTERISK_ACQUIS" > /dev/null; then
                echo "  ✓ Created Asterisk acquisition ($ASTERISK_ACQUIS)"
            else
                echo "  ⚠ Failed to create Asterisk acquisition - create it manually"
            fi
        else
            echo "  ✓ Asterisk acquisition already exists"
        fi

        # ── 5c. Optional: exempt specific carrier ASNs from Asterisk
        # brute-force bans only (SSH/web/geo protection stays normal). Confirmed
        # live: a phone on a CGNAT mobile/satellite carrier (Starlink, T-Mobile
        # home internet) shares one public IP across many customers and rotates
        # it — a device roaming WiFi<->mobile can get banned for someone else's
        # brute-force attempt on that same shared IP, or for its own
        # re-registration burst looking like one. This forks
        # crowdsecurity/asterisk_bf and crowdsecurity/asterisk_user_enum
        # locally with an ASN exclusion added to their filter, then disables
        # the hub originals so there's no double-processing of the same events.
        echo ""
        local ASN_EXEMPT=""
        if [ "$ASK_OPTIONAL" = true ]; then
            prompt_yn "Exempt specific carrier ASNs from Asterisk brute-force bans only? (y/n):" "n" ASN_EXEMPT
        fi
        if [ "$ASN_EXEMPT" = "y" ] || [ "$ASN_EXEMPT" = "Y" ]; then
            sudo cscli collections install crowdsecurity/geoip-enrich 2>/dev/null || true
            echo "  ASNs observed live: T-Mobile 21928, Starlink 14593. A carrier can operate"
            echo "  more than one ASN — if the same carrier bans you again later under a"
            echo "  different number, check the AS column in 'cscli decisions list' and add it."
            local ASN_LIST=""
            prompt_text "  ASN numbers to exempt, space-separated:" "21928 14593" ASN_LIST
            if [ -n "$ASN_LIST" ]; then
                # ASNNumber is a string field internally (like IsoCode in the
                # geo-allowlist scenario above) even though it prints as a bare
                # number in cscli/explain output. Confirmed live: unquoted
                # integer literals here made CrowdSec fatal-crash-loop at
                # startup with "cannot use string as type int in array" —
                # every ASN must be quoted as a string to match.
                local _asn_expr
                _asn_expr="$(printf "'%s', " $ASN_LIST)"
                _asn_expr="${_asn_expr%, }"

                sudo mkdir -p /etc/crowdsec/scenarios
                sudo tee /etc/crowdsec/scenarios/local-asterisk_bf.yaml > /dev/null << ASTBF
type: leaky
name: local/asterisk_bf
description: "Detect Asterisk user bruteforce (ASN-exempt fork of crowdsecurity/asterisk_bf)"
filter: "evt.Meta.log_type == 'asterisk_failed_auth' && !(evt.Enriched.ASNNumber in [${_asn_expr}])"
groupby: evt.Meta.source_ip
leakspeed: 10s
capacity: 5
blackhole: 1m
labels:
  service: asterisk
  confidence: 3
  spoofable: 0
  classification:
    - attack.T1110
  behavior: "sip:bruteforce"
  label: "Asterisk Bruteforce"
  remediation: true
ASTBF

                sudo tee /etc/crowdsec/scenarios/local-asterisk_user_enum.yaml > /dev/null << ASTENUM
type: leaky
name: local/asterisk_user_enum
description: "Detect Asterisk user enumeration bruteforce (ASN-exempt fork of crowdsecurity/asterisk_user_enum)"
filter: "evt.Meta.log_type == 'asterisk_failed_auth' && !(evt.Enriched.ASNNumber in [${_asn_expr}])"
groupby: evt.Meta.source_ip
distinct: evt.Meta.target_user
leakspeed: 10s
capacity: 5
blackhole: 1m
labels:
  service: asterisk
  confidence: 3
  spoofable: 0
  classification:
    - attack.T1087
    - attack.T1589.001
    - attack.T1110
  behavior: "sip:bruteforce"
  label: "Asterisk User Enumeration"
  remediation: true
ASTENUM

                # Disable the hub originals so they don't double-process the
                # same events alongside the ASN-exempt forks written above.
                # --force is required: these scenarios came in as part of the
                # crowdsecurity/asterisk collection, and cscli refuses to
                # remove/disable a collection member without it. Confirmed
                # live: without --force this failed silently (stderr
                # suppressed, "|| true" swallowed the non-zero exit), leaving
                # the un-exempted hub original running side-by-side with the
                # ASN-exempt fork the entire time — the fork's exemption
                # never actually took effect for anyone, since the original
                # scenario kept independently banning the same traffic with
                # no ASN awareness at all.
                if sudo cscli scenarios remove crowdsecurity/asterisk_bf crowdsecurity/asterisk_user_enum --force 2>/dev/null; then
                    echo "  ✓ Wrote ASN-exempt local forks; disabled the hub originals"
                else
                    log_warning "Failed to disable the hub-original asterisk_bf/asterisk_user_enum scenarios —"
                    log_warning "the ASN exemption below will NOT take effect until this is resolved. Run:"
                    log_warning "  sudo cscli scenarios remove crowdsecurity/asterisk_bf crowdsecurity/asterisk_user_enum --force"
                    log_warning "  sudo systemctl restart crowdsec"
                fi
                echo "  ℹ Exempted ASNs: $ASN_LIST — SSH/web/geo-allowlist scenarios are unaffected"
                echo "  ℹ Edit /etc/crowdsec/scenarios/local-asterisk_*.yaml to add/remove ASNs later"
                echo "    (then: sudo systemctl restart crowdsec)"
            fi
        fi
    fi

    # ── 6. Geo-blocking + reputation (the capability fail2ban/Authelia lack) ─
    echo ""
    echo "  Geo-blocking & IP reputation (optional):"
    echo "    Subscribe to community/3rd-party blocklists at:"
    echo "      https://app.crowdsec.net/"
    echo ""
    local GEO_ALLOWLIST=""
    if [ "$ASK_OPTIONAL" = true ]; then
        prompt_yn "Restrict Caddy-fronted web traffic to North America + Europe only (block every other country)? Does NOT affect SSH. (y/n):" "n" GEO_ALLOWLIST
    fi
    if [ "$GEO_ALLOWLIST" = "y" ] || [ "$GEO_ALLOWLIST" = "Y" ]; then
        echo "  Installing geoip-enrich (tags every event with a country code; no MaxMind"
        echo "  account needed — CrowdSec bundles its own redistributable GeoLite2 data)..."
        sudo cscli collections install crowdsecurity/geoip-enrich 2>/dev/null || \
            echo "  ⚠ crowdsecurity/geoip-enrich may already be installed"

        # North America + Europe, ISO 3166-1 alpha-2. Russia, Belarus, and
        # Turkey are left out (common abuse-traffic sources, and not really
        # "Europe" in the sense meant here). Core Eastern Europe (Bulgaria,
        # Czechia, Hungary, Moldova, Poland, Romania, Slovakia, Ukraine) is
        # left out too — the Balkans and Baltics (Albania, Bosnia, Croatia,
        # Estonia, Latvia, Lithuania, Montenegro, North Macedonia, Serbia,
        # Slovenia) are kept in deliberately, since several of them (Estonia
        # especially) don't fit the same risk profile despite the old
        # Cold-War grouping. To adjust, edit the two lists below and re-run,
        # or hand-edit $GEO_SCENARIO directly (it's plain YAML — no reinstall
        # needed, just restart crowdsec after).
        local _GEO_NA="US CA MX"
        local _GEO_EU="AD AL AT BA BE CH CY DE DK EE ES FI FR GB GR HR IE IS IT LI LT LU LV MC ME MK MT NL NO PT RS SE SI SM VA"
        local _geo_expr_list
        _geo_expr_list="$(printf "'%s', " $_GEO_NA $_GEO_EU)"
        _geo_expr_list="${_geo_expr_list%, }"

        # Scope: evt.Line.Labels.type is the acquisition-level "labels: type:"
        # field set in acquis.d/caddy.yaml (section 5 above) — this only ever
        # matches Caddy-sourced events, never SSH/syslog, so a mistake here
        # can't lock out the session running this installer.
        sudo mkdir -p /etc/crowdsec/scenarios
        local GEO_SCENARIO="/etc/crowdsec/scenarios/geo-allowlist-web.yaml"
        local GEO_SCENARIO_CONTENT="type: trigger
name: local/geo-allowlist-web
description: \"Block Caddy-fronted web requests from outside the allowed country list\"
filter: \"evt.Line.Labels.type == 'caddy' && evt.Enriched.IsoCode != '' && !(evt.Enriched.IsoCode in [${_geo_expr_list}])\"
groupby: evt.Meta.source_ip
blackhole: 1m
labels:
  service: http
  type: geo_allowlist
  remediation: true"
        if echo "$GEO_SCENARIO_CONTENT" | sudo tee "$GEO_SCENARIO" > /dev/null; then
            echo "  ✓ Geo-allowlist scenario written ($GEO_SCENARIO)"
            echo "  ⚠ This blocks ALL web visitors outside the allowed countries, including"
            echo "    Let's Encrypt's out-of-region validation checks (deliberately used to"
            echo "    prevent BGP-hijacking attacks) — if a cert renewal ever fails"
            echo "    mysteriously, check this scenario first."
            echo "  ℹ Allowed: $_GEO_NA $_GEO_EU"
            echo "  ℹ After restarting, verify it actually loaded (no typo/syntax issue):"
            echo "      sudo cscli metrics | grep geo-allowlist"
            echo "      sudo systemctl status crowdsec   # should stay 'active', not restart-looping"
        else
            echo "  ⚠ Failed to write geo-allowlist scenario — create it manually"
        fi
    fi

    # ── 7. Optional: push ban alerts to ntfy ─────────────────────────────────
    echo ""
    local CS_NTFY=""
    if [ "$ASK_OPTIONAL" = true ]; then
        prompt_yn "Send CrowdSec ban alerts to an ntfy topic? (y/n):" "n" CS_NTFY
    fi
    if [ "$CS_NTFY" = "y" ] || [ "$CS_NTFY" = "Y" ]; then
        # Prefer a locally-installed ntfy's own base-url as the default, if one
        # exists and actually looks configured (not still the placeholder
        # domain ntfy.sh writes when no SITE_DOMAIN was set at its own install
        # time). Otherwise, nudge toward a hosted instance elsewhere (e.g. a
        # homelab) instead of silently defaulting to the public ntfy.sh.
        local _ntfy_default="https://ntfy.sh/crowdsec-alerts"
        if [ -f "$DOCKER_DIR/ntfy/config/server.yml" ]; then
            local _local_base_url
            _local_base_url="$(grep -oP '(?<=base-url: ")[^"]+' "$DOCKER_DIR/ntfy/config/server.yml" 2>/dev/null || true)"
            if [ -n "$_local_base_url" ] && [ "$_local_base_url" != "https://ntfy.example.com" ]; then
                _ntfy_default="${_local_base_url}/crowdsec-alerts"
                echo "  Detected a configured local ntfy instance at $_local_base_url — using it as the default."
            fi
        fi
        if [ "$_ntfy_default" = "https://ntfy.sh/crowdsec-alerts" ]; then
            echo "  No configured ntfy instance detected on this box. If you have one hosted"
            echo "  elsewhere (e.g. a homelab), enter its topic URL below instead of the public"
            echo "  ntfy.sh default — e.g. https://ntfy.your-homelab.com/crowdsec-alerts"
        fi
        local CS_NTFY_URL=""
        prompt_text "  ntfy topic URL:" "$_ntfy_default" CS_NTFY_URL
        sudo mkdir -p /etc/crowdsec/notifications
        local NTFY_FILE="/etc/crowdsec/notifications/ntfy.yaml"
        local NTFY_CONTENT="type: http
name: ntfy
log_level: info
format: |
  {{range . -}}
  {{range .Decisions -}}
  {{.Value}} banned: {{.Scenario}} for {{.Duration}}
  Check all bans:  sudo cscli decisions list
  Unban this IP:   sudo cscli decisions delete --ip {{.Value}}
  {{end -}}
  {{end -}}
url: $CS_NTFY_URL
method: POST
headers:
  Title: CrowdSec ban
  Priority: high
  Tags: rotating_light"
        if echo "$NTFY_CONTENT" | sudo tee "$NTFY_FILE" > /dev/null; then
            echo "  ✓ Created ntfy notification ($NTFY_FILE)"
            # Wire the notification into the default profile (only once)
            if ! grep -qE "^\s*- ntfy" /etc/crowdsec/profiles.yaml 2>/dev/null; then
                sudo awk '1; /^on_success:/ && !d {print "notifications:"; print "  - ntfy"; d=1}' \
                    /etc/crowdsec/profiles.yaml | sudo tee /etc/crowdsec/profiles.yaml.new > /dev/null \
                    && sudo mv /etc/crowdsec/profiles.yaml.new /etc/crowdsec/profiles.yaml
                echo "  ✓ Enabled ntfy alerts in CrowdSec default profile"
            else
                echo "  ✓ ntfy already referenced in CrowdSec profile"
            fi
            echo "  ℹ Alerts fire when an IP is banned (after repeated failed attempts),"
            echo "    not on every individual failed login."
        else
            echo "  ⚠ Failed to write ntfy notification config"
        fi
    fi

    # ── 7b. Optional: point this agent at a remote/central LAPI ──────────────
    # CrowdSec's real multi-server support: parsers/scenarios/bouncer still
    # run locally (banning only works where traffic actually arrives), but
    # the decision database (LAPI) can live on one central machine instead
    # of every box running its own. Useful if you already have CrowdSec on
    # a homelab and don't want a second LAPI+SQLite DB on this droplet.
    echo ""
    local USE_REMOTE_LAPI=""
    if [ "$ASK_OPTIONAL" = true ]; then
        prompt_yn "Point this agent at a remote/central LAPI instead of running its own (e.g. one already on a homelab)? (y/n):" "n" USE_REMOTE_LAPI
    fi
    if [ "$USE_REMOTE_LAPI" = "y" ] || [ "$USE_REMOTE_LAPI" = "Y" ]; then
        echo ""
        echo "  This registers this machine and disables its local API server."
        echo "  The registration is PENDING until approved on the central LAPI"
        echo "  machine — that approval step can't be automated from here."
        echo ""
        local LAPI_URL="" LAPI_MACHINE=""
        prompt_text "  Central LAPI URL (e.g. http://homelab-ip:8080):" "" LAPI_URL
        prompt_text "  Machine name to register as:" "$(hostname)" LAPI_MACHINE
        if [ -n "$LAPI_URL" ]; then
            if sudo cscli lapi register -u "$LAPI_URL" --machine "$LAPI_MACHINE"; then
                echo "  ✓ Registered with $LAPI_URL as '$LAPI_MACHINE'"

                # Disable the local API server (remove the 'api.server:' block
                # from config.yaml) now that this agent forwards to the
                # central one instead. Backed up first — this is a direct
                # edit to CrowdSec's core config.
                local CS_CONFIG="/etc/crowdsec/config.yaml"
                local CS_BACKUP="$CS_CONFIG.backup.$(date +%Y%m%d-%H%M%S)"
                sudo cp "$CS_CONFIG" "$CS_BACKUP"
                sudo awk '
                    /^  server:/ { skip=1; next }
                    skip && /^([a-zA-Z]|  [a-zA-Z])/ { skip=0 }
                    !skip { print }
                ' "$CS_CONFIG" | sudo tee "$CS_CONFIG.new" > /dev/null \
                    && sudo mv "$CS_CONFIG.new" "$CS_CONFIG"
                echo "  ✓ Local API server disabled in config.yaml (backup: $(basename "$CS_BACKUP"))"
                echo ""
                echo "  ⚠ Not usable yet — on the CENTRAL LAPI machine, run:"
                echo "      sudo cscli machines validate $LAPI_MACHINE"
                echo "    Then restart this agent: sudo systemctl restart crowdsec"
                echo "    If it fails to start afterward, restore the backup and check logs:"
                echo "      sudo cp $CS_BACKUP $CS_CONFIG && sudo systemctl restart crowdsec"
                _REMOTE_LAPI_PENDING="y"
            else
                echo "  ⚠ cscli lapi register failed — keeping the local LAPI. See:"
                echo "    sudo cscli lapi register -u $LAPI_URL --machine $LAPI_MACHINE"
            fi
        else
            echo "  No URL entered — keeping the local LAPI."
        fi
    fi

    # ── 8. Restart services to apply ─────────────────────────────────────────
    if [ "$_REMOTE_LAPI_PENDING" = "y" ]; then
        echo ""
        echo "  Skipping the restart below — it would fail until the machine is"
        echo "  validated on the central LAPI (see above). Restart manually after:"
        echo "    sudo systemctl restart crowdsec"
    else
        local RESTART_CS=""
        prompt_yn "Restart CrowdSec to apply changes? (y/n):" "y" RESTART_CS
        if [ "$RESTART_CS" = "y" ] || [ "$RESTART_CS" = "Y" ]; then
            sudo systemctl enable crowdsec 2>/dev/null || true
            if sudo systemctl restart crowdsec; then
                echo "  ✓ CrowdSec restarted successfully"
                sudo systemctl enable crowdsec-firewall-bouncer 2>/dev/null || true
                sudo systemctl restart crowdsec-firewall-bouncer 2>/dev/null || true
                sleep 2
                sudo cscli metrics 2>/dev/null | head -20 || true
            else
                echo "  ⚠ Failed to restart CrowdSec"
                echo "  Check logs: sudo journalctl -u crowdsec -n 50"
            fi
        fi
    fi

    # ── 9. Docs-only folder under ~/docker for discoverability ───────────────
    write_readme "$DOCS_DIR" << 'CROWDSEC_README'
# CrowdSec — intrusion prevention

CrowdSec is a **system service** (installed via apt), not a Docker container, so
there is no `docker-compose.yml` in this folder — it exists only to document the
install. The real configuration lives under `/etc/crowdsec`.

## What it does

- Detects malicious behaviour (SSH brute force, web scans, SIP brute
  force/enumeration if `asterisk` is installed) by parsing logs.
- Bans offending IPs via the **firewall bouncer** (iptables/nftables).
- Pulls **community IP reputation** blocklists so known-bad IPs are blocked
  before they ever touch your services.
- Optionally enriches events with **geo/ASN** data for geo-blocking.

## Rerunning this script

Rerunning offers **update** (refreshes the agent/collections/acquisitions
only — ASN exemptions, geo-allowlist, ntfy, and remote-LAPI settings are left
exactly as they are) or **fresh** (revisit every optional prompt again, same
as a first install). Nothing here is destructive either way — "fresh" only
overwrites a setting if you actually answer its prompt differently.

## Key commands

```
sudo cscli metrics                      # parsers/scenarios/acquisition health
sudo cscli decisions list               # currently banned IPs
sudo cscli decisions delete --ip <IP>   # unban an IP
sudo cscli decisions add --ip <IP>      # manually ban an IP
sudo cscli alerts list                  # recent alerts
sudo cscli collections list             # installed detection collections
```

## Where configs live

- Log acquisition (what to watch): `/etc/crowdsec/acquis.d/`
  - Caddy access logs: `/etc/crowdsec/acquis.d/caddy.yaml`
    (`/var/log/caddy/*.log` — Caddy writes JSON access logs there)
  - Asterisk SIP auth events (if `asterisk` is installed):
    `/etc/crowdsec/acquis.d/asterisk-digital-ocean.yaml` (filename kept from
    when the droplet edition was its own service, so existing droplets aren't
    given a duplicate acquisition)
    (`~/docker/asterisk/logs/full`, or `~/docker/asterisk-digital-ocean/logs/full`
    on a pre-merge droplet — auth failures, registration scans)
- Notifications: `/etc/crowdsec/notifications/`
  - ntfy ban alerts (if enabled): `/etc/crowdsec/notifications/ntfy.yaml`,
    wired into `/etc/crowdsec/profiles.yaml`
- Bouncer config: `/etc/crowdsec/bouncers/`
- Remote/central LAPI (if enabled): `/etc/crowdsec/local_api_credentials.yaml`
  points at the remote URL; the local API server block is removed from
  `/etc/crowdsec/config.yaml` (backed up as `config.yaml.backup.<timestamp>`
  next to it before editing). Parsers, scenarios, and the firewall bouncer
  still run locally regardless — only the decision database is centralized.

## Multi-server (remote LAPI) notes

- On THIS machine: `sudo cscli lapi register -u <url> --machine <name>`
  registers and disables the local API server.
- On the CENTRAL machine: `sudo cscli machines validate <name>` approves it —
  not automated, since that's a different box.
- Check registration status here: `sudo cscli lapi status`
- Revert: restore the `config.yaml` backup and
  `sudo systemctl restart crowdsec`.

## Geo + reputation notes

- Geo-enrichment (country/ASN tagging) is optional:
  `sudo cscli collections install crowdsecurity/geoip-enrich`
- Subscribe to community / 3rd-party blocklists at https://app.crowdsec.net/
- ntfy alerts fire when an IP is **banned** (after repeated failed attempts),
  not on every individual failed login.
- Geo-allowlist (if enabled): `/etc/crowdsec/scenarios/geo-allowlist-web.yaml`
  bans any Caddy-fronted web request from outside the countries listed in its
  `filter:` line. Web traffic only — SSH is never affected. Edit the country
  list directly in that file, then `sudo systemctl restart crowdsec`. This
  can block Let's Encrypt's out-of-region ACME validation checks; if a cert
  renewal fails mysteriously, check here first.
- ASN-exempt Asterisk brute-force scenarios (if enabled, Asterisk installs
  only): `/etc/crowdsec/scenarios/local-asterisk_bf.yaml` and
  `local-asterisk_user_enum.yaml` — local forks of the stock hub scenarios with
  specific carrier ASNs excluded from their filter (the hub originals get
  disabled so events aren't double-processed). Exists because CGNAT mobile/
  satellite carriers (Starlink, T-Mobile home internet) share one public IP
  across many customers, so a phone roaming WiFi↔mobile can get banned for
  someone else's brute-force attempt on that same shared IP. SSH, web, and the
  geo-allowlist are all unaffected — this only loosens Asterisk auth-failure
  detection for the listed ASNs. Edit the `filter:` line in both files to
  add/remove ASNs, then `sudo systemctl restart crowdsec`. Check the AS column
  in `cscli decisions list` if the same carrier bans you again under a
  different ASN later.

## Service control

```
sudo systemctl status crowdsec
sudo systemctl restart crowdsec
sudo systemctl status crowdsec-firewall-bouncer
sudo journalctl -u crowdsec -n 50
```
CROWDSEC_README

    echo ""
    echo "  Useful commands:"
    echo "    List active bans:   sudo cscli decisions list"
    echo "    List alerts:        sudo cscli alerts list"
    echo "    Manually ban IP:    sudo cscli decisions add --ip 1.2.3.4"
    echo "    Unban IP:           sudo cscli decisions delete --ip 1.2.3.4"
    echo "    Show metrics:       sudo cscli metrics"
    echo ""
}

# Run immediately when executed directly (deferred until after function definition)
[[ "${_RUN_STANDALONE:-0}" == 1 ]] && install_crowdsec
