From 5ddeebffc9d0c068953b7ed74730956355e21527 Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Sun, 29 Mar 2026 09:08:39 +0200 Subject: [PATCH 1/4] Add CrowdSec IP reputation and access control UI for reverse proxy services --- src/interfaces/ReverseProxy.ts | 12 ++ .../ReverseProxyAccessControlRules.tsx | 202 +++++++++++++++--- .../reverse-proxy/ReverseProxyModal.tsx | 1 + .../ReverseProxyEventsAuthMethodCell.tsx | 22 ++ .../events/ReverseProxyEventsReasonCell.tsx | 50 +++++ .../table/ReverseProxyAccessControlCell.tsx | 64 +++++- 6 files changed, 315 insertions(+), 36 deletions(-) diff --git a/src/interfaces/ReverseProxy.ts b/src/interfaces/ReverseProxy.ts index 3bff4dd4..e7e54153 100644 --- a/src/interfaces/ReverseProxy.ts +++ b/src/interfaces/ReverseProxy.ts @@ -22,11 +22,21 @@ export interface ReverseProxy { meta?: ReverseProxyMeta; } +export const CrowdSecMode = { + OFF: "off", + ENFORCE: "enforce", + OBSERVE: "observe", +} as const; + +export type CrowdSecMode = (typeof CrowdSecMode)[keyof typeof CrowdSecMode]; + export interface AccessRestrictions { allowed_cidrs?: string[]; blocked_cidrs?: string[]; allowed_countries?: string[]; blocked_countries?: string[]; + trusted_cidrs?: string[]; + crowdsec_mode?: CrowdSecMode; } export interface ReverseProxyMeta { @@ -102,6 +112,7 @@ export interface ReverseProxyDomain { target_cluster?: string; supports_custom_ports?: boolean; require_subdomain?: boolean; + supports_crowdsec?: boolean; } export enum ReverseProxyDomainType { @@ -149,6 +160,7 @@ export interface ReverseProxyEvent { bytes_upload: number; bytes_download: number; protocol?: EventProtocol; + metadata?: Record; } export function isL4Event(event: ReverseProxyEvent): boolean { diff --git a/src/modules/reverse-proxy/ReverseProxyAccessControlRules.tsx b/src/modules/reverse-proxy/ReverseProxyAccessControlRules.tsx index 0ced8faa..d5afb6b4 100644 --- a/src/modules/reverse-proxy/ReverseProxyAccessControlRules.tsx +++ b/src/modules/reverse-proxy/ReverseProxyAccessControlRules.tsx @@ -1,26 +1,42 @@ -import { useEffect, useMemo, useReducer, useRef } from "react"; +import { useEffect, useMemo, useReducer, useRef, useState } from "react"; import { Label } from "@components/Label"; import HelpText from "@components/HelpText"; import Button from "@components/Button"; import { Input } from "@components/Input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@components/Select"; import cidr from "ip-cidr"; import { + ChevronDownIcon, FlagIcon, MinusCircleIcon, NetworkIcon, PlusIcon, + ShieldAlertIcon, ShieldCheckIcon, ShieldXIcon, + StarIcon, WorkflowIcon, } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@components/DropdownMenu"; import { SelectDropdown, SelectOption, } from "@components/select/SelectDropdown"; import { CountrySelector } from "@/components/ui/CountrySelector"; -import { AccessRestrictions } from "@/interfaces/ReverseProxy"; +import { AccessRestrictions, CrowdSecMode } from "@/interfaces/ReverseProxy"; -type AccessAction = "allow" | "block"; +type AccessAction = "allow" | "block" | "trusted"; type AccessRuleType = "country" | "ip" | "cidr"; const ACTION_OPTIONS: SelectOption[] = [ @@ -34,6 +50,11 @@ const ACTION_OPTIONS: SelectOption[] = [ value: "block", icon: (props) => , }, + { + label: "Trusted", + value: "trusted", + icon: (props) => , + }, ]; const TYPE_OPTIONS: SelectOption[] = [ @@ -54,6 +75,10 @@ const TYPE_OPTIONS: SelectOption[] = [ }, ]; +const TYPE_OPTIONS_NO_COUNTRY: SelectOption[] = TYPE_OPTIONS.filter( + (o) => o.value !== "country", +); + type AccessRule = { id: string; action: AccessAction; @@ -63,6 +88,7 @@ type AccessRule = { type RulesAction = | { type: "add" } + | { type: "add_many"; rules: Omit[] } | { type: "remove"; id: string } | { type: "update"; @@ -71,6 +97,30 @@ type RulesAction = value: string; }; +type CIDRPreset = { + label: string; + cidrs: string[]; +}; + +const TRUSTED_PRESETS: CIDRPreset[] = [ + { + label: "RFC 1918 (Private IPv4)", + cidrs: ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"], + }, + { + label: "CGNAT (100.64/10)", + cidrs: ["100.64.0.0/10"], + }, + { + label: "IPv6 ULA", + cidrs: ["fc00::/7"], + }, + { + label: "Loopback", + cidrs: ["127.0.0.0/8", "::1/128"], + }, +]; + const nextId = () => crypto.randomUUID(); function rulesReducer(state: AccessRule[], action: RulesAction): AccessRule[] { @@ -80,6 +130,13 @@ function rulesReducer(state: AccessRule[], action: RulesAction): AccessRule[] { ...state, { id: nextId(), action: "allow", type: "country", value: "" }, ]; + case "add_many": { + const existing = new Set(state.map((r) => `${r.action}:${r.type}:${r.value}`)); + const newRules = action.rules + .filter((r) => !existing.has(`${r.action}:${r.type}:${r.value}`)) + .map((r) => ({ ...r, id: nextId() })); + return [...state, ...newRules]; + } case "remove": return state.filter((r) => r.id !== action.id); case "update": @@ -88,40 +145,48 @@ function rulesReducer(state: AccessRule[], action: RulesAction): AccessRule[] { if (action.field === "type") { return { ...r, type: action.value as AccessRuleType, value: "" }; } + if (action.field === "action" && action.value === "trusted" && r.type === "country") { + return { ...r, action: action.value as AccessAction, type: "cidr", value: "" }; + } return { ...r, [action.field]: action.value }; }); } } +function pushCidrRules(rules: AccessRule[], values: string[] | undefined, action: AccessAction) { + values?.forEach((v) => { + const isIp = v.endsWith("/32") || v.endsWith("/128"); + rules.push({ id: nextId(), action, type: isIp ? "ip" : "cidr", value: isIp ? v.replace(/\/(32|128)$/, "") : v }); + }); +} + function restrictionsToRules( restrictions: AccessRestrictions | undefined, ): AccessRule[] { if (!restrictions) return []; const rules: AccessRule[] = []; - restrictions.allowed_countries?.forEach((v) => - rules.push({ id: nextId(), action: "allow", type: "country", value: v }), - ); + // Trusted first, then block, then allow. + pushCidrRules(rules, restrictions.trusted_cidrs, "trusted"); + pushCidrRules(rules, restrictions.blocked_cidrs, "block"); restrictions.blocked_countries?.forEach((v) => rules.push({ id: nextId(), action: "block", type: "country", value: v }), ); - restrictions.allowed_cidrs?.forEach((v) => { - const isIp = v.endsWith("/32"); - rules.push({ id: nextId(), action: "allow", type: isIp ? "ip" : "cidr", value: isIp ? v.replace(/\/32$/, "") : v }); - }); - restrictions.blocked_cidrs?.forEach((v) => { - const isIp = v.endsWith("/32"); - rules.push({ id: nextId(), action: "block", type: isIp ? "ip" : "cidr", value: isIp ? v.replace(/\/32$/, "") : v }); - }); + pushCidrRules(rules, restrictions.allowed_cidrs, "allow"); + restrictions.allowed_countries?.forEach((v) => + rules.push({ id: nextId(), action: "allow", type: "country", value: v }), + ); return rules; } function rulesToRestrictions( rules: AccessRule[], + crowdsecMode?: CrowdSecMode, ): AccessRestrictions | undefined { const allowed_countries: string[] = []; const blocked_countries: string[] = []; const allowed_cidrs: string[] = []; const blocked_cidrs: string[] = []; + const trusted_cidrs: string[] = []; for (const rule of rules) { if (!rule.value) continue; @@ -129,17 +194,22 @@ function rulesToRestrictions( if (rule.action === "allow") allowed_countries.push(rule.value); else blocked_countries.push(rule.value); } else { - const value = rule.type === "ip" && !rule.value.includes("/") ? `${rule.value}/32` : rule.value; - if (rule.action === "allow") allowed_cidrs.push(value); + const suffix = rule.value.includes(":") ? "/128" : "/32"; + const value = rule.type === "ip" && !rule.value.includes("/") ? `${rule.value}${suffix}` : rule.value; + if (rule.action === "trusted") trusted_cidrs.push(value); + else if (rule.action === "allow") allowed_cidrs.push(value); else blocked_cidrs.push(value); } } + const hasCrowdSec = crowdsecMode != null && crowdsecMode !== CrowdSecMode.OFF; const hasAny = allowed_countries.length > 0 || blocked_countries.length > 0 || allowed_cidrs.length > 0 || - blocked_cidrs.length > 0; + blocked_cidrs.length > 0 || + trusted_cidrs.length > 0 || + hasCrowdSec; if (!hasAny) return undefined; @@ -148,6 +218,8 @@ function rulesToRestrictions( ...(blocked_countries.length > 0 && { blocked_countries }), ...(allowed_cidrs.length > 0 && { allowed_cidrs }), ...(blocked_cidrs.length > 0 && { blocked_cidrs }), + ...(trusted_cidrs.length > 0 && { trusted_cidrs }), + ...(hasCrowdSec && { crowdsec_mode: crowdsecMode }), }; } @@ -155,6 +227,7 @@ type Props = { value: AccessRestrictions | undefined; onChange: (value: AccessRestrictions | undefined) => void; onValidationChange?: (hasErrors: boolean) => void; + supportsCrowdSec?: boolean; }; function validateRule(rule: AccessRule): string { @@ -172,13 +245,17 @@ function validateRule(rule: AccessRule): string { return ""; } -export const ReverseProxyAccessControlRules = ({ value, onChange, onValidationChange }: Props) => { +export const ReverseProxyAccessControlRules = ({ value, onChange, onValidationChange, supportsCrowdSec }: Props) => { const [rules, dispatch] = useReducer( rulesReducer, value, restrictionsToRules, ); + const [crowdsecMode, setCrowdsecMode] = useState( + value?.crowdsec_mode ?? CrowdSecMode.OFF, + ); + const errors = useMemo( () => Object.fromEntries(rules.map((r) => [r.id, validateRule(r)])), [rules], @@ -196,8 +273,14 @@ export const ReverseProxyAccessControlRules = ({ value, onChange, onValidationCh onValidationChangeRef.current = onValidationChange; useEffect(() => { - onChangeRef.current(rulesToRestrictions(rules)); - }, [rules]); + if (!supportsCrowdSec) { + setCrowdsecMode(CrowdSecMode.OFF); + } + }, [supportsCrowdSec]); + + useEffect(() => { + onChangeRef.current(rulesToRestrictions(rules, crowdsecMode)); + }, [rules, crowdsecMode]); useEffect(() => { onValidationChangeRef.current?.(hasErrors); @@ -205,6 +288,32 @@ export const ReverseProxyAccessControlRules = ({ value, onChange, onValidationCh return (
+ {supportsCrowdSec && ( +
+ + + Block or monitor connections from IPs flagged by CrowdSec. Enforce + blocks immediately, observe logs without blocking. + + +
+ )} +
@@ -212,6 +321,8 @@ export const ReverseProxyAccessControlRules = ({ value, onChange, onValidationCh or CIDR block.
Block rules always take priority over allow rules. +
+ Trusted IPs bypass all restriction layers.
{rules.length > 0 && ( @@ -245,7 +356,7 @@ export const ReverseProxyAccessControlRules = ({ value, onChange, onValidationCh value: v, }) } - options={TYPE_OPTIONS} + options={rule.action === "trusted" ? TYPE_OPTIONS_NO_COUNTRY : TYPE_OPTIONS} compact />
@@ -301,15 +412,46 @@ export const ReverseProxyAccessControlRules = ({ value, onChange, onValidationCh ))} )} - +
+ + + + + + + {TRUSTED_PRESETS.map((preset) => ( + + dispatch({ + type: "add_many", + rules: preset.cidrs.map((c) => ({ + action: "trusted" as const, + type: "cidr" as const, + value: c, + })), + }) + } + > + + {preset.label} + + ))} + + +
); }; diff --git a/src/modules/reverse-proxy/ReverseProxyModal.tsx b/src/modules/reverse-proxy/ReverseProxyModal.tsx index 0ed64be1..8dc89185 100644 --- a/src/modules/reverse-proxy/ReverseProxyModal.tsx +++ b/src/modules/reverse-proxy/ReverseProxyModal.tsx @@ -590,6 +590,7 @@ export default function ReverseProxyModal({ value={accessRestrictions} onChange={setAccessRestrictions} onValidationChange={setAccessControlHasErrors} + supportsCrowdSec={selectedDomain?.supports_crowdsec} /> diff --git a/src/modules/reverse-proxy/events/ReverseProxyEventsAuthMethodCell.tsx b/src/modules/reverse-proxy/events/ReverseProxyEventsAuthMethodCell.tsx index 798ca9f6..f87c171f 100644 --- a/src/modules/reverse-proxy/events/ReverseProxyEventsAuthMethodCell.tsx +++ b/src/modules/reverse-proxy/events/ReverseProxyEventsAuthMethodCell.tsx @@ -7,6 +7,8 @@ import { Mail, Network, RectangleEllipsis, + ShieldAlert, + ShieldOff, Users, } from "lucide-react"; import * as React from "react"; @@ -69,6 +71,26 @@ export const ReverseProxyEventsAuthMethodCell = ({ event }: Props) => { icon: , label: "Geo Unavailable", }; + case "crowdsec_ban": + return { + icon: , + label: "CrowdSec Ban", + }; + case "crowdsec_captcha": + return { + icon: , + label: "CrowdSec Captcha", + }; + case "crowdsec_throttle": + return { + icon: , + label: "CrowdSec Throttle", + }; + case "crowdsec_unavailable": + return { + icon: , + label: "CrowdSec Unavailable", + }; default: return { icon: null, diff --git a/src/modules/reverse-proxy/events/ReverseProxyEventsReasonCell.tsx b/src/modules/reverse-proxy/events/ReverseProxyEventsReasonCell.tsx index c3540fe8..18a32fe4 100644 --- a/src/modules/reverse-proxy/events/ReverseProxyEventsReasonCell.tsx +++ b/src/modules/reverse-proxy/events/ReverseProxyEventsReasonCell.tsx @@ -1,11 +1,61 @@ +import Badge from "@components/Badge"; +import FullTooltip from "@components/FullTooltip"; +import { ListItem } from "@components/ListItem"; +import { Info, ShieldAlert } from "lucide-react"; import * as React from "react"; import { ReverseProxyEvent } from "@/interfaces/ReverseProxy"; +const VERDICT_LABELS: Record = { + crowdsec_ban: "Ban", + crowdsec_captcha: "Captcha", + crowdsec_throttle: "Throttle", +}; + type Props = { event: ReverseProxyEvent; }; export const ReverseProxyEventsReasonCell = ({ event }: Props) => { + const metadata = event.metadata; + const verdict = metadata?.crowdsec_verdict; + + if (verdict && !event.auth_method_used?.startsWith("crowdsec_")) { + const verdictLabel = VERDICT_LABELS[verdict] ?? verdict; + const metaEntries = Object.entries(metadata!).filter( + ([k]) => k !== "crowdsec_verdict", + ); + + return ( + + {metaEntries.map(([key, val]) => ( + } + label={key.replaceAll("_", " ")} + value={{val}} + /> + ))} + + } + > +
+ + + CrowdSec Observe: {verdictLabel} + +
+
+ ); + } + return ( {event.reason || "-"} diff --git a/src/modules/reverse-proxy/table/ReverseProxyAccessControlCell.tsx b/src/modules/reverse-proxy/table/ReverseProxyAccessControlCell.tsx index 823b468f..8121f332 100644 --- a/src/modules/reverse-proxy/table/ReverseProxyAccessControlCell.tsx +++ b/src/modules/reverse-proxy/table/ReverseProxyAccessControlCell.tsx @@ -10,6 +10,7 @@ import { LucideIcon, NetworkIcon, Settings, + ShieldAlert, ShieldCheck, ShieldOff, WorkflowIcon, @@ -19,7 +20,7 @@ import { useMemo } from "react"; import { useCountries } from "@/contexts/CountryProvider"; import { usePermissions } from "@/contexts/PermissionsProvider"; import { useReverseProxies } from "@/contexts/ReverseProxiesProvider"; -import { ReverseProxy } from "@/interfaces/ReverseProxy"; +import { CrowdSecMode, ReverseProxy } from "@/interfaces/ReverseProxy"; type RuleEntry = { key: string; @@ -27,6 +28,7 @@ type RuleEntry = { Icon: LucideIcon; value: string; blocked?: boolean; + trusted?: boolean; }; type Props = { @@ -37,17 +39,30 @@ export default function ReverseProxyAccessControlCell({ reverseProxy, }: Readonly) { const { permission } = usePermissions(); - const { openModal } = useReverseProxies(); + const { openModal, domains } = useReverseProxies(); const { countries } = useCountries(); const canConfigure = !!permission?.services?.update; const restrictions = reverseProxy.access_restrictions; + const supportsCrowdSec = domains?.find( + (d) => d.domain === reverseProxy.proxy_cluster, + )?.supports_crowdsec; + + const hasCrowdSec = + supportsCrowdSec && + restrictions?.crowdsec_mode != null && + restrictions.crowdsec_mode !== CrowdSecMode.OFF; + + const hasTrustedCidrs = (restrictions?.trusted_cidrs?.length ?? 0) > 0; + const ruleCount = (restrictions?.allowed_cidrs?.length ?? 0) + (restrictions?.blocked_cidrs?.length ?? 0) + (restrictions?.allowed_countries?.length ?? 0) + - (restrictions?.blocked_countries?.length ?? 0); + (restrictions?.blocked_countries?.length ?? 0) + + (restrictions?.trusted_cidrs?.length ?? 0) + + (hasCrowdSec ? 1 : 0); const rulesBadge = ruleCount > 0 ? ( @@ -139,8 +154,45 @@ export default function ReverseProxyAccessControlCell({ }); } + if (hasTrustedCidrs) { + const trustedIps = restrictions!.trusted_cidrs!.filter((c) => c.endsWith("/32")); + const trustedCidrs = restrictions!.trusted_cidrs!.filter((c) => !c.endsWith("/32")); + + if (trustedIps.length) { + entries.push({ + key: "trusted-ips", + label: trustedIps.length === 1 ? "Trusted IP" : "Trusted IPs", + Icon: WorkflowIcon, + value: trustedIps.map((c) => c.replace(/\/32$/, "")).join(", "), + trusted: true, + }); + } + + if (trustedCidrs.length) { + entries.push({ + key: "trusted-cidrs", + label: trustedCidrs.length === 1 ? "Trusted CIDR" : "Trusted CIDRs", + Icon: NetworkIcon, + value: trustedCidrs.join(", "), + trusted: true, + }); + } + } + + if (hasCrowdSec) { + entries.push({ + key: "crowdsec", + label: "CrowdSec", + Icon: ShieldAlert, + value: + restrictions?.crowdsec_mode === CrowdSecMode.ENFORCE + ? "Enforce" + : "Observe", + }); + } + return entries; - }, [restrictions, countries]); + }, [restrictions, countries, hasTrustedCidrs, hasCrowdSec]); const showRulesHover = ruleGroups.length > 0; @@ -165,7 +217,7 @@ export default function ReverseProxyAccessControlCell({ onClick={(e) => e.stopPropagation()} >
- {ruleGroups.map(({ key, label, Icon, value, blocked }) => ( + {ruleGroups.map(({ key, label, Icon, value, blocked, trusted }) => (
{label} From d882360fddc42368e48caf1e6e7f4b8708401727 Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Wed, 1 Apr 2026 20:30:35 +0200 Subject: [PATCH 2/4] Fix IPv6 single-host CIDR detection in access control rules --- .../ReverseProxyAccessControlRules.tsx | 2 +- .../table/ReverseProxyAccessControlCell.tsx | 20 ++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/modules/reverse-proxy/ReverseProxyAccessControlRules.tsx b/src/modules/reverse-proxy/ReverseProxyAccessControlRules.tsx index d5afb6b4..b7776edd 100644 --- a/src/modules/reverse-proxy/ReverseProxyAccessControlRules.tsx +++ b/src/modules/reverse-proxy/ReverseProxyAccessControlRules.tsx @@ -155,7 +155,7 @@ function rulesReducer(state: AccessRule[], action: RulesAction): AccessRule[] { function pushCidrRules(rules: AccessRule[], values: string[] | undefined, action: AccessAction) { values?.forEach((v) => { - const isIp = v.endsWith("/32") || v.endsWith("/128"); + const isIp = v.includes(":") ? v.endsWith("/128") : v.endsWith("/32"); rules.push({ id: nextId(), action, type: isIp ? "ip" : "cidr", value: isIp ? v.replace(/\/(32|128)$/, "") : v }); }); } diff --git a/src/modules/reverse-proxy/table/ReverseProxyAccessControlCell.tsx b/src/modules/reverse-proxy/table/ReverseProxyAccessControlCell.tsx index 8121f332..840e2567 100644 --- a/src/modules/reverse-proxy/table/ReverseProxyAccessControlCell.tsx +++ b/src/modules/reverse-proxy/table/ReverseProxyAccessControlCell.tsx @@ -107,21 +107,23 @@ export default function ReverseProxyAccessControlCell({ }); } + const isHostCidr = (c: string) => + c.includes(":") ? c.endsWith("/128") : c.endsWith("/32"); const allowedIps = - restrictions?.allowed_cidrs?.filter((c) => c.endsWith("/32")) ?? []; + restrictions?.allowed_cidrs?.filter(isHostCidr) ?? []; const allowedCidrs = - restrictions?.allowed_cidrs?.filter((c) => !c.endsWith("/32")) ?? []; + restrictions?.allowed_cidrs?.filter((c) => !isHostCidr(c)) ?? []; const blockedIps = - restrictions?.blocked_cidrs?.filter((c) => c.endsWith("/32")) ?? []; + restrictions?.blocked_cidrs?.filter(isHostCidr) ?? []; const blockedCidrs = - restrictions?.blocked_cidrs?.filter((c) => !c.endsWith("/32")) ?? []; + restrictions?.blocked_cidrs?.filter((c) => !isHostCidr(c)) ?? []; if (allowedIps.length) { entries.push({ key: "allowed-ips", label: allowedIps.length === 1 ? "Allowed IP" : "Allowed IPs", Icon: WorkflowIcon, - value: allowedIps.map((c) => c.replace(/\/32$/, "")).join(", "), + value: allowedIps.map((c) => c.replace(/\/(32|128)$/, "")).join(", "), }); } @@ -139,7 +141,7 @@ export default function ReverseProxyAccessControlCell({ key: "blocked-ips", label: blockedIps.length === 1 ? "Blocked IP" : "Blocked IPs", Icon: WorkflowIcon, - value: blockedIps.map((c) => c.replace(/\/32$/, "")).join(", "), + value: blockedIps.map((c) => c.replace(/\/(32|128)$/, "")).join(", "), blocked: true, }); } @@ -155,15 +157,15 @@ export default function ReverseProxyAccessControlCell({ } if (hasTrustedCidrs) { - const trustedIps = restrictions!.trusted_cidrs!.filter((c) => c.endsWith("/32")); - const trustedCidrs = restrictions!.trusted_cidrs!.filter((c) => !c.endsWith("/32")); + const trustedIps = restrictions!.trusted_cidrs!.filter(isHostCidr); + const trustedCidrs = restrictions!.trusted_cidrs!.filter((c) => !isHostCidr(c)); if (trustedIps.length) { entries.push({ key: "trusted-ips", label: trustedIps.length === 1 ? "Trusted IP" : "Trusted IPs", Icon: WorkflowIcon, - value: trustedIps.map((c) => c.replace(/\/32$/, "")).join(", "), + value: trustedIps.map((c) => c.replace(/\/(32|128)$/, "")).join(", "), trusted: true, }); } From 57faaf5ea040bf30f776793da261edd6d3f2fd7d Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Thu, 9 Apr 2026 16:24:46 +0200 Subject: [PATCH 3/4] Remove trusted_cidrs from CrowdSec integration --- src/interfaces/ReverseProxy.ts | 1 - .../ReverseProxyAccessControlRules.tsx | 95 +------------------ .../table/ReverseProxyAccessControlCell.tsx | 35 +------ 3 files changed, 6 insertions(+), 125 deletions(-) diff --git a/src/interfaces/ReverseProxy.ts b/src/interfaces/ReverseProxy.ts index e7e54153..192dd705 100644 --- a/src/interfaces/ReverseProxy.ts +++ b/src/interfaces/ReverseProxy.ts @@ -35,7 +35,6 @@ export interface AccessRestrictions { blocked_cidrs?: string[]; allowed_countries?: string[]; blocked_countries?: string[]; - trusted_cidrs?: string[]; crowdsec_mode?: CrowdSecMode; } diff --git a/src/modules/reverse-proxy/ReverseProxyAccessControlRules.tsx b/src/modules/reverse-proxy/ReverseProxyAccessControlRules.tsx index b7776edd..1a441bf1 100644 --- a/src/modules/reverse-proxy/ReverseProxyAccessControlRules.tsx +++ b/src/modules/reverse-proxy/ReverseProxyAccessControlRules.tsx @@ -12,7 +12,6 @@ import { } from "@components/Select"; import cidr from "ip-cidr"; import { - ChevronDownIcon, FlagIcon, MinusCircleIcon, NetworkIcon, @@ -20,15 +19,8 @@ import { ShieldAlertIcon, ShieldCheckIcon, ShieldXIcon, - StarIcon, WorkflowIcon, } from "lucide-react"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@components/DropdownMenu"; import { SelectDropdown, SelectOption, @@ -36,7 +28,7 @@ import { import { CountrySelector } from "@/components/ui/CountrySelector"; import { AccessRestrictions, CrowdSecMode } from "@/interfaces/ReverseProxy"; -type AccessAction = "allow" | "block" | "trusted"; +type AccessAction = "allow" | "block"; type AccessRuleType = "country" | "ip" | "cidr"; const ACTION_OPTIONS: SelectOption[] = [ @@ -50,11 +42,6 @@ const ACTION_OPTIONS: SelectOption[] = [ value: "block", icon: (props) => , }, - { - label: "Trusted", - value: "trusted", - icon: (props) => , - }, ]; const TYPE_OPTIONS: SelectOption[] = [ @@ -75,10 +62,6 @@ const TYPE_OPTIONS: SelectOption[] = [ }, ]; -const TYPE_OPTIONS_NO_COUNTRY: SelectOption[] = TYPE_OPTIONS.filter( - (o) => o.value !== "country", -); - type AccessRule = { id: string; action: AccessAction; @@ -88,7 +71,6 @@ type AccessRule = { type RulesAction = | { type: "add" } - | { type: "add_many"; rules: Omit[] } | { type: "remove"; id: string } | { type: "update"; @@ -97,30 +79,6 @@ type RulesAction = value: string; }; -type CIDRPreset = { - label: string; - cidrs: string[]; -}; - -const TRUSTED_PRESETS: CIDRPreset[] = [ - { - label: "RFC 1918 (Private IPv4)", - cidrs: ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"], - }, - { - label: "CGNAT (100.64/10)", - cidrs: ["100.64.0.0/10"], - }, - { - label: "IPv6 ULA", - cidrs: ["fc00::/7"], - }, - { - label: "Loopback", - cidrs: ["127.0.0.0/8", "::1/128"], - }, -]; - const nextId = () => crypto.randomUUID(); function rulesReducer(state: AccessRule[], action: RulesAction): AccessRule[] { @@ -130,13 +88,6 @@ function rulesReducer(state: AccessRule[], action: RulesAction): AccessRule[] { ...state, { id: nextId(), action: "allow", type: "country", value: "" }, ]; - case "add_many": { - const existing = new Set(state.map((r) => `${r.action}:${r.type}:${r.value}`)); - const newRules = action.rules - .filter((r) => !existing.has(`${r.action}:${r.type}:${r.value}`)) - .map((r) => ({ ...r, id: nextId() })); - return [...state, ...newRules]; - } case "remove": return state.filter((r) => r.id !== action.id); case "update": @@ -145,9 +96,6 @@ function rulesReducer(state: AccessRule[], action: RulesAction): AccessRule[] { if (action.field === "type") { return { ...r, type: action.value as AccessRuleType, value: "" }; } - if (action.field === "action" && action.value === "trusted" && r.type === "country") { - return { ...r, action: action.value as AccessAction, type: "cidr", value: "" }; - } return { ...r, [action.field]: action.value }; }); } @@ -165,8 +113,6 @@ function restrictionsToRules( ): AccessRule[] { if (!restrictions) return []; const rules: AccessRule[] = []; - // Trusted first, then block, then allow. - pushCidrRules(rules, restrictions.trusted_cidrs, "trusted"); pushCidrRules(rules, restrictions.blocked_cidrs, "block"); restrictions.blocked_countries?.forEach((v) => rules.push({ id: nextId(), action: "block", type: "country", value: v }), @@ -186,7 +132,6 @@ function rulesToRestrictions( const blocked_countries: string[] = []; const allowed_cidrs: string[] = []; const blocked_cidrs: string[] = []; - const trusted_cidrs: string[] = []; for (const rule of rules) { if (!rule.value) continue; @@ -196,8 +141,7 @@ function rulesToRestrictions( } else { const suffix = rule.value.includes(":") ? "/128" : "/32"; const value = rule.type === "ip" && !rule.value.includes("/") ? `${rule.value}${suffix}` : rule.value; - if (rule.action === "trusted") trusted_cidrs.push(value); - else if (rule.action === "allow") allowed_cidrs.push(value); + if (rule.action === "allow") allowed_cidrs.push(value); else blocked_cidrs.push(value); } } @@ -208,7 +152,6 @@ function rulesToRestrictions( blocked_countries.length > 0 || allowed_cidrs.length > 0 || blocked_cidrs.length > 0 || - trusted_cidrs.length > 0 || hasCrowdSec; if (!hasAny) return undefined; @@ -218,7 +161,6 @@ function rulesToRestrictions( ...(blocked_countries.length > 0 && { blocked_countries }), ...(allowed_cidrs.length > 0 && { allowed_cidrs }), ...(blocked_cidrs.length > 0 && { blocked_cidrs }), - ...(trusted_cidrs.length > 0 && { trusted_cidrs }), ...(hasCrowdSec && { crowdsec_mode: crowdsecMode }), }; } @@ -321,8 +263,6 @@ export const ReverseProxyAccessControlRules = ({ value, onChange, onValidationCh or CIDR block.
Block rules always take priority over allow rules. -
- Trusted IPs bypass all restriction layers.
{rules.length > 0 && ( @@ -356,7 +296,7 @@ export const ReverseProxyAccessControlRules = ({ value, onChange, onValidationCh value: v, }) } - options={rule.action === "trusted" ? TYPE_OPTIONS_NO_COUNTRY : TYPE_OPTIONS} + options={TYPE_OPTIONS} compact />
@@ -422,35 +362,6 @@ export const ReverseProxyAccessControlRules = ({ value, onChange, onValidationCh Add Rule - - - - - - {TRUSTED_PRESETS.map((preset) => ( - - dispatch({ - type: "add_many", - rules: preset.cidrs.map((c) => ({ - action: "trusted" as const, - type: "cidr" as const, - value: c, - })), - }) - } - > - - {preset.label} - - ))} - - ); diff --git a/src/modules/reverse-proxy/table/ReverseProxyAccessControlCell.tsx b/src/modules/reverse-proxy/table/ReverseProxyAccessControlCell.tsx index 840e2567..e2af02ef 100644 --- a/src/modules/reverse-proxy/table/ReverseProxyAccessControlCell.tsx +++ b/src/modules/reverse-proxy/table/ReverseProxyAccessControlCell.tsx @@ -28,7 +28,6 @@ type RuleEntry = { Icon: LucideIcon; value: string; blocked?: boolean; - trusted?: boolean; }; type Props = { @@ -54,14 +53,11 @@ export default function ReverseProxyAccessControlCell({ restrictions?.crowdsec_mode != null && restrictions.crowdsec_mode !== CrowdSecMode.OFF; - const hasTrustedCidrs = (restrictions?.trusted_cidrs?.length ?? 0) > 0; - const ruleCount = (restrictions?.allowed_cidrs?.length ?? 0) + (restrictions?.blocked_cidrs?.length ?? 0) + (restrictions?.allowed_countries?.length ?? 0) + (restrictions?.blocked_countries?.length ?? 0) + - (restrictions?.trusted_cidrs?.length ?? 0) + (hasCrowdSec ? 1 : 0); const rulesBadge = @@ -156,31 +152,6 @@ export default function ReverseProxyAccessControlCell({ }); } - if (hasTrustedCidrs) { - const trustedIps = restrictions!.trusted_cidrs!.filter(isHostCidr); - const trustedCidrs = restrictions!.trusted_cidrs!.filter((c) => !isHostCidr(c)); - - if (trustedIps.length) { - entries.push({ - key: "trusted-ips", - label: trustedIps.length === 1 ? "Trusted IP" : "Trusted IPs", - Icon: WorkflowIcon, - value: trustedIps.map((c) => c.replace(/\/(32|128)$/, "")).join(", "), - trusted: true, - }); - } - - if (trustedCidrs.length) { - entries.push({ - key: "trusted-cidrs", - label: trustedCidrs.length === 1 ? "Trusted CIDR" : "Trusted CIDRs", - Icon: NetworkIcon, - value: trustedCidrs.join(", "), - trusted: true, - }); - } - } - if (hasCrowdSec) { entries.push({ key: "crowdsec", @@ -194,7 +165,7 @@ export default function ReverseProxyAccessControlCell({ } return entries; - }, [restrictions, countries, hasTrustedCidrs, hasCrowdSec]); + }, [restrictions, countries, hasCrowdSec]); const showRulesHover = ruleGroups.length > 0; @@ -219,7 +190,7 @@ export default function ReverseProxyAccessControlCell({ onClick={(e) => e.stopPropagation()} >
- {ruleGroups.map(({ key, label, Icon, value, blocked, trusted }) => ( + {ruleGroups.map(({ key, label, Icon, value, blocked }) => (
{label} From fcfc0bfeb9b8ade1ec6ce5710d7a4d7af7cadd20 Mon Sep 17 00:00:00 2001 From: Eduard Gert Date: Tue, 21 Apr 2026 11:11:07 +0200 Subject: [PATCH 4/4] Update crowdsec selection --- src/assets/integrations/crowdsec.png | Bin 0 -> 17966 bytes .../ReverseProxyAccessControlRules.tsx | 61 +++++----- .../ReverseProxyCrowdSecIPReputation.tsx | 109 ++++++++++++++++++ 3 files changed, 135 insertions(+), 35 deletions(-) create mode 100644 src/assets/integrations/crowdsec.png create mode 100644 src/modules/reverse-proxy/ReverseProxyCrowdSecIPReputation.tsx diff --git a/src/assets/integrations/crowdsec.png b/src/assets/integrations/crowdsec.png new file mode 100644 index 0000000000000000000000000000000000000000..6f003d3ac89f31e8fe437fe1d417fac66a196fe9 GIT binary patch literal 17966 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 literal 0 HcmV?d00001 diff --git a/src/modules/reverse-proxy/ReverseProxyAccessControlRules.tsx b/src/modules/reverse-proxy/ReverseProxyAccessControlRules.tsx index 1a441bf1..7dd9fda0 100644 --- a/src/modules/reverse-proxy/ReverseProxyAccessControlRules.tsx +++ b/src/modules/reverse-proxy/ReverseProxyAccessControlRules.tsx @@ -3,20 +3,12 @@ import { Label } from "@components/Label"; import HelpText from "@components/HelpText"; import Button from "@components/Button"; import { Input } from "@components/Input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@components/Select"; import cidr from "ip-cidr"; import { FlagIcon, MinusCircleIcon, NetworkIcon, PlusIcon, - ShieldAlertIcon, ShieldCheckIcon, ShieldXIcon, WorkflowIcon, @@ -27,6 +19,7 @@ import { } from "@components/select/SelectDropdown"; import { CountrySelector } from "@/components/ui/CountrySelector"; import { AccessRestrictions, CrowdSecMode } from "@/interfaces/ReverseProxy"; +import { ReverseProxyCrowdSecIPReputation } from "@/modules/reverse-proxy/ReverseProxyCrowdSecIPReputation"; type AccessAction = "allow" | "block"; type AccessRuleType = "country" | "ip" | "cidr"; @@ -101,10 +94,19 @@ function rulesReducer(state: AccessRule[], action: RulesAction): AccessRule[] { } } -function pushCidrRules(rules: AccessRule[], values: string[] | undefined, action: AccessAction) { +function pushCidrRules( + rules: AccessRule[], + values: string[] | undefined, + action: AccessAction, +) { values?.forEach((v) => { const isIp = v.includes(":") ? v.endsWith("/128") : v.endsWith("/32"); - rules.push({ id: nextId(), action, type: isIp ? "ip" : "cidr", value: isIp ? v.replace(/\/(32|128)$/, "") : v }); + rules.push({ + id: nextId(), + action, + type: isIp ? "ip" : "cidr", + value: isIp ? v.replace(/\/(32|128)$/, "") : v, + }); }); } @@ -140,7 +142,10 @@ function rulesToRestrictions( else blocked_countries.push(rule.value); } else { const suffix = rule.value.includes(":") ? "/128" : "/32"; - const value = rule.type === "ip" && !rule.value.includes("/") ? `${rule.value}${suffix}` : rule.value; + const value = + rule.type === "ip" && !rule.value.includes("/") + ? `${rule.value}${suffix}` + : rule.value; if (rule.action === "allow") allowed_cidrs.push(value); else blocked_cidrs.push(value); } @@ -187,7 +192,12 @@ function validateRule(rule: AccessRule): string { return ""; } -export const ReverseProxyAccessControlRules = ({ value, onChange, onValidationChange, supportsCrowdSec }: Props) => { +export const ReverseProxyAccessControlRules = ({ + value, + onChange, + onValidationChange, + supportsCrowdSec, +}: Props) => { const [rules, dispatch] = useReducer( rulesReducer, value, @@ -231,29 +241,10 @@ export const ReverseProxyAccessControlRules = ({ value, onChange, onValidationCh return (
{supportsCrowdSec && ( -
- - - Block or monitor connections from IPs flagged by CrowdSec. Enforce - blocks immediately, observe logs without blocking. - - -
+ )}
diff --git a/src/modules/reverse-proxy/ReverseProxyCrowdSecIPReputation.tsx b/src/modules/reverse-proxy/ReverseProxyCrowdSecIPReputation.tsx new file mode 100644 index 00000000..18ce9976 --- /dev/null +++ b/src/modules/reverse-proxy/ReverseProxyCrowdSecIPReputation.tsx @@ -0,0 +1,109 @@ +import * as React from "react"; +import { ReactNode } from "react"; +import { Label } from "@components/Label"; +import HelpText from "@components/HelpText"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@components/Select"; +import { EyeIcon, PowerOffIcon, ShieldCheckIcon } from "lucide-react"; +import { HelpTooltip } from "@components/HelpTooltip"; +import { CrowdSecMode } from "@/interfaces/ReverseProxy"; +import Image from "next/image"; +import CrowdSecIconImage from "@/assets/integrations/crowdsec.png"; + +type Props = { + value: CrowdSecMode; + onChange: (value: CrowdSecMode) => void; +}; + +type CrowdSecOption = { + label: string; + description?: string; + icon: ReactNode; +}; + +const CROWDSEC_OPTIONS: Record = { + [CrowdSecMode.OFF]: { + label: "Disabled", + icon: , + }, + [CrowdSecMode.ENFORCE]: { + label: "Enforce", + description: + "Blocked IPs are denied immediately. If the bouncer is not yet synced, connections are denied (fail-closed).", + icon: , + }, + [CrowdSecMode.OBSERVE]: { + label: "Observe", + description: + "Blocked IPs are logged but not denied. Use this to evaluate CrowdSec before enforcing.", + icon: , + }, +}; + +export const ReverseProxyCrowdSecIPReputation = ({ + value, + onChange, +}: Props) => { + const selected = CROWDSEC_OPTIONS[value]; + + return ( +
+
+
+ {"CrowdSec"} +
+
+ + + Detect malicious IPs with CrowdSec.{" "} + Enforce to block them or{" "} + Observe to only log without + blocking. + +
+
+ + +
+ ); +};