feat(security): ops alerts, shared backoff, atomic quota and daily stats for CrowdSec
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 29s
CI / tests-integration (push) Successful in 1m36s
CI / tests-unit (push) Successful in 1m40s
CI / tests-ui (push) Successful in 2m28s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 2m3s
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 29s
CI / tests-integration (push) Successful in 1m36s
CI / tests-unit (push) Successful in 1m40s
CI / tests-ui (push) Successful in 2m28s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 2m3s
Add an alerting/stats layer over the existing CrowdSec integration: - New crowdsec-alerts.ts: cooldown-gated ops alerts (Redis NX lock, TTL from HEALTH_ALERT_COOLDOWN_MIN) fanning out through the app's sendAlert service. Raised for daily quota exhaustion, block bursts (5-min window past CROWDSEC_ALERT_BLOCK_BURST), and signal-push failures. - New crowdsec-stats.ts: daily counters (lookups/blocks/reports/report_fail) in Redis with a 14-day reader for the admin panel. - Shared 403/429 backoff: the pause marker now lives in Redis (crowdsec:backoff-until) so every instance honours it, not just the process that hit the limit. - Atomic quota reservation: INCR-before-call with self-rollback on overshoot, so concurrent instances can never slip calls past the daily ceiling. - Admin anti-DDoS page gains a last-14-days activity table next to the quota bar.
This commit is contained in:
1 parent
5e4fc9ab59
commit
301edd2c9a
9 files changed
+620
-28
No files matched your search
@@ -2,11 +2,13 @@ import "server-only";
|
||||
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
import { env } from "@/env";
|
||||
import { raiseCrowdsecAlert } from "@/lib/crowdsec-alerts";
|
||||
import type {
|
||||
CrowdsecBlockMeta,
|
||||
CrowdsecConnectionStatus,
|
||||
CrowdsecVerdict,
|
||||
} from "@/lib/crowdsec-api";
|
||||
import { bumpCrowdsecStat } from "@/lib/crowdsec-stats";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { redis } from "@/lib/redis";
|
||||
import { UNKNOWN_CLIENT_IP } from "./client-ip";
|
||||
@@ -390,6 +392,7 @@ async function pushSignal(input: {
|
||||
}
|
||||
const status: CrowdsecReportStatus = { ok: true, at: Date.now() };
|
||||
await setLastCrowdsecReport(status);
|
||||
void bumpCrowdsecStat("reports");
|
||||
logger.info("[crowdsec-report] Detection shared with the community", {
|
||||
ip: input.ip,
|
||||
category: input.category,
|
||||
@@ -404,6 +407,19 @@ async function pushSignal(input: {
|
||||
message: String(error),
|
||||
};
|
||||
await setLastCrowdsecReport(status);
|
||||
void bumpCrowdsecStat("report_fail");
|
||||
// Ops alert, cooldown-gated: a silently broken channel means the
|
||||
// community never learns about the blocks we keep sharing.
|
||||
void raiseCrowdsecAlert("report", {
|
||||
type: "ddos",
|
||||
severity: "warning",
|
||||
message:
|
||||
"CrowdSec signal push failed — detections are not reaching the community.",
|
||||
context: {
|
||||
ip: input.ip,
|
||||
detail: String(error).slice(0, 300),
|
||||
},
|
||||
});
|
||||
logger.error("[crowdsec-report] Signal push failed", {
|
||||
ip: input.ip,
|
||||
err: error,
|
||||
|
||||
Reference in new issue
Block a user