feat(waf): Phase 2 — edgeguard-waf Binary + SPOE + Coraza Engine — v1.2.67
- cmd/edgeguard-waf/: neues Binary — lädt WAF-Configs aus DB, startet SPOE-Agent auf 127.0.0.1:9000, refreshed Configs alle 30s - internal/waf/engine.go: BuildEngine() — Coraza WAF aus WafConfig bauen (SecLang-Direktiven: RuleEngine, PL, CRS-Include, Exclusions, Custom) - internal/waf/manager.go: Manager — per-Hostname Coraza-Engine-Cache (thread-safe, Lazy-Init via Reload(), Port-Strip, IPv6-Brackets) - internal/waf/spoe.go: SPOEAgent — haproxy-go SPOE-Handler (src/method/path/query/ver/host/headers aus HAProxy-Vars, Coraza-Transaction, Blocking: txn.waf.status=403 setzen) - services/waf/waf.go: ListAllWithDomain() — JOIN domains+waf_configs - go.mod: coraza/v3 v3.7.0 + dropmorepackets/haproxy-go v0.0.8 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
102
cmd/edgeguard-waf/main.go
Normal file
102
cmd/edgeguard-waf/main.go
Normal file
@@ -0,0 +1,102 @@
|
||||
// Command edgeguard-waf is the per-domain WAF SPOE agent for EdgeGuard.
|
||||
// HAProxy connects to it via the SPOE protocol (127.0.0.1:9000).
|
||||
// It loads per-domain WAF configs from PostgreSQL and uses Coraza v3
|
||||
// with the OWASP Core Rule Set to inspect HTTP requests.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/database"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/waf"
|
||||
intwaf "git.netcell-it.de/projekte/edgeguard-native/internal/waf"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
dsn := database.ConnStringFromEnv()
|
||||
pool, err := database.Open(ctx, dsn)
|
||||
if err != nil {
|
||||
slog.Error("waf: db connect", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
if err := database.Migrate(ctx, ""); err != nil {
|
||||
slog.Error("waf: migrate", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
repo := waf.New(pool)
|
||||
|
||||
crsDir := os.Getenv("EDGEGUARD_WAF_CRS_DIR")
|
||||
if crsDir == "" {
|
||||
crsDir = intwaf.DefaultCRSDir
|
||||
}
|
||||
spoeAddr := os.Getenv("EDGEGUARD_WAF_ADDR")
|
||||
if spoeAddr == "" {
|
||||
spoeAddr = intwaf.DefaultSPOEAddr
|
||||
}
|
||||
|
||||
mgr := intwaf.NewManager(crsDir)
|
||||
|
||||
// Initial load.
|
||||
if err := reload(ctx, repo, mgr); err != nil {
|
||||
slog.Error("waf: initial load", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Periodic config refresh every 30 seconds.
|
||||
go func() {
|
||||
t := time.NewTicker(30 * time.Second)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
if err := reload(ctx, repo, mgr); err != nil {
|
||||
slog.Warn("waf: reload", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
agent := intwaf.SPOEAgent{
|
||||
Manager: mgr,
|
||||
Addr: spoeAddr,
|
||||
}
|
||||
|
||||
slog.Info("waf: SPOE agent starting", "addr", spoeAddr, "crs", crsDir)
|
||||
if err := agent.ListenAndServe(ctx); err != nil && ctx.Err() == nil {
|
||||
slog.Error("waf: SPOE agent stopped", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// reload fetches all domain+waf_config pairs from DB and rebuilds engines.
|
||||
func reload(ctx context.Context, repo *waf.Repo, mgr *intwaf.Manager) error {
|
||||
configs, err := repo.ListAllWithDomain(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
domains := make([]intwaf.DomainConfig, 0, len(configs))
|
||||
for _, c := range configs {
|
||||
domains = append(domains, intwaf.DomainConfig{
|
||||
Hostname: c.Hostname,
|
||||
Config: c.Config,
|
||||
})
|
||||
}
|
||||
return mgr.Reload(domains)
|
||||
}
|
||||
|
||||
// Ensure models package is used (imported transitively via services/waf).
|
||||
var _ = models.WafConfig{}
|
||||
Reference in New Issue
Block a user