// 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) } } } }() alertWriter := intwaf.NewAlertWriter(pool, 512) agent := intwaf.SPOEAgent{ Manager: mgr, AlertWriter: alertWriter, 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{}