Sign inSign up

developingchet/cs-abuseipdb-bouncer

By developingchet

•Updated 3 days ago

CrowdSec to AbuseIPDB bouncer. Concurrent worker pool, ACID bbolt, Cosign-signed, distroless v2.0

Image
Security
Monitoring & observability
0

10K+

developingchet/cs-abuseipdb-bouncer repository overview

⁠cs-abuseipdb-bouncer

A production-ready, security-hardened CrowdSec bouncer that reports malicious IPs to AbuseIPDB in real-time. Single static Go binary, distroless container, Prometheus metrics.


⁠Features

FeatureDetail
Concurrent Worker PoolA configurable pool of goroutines sends reports to AbuseIPDB in parallel. High-frequency ban waves no longer stall the main event loop. Backpressure is handled via a bounded channel; overflow is counted in the buffer_full filter metric. Failed reports (network errors, 5xx, 429) are persisted and retried, honouring AbuseIPDB's Retry-After header.
ACID StatePer-IP cooldown and daily quota are stored in a bbolt⁠ embedded database (state.db). Quota and cooldown checks execute as single atomic transactions — no TOCTOU races, crash-consistent, survives container restarts.
Prometheus MetricsEleven metrics are exported on GET /metrics (port 9090 by default): decisions processed, reports sent, decisions skipped (by filter), API errors (by type), daily quota remaining, state.db file size, retry-queue size / enqueues / attempts, and last-successful LAPI pull / AbuseIPDB report timestamps. Ready for Grafana / Alertmanager.
Honest Health Checks/healthz and /readyz reflect the last successful LAPI pull and AbuseIPDB call. The Docker HEALTHCHECK queries them over HTTP — it never touches state.db and never spends API quota.
Distroless SecurityThe runtime image is gcr.io/distroless/static-debian12:nonroot. No shell, no package manager, no libc. Runs as UID 65532 with zero Linux capabilities and a read-only filesystem. Seccomp syscall allowlist applied by default in the provided docker-compose.yml.
Supply-Chain ProvenanceEvery release is signed with Cosign⁠ (keyless OIDC — no stored private key) and accompanied by a CycloneDX SBOM attached as a Cosign attestation and a GitHub Release asset.
Multi-ArchitecturePre-built images for linux/amd64 and linux/arm64 on Docker Hub. Static Go binary — no libc, no CGO.
Zero-Touch ReleasesEvery version tag triggers a GitHub Actions pipeline: tests (with -race) → Trivy CVE scan (blocks on HIGH/CRITICAL) → multi-arch Docker build → Cosign sign + SBOM → Docker Hub push → GitHub Release with binaries and SHA-256 checksums.

⁠Quick Start

# docker-compose.yml (minimal)
services:
  abuseipdb-bouncer:
    image: developingchet/cs-abuseipdb-bouncer:latest
    restart: unless-stopped
    environment:
      - CROWDSEC_LAPI_URL=http://crowdsec:8080
      - CROWDSEC_LAPI_KEY=${CROWDSEC_LAPI_KEY}
      - ABUSEIPDB_API_KEY=${ABUSEIPDB_API_KEY}
      - DATA_DIR=/data
      - METRICS_ADDR=:9090
    volumes:
      - bouncer-state:/data
    ports:
      - "127.0.0.1:9090:9090"
    tmpfs:
      - /tmp:size=10M,uid=65532,gid=65532,mode=1777
    read_only: true
    cap_drop: [ALL]
    security_opt: [no-new-privileges:true]
    networks:
      - crowdsec-net

volumes:
  bouncer-state:

networks:
  crowdsec-net:
    external: true

Get your LAPI key: docker exec crowdsec cscli bouncers add abuseipdb-bouncer


⁠Environment Variables

⁠Required
VariableDescription
CROWDSEC_LAPI_URLURL of the CrowdSec Local API (e.g. http://crowdsec:8080)
CROWDSEC_LAPI_KEYBouncer API key from cscli bouncers add
ABUSEIPDB_API_KEYAbuseIPDB v2 API key from abuseipdb.com/account/api
⁠Optional
VariableDefaultDescription
DATA_DIR/dataDirectory for state.db (bbolt database). Mount a named volume here.
METRICS_ENABLEDtrueSet to false to disable the /metrics, /healthz, /readyz HTTP server entirely (no port opened). Takes precedence over METRICS_ADDR. The image HEALTHCHECK needs this server — disable the healthcheck too if you turn it off.
METRICS_ADDR:9090Address for /metrics, /healthz, /readyz. Ignored when METRICS_ENABLED=false. Set to empty string to disable.
CONFIG_FILE(none)Optional path to a YAML config file (alternative / supplement to env vars).
ABUSEIPDB_DAILY_LIMIT1000Daily report quota (free=1000, webmaster=3000, premium=50000).
ABUSEIPDB_PRECHECKfalsePre-check each IP with /check before reporting (skips whitelisted IPs). Uses one call from the separate /check daily quota per decision.
ABUSEIPDB_MIN_DURATION0Skip decisions shorter than N seconds (e.g. 300 ignores 5-minute test bans).
IP_WHITELIST(none)Comma-separated IPs/CIDRs to skip reporting (e.g. 203.0.113.0/24,2001:db8::/32).
COOLDOWN_DURATION15mPer-IP cooldown matching AbuseIPDB's deduplication window.
POLL_INTERVAL2sLAPI decision stream polling frequency (minimum: 2s).
LAPI_TIMEOUT10sHTTP timeout for LAPI requests (minimum: 200ms).
LOG_LEVELinfotrace, debug, info, warn, or error.
LOG_FORMATjsonjson (structured, for SIEM) or text (human-readable).
TLS_SKIP_VERIFYfalseSkip TLS verification — only for self-signed LAPI certificates.
WORKER_COUNT4Number of goroutines that concurrently send reports to AbuseIPDB (range: 1–64).
WORKER_BUFFER256Size of the in-memory job queue between the event loop and workers (range: 1–10000).
JANITOR_INTERVAL5mHow often the background janitor prunes expired cooldown entries and updates the DB size metric (minimum: 30s).
RETRY_CHECK_INTERVAL30sHow often to check the retry queue for failed / rate-limited decisions ready to resend (minimum: 10s).
USAGE_METRICS_ENABLEDtrueEnable/disable periodic LAPI telemetry push (POST /v1/usage-metrics).
USAGE_METRICS_INTERVAL30mInterval for LAPI telemetry push (minimum: 10m).

⁠Observability

⁠Endpoints
EndpointDescription
GET /metricsPrometheus metrics in text exposition format
GET /healthzLiveness — HTTP 200 while LAPI was polled successfully within max(5 × POLL_INTERVAL, 2m) and fewer than 5 consecutive AbuseIPDB calls failed; 503 otherwise (JSON body with the reason)
GET /readyzReadiness — as /healthz, plus 503 until the first successful LAPI pull
⁠Prometheus Metrics
MetricTypeLabelsDescription
cs_abuseipdb_decisions_processed_totalCounter—All decisions received from the LAPI stream
cs_abuseipdb_reports_sent_totalCounter—Successful reports sent to AbuseIPDB
cs_abuseipdb_decisions_skipped_totalCounterfilterDecisions dropped, by filter or outcome
cs_abuseipdb_api_errors_totalCountertypeAPI errors by type: rate_limit, auth, validation, network, timeout
cs_abuseipdb_quota_remainingGauge—Remaining daily report quota (resets at UTC midnight)
cs_abuseipdb_bbolt_db_size_bytesGauge—Size of state.db in bytes, updated by the janitor
cs_abuseipdb_retry_queue_sizeGauge—Decisions waiting in the persistent retry queue
cs_abuseipdb_retry_queue_enqueued_totalCounter—Decisions queued for retry
cs_abuseipdb_retry_attempts_totalCounter—Retry attempts submitted from the queue
cs_abuseipdb_last_lapi_pull_timestamp_secondsGauge—Unix time of the last successful LAPI poll
cs_abuseipdb_last_report_success_timestamp_secondsGauge—Unix time of the last successful AbuseIPDB report

Scrape example:

curl http://localhost:9090/metrics | grep cs_abuseipdb

⁠Security

⁠Distroless Runtime

The container image is built FROM gcr.io/distroless/static-debian12:nonroot. This means:

  • No shell — eliminates RCE via shell injection
  • No package manager — no apt, apk, or pip to install tools post-compromise
  • Minimal CVE surface — only the Go binary and CA certificates
⁠Seccomp Profile

The repository ships security/seccomp-bouncer.json, a minimal OCI seccomp profile that allows only the syscalls the binary actually needs.

The file must exist on your host. It is read from the host filesystem at container start — it is not embedded in the image. Docker will refuse to start if the path does not exist.

If you cloned the repo, ./security/seccomp-bouncer.json is already present. Otherwise, download it first:

mkdir -p security
curl -fsSL \
  https://raw.githubusercontent.com/developingchet/cs-abuseipdb-bouncer/main/security/seccomp-bouncer.json \
  -o security/seccomp-bouncer.json

Then apply it in your compose file:

security_opt:
  - no-new-privileges:true
  - "seccomp:./security/seccomp-bouncer.json"

If you prefer not to download the file, simply omit that line — cap_drop: ALL, read_only: true, and the distroless nonroot image still provide strong isolation without it.

If the container crash-loops with operation not permitted, the installed profile may be outdated. See Troubleshooting⁠ for the fix.

⁠Log Redaction

API keys and Bearer tokens are automatically redacted from all log output before they reach stderr. The regex patterns match 80-character hex strings (AbuseIPDB key format), Bearer <token> values and X-Api-Key: header values (CrowdSec LAPI key).

⁠Supply-Chain Verification

Verify the image signature with Cosign:

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 available as a GitHub Release asset and embedded as a Cosign attestation on the image.


⁠Migration from v1.x

Aspectv1.xv2.0
State env varSTATE_DIRDATA_DIR
Default path/tmp/cs-abuseipdb/data
Storage formatdaily file + cooldown/ directorystate.db (bbolt embedded database)
Volume mountbouncer-state:/tmp/cs-abuseipdbbouncer-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

No migration script is needed. On first start, v2.0 creates a fresh state.db. The quota counter resets at UTC midnight anyway, and 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

Tag summary

Content type

Image

Digest

sha256:0e869ec31…

Size

5.5 MB

Last updated

3 days ago

docker pull developingchet/cs-abuseipdb-bouncer