feat(security): opt-in local CrowdSec LAPI bouncer on the Docker engine
Gitea Actions Runner Test / test-job (push) Successful in 0s
CI / check (push) Successful in 30s
CI / tests-unit (push) Successful in 1m37s
CI / tests-integration (push) Successful in 1m55s
CI / tests-ui (push) Successful in 2m23s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 2m38s
Gitea Actions Runner Test / test-job (push) Successful in 0s
CI / check (push) Successful in 30s
CI / tests-unit (push) Successful in 1m37s
CI / tests-integration (push) Successful in 1m55s
CI / tests-ui (push) Successful in 2m23s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 2m38s
This commit is contained in:
1 parent
3e1a3f92c8
commit
84d53139a9
15 files changed
+1002
-2
No files matched your search
@@ -112,6 +112,21 @@ CROWDSEC_REPORT_ENROLL_KEY=
|
||||
# Central API base — override only for tests/staging.
|
||||
CROWDSEC_CAPI_BASE_URL=https://api.crowdsec.net/v3
|
||||
|
||||
# --- CROWDSEC LOCAL (opt-in engine on this Docker host, no proxy changes) ---
|
||||
# App-layer LAPI bouncer: the anti-DDoS gate asks the local engine per client
|
||||
# IP (short-cached) and blocks ban/captcha decisions before its own buckets.
|
||||
# Start everything with `bash cms security`; it writes the key below into .env
|
||||
# and starts the CrowdSec engine bound to 127.0.0.1. Set to "true" to load the
|
||||
# bouncer without the local engine (not recommended).
|
||||
CROWDSEC_LOCAL_ENABLED=false
|
||||
# Host access-log directory mounted into the engine for detection (Nginx only).
|
||||
CROWDSEC_NGINX_LOG_DIR=/var/log/nginx
|
||||
# Change LAPI port AND LAPI URL together when 18080 is already taken.
|
||||
CROWDSEC_LAPI_PORT=18080
|
||||
CROWDSEC_LAPI_URL=http://127.0.0.1:18080
|
||||
# Generated by `bash cms security`; keep in .env, never commit a value.
|
||||
CROWDSEC_LAPI_API_KEY=
|
||||
|
||||
# --- PATHS ---
|
||||
BADGE_UPLOAD_DIR=./public/assets/images/badges
|
||||
EMULATOR_JAR_PATH=./emulator/Arcturus.jar
|
||||
|
||||
@@ -775,6 +775,88 @@ Open **DevOps → Anti-DDoS protection**
|
||||
|
||||
---
|
||||
|
||||
## Local CrowdSec Engine (opt-in)
|
||||
|
||||
The repository ships a self-contained CrowdSec engine that runs on the same
|
||||
Docker host. It reads the host Nginx access log, runs
|
||||
`crowdsecurity/crowdsec:1.8.1` in its own Compose project and exposes LAPI only
|
||||
on `127.0.0.1:18080`. When enabled, the app-layer anti-DDoS gate
|
||||
(`src/lib/crowdsec-local.ts`) asks the local LAPI per client IP (short-cached)
|
||||
and blocks `ban` / `captcha` decisions before its own rate buckets run.
|
||||
No reverse-proxy, Traefik, Cloudflare or firewall configuration is changed.
|
||||
|
||||
### Step 1 — Enable the engine and register the bouncer
|
||||
|
||||
```bash
|
||||
bash cms security
|
||||
```
|
||||
|
||||
This generates `CROWDSEC_LAPI_API_KEY` (random 64 hex chars), writes the
|
||||
CrowdSec flags into `.env`, starts the engine and registers the `cms` bouncer
|
||||
against the local LAPI. The engine does **not** enroll into the CrowdSec
|
||||
Central API (`DISABLE_ONLINE_API=true`): detection stays local.
|
||||
|
||||
### Step 2 — Restart the CMS so it loads the bouncer credentials
|
||||
|
||||
A CI-managed `epicnext-cms-app` picks the new `.env` values up on its next
|
||||
deployment. For a clone running via the updater:
|
||||
|
||||
```bash
|
||||
bash cms update --skip-pull
|
||||
```
|
||||
|
||||
or restart the container directly (`docker compose restart cms`). Without the
|
||||
restart the gate has not loaded the LAPI URL/key yet.
|
||||
|
||||
### Step 3 — Verify
|
||||
|
||||
```bash
|
||||
bash cms security status
|
||||
```
|
||||
|
||||
Expect `CROWDSEC_LOCAL_ENABLED=yes` and `LAPI health: OK (127.0.0.1:18080)`.
|
||||
In the admin panel, **DevOps → Anti-DDoS protection** shows live block
|
||||
statistics split per origin (`community` vs `local`).
|
||||
|
||||
### Step 4 — Stop the engine again (optional)
|
||||
|
||||
```bash
|
||||
bash cms security disable
|
||||
```
|
||||
|
||||
Stops the container and sets `CROWDSEC_LOCAL_ENABLED=false`. Volumes and the
|
||||
`.env` key are kept.
|
||||
|
||||
### Environment variables
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
| ---------------------------- | ----------------------------- | ------------------------------------ |
|
||||
| `CROWDSEC_LOCAL_ENABLED` | `false` | Master switch for the local stack |
|
||||
| `CROWDSEC_LAPI_URL` | `http://127.0.0.1:18080` | LAPI endpoint (loopback only) |
|
||||
| `CROWDSEC_LAPI_PORT` | `18080` | Host port the engine maps to LAPI |
|
||||
| `CROWDSEC_LAPI_API_KEY` | — | Bouncer key; required when enabled |
|
||||
| `CROWDSEC_LAPI_TIMEOUT_MS` | `500` | Per-decision request timeout |
|
||||
| `CROWDSEC_LAPI_RETRY_MS` | `500` | Backoff before retrying LAPI |
|
||||
| `CROWDSEC_NGINX_LOG_DIR` | `/var/log/nginx` | Access-log directory for the engine |
|
||||
|
||||
### Notes and limitations
|
||||
|
||||
- Changing the port means updating `CROWDSEC_LAPI_PORT` **and**
|
||||
`CROWDSEC_LAPI_URL` together, then re-running `bash cms security`.
|
||||
- Rotate the key by editing `CROWDSEC_LAPI_API_KEY` in `.env`, running
|
||||
`bash cms security` again (re-registers the bouncer) and restarting the CMS.
|
||||
- The gate is **fail-closed at startup** when the feature is enabled without a
|
||||
key (startup aborts with a clear message). At runtime a LAPI network error
|
||||
**fails open** (traffic is allowed, decisions paused); a 403 from LAPI
|
||||
pauses local decisions for 5 minutes.
|
||||
- This bouncer is **application-layer**: it sheds known-bad IPs at the CMS
|
||||
process only. It does not drop traffic before the origin, does not protect
|
||||
other host ports/services, and depends on the client IP being trustworthy at
|
||||
the ingress. Keep the upstream protections (Cloudflare IP rules, proxy rate
|
||||
limits) for defense before the origin.
|
||||
|
||||
---
|
||||
|
||||
## Production Deployment (PM2)
|
||||
|
||||
```bash
|
||||
|
||||
@@ -4,6 +4,7 @@ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
case "${1:-help}" in
|
||||
install) shift; exec bash "$DIR/scripts/docker-install.sh" "$@" ;;
|
||||
update) shift; exec bash "$DIR/scripts/docker-update.sh" "$@" ;;
|
||||
help|--help|-h) printf '%s\n' 'bash cms install Configure and install on a Linux Docker host' 'bash cms update Update using saved settings; --skip-pull uses checked-out release' ;;
|
||||
*) echo "Unknown command. Use: bash cms install | update" >&2; exit 1 ;;
|
||||
security) shift; exec bash "$DIR/scripts/crowdsec-setup.sh" "$@" ;;
|
||||
help|--help|-h) printf '%s\n' 'bash cms install Configure and install on a Linux Docker host' 'bash cms update Update using saved settings; --skip-pull uses checked-out release' 'bash cms security Configure the opt-in local CrowdSec stack (enable|status|disable)' ;;
|
||||
*) echo "Unknown command. Use: bash cms install | update | security" >&2; exit 1 ;;
|
||||
esac
|
||||
@@ -0,0 +1,4 @@
|
||||
filenames:
|
||||
- /var/log/nginx/access.log
|
||||
labels:
|
||||
type: nginx
|
||||
@@ -0,0 +1,41 @@
|
||||
services:
|
||||
crowdsec:
|
||||
image: crowdsecurity/crowdsec:${CROWDSEC_VERSION:-1.8.1}
|
||||
container_name: epicnext-crowdsec
|
||||
restart: unless-stopped
|
||||
profiles: ["security"]
|
||||
environment:
|
||||
COLLECTIONS: ${CROWDSEC_COLLECTIONS:-crowdsecurity/nginx}
|
||||
BOUNCER_KEY_cms: ${CROWDSEC_LAPI_API_KEY:?CROWDSEC_LAPI_API_KEY must be set}
|
||||
DISABLE_ONLINE_API: "true"
|
||||
GID: "${CROWDSEC_GID:-0}"
|
||||
TZ: "${TZ:-UTC}"
|
||||
ports:
|
||||
- "${CROWDSEC_LAPI_BIND_HOST:-127.0.0.1}:${CROWDSEC_LAPI_PORT:-18080}:8080"
|
||||
volumes:
|
||||
- ./acquis.d:/etc/crowdsec/acquis.d:ro
|
||||
- ${CROWDSEC_NGINX_LOG_DIR:-/var/log/nginx}:/var/log/nginx:ro
|
||||
- crowdsec-config:/etc/crowdsec
|
||||
- crowdsec-data:/var/lib/crowdsec/data
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
pids_limit: 256
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"wget -q -O - http://127.0.0.1:8080/health >/dev/null 2>&1",
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
volumes:
|
||||
crowdsec-config:
|
||||
crowdsec-data:
|
||||
@@ -85,6 +85,22 @@ The direct template intentionally records the CDN/edge socket address when place
|
||||
|
||||
`pnpm test:integration` additionally starts disposable Nginx containers from the actual templates, supplies a temporary test certificate, and sends real HTTPS requests with forged identity headers. It checks direct-mode replacement even with an inherited real-IP rule, rejection of untrusted peers, and acceptance through an explicitly trusted peer. This requires Docker Engine and the OpenSSL CLI and does not read deployment credentials. The templates must still pass `nginx -t` on the intended host after its hostname/certificate substitution, then the listener and trusted-header checks above; the disposable fixture cannot certify that host or its firewall.
|
||||
|
||||
## Opt-in: CrowdSec on the same Docker host
|
||||
|
||||
A self-contained CrowdSec engine ships in `deployment/crowdsec`. It reads the host Nginx access log, runs `crowdsecurity/crowdsec:1.8.1` in its own Compose project and exposes LAPI only on `127.0.0.1:18080`. No reverse-proxy, Traefik, Cloudflare or firewall configuration is changed.
|
||||
|
||||
```sh
|
||||
bash cms security
|
||||
```
|
||||
|
||||
The command generates `CROWDSEC_LAPI_API_KEY`, writes the CrowdSec flags into `.env`, starts the engine and registers the `cms` bouncer. The anti-DDoS gate then consults the local LAPI per client IP (short-cached) and blocks `ban`/`captcha` decisions before its own rate buckets. `bash cms security status` reports engine state and `bash cms security disable` stops the engine and flips the toggle off.
|
||||
|
||||
The engine does not enroll into the CrowdSec Central API (`DISABLE_ONLINE_API=true`): detection stays local. The app still has its separate opt-in traffic-sharing channel via `CROWDSEC_REPORT_ENABLED`. Change `CROWDSEC_LAPI_PORT` and `CROWDSEC_LAPI_URL` together when `18080` is already in use. `CROWDSEC_NGINX_LOG_DIR` overrides the log directory the engine acquires.
|
||||
|
||||
This bouncer is application-layer: it sheds known-bad IPs at the CMS process and only for traffic that reaches the Next.js proxy. It does not drop traffic before the origin, does not protect other host ports/services, and depends on the client IP being trustworthy at the ingress. Keep the upstream protections (Cloudflare IP rules, proxy rate limits) for defense before the origin.
|
||||
|
||||
The running CMS loads the new env values on its next restart or deployment. For a CI-managed `epicnext-cms-app`, the next deploy (which sources `.env`) applies them; for a clone, `bash cms update --skip-pull` restarts it. `.env` now holds the LAPI key — keep its permissions restrictive.
|
||||
|
||||
## Routine and selected-release updates
|
||||
|
||||
```sh
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -15,3 +15,29 @@ it("imports runtime validation without starting the CMS", () => {
|
||||
);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
});
|
||||
|
||||
it("rejects the local CrowdSec bouncer without a key", () => {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"--input-type=module",
|
||||
"-e",
|
||||
"import {validateRuntime} from './scripts/docker-start.mjs';try{validateRuntime({HOTEL_NAME:'x',AUTH_SECRET:'01234567890123456789012345678901',DATABASE_URL:'mysql://u:p@h/db',APP_URL:'http://h',CROWDSEC_LOCAL_ENABLED:'true'});process.exit(1)}catch(error){if(!String(error.message).includes('CROWDSEC_LAPI_API_KEY'))throw error}",
|
||||
],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
});
|
||||
|
||||
it("accepts a complete local CrowdSec configuration", () => {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"--input-type=module",
|
||||
"-e",
|
||||
"import {validateRuntime} from './scripts/docker-start.mjs';validateRuntime({HOTEL_NAME:'x',AUTH_SECRET:'01234567890123456789012345678901',DATABASE_URL:'mysql://u:p@h/db',APP_URL:'http://h',CROWDSEC_LOCAL_ENABLED:'true',CROWDSEC_LAPI_API_KEY:'fixture-key',CROWDSEC_LAPI_URL:'http://127.0.0.1:18080'})",
|
||||
],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
});
|
||||
@@ -23,6 +23,22 @@ export function validateRuntime(settings) {
|
||||
invalid.push(key);
|
||||
}
|
||||
}
|
||||
const localEnabled = ["true", "1"].includes(
|
||||
String(settings.CROWDSEC_LOCAL_ENABLED ?? "")
|
||||
.trim()
|
||||
.toLowerCase(),
|
||||
);
|
||||
if (localEnabled) {
|
||||
if (!settings.CROWDSEC_LAPI_API_KEY?.trim())
|
||||
invalid.push("CROWDSEC_LAPI_API_KEY");
|
||||
if (settings.CROWDSEC_LAPI_URL) {
|
||||
try {
|
||||
new URL(settings.CROWDSEC_LAPI_URL);
|
||||
} catch {
|
||||
invalid.push("CROWDSEC_LAPI_URL");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (invalid.length)
|
||||
throw new Error(`Invalid runtime configuration: ${invalid.join(", ")}`);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
copyFileSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
@@ -84,3 +85,93 @@ it.skipIf(!hasCompose)(
|
||||
},
|
||||
30_000,
|
||||
);
|
||||
|
||||
it("documents the local CrowdSec switches in .env.example", () => {
|
||||
const examples = readFileSync(path.join(root, ".env.example"), "utf8");
|
||||
for (const key of [
|
||||
"CROWDSEC_LOCAL_ENABLED",
|
||||
"CROWDSEC_LAPI_URL",
|
||||
"CROWDSEC_LAPI_PORT",
|
||||
"CROWDSEC_LAPI_API_KEY",
|
||||
"CROWDSEC_NGINX_LOG_DIR",
|
||||
]) {
|
||||
expect(examples).toContain(key);
|
||||
}
|
||||
});
|
||||
|
||||
it.skipIf(!hasCompose)(
|
||||
"renders the standalone CrowdSec stack with a loopback-only LAPI",
|
||||
() => {
|
||||
const directory = mkdtempSync(path.join(tmpdir(), "cms-crowdsec-"));
|
||||
try {
|
||||
mkdirSync(path.join(directory, "deployment/crowdsec/acquis.d"), {
|
||||
recursive: true,
|
||||
});
|
||||
copyFileSync(
|
||||
path.join(root, "deployment/crowdsec/compose.crowdsec.yml"),
|
||||
path.join(directory, "deployment/crowdsec/compose.crowdsec.yml"),
|
||||
);
|
||||
copyFileSync(
|
||||
path.join(root, "deployment/crowdsec/acquis.d/nginx.yaml"),
|
||||
path.join(directory, "deployment/crowdsec/acquis.d/nginx.yaml"),
|
||||
);
|
||||
writeFileSync(
|
||||
path.join(directory, ".env"),
|
||||
[
|
||||
"CROWDSEC_LAPI_API_KEY=fixture-key",
|
||||
"CROWDSEC_LAPI_PORT=18080",
|
||||
"CROWDSEC_LAPI_URL=http://127.0.0.1:18080",
|
||||
"CROWDSEC_NGINX_LOG_DIR=/var/log/nginx",
|
||||
].join("\n"),
|
||||
);
|
||||
const environment = { ...process.env };
|
||||
for (const key of Object.keys(environment))
|
||||
if (
|
||||
key.startsWith("COMPOSE_") ||
|
||||
key.startsWith("CROWDSEC_") ||
|
||||
key.startsWith("TZ")
|
||||
)
|
||||
delete environment[key];
|
||||
const result = spawnSync(
|
||||
"docker",
|
||||
[
|
||||
"compose",
|
||||
"--project-name",
|
||||
"crowdsec-fixture",
|
||||
"--env-file",
|
||||
".env",
|
||||
"-f",
|
||||
"deployment/crowdsec/compose.crowdsec.yml",
|
||||
"--profile",
|
||||
"security",
|
||||
"config",
|
||||
"--format",
|
||||
"json",
|
||||
],
|
||||
{ cwd: directory, env: environment, encoding: "utf8", timeout: 15_000 },
|
||||
);
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
const config = JSON.parse(result.stdout);
|
||||
const service = config.services.crowdsec;
|
||||
expect(service).toBeDefined();
|
||||
expect(service.image).toContain("crowdsecurity/crowdsec:");
|
||||
expect(service.environment.BOUNCER_KEY_cms).toBe("fixture-key");
|
||||
expect(service.environment.DISABLE_ONLINE_API).toBe("true");
|
||||
expect(
|
||||
service.ports.some(
|
||||
(published) =>
|
||||
published.host_ip === "127.0.0.1" &&
|
||||
published.published === "18080" &&
|
||||
published.target === 8080,
|
||||
),
|
||||
).toBe(true);
|
||||
const targets = service.volumes.map((volume) => volume.target);
|
||||
expect(targets).toContain("/var/log/nginx");
|
||||
expect(targets).toContain("/etc/crowdsec/acquis.d");
|
||||
expect(service.healthcheck.test.join(" ")).toContain("wget");
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
30_000,
|
||||
);
|
||||
+25
@@ -197,6 +197,23 @@ const schema = z
|
||||
.string()
|
||||
.url()
|
||||
.default("https://api.crowdsec.net/v3"),
|
||||
// Local CrowdSec engine shipped as an opt-in Docker stack in
|
||||
// deployment/crowdsec. When enabled, the anti-DDoS gate asks the local
|
||||
// LAPI (bouncer) for each client IP before its own buckets and blocks
|
||||
// ban/captcha decisions immediately. The key lives in env only.
|
||||
CROWDSEC_LOCAL_ENABLED: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((value) => value === "true" || value === "1"),
|
||||
CROWDSEC_LAPI_URL: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((value) =>
|
||||
value?.trim() ? value.trim() : "http://127.0.0.1:18080",
|
||||
)
|
||||
.pipe(z.string().url()),
|
||||
CROWDSEC_LAPI_API_KEY: z.string().optional(),
|
||||
CROWDSEC_LAPI_TIMEOUT_MS: z.coerce.number().int().positive().default(500),
|
||||
// Watcher credentials for signal push. When omitted, a stable pair is
|
||||
// generated once and persisted in Redis (48-char alnum machine id,
|
||||
// per the CAPI schema).
|
||||
@@ -232,6 +249,14 @@ const schema = z
|
||||
path: ["PAYPAL_CLIENT_ID"],
|
||||
});
|
||||
}
|
||||
if (data.CROWDSEC_LOCAL_ENABLED && !data.CROWDSEC_LAPI_API_KEY) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message:
|
||||
"CROWDSEC_LAPI_API_KEY is required when CROWDSEC_LOCAL_ENABLED=true",
|
||||
path: ["CROWDSEC_LAPI_API_KEY"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
type Env = z.infer<typeof schema>;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -2,6 +2,7 @@ import { NextRequest } from "next/server";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { invalidateAntiddosConfig } from "@/lib/antiddos-config";
|
||||
import { resetCrowdsecCache } from "@/lib/crowdsec-api";
|
||||
import { resetCrowdsecLocalCache } from "@/lib/crowdsec-local";
|
||||
import { enforceDdosRateLimit } from "@/lib/ddos-guard";
|
||||
|
||||
// The gate's block escalation (and thus the CrowdSec hook) only runs when
|
||||
@@ -123,6 +124,7 @@ describe("anti-DDoS automatic CrowdSec blocks", () => {
|
||||
state.z.clear();
|
||||
state.sendAlert.mockReset();
|
||||
resetCrowdsecCache();
|
||||
resetCrowdsecLocalCache();
|
||||
invalidateAntiddosConfig();
|
||||
fetchMock = vi.fn();
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
@@ -141,6 +143,7 @@ describe("anti-DDoS automatic CrowdSec blocks", () => {
|
||||
vi.unstubAllEnvs();
|
||||
state.map.clear();
|
||||
resetCrowdsecCache();
|
||||
resetCrowdsecLocalCache();
|
||||
invalidateAntiddosConfig();
|
||||
});
|
||||
|
||||
@@ -244,4 +247,29 @@ describe("anti-DDoS automatic CrowdSec blocks", () => {
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("blocks immediately on a local LAPI ban decision (app-layer bouncer)", async () => {
|
||||
vi.stubEnv("CROWDSEC_LOCAL_ENABLED", "true");
|
||||
vi.stubEnv("CROWDSEC_LAPI_URL", "http://127.0.0.1:18080");
|
||||
vi.stubEnv("CROWDSEC_LAPI_API_KEY", "local-bouncer-key");
|
||||
fetchMock.mockImplementation(async (input) => {
|
||||
if (String(input).startsWith("http://127.0.0.1:18080/")) {
|
||||
return jsonResponse([
|
||||
{
|
||||
origin: "crowdsec",
|
||||
type: "ban",
|
||||
scope: "ip",
|
||||
value: "198.51.100.88",
|
||||
duration: "1h",
|
||||
},
|
||||
]);
|
||||
}
|
||||
return jsonResponse({ message: "unexpected upstream" }, 500);
|
||||
});
|
||||
|
||||
const ip = "198.51.100.88";
|
||||
const blocks = await pump(proxiedRequest(ip), 1);
|
||||
expect(blocks).toBe(1);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import { resolveClientIp } from "@/lib/client-ip";
|
||||
import { isCloudflareProxied } from "@/lib/cloudflare";
|
||||
import { maybeAutoBlockCloudflare } from "@/lib/cloudflare-api";
|
||||
import { maybeAutoBlockCrowdsec } from "@/lib/crowdsec-api";
|
||||
import { checkCrowdsecLocalBlock } from "@/lib/crowdsec-local";
|
||||
import { reportCrowdsecSignal } from "@/lib/crowdsec-report";
|
||||
import {
|
||||
bumpCrowdsecBreakdownStat,
|
||||
@@ -84,6 +85,14 @@ export async function enforceDdosRateLimit(
|
||||
}
|
||||
}
|
||||
|
||||
const localBlock = await checkCrowdsecLocalBlock(ip);
|
||||
if (localBlock.blocked) {
|
||||
return {
|
||||
outcome: "block",
|
||||
retryAfterSeconds: Math.max(localBlock.retryAfterSeconds, 1),
|
||||
};
|
||||
}
|
||||
|
||||
const global = await rateLimit(
|
||||
"antiddos:global:all",
|
||||
config.global.limit,
|
||||
|
||||
Reference in new issue
Block a user