feat(security): auto-block repeat offenders via CrowdSec community reputation
Gitea Actions Runner Test / test-job (push) Successful in 0s
CI / check (push) Failing after 17s
CI / tests-unit (push) Skipped
CI / tests-integration (push) Skipped
CI / tests-ui (push) Skipped
CI / preflight (push) Skipped
CI / deploy (push) Skipped
Gitea Actions Runner Test / test-job (push) Successful in 0s
CI / check (push) Failing after 17s
CI / tests-unit (push) Skipped
CI / tests-integration (push) Skipped
CI / tests-ui (push) Skipped
CI / preflight (push) Skipped
CI / deploy (push) Skipped
- new crowdsec-api lib: CTI lookup (GET /smoke/{ip}, freemium x-api-key), verdict parser with false-positive veto, 1h Redis + in-memory verdict cache, NX lock dedupe, 403/429 backoff; writes only the shared antiddos:block:{ip} key (value "crowdsec") and never touches Cloudflare
- gate fires it fire-and-forget for IPs that already tripped a rate bucket, so known-bad IPs are hard-blocked before the local maxViolations threshold
- runtime config: crowdsecAutoBlock toggle, score threshold (0-5, default 4), block TTL (default 24h); boot defaults CROWDSEC_AUTO_BLOCK_ENABLED / CROWDSEC_BLOCK_SCORE / CROWDSEC_BLOCK_TTL_SECONDS
- admin panel: CrowdSec stat card, verify-connection action, score/TTL settings, CrowdSec source badge in the blocked-IPs list
- credentials live in env only (CROWDSEC_API_KEY); block is enforced per-request via proxy on the resolved X-Forwarded-For / CF-Connecting-IP
- tests: crowdsec-api unit suite + ddos-guard integration suite (early-block, threshold, cache dedupe, backoff)
This commit is contained in:
1 parent
64edb81ab7
commit
f32a6dadd0
9 files changed
+1414
-5
No files matched your search
@@ -15,6 +15,10 @@ import {
|
||||
setLastCloudflareVerify,
|
||||
verifyCloudflareConnection,
|
||||
} from "@/lib/cloudflare-api";
|
||||
import {
|
||||
setLastCrowdsecVerify,
|
||||
verifyCrowdsecConnection,
|
||||
} from "@/lib/crowdsec-api";
|
||||
import { db, WebsiteSetting } from "@/lib/db";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
@@ -33,6 +37,17 @@ function positiveInt(raw: FormDataEntryValue | null, fallback: number): number {
|
||||
return Math.floor(n);
|
||||
}
|
||||
|
||||
function clampInt(
|
||||
raw: FormDataEntryValue | null,
|
||||
fallback: number,
|
||||
min: number,
|
||||
max: number,
|
||||
): number {
|
||||
const n = Number(str(raw));
|
||||
if (!Number.isFinite(n)) return fallback;
|
||||
return Math.min(max, Math.max(min, Math.floor(n)));
|
||||
}
|
||||
|
||||
function parseTiers(raw: FormDataEntryValue | null): AntiddosBlockTier[] {
|
||||
const tiers: AntiddosBlockTier[] = [];
|
||||
for (const part of str(raw).split(",")) {
|
||||
@@ -99,6 +114,17 @@ function configFromForm(formData: FormData): AntiddosConfig {
|
||||
defaults.globalHaltMs,
|
||||
),
|
||||
cloudflareAutoBlock: str(formData.get("cfa_auto_block")) === "1",
|
||||
crowdsecAutoBlock: str(formData.get("cs_auto_block")) === "1",
|
||||
crowdsecBlockScore: clampInt(
|
||||
formData.get("cs_block_score"),
|
||||
defaults.crowdsecBlockScore,
|
||||
0,
|
||||
5,
|
||||
),
|
||||
crowdsecBlockTtlSeconds: positiveInt(
|
||||
formData.get("cs_block_ttl_sec"),
|
||||
defaults.crowdsecBlockTtlSeconds,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -123,6 +149,9 @@ async function persistSettings(config: AntiddosConfig): Promise<void> {
|
||||
],
|
||||
["antiddos_global_halt_ms", String(config.globalHaltMs)],
|
||||
["antiddos_cfa_auto_block", config.cloudflareAutoBlock ? "1" : "0"],
|
||||
["antiddos_cs_auto_block", config.crowdsecAutoBlock ? "1" : "0"],
|
||||
["antiddos_cs_block_score", String(config.crowdsecBlockScore)],
|
||||
["antiddos_cs_block_ttl", String(config.crowdsecBlockTtlSeconds)],
|
||||
];
|
||||
await Promise.all(
|
||||
entries.map(([key, value]) =>
|
||||
@@ -230,6 +259,19 @@ export async function removeCloudflareRule(formData: FormData): Promise<void> {
|
||||
revalidatePath("/admin/devops/antiddos");
|
||||
}
|
||||
|
||||
/** Test the configured CrowdSec API credentials against the CTI endpoint. */
|
||||
export async function verifyCrowdsecConfiguration(): Promise<void> {
|
||||
const staff = await requirePermission(PERMS.SETTINGS_VIEW);
|
||||
const status = await verifyCrowdsecConnection();
|
||||
await setLastCrowdsecVerify(status);
|
||||
logger.info("CrowdSec API configuration verified", {
|
||||
staff: staff.username,
|
||||
ok: status.ok,
|
||||
message: status.message,
|
||||
});
|
||||
revalidatePath("/admin/devops/antiddos");
|
||||
}
|
||||
|
||||
/** Test the configured Cloudflare API credentials against the zone. */
|
||||
export async function verifyCloudflareConfiguration(): Promise<void> {
|
||||
const staff = await requirePermission(PERMS.SETTINGS_VIEW);
|
||||
|
||||
Reference in new issue
Block a user