mirror of
https://github.com/hanzoai/ingress.git
synced 2026-11-03 00:49:13 +00:00
OWASP Coraza with the Core Rule Set v4 embedded, declared as `waf` on every provider: file, labels and the Middleware CR. Rules load Core Rule Set, then files, then inline directives. The engine state is written after every rule. The Core Rule Set's own recommended configuration opens with SecRuleEngine DetectionOnly, so a state written first is overridden by the rules it governs and the firewall loads all of CRS and refuses nothing; detectionOnly on the middleware is the one place the mode is decided. geoBlock rode along: it was declared in dynamic config only, absent from MiddlewareSpec, both deep copies and the CRD schema, so a Middleware CR carrying it produced a middleware without it. The +groupName marker now matches the registered group, which is what makes controller-gen's output land where code-gen.sh reads it; the stale traefik.io_*.yaml dumps go. CI asks for the arm64 runner git.hanzo.ai actually has and builds both platforms.
112 lines
3.6 KiB
Go
112 lines
3.6 KiB
Go
// Package waf implements the web application firewall middleware.
|
|
//
|
|
// The engine is OWASP Coraza, which reads ModSecurity SecLang and is
|
|
// rule-compatible with the OWASP Core Rule Set. Rules reach it three ways,
|
|
// applied in this order so the later ones refine the earlier: the embedded
|
|
// Core Rule Set, files named by the configuration, then inline directives.
|
|
//
|
|
// A ruleset that matches interrupts the request and the configured status is
|
|
// returned. In DetectionOnly the same match is logged and the request
|
|
// proceeds, which is how a ruleset is measured against live traffic before it
|
|
// is allowed to refuse anything.
|
|
//
|
|
// The engine state is written after every rule, so this middleware decides
|
|
// whether the firewall refuses and no ruleset can decide it instead. That
|
|
// ordering is load-bearing rather than tidy: the Core Rule Set ships with the
|
|
// recommended engine configuration, whose seventh line is SecRuleEngine
|
|
// DetectionOnly, so a state written first is overridden by the rules it was
|
|
// meant to govern and the result loads all of OWASP CRS and refuses nothing.
|
|
//
|
|
// A configuration that loads no rule at all is refused where it is built. An
|
|
// engine with an empty ruleset answers every request successfully and inspects
|
|
// none of them, which reads as a firewall and is not one.
|
|
package waf
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
coreruleset "github.com/corazawaf/coraza-coreruleset/v4"
|
|
"github.com/corazawaf/coraza/v3"
|
|
corazahttp "github.com/corazawaf/coraza/v3/http"
|
|
"github.com/corazawaf/coraza/v3/types"
|
|
|
|
"github.com/hanzoai/ingress/pkg/config/dynamic"
|
|
"github.com/hanzoai/ingress/pkg/middlewares"
|
|
)
|
|
|
|
const typeName = "WAF"
|
|
|
|
// coreDirectives loads the recommended engine configuration, the Core Rule Set
|
|
// defaults, then the rules themselves. The three are one unit: the rules read
|
|
// variables the setup file defines, so loading them without it leaves the
|
|
// paranoia level and anomaly thresholds unset.
|
|
const coreDirectives = `Include @coraza.conf-recommended
|
|
Include @crs-setup.conf.example
|
|
Include @owasp_crs/*.conf`
|
|
|
|
type waf struct {
|
|
handler http.Handler
|
|
name string
|
|
}
|
|
|
|
// New builds the firewall in front of next.
|
|
func New(ctx context.Context, next http.Handler, config dynamic.WAF, name string) (http.Handler, error) {
|
|
logger := middlewares.GetLogger(ctx, name, typeName)
|
|
|
|
cfg := coraza.NewWAFConfig().
|
|
WithRootFS(coreruleset.FS).
|
|
WithErrorCallback(func(rule types.MatchedRule) {
|
|
logger.Warn().
|
|
Int("rule", rule.Rule().ID()).
|
|
Str("uri", rule.URI()).
|
|
Str("transaction", rule.TransactionID()).
|
|
Msg(rule.ErrorLog())
|
|
})
|
|
|
|
rules := 0
|
|
if config.CoreRuleSet {
|
|
cfg = cfg.WithDirectives(coreDirectives)
|
|
rules++
|
|
}
|
|
for _, path := range config.DirectivesFiles {
|
|
cfg = cfg.WithDirectivesFromFile(path)
|
|
rules++
|
|
}
|
|
if strings.TrimSpace(config.Directives) != "" {
|
|
cfg = cfg.WithDirectives(config.Directives)
|
|
rules++
|
|
}
|
|
if rules == 0 {
|
|
return nil, fmt.Errorf("%s: no rules — set coreRuleSet, directives or directivesFiles", typeName)
|
|
}
|
|
|
|
engine := "SecRuleEngine On"
|
|
if config.DetectionOnly {
|
|
engine = "SecRuleEngine DetectionOnly"
|
|
}
|
|
cfg = cfg.WithDirectives(engine)
|
|
|
|
engineWAF, err := coraza.NewWAF(cfg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", typeName, err)
|
|
}
|
|
|
|
logger.Debug().
|
|
Bool("coreRuleSet", config.CoreRuleSet).
|
|
Bool("detectionOnly", config.DetectionOnly).
|
|
Msg("Creating middleware")
|
|
|
|
return &waf{handler: corazahttp.WrapHandler(engineWAF, next), name: name}, nil
|
|
}
|
|
|
|
func (w *waf) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|
w.handler.ServeHTTP(rw, req)
|
|
}
|
|
|
|
func (w *waf) GetTracingInformation() (string, string) {
|
|
return w.name, typeName
|
|
}
|