feat(waf): Phase 3 — HAProxy SPOE-Integration — v1.2.68
- haproxy.cfg.tpl: filter spoe + deny_status 403 + spoe-edgeguard-waf
Backend (nur gerendert wenn WAFEnabled=true)
- haproxy.go: WAFEnabled in View; WafRepo.ListEnabled() prüft ob WAF
aktiv; SPOE-Config-File (coraza-spoe.cfg) wird bei WAFEnabled
atomar geschrieben; SPOEConfigPath konfigurierbar
- waf/spoe.go: uri statt path+query (HAProxy url-Sample = volle URI)
SPOE-Config definiert:
- Agent: edgeguard-waf-agent, var-prefix=waf, timeout processing 50ms
- Message: src, method, uri=url, ver=req.ver, headers=req.hdrs,
host=req.hdr(host)
- Backend: spoe-edgeguard-waf → 127.0.0.1:9000
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -80,6 +80,11 @@ frontend public_https
|
||||
bind [::]:443 ssl crt /etc/edgeguard/tls/ alpn h2,http/1.1
|
||||
bind quic6@:443 ssl crt /etc/edgeguard/tls/ alpn h3
|
||||
{{- end}}
|
||||
{{- if .WAFEnabled}}
|
||||
# WAF: SPOE-Filter — edgeguard-waf inspiziert jeden Request.
|
||||
# filter muss vor allen http-request/http-response-Direktiven stehen.
|
||||
filter spoe engine edgeguard-waf cfg-file /etc/edgeguard/haproxy/coraza-spoe.cfg
|
||||
{{- end}}
|
||||
|
||||
# Alt-Svc: signalisiert dass h3 auf demselben Port verfügbar ist.
|
||||
# ma=86400 = Browser darf den Hinweis 24h cachen.
|
||||
@@ -91,6 +96,10 @@ frontend public_https
|
||||
# echte Source-IP ohne XFF-Chain-Parsing brauchen.
|
||||
http-request set-header X-Forwarded-Proto https
|
||||
http-request set-header X-Real-IP %[src]
|
||||
{{- if .WAFEnabled}}
|
||||
# WAF: Request blockieren wenn edgeguard-waf txn.waf.status gesetzt hat.
|
||||
http-request deny deny_status 403 if { var(txn.waf.status) -m found }
|
||||
{{- end}}
|
||||
|
||||
{{- if .GlobalMaintenance}}
|
||||
# Whole-Box-Maintenance — Settings → Maintenance-Mode aktiv. Dieser
|
||||
@@ -195,6 +204,16 @@ backend rl_{{$d.ID}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
{{- if .WAFEnabled}}
|
||||
|
||||
# SPOE-Backend für edgeguard-waf (TCP, kein HTTP-Parsing).
|
||||
backend spoe-edgeguard-waf
|
||||
mode tcp
|
||||
timeout connect 100ms
|
||||
timeout server 50ms
|
||||
server spoe-waf-1 127.0.0.1:9000
|
||||
{{- end}}
|
||||
|
||||
{{- range $b := .Backends}}
|
||||
|
||||
backend eg_backend_{{$b.ID}}
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/domains"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/routingrules"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/setup"
|
||||
wafsvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/waf"
|
||||
)
|
||||
|
||||
//go:embed haproxy.cfg.tpl
|
||||
@@ -64,25 +65,29 @@ type Generator struct {
|
||||
ServersRepo *backendservers.Repo
|
||||
RoutingRepo *routingrules.Repo
|
||||
HeadersRepo *domainheaders.Repo
|
||||
WafRepo *wafsvc.Repo
|
||||
|
||||
// SetupStore (optional): wenn gesetzt, lesen wir Whole-Box-
|
||||
// Maintenance-Status hieraus und reichen ihn als View.GlobalMaintenance
|
||||
// ans Template weiter.
|
||||
SetupStore *setup.Store
|
||||
|
||||
OutputPath string
|
||||
SkipReload bool
|
||||
OutputPath string
|
||||
SPOEConfigPath string
|
||||
SkipReload bool
|
||||
}
|
||||
|
||||
func New(pool *pgxpool.Pool) *Generator {
|
||||
return &Generator{
|
||||
Pool: pool,
|
||||
DomainsRepo: domains.New(pool),
|
||||
BackendsRepo: backends.New(pool),
|
||||
ServersRepo: backendservers.New(pool),
|
||||
RoutingRepo: routingrules.New(pool),
|
||||
HeadersRepo: domainheaders.New(pool),
|
||||
SetupStore: setup.NewStore(setup.DefaultDir),
|
||||
Pool: pool,
|
||||
DomainsRepo: domains.New(pool),
|
||||
BackendsRepo: backends.New(pool),
|
||||
ServersRepo: backendservers.New(pool),
|
||||
RoutingRepo: routingrules.New(pool),
|
||||
HeadersRepo: domainheaders.New(pool),
|
||||
WafRepo: wafsvc.New(pool),
|
||||
SetupStore: setup.NewStore(setup.DefaultDir),
|
||||
SPOEConfigPath: filepath.Join(configgen.EtcEdgeguard, "haproxy", "coraza-spoe.cfg"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +121,17 @@ func (g *Generator) Render(ctx context.Context) error {
|
||||
if err := configgen.AtomicWrite(out, buf.Bytes(), 0o644); err != nil {
|
||||
return fmt.Errorf("haproxy: write: %w", err)
|
||||
}
|
||||
// Write SPOE config whenever WAF is enabled; remove it when disabled
|
||||
// so HAProxy doesn't fail on a missing backend reference.
|
||||
if view.WAFEnabled {
|
||||
spoeOut := g.SPOEConfigPath
|
||||
if spoeOut == "" {
|
||||
spoeOut = filepath.Join(configgen.EtcEdgeguard, "haproxy", "coraza-spoe.cfg")
|
||||
}
|
||||
if err := configgen.AtomicWrite(spoeOut, []byte(spoeCfg), 0o644); err != nil {
|
||||
return fmt.Errorf("haproxy: write spoe config: %w", err)
|
||||
}
|
||||
}
|
||||
if g.SkipReload {
|
||||
return nil
|
||||
}
|
||||
@@ -125,6 +141,22 @@ func (g *Generator) Render(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// spoeCfg is the static SPOE configuration for edgeguard-waf.
|
||||
// It tells HAProxy which variables to forward and where the agent listens.
|
||||
const spoeCfg = `# Generated by edgeguard-api. DO NOT EDIT.
|
||||
[spoe-agent edgeguard-waf-agent]
|
||||
messages edgeguard-waf-req
|
||||
option var-prefix waf
|
||||
timeout hello 100ms
|
||||
timeout idle 30s
|
||||
timeout processing 50ms
|
||||
use-backend spoe-edgeguard-waf
|
||||
|
||||
[spoe-message edgeguard-waf-req]
|
||||
args src=src method=method uri=url ver=req.ver headers=req.hdrs host=req.hdr(host)
|
||||
event on-frontend-http-request
|
||||
`
|
||||
|
||||
// View is what the template consumes. Routes per domain are pre-
|
||||
// joined here so the template can stay declarative; Servers leben pro
|
||||
// BackendView, damit das Template einen `backend …`-Block mit den N
|
||||
@@ -148,6 +180,11 @@ type View struct {
|
||||
// IPv6Enabled: wenn true fügt das Template zusätzliche
|
||||
// bind-Direktiven für [::]:80, [::]:443 und [::]:3443 hinzu.
|
||||
IPv6Enabled bool
|
||||
|
||||
// WAFEnabled: wenn true wird der SPOE-Filter für edgeguard-waf
|
||||
// in public_https eingebunden und das spoe-Backend gerendert.
|
||||
// Wird gesetzt sobald mindestens eine Domain WAF enabled hat.
|
||||
WAFEnabled bool
|
||||
}
|
||||
|
||||
type DomainView struct {
|
||||
@@ -289,6 +326,14 @@ func (g *Generator) loadView(ctx context.Context) (*View, error) {
|
||||
}
|
||||
}
|
||||
v := &View{Domains: domViews, Backends: activeBackends, HTTPDomains: httpDomains}
|
||||
|
||||
// Check whether any domain has WAF enabled.
|
||||
if g.WafRepo != nil {
|
||||
if wafEnabled, err := g.WafRepo.ListEnabled(ctx); err == nil {
|
||||
v.WAFEnabled = len(wafEnabled) > 0
|
||||
}
|
||||
}
|
||||
|
||||
if g.SetupStore != nil {
|
||||
if st, err := g.SetupStore.Load(); err == nil && st != nil {
|
||||
v.GlobalMaintenance = st.MaintenanceMode
|
||||
|
||||
Reference in New Issue
Block a user