feat(security): mirror anti-DDoS blocks to Cloudflare edge via API
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 30s
CI / tests-integration (push) Successful in 1m38s
CI / tests-unit (push) Failing after 1m40s
CI / tests-ui (push) Successful in 2m28s
CI / preflight (push) Skipped
CI / deploy (push) Skipped

- gate creates a zone IP Access Rule (block) for proxied offenders that hit the block threshold, deduped until the tiered block expires
- cloudflare-api lib: verified endpoints, create/delete/verify/list helpers, Redis-backed tracking + 30s TTL sweep (instrumentation worker + admin render)
- runtime toggle cloudflareAutoBlock in antiddos config; boot default CLOUDFLARE_AUTO_BLOCK_ENABLED
- admin panel: Cloudflare edge-blocks card with verify + remove-rule actions; unban also lifts the edge block
- credentials live in env only (CLOUDFLARE_API_TOKEN / CLOUDFLARE_ZONE_ID)
This commit is contained in:
openhands committed 2026-09-22 23:27:15 +02:00
1 parent f0c27eb815
commit 4479753160
9 files changed
+1157 -1

No files matched your search

+44
View File
@@ -10,6 +10,11 @@ import {
antiddosDefaultsFromEnv,
invalidateAntiddosConfig,
} from "@/lib/antiddos-config";
import {
removeCloudflareBlock,
setLastCloudflareVerify,
verifyCloudflareConnection,
} from "@/lib/cloudflare-api";
import { db, WebsiteSetting } from "@/lib/db";
import { logger } from "@/lib/logger";
import { PERMS } from "@/lib/permissions";
@@ -93,6 +98,7 @@ function configFromForm(formData: FormData): AntiddosConfig {
formData.get("global_halt_ms"),
defaults.globalHaltMs,
),
cloudflareAutoBlock: str(formData.get("cfa_auto_block")) === "1",
};
}
@@ -116,6 +122,7 @@ async function persistSettings(config: AntiddosConfig): Promise<void> {
.join(","),
],
["antiddos_global_halt_ms", String(config.globalHaltMs)],
["antiddos_cfa_auto_block", config.cloudflareAutoBlock ? "1" : "0"],
];
await Promise.all(
entries.map(([key, value]) =>
@@ -190,12 +197,49 @@ export async function unbanAntiddosIp(formData: FormData): Promise<void> {
redis.del(`antiddos:v:${ip}`),
]);
}
// Also lift a matching Cloudflare edge block (best effort).
const cloudflare = await removeCloudflareBlock(ip);
logger.info("Anti-DDoS block manually removed", {
staff: staff.username,
ip,
cloudflareCleared: cloudflare.removed,
});
} catch (err) {
logger.error("Failed to remove anti-DDoS block", { err, ip });
}
revalidatePath("/admin/devops/antiddos");
}
/** Remove an automatic Cloudflare edge block for a tracked IP. */
export async function removeCloudflareRule(formData: FormData): Promise<void> {
const staff = await requirePermission(PERMS.SETTINGS_VIEW);
const ip = str(formData.get("ip")).trim();
if (!ip) return;
const result = await removeCloudflareBlock(ip);
logger.info(
result.removed
? "Cloudflare automatic block removed"
: "Cloudflare automatic block removal skipped",
{
staff: staff.username,
ip,
message: result.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);
const status = await verifyCloudflareConnection();
await setLastCloudflareVerify(status);
logger.info("Cloudflare API configuration verified", {
staff: staff.username,
ok: status.ok,
zoneName: status.zoneName,
message: status.message,
});
revalidatePath("/admin/devops/antiddos");
}