- 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>
103 lines
2.5 KiB
Go
103 lines
2.5 KiB
Go
// 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{}
|