A high-performance API Security Proxy written in Go. Sits between a Load Balancer and a Backend to detect and block onboarding abuse — fake signups, bot-driven registration flows, and scripted account creation.
Zero external dependencies. Sub-millisecond decision latency.
Every request is fingerprinted into an EntityID (IP/24 + User-Agent) and passed through two detection layers before being forwarded:
- L1 — token-bucket rate limiter + burst-window detection per entity
- L2 — signup funnel sequence check (
/home→/register)
Detections are emitted as JSON SecurityEvents to stdout. In shadow mode (default) every request is still forwarded — you observe the signal before enabling enforcement.
- Go 1.22+
git clone https://github.com/adva-mo/abuseShield.git
cd abuseShield
go build -o abuseshield ./cmd/abuseshield/cp config.example.json config.jsonThen edit config.json:
- Set
upstream_urlto your backend - Set
kill_switch_secretto a strong random value - Leave
shadow_mode: trueuntil you're confident in the signal quality
./abuseshield -config config.jsonTo simulate traffic against a mock backend:
Terminal 1 — mock upstream (accepts all HTTP methods, always returns 200)
python3 scripts/mock_upstream.pyTerminal 2 — AbuseShield
./abuseshield -config config.json 2>&1 | tee /tmp/shield.logTerminal 3 — simulate bot and real-user flows
python3 scripts/test_abuse.pyThe script runs four flows against AbuseShield and prints a live metrics summary at the end:
- Flow A — single bot entity fires 100 concurrent
POST /register(20 workers). ExpectSUSPICIOUS(sequence_violation) on the first few, thenBLOCK(burst_detected) once the token bucket empties. - Flow B — 8 distinct bot IPs × 15 requests each, all concurrent. Each entity trips burst detection independently, verifying per-entity sharding.
- Flow C — real user:
GET /home→ 1.5 s pause →POST /register. Both requests should beALLOW. - Flow D (optional) — kill-switch toggle. Pass
--kill-switchto enable this flow.
Inspect SecurityEvent logs:
python3 scripts/print_events.py /tmp/shield.logInspect live metrics:
python3 scripts/print_metrics.pyRun the full test suite (includes race detector):
go test -race ./...Run only the end-to-end suite:
go test -race ./test/e2e/The e2e suite spins up a real AbuseShield + upstream stack via httptest and covers: request proxying, IP rate limiting, XFF extraction and spoofing, API key limiting, hot-key cooldown, 429 response shape, and metrics counter accuracy — no mocks, no stubs.
Copy config.example.json to config.json. All fields have safe defaults — the only required field is upstream_url.
| Field | Default | Description |
|---|---|---|
listen_addr |
:8080 |
Address and port AbuseShield listens on (e.g. ":8080" or "0.0.0.0:8080") |
upstream_url |
— | Required. Full URL of the backend to proxy to (e.g. "http://api:3000") |
| Field | Default | Description |
|---|---|---|
shadow_mode |
true |
When true, detections are logged as SecurityEvents but nothing is blocked — all requests are forwarded. Disable once you are confident in signal quality. |
block_on_suspicious |
false |
When true, requests flagged SUSPICIOUS (e.g. sequence_violation) are also blocked, not just logged. Has no effect in shadow mode. |
kill_switch |
false |
Start with the kill switch active — skips all detection and proxies everything. Normally toggled at runtime via the admin endpoint. |
kill_switch_secret |
— | Shared secret that authenticates POST /admin/kill-switch requests. Set a strong random value; never commit it. |
event_buffer_size |
1000 |
Depth of the async SecurityEvent log buffer. Increase if you see events_dropped in metrics under high traffic. |
Each entity is a fingerprint of IP/24 + User-Agent. L1 fires when an entity exceeds its token bucket or hammers the server in a short window.
| Field | Default | Description |
|---|---|---|
entity_rate_per_sec |
2.5 |
Sustained request rate allowed per entity (tokens refilled per second). |
entity_burst |
5 |
Maximum burst an entity can send before L1 fires. Lower values are stricter. |
entity_burst_window_sec |
2.0 |
Time window (seconds) used to measure burst. Requests beyond entity_burst within this window trigger burst_detected. |
L2 checks that clients follow the expected navigation order. Hitting the target without first visiting the gate is a strong bot signal.
Once an entity visits the gate, it is considered "seen" for up to 5 minutes of inactivity — after that the entity is evicted and the check resets. Entities that do visit the gate but then flood the target are caught by L1, not L2; the two layers are complementary.
| Field | Default | Description |
|---|---|---|
funnel_gate |
"/home" |
Path the client must visit before the target (e.g. a landing page). |
funnel_target |
"/register" |
Protected path. A direct hit without a prior gate visit triggers sequence_violation. Must differ from funnel_gate. |
Hard per-IP and per-key limits enforced before requests reach the detection engine. Skipped in shadow mode.
| Field | Default | Description |
|---|---|---|
ip_rate_per_sec |
10 |
Sustained request rate allowed per IP (tokens refilled per second). |
ip_burst |
20 |
Maximum burst per IP before a 429 is returned. |
key_rate_per_sec |
100 |
Sustained request rate allowed per API key (X-API-Key header). |
key_burst |
200 |
Maximum burst per API key before a 429 is returned. |
hot_key_multiplier |
3.0 |
An IP or key that sustains more than multiplier × rate_per_sec requests per second is put into cooldown and locked out for cooldown_seconds. |
cooldown_seconds |
60 |
How long a hot IP or key is locked out (all requests return 429) after triggering the hot-key threshold. |
Requests matching any allowlist entry bypass L0, L1, and L2 entirely and go straight to the upstream.
| Field | Default | Description |
|---|---|---|
allowlist.ips |
[] |
Exact IPs or CIDR ranges that are always trusted (e.g. "10.0.0.0/8", "192.168.1.50"). |
allowlist.paths |
[] |
Path prefixes that are always trusted (e.g. "/health" also covers "/health/check"). |
allowlist.api_keys |
[] |
API keys (X-API-Key header) that are always trusted. |
Immediately disables all detection. Requests pass through without any checks. No SecurityEvents are logged while active.
# Enable
curl -X POST "http://localhost:8080/admin/kill-switch?enable=true" \
-H "X-Kill-Switch-Secret: your-secret"
# Disable
curl -X POST "http://localhost:8080/admin/kill-switch?enable=false" \
-H "X-Kill-Switch-Secret: your-secret"Prometheus-compatible plaintext metrics endpoint. Bypasses all rate limiting and detection. Point your Prometheus scraper here:
# prometheus.yml
- job_name: abuseshield
static_configs:
- targets: ["localhost:8080"]
metrics_path: /metricsFor human-readable output during local testing, use the included script instead:
python3 scripts/print_metrics.py- Use
allowlist.ipsfor trusted internal services or monitoring agents that should never be rate-limited or inspected. Supports exact IPs and CIDR ranges. - Use
allowlist.pathsfor health-check or internal endpoints (e.g./health,/internal/). Prefix-matched, so/healthcovers/health/checktoo. - Use
allowlist.api_keysfor internal service-to-service calls that carry a known key. Allowlisted requests skip L0, L1, and L2 entirely and go straight to the upstream. - AbuseShield is designed to sit behind a trusted Load Balancer that sets
X-Forwarded-For. The proxy reads the rightmost XFF entry as the client IP. - The binary has no external dependencies — deploy as a single static binary.
- SecurityEvents are written to stdout as JSON lines. Pipe to your log aggregator (
| fluentd,| vector, etc.). config.jsonmay containkill_switch_secret— never commit it. Useconfig.example.jsonas the template.
MIT