CrowdSec AbuseIPDB Bouncer Logo

# CrowdSec AbuseIPDB Bouncer [![Build](https://github.com/developingchet/cs-abuseipdb-bouncer/actions/workflows/release.yml/badge.svg)](https://github.com/developingchet/cs-abuseipdb-bouncer/actions/workflows/release.yml) [![CI](https://github.com/developingchet/cs-abuseipdb-bouncer/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/developingchet/cs-abuseipdb-bouncer/actions/workflows/ci.yml) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/developingchet/cs-abuseipdb-bouncer/badge)](https://scorecard.dev/viewer/?uri=github.com/developingchet/cs-abuseipdb-bouncer) [![Go Version](https://img.shields.io/github/go-mod/go-version/developingchet/cs-abuseipdb-bouncer)](https://github.com/developingchet/cs-abuseipdb-bouncer) [![Docker Pulls](https://img.shields.io/docker/pulls/developingchet/cs-abuseipdb-bouncer)](https://hub.docker.com/r/developingchet/cs-abuseipdb-bouncer) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) A production-ready, security-hardened CrowdSec bouncer that automatically reports malicious IP addresses to AbuseIPDB in real-time. ## Overview This bouncer connects directly to the CrowdSec Local API and reports new ban decisions to AbuseIPDB's threat intelligence database. It is implemented as a single static Go binary running in a distroless container with no shell, no root privileges, and a read-only filesystem. **Key capabilities:** - Real-time reporting via the CrowdSec LAPI decision stream - Concurrent worker pool — multiple HTTP round-trips to AbuseIPDB run in parallel, so high-frequency ban waves no longer stall the event loop - Intelligent scenario-to-category mapping (covers 22 of 23 AbuseIPDB categories; category 2 DNS Poisoning is not generated by current scenario patterns) - Per-IP cooldown enforcement (15-minute default, matching AbuseIPDB's deduplication window) - Daily quota tracking with UTC midnight reset and disk persistence - Optional pre-check to skip AbuseIPDB-whitelisted IPs before consuming report quota - Exponential backoff retry; failed reports (network errors, 5xx, 429 rate limits) are persisted and retried, honouring AbuseIPDB's `Retry-After` / `X-RateLimit-Reset` headers; duplicate-report 429s and invalid-parameter 422s are dropped, not retried - Health endpoints (`/healthz`, `/readyz`) driven by the last successful LAPI pull and AbuseIPDB call — probes never touch `state.db` and never spend API quota - State persists across container restarts via named Docker volume - Structured JSON logging compatible with Loki, Splunk, and other SIEM tools - Zero PII in reports -- only scenario names are transmitted **Security properties:** - Runs as UID 65532 (nonroot) inside a distroless container - No shell, no package manager, no OS utilities in the runtime image - `cap_drop: ALL` and `read_only: true` by default in docker-compose.yml - Seccomp syscall allowlist (`security/seccomp-bouncer.json`) blocks all syscalls not required by the binary - TLS 1.2+ enforced on all outbound connections - API keys loaded from environment variables or `*_FILE` secrets; 80-character hex keys, Bearer tokens and `X-Api-Key` headers are automatically redacted from log output - Docker image is signed with [Cosign](https://docs.sigstore.dev/cosign/overview/) (keyless OIDC) and accompanied by a CycloneDX SBOM on every release ## Compliance This bouncer follows the CrowdSec Remediation Component Specification for Stream Mode, User-Agent identification, and LAPI Telemetry. This repository is a reporter-only outbound sink: - AppSec forwarding is intentionally omitted. - CAPTCHA/remediation HTML is intentionally omitted. - No inline traffic enforcement is implemented (no reverse proxy, firewall, or routing logic). LAPI requests identify the component as `cs-abuseipdb-bouncer/`. ## How It Works ``` ┌──────────────────────────────┐ │ CrowdSec LAPI │ │ (port 8080 / 8443) │ └──────────────┬───────────────┘ │ polls every POLL_INTERVAL (default 10s; │ the bundled docker-compose.yml sets 30s) │ GET /v1/decisions/stream │ v ┌─────────────────────────────────────────────────────┐ │ cs-abuseipdb-bouncer (main event loop) │ │ │ │ Pre-queue filters (stateless, no I/O): │ │ 1. action = "add" │ │ 2. exclude impossible-travel scenarios │ │ 3. origin = crowdsec / cscli │ │ 4. scope = ip │ │ 5. value is present │ │ 6. value is a single valid IP address │ │ 7. reject private IPs │ │ 8. reject IP_WHITELIST matches (only when set) │ │ 9. min duration check │ │ │ │ │ (non-blocking enqueue; drop → │ │ │ buffer_full metric on overflow) │ │ v │ │ ┌─────────────────────────┐ │ │ │ job queue (default 256)│ │ │ └──────┬────────┬─────────┘ │ │ │ │ (N workers, default 4) │ │ v v │ │ ┌──────────┐ ┌──────────┐ │ │ │ Worker 1 │ │ Worker N │ 10. cooldown check │ │ │ atomic │ │ atomic │ 11. quota check │ │ │ bbolt tx │ │ bbolt tx │ (single transaction) │ │ └──────────┘ └──────────┘ │ │ │ │ Janitor goroutine (every 5m): │ │ - prune expired cooldown entries from state.db │ │ - update cs_abuseipdb_bbolt_db_size_bytes gauge │ └─────────────────────────────────────────────────────┘ │ ┌──────────────────────────┐ │ HTTPS POST │ Prometheus :9090 │ │ /api/v2/report │ GET /metrics │ │ retry: 3 attempts │ GET /healthz │ │ 5s/10s backoff │ GET /readyz │ │ └──────────────────────────┘ v ┌──────────────────────────────┐ │ AbuseIPDB v2 API │ │ /report endpoint │ └──────────────────────────────┘ ``` New decisions stream continuously from the LAPI. Each decision passes through eight stateless pre-queue filters (nine when `IP_WHITELIST` is set), then is handed off to a concurrent worker pool. Workers perform the quota and cooldown checks atomically (single bbolt transaction each) before dispatching to the AbuseIPDB sink. State is persisted to a named Docker volume so daily counts and per-IP cooldowns survive container restarts. In parallel, a background usage-metrics worker aggregates successful AbuseIPDB submissions and pushes telemetry to `POST /v1/usage-metrics` every `30m` by default (`USAGE_METRICS_INTERVAL`, minimum `10m`). ## Quick Start **Prerequisites:** - Docker with Compose v2 - Running CrowdSec instance with accessible LAPI - AbuseIPDB account (free tier: 1000 reports/day) **Steps:** ```bash # Register the bouncer with CrowdSec and save the printed key docker exec crowdsec cscli bouncers add abuseipdb-bouncer # Clone the repository git clone https://github.com/developingchet/cs-abuseipdb-bouncer.git cd cs-abuseipdb-bouncer # Configure environment cp .env.example .env chmod 600 .env nano .env # Set CROWDSEC_LAPI_KEY and ABUSEIPDB_API_KEY # Build and start docker compose build docker compose up -d # Verify startup docker logs -f abuseipdb-bouncer docker exec crowdsec cscli bouncers list ``` > If the container exits immediately with `permission denied` on `/data/state.db`, see [Volume Permissions](docs/TROUBLESHOOTING.md#volume-permission-denied-at-startup) in the troubleshooting guide. Expected startup log (JSON format): ```json {"level":"info","limit":1000,"used_today":0,"cooldown":"15m0s","precheck":false,"min_duration":"0s","log_level":"info","workers":4,"message":"bouncer started"} ``` ## Documentation - **[Setup Guide](docs/SETUP.md)** - Complete installation procedures - **[Configuration Reference](docs/CONFIGURATION.md)** - All environment variables explained - **[Troubleshooting](docs/TROUBLESHOOTING.md)** - Common issues and solutions - **[Design Rationale](docs/DESIGN.md)** - Architecture decisions and security analysis - **[External References](docs/REFERENCES.md)** - Links to official documentation - **[Compliance Checklist](COMPLIANCE.md)** - Technical mapping to CrowdSec remediation-component requirements ## Repository Structure ``` . ├── .github/ │ ├── workflows/ # CI, release, image scan, Scorecard, Socket, Docker Hub description │ ├── tools/ # Pinned CI tool versions (govulncheck) │ ├── requirements/ # Pinned Socket CLI requirements │ ├── CODEOWNERS │ └── dependabot.yml ├── cmd/ │ └── bouncer/ │ ├── main.go # CLI entry point (Cobra) │ ├── main_test.go │ └── healthcheck.go # `bouncer healthcheck` subcommand (Docker HEALTHCHECK) ├── internal/ │ ├── bouncer/ │ │ ├── bouncer.go # Main run loop + pre-queue filter pipeline │ │ ├── bouncer_test.go │ │ ├── health.go # /healthz and /readyz state and handlers │ │ ├── health_test.go │ │ ├── janitor.go # Background cooldown pruner + DB size metric │ │ ├── janitor_test.go │ │ ├── pool.go # Concurrent worker pool │ │ ├── pool_test.go │ │ ├── pool_outcome_test.go │ │ ├── retry_test.go │ │ ├── runtime_test.go │ │ └── testmain_test.go │ ├── config/ │ │ ├── config.go # Env-based configuration │ │ └── config_test.go │ ├── decision/ │ │ ├── filter.go # Filter pipeline │ │ ├── filter_test.go │ │ ├── filter_error_test.go │ │ ├── fuzz_test.go │ │ ├── ip.go # Private IP detection │ │ └── ip_test.go │ ├── logger/ │ │ ├── logrus.go # Forwards go-cs-bouncer's logrus output into zerolog │ │ ├── logrus_test.go │ │ ├── redact.go # RedactWriter: masks API keys and Bearer tokens in logs │ │ ├── redact_test.go │ │ ├── fuzz_test.go │ │ └── testdata/ # Fuzz corpus │ ├── sink/ │ │ ├── sink.go # Sink interface │ │ ├── sink_test.go │ │ ├── errors.go # Typed sink errors (duplicate, rate limit, permanent, unauthorized) │ │ └── abuseipdb/ │ │ ├── categories.go # Category ID constants │ │ ├── categories_test.go │ │ ├── client.go # HTTP client (sync.Pool for response buffers) │ │ ├── client_test.go │ │ ├── client_metrics_test.go │ │ ├── mapper.go # Scenario-to-category mapping │ │ └── mapper_test.go │ ├── metrics/ │ │ ├── metrics.go # Prometheus metric definitions │ │ ├── metrics_test.go │ │ └── register_test.go │ ├── telemetry/ │ │ ├── payload.go # LAPI /usage-metrics payload builder │ │ ├── payload_test.go │ │ ├── counter.go # In-memory processed counter for telemetry windows │ │ ├── counter_test.go │ │ ├── sender.go # Background telemetry push worker │ │ └── sender_test.go │ └── storage/ │ ├── store.go # Storage interface │ ├── bbolt.go # ACID bbolt persistence (quota, cooldown and retry queue, atomic ops) │ ├── bbolt_test.go │ ├── bbolt_concurrent_test.go # Concurrent correctness tests (-race) │ ├── mock.go # In-memory store for testing │ └── mock_test.go ├── scripts/ │ └── validate-seccomp.sh # Static check of the seccomp profile's required syscalls ├── security/ │ └── seccomp-bouncer.json # OCI seccomp profile (minimal syscall allowlist) ├── docs/ │ ├── CONFIGURATION.md │ ├── DESIGN.md │ ├── REFERENCES.md │ ├── SETUP.md │ └── TROUBLESHOOTING.md ├── .env.example # Environment variable template ├── .gitignore ├── .golangci.yml # golangci-lint configuration ├── COMPLIANCE.md # CrowdSec remediation-component compliance checklist ├── CONTRIBUTING.md ├── DOCKERHUB.md # Docker Hub repository description ├── Dockerfile # Multi-stage: golang -> distroless ├── docker-compose.yml ├── go.mod ├── go.sum ├── LICENSE ├── Makefile # Test, coverage, lint and build targets ├── SECURITY.md # Vulnerability disclosure policy ├── socket.yml # Socket.dev supply-chain scan configuration └── README.md ``` ## Configuration Summary All configuration is via environment variables. Required: ```bash CROWDSEC_LAPI_URL=http://crowdsec:8080 # URL of the CrowdSec LAPI ABUSEIPDB_API_KEY= # LAPI auth mode A (API key) CROWDSEC_LAPI_KEY= # LAPI auth mode B (mTLS) # CROWDSEC_LAPI_TLS_CERT_PATH=/etc/crowdsec/client.crt # CROWDSEC_LAPI_TLS_KEY_PATH=/etc/crowdsec/client.key # CROWDSEC_LAPI_TLS_CA_CERT_PATH=/etc/crowdsec/ca.crt ``` Optional (all have defaults): ```bash ABUSEIPDB_DAILY_LIMIT=1000 # Free=1000, Webmaster=3000, Premium=50000 ABUSEIPDB_PRECHECK=false # Pre-check /check endpoint before reporting ABUSEIPDB_MIN_DURATION=0 # Skip decisions shorter than N seconds COOLDOWN_DURATION=15m # Per-IP cooldown window POLL_INTERVAL=10s # LAPI polling frequency (min 2s; the bundled docker-compose.yml sets 30s) LOG_LEVEL=info # trace, debug, info, warn, error LOG_FORMAT=json # json or text TLS_SKIP_VERIFY=false # Skip TLS verification (self-signed certs) DATA_DIR=/data # Directory for state.db (mount a named volume here) METRICS_ADDR=127.0.0.1:9090 # Address for /metrics, /healthz, /readyz (empty = disabled; the Docker image uses :9090) USAGE_METRICS_ENABLED=true # Enable periodic LAPI /usage-metrics push USAGE_METRICS_INTERVAL=30m # Telemetry push interval (min 10m) CONFIG_FILE= # Optional path to YAML config file # Concurrent worker pool (v2.0) WORKER_COUNT=4 # Goroutines sending reports concurrently (1–64) WORKER_BUFFER=256 # In-memory job queue size (1–10000) JANITOR_INTERVAL=5m # How often to prune state.db and update DB size metric (min 30s) RETRY_CHECK_INTERVAL=30s # How often to resend queued (failed / rate-limited) decisions (min 10s) LAPI_TIMEOUT=10s # HTTP timeout for LAPI requests (min 200ms) ``` See [CONFIGURATION.md](docs/CONFIGURATION.md) for complete details. ## Scenario Mapping CrowdSec scenario names are mapped to AbuseIPDB categories via ordered substring matching. The author prefix is stripped and the name is lowercased before matching. | Scenario Pattern | AbuseIPDB Categories | Description | |------------------|---------------------|-------------| | `*ssh*` | 22, 18 | SSH brute-force | | `*sqli*`, `*sql-inj*` | 16, 21 | SQL injection | | `*wordpress*`, `*drupal*` | 18, 21 | CMS attacks | | `*http-dos*`, `*ddos*` | 4 | Denial of service | | `*cve*`, `*log4*`, `*rce*` | 21, 20 | CVE exploitation | | `*brute*`, `*-bf*`, `*rdp*` | 18 | Generic brute-force | | (fallback) | 15 | Hacking | Full 27-rule mapping table in [CONFIGURATION.md](docs/CONFIGURATION.md#scenario-category-mapping). ## Filtered Decisions These decisions are intentionally never reported: - **impossible-travel scenarios** - Account compromise heuristics, not IP-based abuse - **CAPI and lists origins** - Community blocklist IPs that are already globally known - **Private / reserved IP addresses** - RFC1918, loopback (127.0.0.0/8), link-local, CGNAT (100.64.0.0/10), benchmarking (198.18.0.0/15), multicast, 240.0.0.0/4, and IPv6 loopback/link-local/ULA/multicast — IPv4-mapped IPv6 (`::ffff:10.0.0.1`) is checked as IPv4 - **Non-IP scopes** - Ranges, ASNs, country-level decisions (AbuseIPDB only accepts single IPs) - **Malformed IP values** - Anything other than a bare address or a `/32` / `/128` prefix, including wider networks and zoned IPv6 addresses (skip reason `invalid_ip`) - **Whitelisted IPs** - Addresses matching `IP_WHITELIST`, when it is set (skip reason `whitelist`) - **Short decisions** - Bans shorter than `ABUSEIPDB_MIN_DURATION`, when it is set (skip reason `min_duration`) - **IPs within the cooldown window** - Already reported within the past 15 minutes - **Decisions exceeding the daily quota** - Once the limit is reached, no further reports are sent that UTC day ## State Persistence State is stored in a single `state.db` file (bbolt embedded database) inside `DATA_DIR` (default `/data`, mounted as a named Docker volume): ``` /data/ └── state.db # bbolt database with three buckets: ├── quota # JSON record: {"count":42,"date":"2026-02-17"} ├── cooldown # Per-IP binary Unix timestamps (big-endian int64) │ # key: 203.0.113.42 (IPv4, unchanged) │ # key: 2001_db8__1 (IPv6, colons → underscores) └── retry # Per-IP JSON record, same keys as cooldown: # {"ip":"203.0.113.42","scenario":"crowdsecurity/ssh-bf", # "retry_after":1739837130,"attempts":1} ``` Volume mount: `bouncer-state:/data` The daily quota record resets automatically at UTC midnight. Cooldown entries are pruned by the background janitor (every 5 minutes by default, configurable via `JANITOR_INTERVAL`). The retry bucket holds decisions waiting for another delivery attempt. All three survive container restarts via the named Docker volume. ## Observability The HTTP server (enabled by default at `127.0.0.1:9090`; the Docker image listens on `:9090` inside the container) exposes three unauthenticated endpoints, so publish or bind it only to loopback or a private network (the bouncer logs a warning at startup when `METRICS_ADDR` is not a loopback address): | Endpoint | Description | |----------|-------------| | `GET /metrics` | Prometheus metrics in text exposition format | | `GET /healthz` | Liveness — HTTP 200 while the LAPI has been polled successfully within `max(5 × POLL_INTERVAL, 2m)` and fewer than 5 consecutive AbuseIPDB calls have failed; 503 otherwise. The same `max(5 × POLL_INTERVAL, 2m)` applies as a startup grace before the first pull. | | `GET /readyz` | Readiness — as `/healthz`, and additionally 503 until the first successful LAPI pull | Both health endpoints return a JSON body (`status`, `reason`, `last_lapi_pull`, `last_abuseipdb_ok`, `last_abuseipdb_failure`, `consecutive_abuseipdb_failures`) and are served from memory: they never open `state.db` and never call AbuseIPDB. The Docker `HEALTHCHECK` runs `bouncer healthcheck`, which GETs `/healthz` on `METRICS_ADDR` — so it requires `METRICS_ENABLED=true`. Prometheus metrics: | Metric | Type | Labels | Description | |--------|------|--------|-------------| | `cs_abuseipdb_decisions_processed_total` | Counter | — | All decisions received from the LAPI stream | | `cs_abuseipdb_reports_sent_total` | Counter | — | Successful reports sent to AbuseIPDB | | `cs_abuseipdb_decisions_skipped_total` | Counter | `filter` | Decisions dropped, by filter or outcome (`cooldown`, `quota`, `duplicate`, `rejected`, `retry_exhausted`, `buffer_full`, …) | | `cs_abuseipdb_api_errors_total` | Counter | `type` | API errors: `rate_limit`, `auth`, `validation`, `network`, `timeout` | | `cs_abuseipdb_quota_remaining` | Gauge | — | Remaining daily quota (resets at UTC midnight) | | `cs_abuseipdb_bbolt_db_size_bytes` | Gauge | — | Size of `state.db` in bytes, updated by the janitor | | `cs_abuseipdb_retry_queue_size` | Gauge | — | Decisions waiting in the persistent retry queue | | `cs_abuseipdb_retry_queue_enqueued_total` | Counter | — | Decisions queued for retry (429 or transient failure) | | `cs_abuseipdb_retry_attempts_total` | Counter | — | Retry attempts submitted from the queue | | `cs_abuseipdb_last_lapi_pull_timestamp_seconds` | Gauge | — | Unix time of the last successful LAPI poll | | `cs_abuseipdb_last_report_success_timestamp_seconds` | Gauge | — | Unix time of the last successful AbuseIPDB report | ```bash curl http://localhost:9090/metrics | grep cs_abuseipdb ``` ## Logging JSON format (default): ```json {"level":"info","ip":"203.0.113.42","sink":"abuseipdb","time":1739836530,"message":"reported"} {"level":"debug","ip":"192.168.1.1","filter":"private_ip","detail":"ip=192.168.1.1 is private/reserved","time":1739836531,"message":"decision filtered (pre-queue)"} ``` Human-readable format (`LOG_FORMAT=text`): ``` 12:15PM INF reported ip=203.0.113.42 sink=abuseipdb 12:15PM DBG decision filtered (pre-queue) detail="ip=192.168.1.1 is private/reserved" filter=private_ip ip=192.168.1.1 ``` Enable debug logging: ```bash # In .env: LOG_LEVEL=debug docker compose up -d --force-recreate abuseipdb-bouncer ``` ## Supply-Chain Verification Every release is signed with [Cosign](https://docs.sigstore.dev/cosign/overview/) using keyless OIDC (no stored private key). Verify the image signature: ```bash cosign verify developingchet/cs-abuseipdb-bouncer:latest \ --certificate-identity-regexp="https://github.com/developingchet/cs-abuseipdb-bouncer/.github/workflows/release.yml@refs/tags/.*" \ --certificate-oidc-issuer="https://token.actions.githubusercontent.com" ``` A CycloneDX SBOM is attached to every GitHub Release and embedded as a Cosign attestation on the Docker image. The image is also published to GitHub Container Registry as `ghcr.io/developingchet/cs-abuseipdb-bouncer`, with the same tags and digest. It carries its own signature and SBOM attestation there, so the command above works with the `ghcr.io` name too. ## Building The binary compiles to approximately 13MB. Docker is required (Go does not need to be installed locally): ```bash docker compose build ``` To run tests directly (requires Go 1.27+; `-race` also needs a C compiler / CGO): ```bash go test -race ./... -count=1 -timeout=120s ``` ## Upgrade notes ### Upgrading to v2.5.0 **Metrics listener on loopback by default.** From v2.5.0, `METRICS_ADDR` defaults to `127.0.0.1:9090` instead of `:9090`. The Docker image sets `METRICS_ADDR=:9090`, so container deployments are unaffected. A native install (systemd, Windows) that is scraped by Prometheus from another host stops answering that scraper after upgrading to v2.5.0, and no warning is logged because the new default is a loopback address. Set `METRICS_ADDR` explicitly, for example `0.0.0.0:9090` or the private interface address the scraper uses, and keep the port behind a firewall since the endpoints are unauthenticated. The bouncer logs a warning at startup whenever `METRICS_ADDR` is not a loopback address. **New `invalid_ip` skip reason.** A decision whose value is not a single IP address (malformed text, a network wider than `/32` or `/128`, or a zoned IPv6 address) is now dropped before the cooldown and quota checks and counted under `filter="invalid_ip"` in `cs_abuseipdb_decisions_skipped_total`. Earlier versions spent a cooldown entry and a quota unit on such values before AbuseIPDB refused them. Dashboards and alerts that enumerate `filter` label values should include the new one. ### Upgrading to v2.5.3 **`POLL_INTERVAL` now defaults to `10s`.** The built-in default changes from `2s` to `10s`, matching CrowdSec's recommended pull period and cutting default LAPI request traffic by 80%. The configurable minimum remains `2s`. Reporting latency follows the interval: a decision can now reach AbuseIPDB up to 10 s after CrowdSec issues it, instead of 2 s. Deployments that never set `POLL_INTERVAL` and run the binary directly (systemd, Windows, bare Docker without the bundled compose file) move from 2-second to 10-second polling. The bundled `docker-compose.yml` is unaffected — it sets `POLL_INTERVAL=30s` when the variable is unset. To keep the previous behaviour, set `POLL_INTERVAL=2s` explicitly. Dashboards that assumed per-poll latencies (e.g. alerting on `cs_abuseipdb_last_lapi_pull_timestamp_seconds` staleness) are also unaffected: `/healthz` marks upstream stale after `max(5 × POLL_INTERVAL, 2m)`, which was already 2 minutes and does not change. ## Migration from v1.x | Aspect | v1.x | v2.0 | |--------|------|-------| | State env var | `STATE_DIR` | `DATA_DIR` | | Default path | `/tmp/cs-abuseipdb` | `/data` | | Storage format | `daily` file + `cooldown/` directory | `state.db` (bbolt embedded database) | | Volume mount | `bouncer-state:/tmp/cs-abuseipdb` | `bouncer-state:/data` | | New in v2.0 | — | `METRICS_ENABLED`, `METRICS_ADDR`, `CONFIG_FILE`, `WORKER_COUNT`, `WORKER_BUFFER`, `JANITOR_INTERVAL`, `RETRY_CHECK_INTERVAL`, `USAGE_METRICS_ENABLED`, `USAGE_METRICS_INTERVAL`, `LAPI_TIMEOUT`, `IP_WHITELIST` | | Supply-chain | — | Cosign keyless signing + CycloneDX SBOM | | Runtime hardening | — | Seccomp syscall allowlist + log key redaction | **No migration script is required.** On first start v2.0 creates a fresh `state.db`. The quota counter resets at UTC midnight anyway; cooldowns rebuild within one 15-minute window. Update your compose file: 1. Change `STATE_DIR=/tmp/cs-abuseipdb` → `DATA_DIR=/data` 2. Change `- bouncer-state:/tmp/cs-abuseipdb` → `- bouncer-state:/data` 3. Optionally add `METRICS_ADDR=:9090` and expose port 9090 All new variables (`WORKER_COUNT`, `WORKER_BUFFER`, `JANITOR_INTERVAL`) have sensible defaults and require no changes to existing deployments. ## Contributing Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. Areas where help is needed: - Additional scenario pattern mappings - Integration guides (Kubernetes, Proxmox, Synology) ## License MIT License. See [LICENSE](LICENSE) for full text. ## Support - **Issues:** https://github.com/developingchet/cs-abuseipdb-bouncer/issues - **Discussions:** https://github.com/developingchet/cs-abuseipdb-bouncer/discussions - **CrowdSec Discord:** https://discord.gg/crowdsec ## Acknowledgments Thanks to the CrowdSec team for the `go-cs-bouncer` library and to AbuseIPDB for maintaining the IP reputation database.