feat(waf): CRS-App-Exclusion-Plugins (Nextcloud/WordPress/Drupal) pro Domain — v1.3.12

Statt manueller SecRuleRemoveById-IDs kann man pro Domain offizielle OWASP-CRS-
Exclusion-Plugins aktivieren — pfad-genaue, upstream-gepflegte App-Ausnahmen.

- Migration 0046: waf_configs.crs_plugins text[].
- Engine (engine.go): je gewähltem Plugin werden config/before VOR den CRS-Rules
  und after DANACH inkludiert (exakt nach OWASP-CRS-Plugin-Spec); nur die für
  DIESE Domain gewählten, nur wenn die Datei existiert. Whitelist KnownCRSPlugins.
- Handler: crs_plugins im Upsert-Body + Whitelist-Validierung (Include-Pfad-
  Injection-Schutz).
- Packaging (postinst): lädt die Plugins (coreruleset/<name>-plugin) nach
  <crs>/plugins/ — self-healing auf jedem configure, nur fehlende.
- UI: Multi-Select „App-Profile (CRS-Plugins)" im WAF-Config-Drawer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-08-03 10:28:03 +02:00
parent 088910ee19
commit 37f729381d
11 changed files with 220 additions and 19 deletions

View File

@@ -1 +1 @@
1.3.11 1.3.12

View File

@@ -0,0 +1,19 @@
-- +goose Up
-- +goose StatementBegin
-- CRS-App-Exclusion-Plugins pro Domain (OWASP-CRS-Plugin-System). Liste von
-- Plugin-Namen (z. B. 'nextcloud','wordpress','drupal'). Der WAF-Renderer
-- inkludiert je gewähltem Plugin dessen config/before/after-Dateien aus
-- <crsDir>/plugins/ an den korrekten Punkten (config+before VOR den CRS-Rules,
-- after DANACH) → pfad-genaue, upstream-gepflegte App-Ausnahmen statt manueller
-- SecRuleRemoveById-IDs. waf_configs ist repliziert; der Renderer läuft pro
-- Node lokal, daher kein Cross-Node-Effekt außer der Config selbst.
ALTER TABLE waf_configs
ADD COLUMN IF NOT EXISTS crs_plugins TEXT[] NOT NULL DEFAULT '{}';
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
ALTER TABLE waf_configs DROP COLUMN IF EXISTS crs_plugins;
-- +goose StatementEnd

View File

@@ -16,6 +16,7 @@ import (
"git.netcell-it.de/projekte/edgeguard-native/internal/models" "git.netcell-it.de/projekte/edgeguard-native/internal/models"
"git.netcell-it.de/projekte/edgeguard-native/internal/services/audit" "git.netcell-it.de/projekte/edgeguard-native/internal/services/audit"
wafsvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/waf" wafsvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/waf"
intwaf "git.netcell-it.de/projekte/edgeguard-native/internal/waf"
) )
// wafRuleIDRe erlaubt nur einzelne CRS-Rule-IDs oder Ranges ("942100" / // wafRuleIDRe erlaubt nur einzelne CRS-Rule-IDs oder Ranges ("942100" /
@@ -80,13 +81,14 @@ func (h *WafHandler) Get(c *gin.Context) {
// upsertBody is the accepted JSON for PUT /waf/configs/:domain_id. // upsertBody is the accepted JSON for PUT /waf/configs/:domain_id.
type upsertBody struct { type upsertBody struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
Mode string `json:"mode"` Mode string `json:"mode"`
ParanoiaLevel int `json:"paranoia_level"` ParanoiaLevel int `json:"paranoia_level"`
RuleExclusions []string `json:"rule_exclusions"` RuleExclusions []string `json:"rule_exclusions"`
ExclusionNotes map[string]string `json:"exclusion_notes"` CRSPlugins []string `json:"crs_plugins"`
TrustedProxies []string `json:"trusted_proxies"` ExclusionNotes map[string]string `json:"exclusion_notes"`
CustomRules string `json:"custom_rules"` TrustedProxies []string `json:"trusted_proxies"`
CustomRules string `json:"custom_rules"`
} }
// Upsert creates or updates the WAF config for a domain. // Upsert creates or updates the WAF config for a domain.
@@ -110,9 +112,20 @@ func (h *WafHandler) Upsert(c *gin.Context) {
if body.RuleExclusions == nil { if body.RuleExclusions == nil {
body.RuleExclusions = []string{} body.RuleExclusions = []string{}
} }
if body.CRSPlugins == nil {
body.CRSPlugins = []string{}
}
if body.TrustedProxies == nil { if body.TrustedProxies == nil {
body.TrustedProxies = []string{} body.TrustedProxies = []string{}
} }
// CRS-Plugins müssen aus der bekannten Whitelist stammen — sie werden zu
// Include-Pfaden, ein unbekannter Name wäre Pfad-Injection.
for _, p := range body.CRSPlugins {
if _, ok := intwaf.KnownCRSPlugins[strings.TrimSpace(p)]; !ok {
response.BadRequest(c, errors.New("unbekanntes CRS-Plugin: "+p))
return
}
}
if body.ExclusionNotes == nil { if body.ExclusionNotes == nil {
body.ExclusionNotes = map[string]string{} body.ExclusionNotes = map[string]string{}
@@ -144,6 +157,7 @@ func (h *WafHandler) Upsert(c *gin.Context) {
Mode: body.Mode, Mode: body.Mode,
ParanoiaLevel: body.ParanoiaLevel, ParanoiaLevel: body.ParanoiaLevel,
RuleExclusions: body.RuleExclusions, RuleExclusions: body.RuleExclusions,
CRSPlugins: body.CRSPlugins,
ExclusionNotes: body.ExclusionNotes, ExclusionNotes: body.ExclusionNotes,
TrustedProxies: body.TrustedProxies, TrustedProxies: body.TrustedProxies,
CustomRules: body.CustomRules, CustomRules: body.CustomRules,

View File

@@ -11,6 +11,10 @@ type WafConfig struct {
Mode string `gorm:"column:mode" json:"mode"` // "detection" | "blocking" Mode string `gorm:"column:mode" json:"mode"` // "detection" | "blocking"
ParanoiaLevel int `gorm:"column:paranoia_level" json:"paranoia_level"` // 14 ParanoiaLevel int `gorm:"column:paranoia_level" json:"paranoia_level"` // 14
RuleExclusions []string `gorm:"column:rule_exclusions;type:text[]" json:"rule_exclusions"` RuleExclusions []string `gorm:"column:rule_exclusions;type:text[]" json:"rule_exclusions"`
// CRSPlugins: aktivierte OWASP-CRS-App-Exclusion-Plugins (z. B.
// "nextcloud","wordpress"). Der Renderer inkludiert je Plugin dessen
// config/before/after-Dateien aus <crsDir>/plugins/.
CRSPlugins []string `gorm:"column:crs_plugins;type:text[]" json:"crs_plugins"`
ExclusionNotes map[string]string `gorm:"column:exclusion_notes;type:jsonb" json:"exclusion_notes"` // rule_id → note ExclusionNotes map[string]string `gorm:"column:exclusion_notes;type:jsonb" json:"exclusion_notes"` // rule_id → note
TrustedProxies []string `gorm:"column:trusted_proxies;type:text[]" json:"trusted_proxies"` TrustedProxies []string `gorm:"column:trusted_proxies;type:text[]" json:"trusted_proxies"`
CustomRules string `gorm:"column:custom_rules" json:"custom_rules"` CustomRules string `gorm:"column:custom_rules" json:"custom_rules"`

View File

@@ -22,7 +22,7 @@ func New(pool *pgxpool.Pool) *Repo { return &Repo{Pool: pool} }
const baseSelect = ` const baseSelect = `
SELECT id, domain_id, enabled, mode, paranoia_level, SELECT id, domain_id, enabled, mode, paranoia_level,
rule_exclusions, exclusion_notes, trusted_proxies, custom_rules, updated_at rule_exclusions, crs_plugins, exclusion_notes, trusted_proxies, custom_rules, updated_at
FROM waf_configs FROM waf_configs
` `
@@ -30,7 +30,7 @@ func scan(row pgx.Row) (*models.WafConfig, error) {
var c models.WafConfig var c models.WafConfig
err := row.Scan( err := row.Scan(
&c.ID, &c.DomainID, &c.Enabled, &c.Mode, &c.ParanoiaLevel, &c.ID, &c.DomainID, &c.Enabled, &c.Mode, &c.ParanoiaLevel,
&c.RuleExclusions, &c.ExclusionNotes, &c.TrustedProxies, &c.CustomRules, &c.UpdatedAt, &c.RuleExclusions, &c.CRSPlugins, &c.ExclusionNotes, &c.TrustedProxies, &c.CustomRules, &c.UpdatedAt,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -82,22 +82,23 @@ func (r *Repo) Upsert(ctx context.Context, c models.WafConfig) (*models.WafConfi
row := r.Pool.QueryRow(ctx, ` row := r.Pool.QueryRow(ctx, `
INSERT INTO waf_configs INSERT INTO waf_configs
(domain_id, enabled, mode, paranoia_level, (domain_id, enabled, mode, paranoia_level,
rule_exclusions, exclusion_notes, trusted_proxies, custom_rules, updated_at) rule_exclusions, crs_plugins, exclusion_notes, trusted_proxies, custom_rules, updated_at)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)
ON CONFLICT (domain_id) DO UPDATE SET ON CONFLICT (domain_id) DO UPDATE SET
enabled = EXCLUDED.enabled, enabled = EXCLUDED.enabled,
mode = EXCLUDED.mode, mode = EXCLUDED.mode,
paranoia_level = EXCLUDED.paranoia_level, paranoia_level = EXCLUDED.paranoia_level,
rule_exclusions = EXCLUDED.rule_exclusions, rule_exclusions = EXCLUDED.rule_exclusions,
crs_plugins = EXCLUDED.crs_plugins,
exclusion_notes = EXCLUDED.exclusion_notes, exclusion_notes = EXCLUDED.exclusion_notes,
trusted_proxies = EXCLUDED.trusted_proxies, trusted_proxies = EXCLUDED.trusted_proxies,
custom_rules = EXCLUDED.custom_rules, custom_rules = EXCLUDED.custom_rules,
updated_at = EXCLUDED.updated_at updated_at = EXCLUDED.updated_at
RETURNING id, domain_id, enabled, mode, paranoia_level, RETURNING id, domain_id, enabled, mode, paranoia_level,
rule_exclusions, exclusion_notes, trusted_proxies, custom_rules, updated_at rule_exclusions, crs_plugins, exclusion_notes, trusted_proxies, custom_rules, updated_at
`, `,
c.DomainID, c.Enabled, c.Mode, c.ParanoiaLevel, c.DomainID, c.Enabled, c.Mode, c.ParanoiaLevel,
c.RuleExclusions, c.ExclusionNotes, c.TrustedProxies, c.CustomRules, c.UpdatedAt, c.RuleExclusions, c.CRSPlugins, c.ExclusionNotes, c.TrustedProxies, c.CustomRules, c.UpdatedAt,
) )
return scan(row) return scan(row)
} }

View File

@@ -48,12 +48,23 @@ func buildDirectives(cfg models.WafConfig, crsDir string) string {
pl = 1 pl = 1
} }
fmt.Fprintf(&sb, "SecAction \"id:900000,phase:1,nolog,pass,t:none,setvar:tx.paranoia_level=%d\"\n", pl) fmt.Fprintf(&sb, "SecAction \"id:900000,phase:1,nolog,pass,t:none,setvar:tx.paranoia_level=%d\"\n", pl)
setupConf := filepath.Join(crsDir, "crs-setup.conf") includeIfExists(&sb, filepath.Join(crsDir, "crs-setup.conf"))
if _, err := os.Stat(setupConf); err == nil {
fmt.Fprintf(&sb, "Include %s\n", setupConf) // CRS-App-Exclusion-Plugins: config + before laufen VOR den CRS-Rules
// (setzen Enable-Vars + pfad-scoped ctl:ruleRemoveById), after DANACH —
// exakt nach OWASP-CRS-Plugin-Spec. Es werden NUR die für DIESE Domain
// gewählten Plugins inkludiert (per-Domain, nicht global).
plugins := resolveCRSPlugins(cfg.CRSPlugins)
for _, prefix := range plugins {
includeIfExists(&sb, filepath.Join(crsDir, "plugins", prefix+"-config.conf"))
includeIfExists(&sb, filepath.Join(crsDir, "plugins", prefix+"-before.conf"))
}
// rules/*.conf ist ein Glob (kein Stat) — crsAvailable() hat oben bereits
// bestätigt, dass mind. eine .conf existiert.
fmt.Fprintf(&sb, "Include %s\n", filepath.Join(crsDir, "rules", "*.conf"))
for _, prefix := range plugins {
includeIfExists(&sb, filepath.Join(crsDir, "plugins", prefix+"-after.conf"))
} }
rulesGlob := filepath.Join(crsDir, "rules", "*.conf")
fmt.Fprintf(&sb, "Include %s\n", rulesGlob)
} }
// Rule exclusions (applied after CRS load so they override CRS). // Rule exclusions (applied after CRS load so they override CRS).
@@ -78,6 +89,35 @@ func buildDirectives(cfg models.WafConfig, crsDir string) string {
return sb.String() return sb.String()
} }
// KnownCRSPlugins mappt den kurzen Plugin-Namen (gespeichert in
// waf_configs.crs_plugins, im UI gewählt) auf sein Datei-Prefix in
// <crsDir>/plugins/. Nur diese werden paketiert (postinst) und akzeptiert.
var KnownCRSPlugins = map[string]string{
"nextcloud": "nextcloud-rule-exclusions",
"wordpress": "wordpress-rule-exclusions",
"drupal": "drupal-rule-exclusions",
}
// resolveCRSPlugins mappt gewählte Plugin-Namen auf ihre Datei-Prefixe und
// filtert unbekannte/leere raus — defensiv, nie ungültige Includes rendern.
func resolveCRSPlugins(names []string) []string {
out := make([]string, 0, len(names))
for _, n := range names {
if prefix, ok := KnownCRSPlugins[strings.TrimSpace(n)]; ok {
out = append(out, prefix)
}
}
return out
}
// includeIfExists rendert eine Include-Zeile nur, wenn die Datei existiert — so
// bricht ein gewähltes-aber-nicht-installiertes Plugin die Config nicht.
func includeIfExists(sb *strings.Builder, path string) {
if _, err := os.Stat(path); err == nil {
fmt.Fprintf(sb, "Include %s\n", path)
}
}
func ruleEngineMode(mode string) string { func ruleEngineMode(mode string) string {
switch mode { switch mode {
case "blocking": case "blocking":

View File

@@ -0,0 +1,60 @@
package waf
import (
"os"
"path/filepath"
"strings"
"testing"
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
)
// mustWrite legt eine Datei (inkl. Verzeichnis) an.
func mustWrite(t *testing.T, p, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
func TestBuildDirectives_CRSPluginIncludeOrder(t *testing.T) {
crs := t.TempDir()
mustWrite(t, filepath.Join(crs, "crs-setup.conf"), "# setup\n")
mustWrite(t, filepath.Join(crs, "rules", "REQUEST-942.conf"), "# rules\n")
mustWrite(t, filepath.Join(crs, "plugins", "nextcloud-rule-exclusions-config.conf"), "# nc config\n")
mustWrite(t, filepath.Join(crs, "plugins", "nextcloud-rule-exclusions-before.conf"), "# nc before\n")
cfg := models.WafConfig{Mode: "blocking", ParanoiaLevel: 1, CRSPlugins: []string{"nextcloud", "unknown-x"}}
out := buildDirectives(cfg, crs)
iSetup := strings.Index(out, "crs-setup.conf")
iCfg := strings.Index(out, "nextcloud-rule-exclusions-config.conf")
iBefore := strings.Index(out, "nextcloud-rule-exclusions-before.conf")
iRules := strings.Index(out, filepath.Join("rules", "*.conf"))
if iSetup < 0 || iCfg < 0 || iBefore < 0 || iRules < 0 {
t.Fatalf("erwartete Includes fehlen:\n%s", out)
}
// config + before MÜSSEN vor den CRS-Rules stehen (Plugin-Spec).
if iSetup >= iCfg || iCfg >= iBefore || iBefore >= iRules {
t.Errorf("falsche Include-Reihenfolge (setup=%d cfg=%d before=%d rules=%d):\n%s", iSetup, iCfg, iBefore, iRules, out)
}
// Unbekanntes Plugin darf NICHT inkludiert werden (Whitelist).
if strings.Contains(out, "unknown-x") {
t.Errorf("unbekanntes Plugin wurde inkludiert:\n%s", out)
}
// Nicht existente after.conf → keine Include-Zeile.
if strings.Contains(out, "nextcloud-rule-exclusions-after.conf") {
t.Errorf("nicht existente after.conf wurde inkludiert:\n%s", out)
}
}
func TestResolveCRSPlugins(t *testing.T) {
got := resolveCRSPlugins([]string{"nextcloud", " wordpress ", "bogus", ""})
want := "nextcloud-rule-exclusions,wordpress-rule-exclusions"
if strings.Join(got, ",") != want {
t.Errorf("resolveCRSPlugins=%v want %q", got, want)
}
}

View File

@@ -1934,6 +1934,9 @@
"enabled": "Aktiviert", "enabled": "Aktiviert",
"mode": "Modus", "mode": "Modus",
"paranoia": "Paranoia-Level", "paranoia": "Paranoia-Level",
"crsPlugins": "App-Profile (CRS-Plugins)",
"crsPluginsHint": "Offizielle OWASP-CRS-Exclusion-Plugins für bekannte Apps — deaktivieren automatisch die typischen False-Positive-Regeln pfad-genau (z.B. Nextcloud-WebDAV, WordPress-Editor). Sauberer als manuelle Regel-IDs.",
"crsPluginsPlaceholder": "App-Profile wählen (optional)",
"exclusions": "Regel-Ausnahmen", "exclusions": "Regel-Ausnahmen",
"exclusionsHint": "Kommagetrennte Regel-IDs die deaktiviert werden (z.B. 920350, 941130).", "exclusionsHint": "Kommagetrennte Regel-IDs die deaktiviert werden (z.B. 920350, 941130).",
"trustedProxies": "Vertrauenswürdige Proxys", "trustedProxies": "Vertrauenswürdige Proxys",

View File

@@ -1934,6 +1934,9 @@
"enabled": "Enabled", "enabled": "Enabled",
"mode": "Mode", "mode": "Mode",
"paranoia": "Paranoia Level", "paranoia": "Paranoia Level",
"crsPlugins": "App profiles (CRS plugins)",
"crsPluginsHint": "Official OWASP CRS exclusion plugins for well-known apps — automatically disable the typical false-positive rules in a path-scoped way (e.g. Nextcloud WebDAV, WordPress editor). Cleaner than manual rule IDs.",
"crsPluginsPlaceholder": "Select app profiles (optional)",
"exclusions": "Rule Exclusions", "exclusions": "Rule Exclusions",
"exclusionsHint": "Comma-separated rule IDs to disable (e.g. 920350, 941130).", "exclusionsHint": "Comma-separated rule IDs to disable (e.g. 920350, 941130).",
"trustedProxies": "Trusted Proxies", "trustedProxies": "Trusted Proxies",

View File

@@ -33,11 +33,19 @@ interface WafConfig {
mode: 'detection' | 'blocking' mode: 'detection' | 'blocking'
paranoia_level: number paranoia_level: number
rule_exclusions: string[] rule_exclusions: string[]
crs_plugins: string[]
exclusion_notes: Record<string, string> exclusion_notes: Record<string, string>
trusted_proxies: string[] trusted_proxies: string[]
custom_rules: string custom_rules: string
} }
// CRS-App-Exclusion-Plugins — muss zur Backend-Whitelist (KnownCRSPlugins) passen.
const CRS_PLUGIN_OPTIONS = [
{ value: 'nextcloud', label: 'Nextcloud' },
{ value: 'wordpress', label: 'WordPress' },
{ value: 'drupal', label: 'Drupal' },
]
// ---------- API helpers ----------------------------------------------------- // ---------- API helpers -----------------------------------------------------
async function fetchDomains(): Promise<Domain[]> { async function fetchDomains(): Promise<Domain[]> {
@@ -65,6 +73,7 @@ function defaultConfig(domainId: number): WafConfig {
mode: 'detection', mode: 'detection',
paranoia_level: 1, paranoia_level: 1,
rule_exclusions: [], rule_exclusions: [],
crs_plugins: [],
exclusion_notes: {}, exclusion_notes: {},
trusted_proxies: [], trusted_proxies: [],
custom_rules: '', custom_rules: '',
@@ -75,6 +84,7 @@ interface WafFormValues {
enabled: boolean enabled: boolean
mode: 'detection' | 'blocking' mode: 'detection' | 'blocking'
paranoia_level: number paranoia_level: number
crs_plugins: string[]
trusted_proxies_str: string trusted_proxies_str: string
custom_rules: string custom_rules: string
} }
@@ -147,6 +157,7 @@ function ConfigDrawer({ domainName, domainId, onClose }: ConfigDrawerProps) {
mode: vals.mode, mode: vals.mode,
paranoia_level: vals.paranoia_level, paranoia_level: vals.paranoia_level,
rule_exclusions: cfg?.rule_exclusions ?? [], rule_exclusions: cfg?.rule_exclusions ?? [],
crs_plugins: vals.crs_plugins ?? [],
exclusion_notes: cfg?.exclusion_notes ?? {}, exclusion_notes: cfg?.exclusion_notes ?? {},
trusted_proxies: proxies, trusted_proxies: proxies,
custom_rules: vals.custom_rules ?? '', custom_rules: vals.custom_rules ?? '',
@@ -190,6 +201,20 @@ function ConfigDrawer({ domainName, domainId, onClose }: ConfigDrawerProps) {
</Select> </Select>
</Form.Item> </Form.Item>
<Form.Item
label={t('waf.config.crsPlugins')}
name="crs_plugins"
help={t('waf.config.crsPluginsHint')}
>
<Select
mode="multiple"
allowClear
disabled={isViewer}
placeholder={t('waf.config.crsPluginsPlaceholder')}
options={CRS_PLUGIN_OPTIONS}
/>
</Form.Item>
{/* Exclusions list — shows existing exclusions with notes + remove button */} {/* Exclusions list — shows existing exclusions with notes + remove button */}
<Form.Item label={t('waf.config.exclusions')}> <Form.Item label={t('waf.config.exclusions')}>
{(cfg?.rule_exclusions ?? []).length === 0 ? ( {(cfg?.rule_exclusions ?? []).length === 0 ? (
@@ -224,6 +249,7 @@ function ConfigDrawer({ domainName, domainId, onClose }: ConfigDrawerProps) {
mode: cfg?.mode ?? 'detection', mode: cfg?.mode ?? 'detection',
paranoia_level: cfg?.paranoia_level ?? 1, paranoia_level: cfg?.paranoia_level ?? 1,
rule_exclusions: newExclusions, rule_exclusions: newExclusions,
crs_plugins: cfg?.crs_plugins ?? [],
exclusion_notes: newNotes, exclusion_notes: newNotes,
trusted_proxies: cfg?.trusted_proxies ?? [], trusted_proxies: cfg?.trusted_proxies ?? [],
custom_rules: cfg?.custom_rules ?? '', custom_rules: cfg?.custom_rules ?? '',

View File

@@ -937,6 +937,37 @@ KEEPALIVEDDROPIN
rm -rf "${CRS_TMP}" rm -rf "${CRS_TMP}"
fi fi
# ── CRS App-Exclusion-Plugins (Nextcloud / WordPress / Drupal) ──────
# Offizielle OWASP-CRS-Plugins (separate Repos coreruleset/<name>-plugin).
# Ihre plugins/*.conf landen in <crs>/plugins/; der WAF-Renderer bindet
# je Domain die GEWÄHLTEN ein (waf_configs.crs_plugins). Läuft auf JEDEM
# configure (self-healing für Bestandsinstalls), aber nur wenn das
# jeweilige Plugin noch fehlt — admin-Anpassungen bleiben unangetastet.
if [ -d "$WAF_CRS_DIR/rules" ]; then
install -d -m 0755 "$WAF_CRS_DIR/plugins"
for plugin in nextcloud-rule-exclusions wordpress-rule-exclusions drupal-rule-exclusions; do
if [ -f "$WAF_CRS_DIR/plugins/${plugin}-before.conf" ] \
|| [ -f "$WAF_CRS_DIR/plugins/${plugin}-config.conf" ]; then
continue
fi
P_TMP="$(mktemp -d)"
if curl -sL --max-time 45 \
"https://github.com/coreruleset/${plugin}-plugin/archive/refs/heads/main.tar.gz" \
-o "${P_TMP}/p.tgz" 2>/dev/null; then
tar xzf "${P_TMP}/p.tgz" -C "${P_TMP}" 2>/dev/null || true
if ls "${P_TMP}"/*/plugins/${plugin}-*.conf >/dev/null 2>&1; then
install -m 0644 "${P_TMP}"/*/plugins/${plugin}-*.conf \
"$WAF_CRS_DIR/plugins/" 2>/dev/null \
&& echo "postinst: CRS-Plugin ${plugin} installiert"
fi
else
echo "postinst: CRS-Plugin ${plugin} Download fehlgeschlagen (kein Internet?)" >&2
fi
rm -rf "${P_TMP}"
done
chown -R "$EG_USER":"$EG_USER" "$WAF_CRS_DIR/plugins" 2>/dev/null || true
fi
# ── systemd: pick up new units + restart haproxy with our cfg # ── systemd: pick up new units + restart haproxy with our cfg
systemctl daemon-reload systemctl daemon-reload
systemctl restart haproxy.service || true systemctl restart haproxy.service || true