Pro-Domain-Flag crowdsec_trusted (Migration 0048). Ist es gesetzt, rendert der neue crowdsec-whitelist-Generator den Hostname host-genau in /etc/crowdsec/parsers/s02-enrich/edgeguard-admin-hosts-whitelist.yaml (evt.Parsed.http_host) und reloadet crowdsec. Löst das Problem, dass Admin-SPAs (viele /api/-Requests pro Aktion) http-crawl-non_statics triggern und die Admin-IP bannen — jetzt im Frontend steuerbar statt manueller Node-Datei. - Generator internal/crowdsec/whitelist.go (configgen.Generator, no-op ohne CrowdSec), registriert in edgeguard-ctl render-config + in den Domains-Reloader komponiert (Domain-Edit → Whitelist re-render). Aus der replizierten DB gerendert → überlebt Node-Neuaufbau (Ersatz für die manuelle Node-Datei). - postinst: sudoers reload crowdsec + edgeguard-owned Whitelist-Datei anlegen (dir root-owned → Generator überschreibt nur die vorab-chownte Datei) + Initial-Render. - UI: Switch „Vertrauenswürdiges Admin-Panel" im Domain-Detail. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
108 lines
3.6 KiB
Go
108 lines
3.6 KiB
Go
package crowdsec
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"git.netcell-it.de/projekte/edgeguard-native/internal/configgen"
|
|
)
|
|
|
|
// WhitelistPath ist die aus dem Domain-Flag crowdsec_trusted gerenderte
|
|
// CrowdSec-Parser-Whitelist. s02-enrich läuft vor den Scenarios, sodass
|
|
// whitelisted Events gar nicht erst in http-crawl-non_statics o. Ä. zählen.
|
|
const WhitelistPath = "/etc/crowdsec/parsers/s02-enrich/edgeguard-admin-hosts-whitelist.yaml"
|
|
|
|
// WhitelistGenerator rendert eine host-genaue CrowdSec-Whitelist aus allen
|
|
// Domains mit crowdsec_trusted=true. Vertrauenswürdige Admin-Panels (SPAs, die
|
|
// pro Aktion viele /api/-Requests feuern) würden sonst das Scenario
|
|
// http-crawl-non_statics auslösen und die Admin-IP bannen. No-op, wenn CrowdSec auf diesem Node nicht
|
|
// installiert ist (managed-wenn-installiert).
|
|
type WhitelistGenerator struct {
|
|
pool *pgxpool.Pool
|
|
SkipReload bool
|
|
}
|
|
|
|
func NewWhitelistGenerator(pool *pgxpool.Pool) *WhitelistGenerator {
|
|
return &WhitelistGenerator{pool: pool}
|
|
}
|
|
|
|
func (g *WhitelistGenerator) Name() string { return "crowdsec-whitelist" }
|
|
|
|
func (g *WhitelistGenerator) Render(ctx context.Context) error {
|
|
// Managed-wenn-installiert: ohne CrowdSec kein Whitelist-File.
|
|
if !IsInstalled() {
|
|
return nil
|
|
}
|
|
hosts, err := g.trustedHosts(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("crowdsec-whitelist: query: %w", err)
|
|
}
|
|
// Direktes Schreiben (kein tmp+rename): /etc/crowdsec/parsers/... ist
|
|
// root-owned, edgeguard darf nur die eine (postinst-chownte) Datei
|
|
// überschreiben — analog chrony/unbound.
|
|
if err := os.WriteFile(WhitelistPath, renderWhitelist(hosts), 0o644); err != nil {
|
|
return fmt.Errorf("crowdsec-whitelist: write %s: %w", WhitelistPath, err)
|
|
}
|
|
if g.SkipReload {
|
|
return nil
|
|
}
|
|
return configgen.ReloadService("crowdsec")
|
|
}
|
|
|
|
// RenderToString gibt die gerenderte Whitelist zurück (Config-Preview), ohne zu
|
|
// schreiben oder zu reloaden.
|
|
func (g *WhitelistGenerator) RenderToString(ctx context.Context) (string, error) {
|
|
hosts, err := g.trustedHosts(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(renderWhitelist(hosts)), nil
|
|
}
|
|
|
|
func (g *WhitelistGenerator) trustedHosts(ctx context.Context) ([]string, error) {
|
|
rows, err := g.pool.Query(ctx,
|
|
`SELECT name FROM domains WHERE crowdsec_trusted = true AND active = true ORDER BY name`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var hosts []string
|
|
for rows.Next() {
|
|
var n string
|
|
if err := rows.Scan(&n); err != nil {
|
|
return nil, err
|
|
}
|
|
hosts = append(hosts, n)
|
|
}
|
|
return hosts, rows.Err()
|
|
}
|
|
|
|
// renderWhitelist baut die CrowdSec-Parser-Whitelist-YAML. Ohne vertrauens-
|
|
// würdige Hosts bleibt die Ausdrucksliste leer → `in []` matcht nie → es wird
|
|
// nichts whitelisted (Datei bleibt gültig). Pure Funktion (testbar).
|
|
func renderWhitelist(hosts []string) []byte {
|
|
quoted := make([]string, 0, len(hosts))
|
|
for _, h := range hosts {
|
|
h = strings.TrimSpace(h)
|
|
if h == "" {
|
|
continue
|
|
}
|
|
// Einfachquote + eingebettete Quotes verdoppeln (expr-String-Literal).
|
|
quoted = append(quoted, "'"+strings.ReplaceAll(h, "'", "''")+"'")
|
|
}
|
|
var b bytes.Buffer
|
|
b.WriteString("# Generated by edgeguard-api from domains.crowdsec_trusted. DO NOT EDIT.\n")
|
|
b.WriteString("name: edgeguard/admin-hosts-whitelist\n")
|
|
b.WriteString("description: Trusted admin panels (SPA fires many /api/ requests) exempted from CrowdSec - not a crawl.\n")
|
|
b.WriteString("whitelist:\n")
|
|
b.WriteString(" reason: edgeguard trusted admin host (SPA, not crawl/probing)\n")
|
|
b.WriteString(" expression:\n")
|
|
fmt.Fprintf(&b, " - \"evt.Parsed.http_host in [%s]\"\n", strings.Join(quoted, ", "))
|
|
return b.Bytes()
|
|
}
|