Files
zeekay efee4c9fb1 Add the WAF middleware
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.
2026-09-03 16:14:35 -07:00

476 lines
14 KiB
Go

package middleware
import (
"context"
"errors"
"fmt"
"net/http"
"reflect"
"github.com/containous/alice"
"github.com/hanzoai/ingress/pkg/config/runtime"
"github.com/hanzoai/ingress/pkg/middlewares/addprefix"
"github.com/hanzoai/ingress/pkg/middlewares/auth"
"github.com/hanzoai/ingress/pkg/middlewares/buffering"
"github.com/hanzoai/ingress/pkg/middlewares/chain"
"github.com/hanzoai/ingress/pkg/middlewares/circuitbreaker"
"github.com/hanzoai/ingress/pkg/middlewares/compress"
"github.com/hanzoai/ingress/pkg/middlewares/contenttype"
"github.com/hanzoai/ingress/pkg/middlewares/customerrors"
"github.com/hanzoai/ingress/pkg/middlewares/encodedcharacters"
"github.com/hanzoai/ingress/pkg/middlewares/gatewayapi/headermodifier"
gapiredirect "github.com/hanzoai/ingress/pkg/middlewares/gatewayapi/redirect"
"github.com/hanzoai/ingress/pkg/middlewares/gatewayapi/urlrewrite"
"github.com/hanzoai/ingress/pkg/middlewares/geoblock"
"github.com/hanzoai/ingress/pkg/middlewares/grpcweb"
"github.com/hanzoai/ingress/pkg/middlewares/headers"
"github.com/hanzoai/ingress/pkg/middlewares/inflightreq"
"github.com/hanzoai/ingress/pkg/middlewares/ingressnginx/authtlspasscertificatetoupstream"
"github.com/hanzoai/ingress/pkg/middlewares/ipallowlist"
"github.com/hanzoai/ingress/pkg/middlewares/ipwhitelist"
"github.com/hanzoai/ingress/pkg/middlewares/observability"
"github.com/hanzoai/ingress/pkg/middlewares/passtlsclientcert"
"github.com/hanzoai/ingress/pkg/middlewares/ratelimiter"
"github.com/hanzoai/ingress/pkg/middlewares/redirect"
"github.com/hanzoai/ingress/pkg/middlewares/replacepath"
"github.com/hanzoai/ingress/pkg/middlewares/replacepathregex"
"github.com/hanzoai/ingress/pkg/middlewares/retry"
"github.com/hanzoai/ingress/pkg/middlewares/staticfiles"
"github.com/hanzoai/ingress/pkg/middlewares/stripprefix"
"github.com/hanzoai/ingress/pkg/middlewares/stripprefixregex"
"github.com/hanzoai/ingress/pkg/middlewares/waf"
"github.com/hanzoai/ingress/pkg/server/provider"
"github.com/hanzoai/ingress/pkg/server/recursion"
"github.com/rs/zerolog/log"
)
// Builder the middleware builder.
type Builder struct {
configs map[string]*runtime.MiddlewareInfo
pluginBuilder PluginsBuilder
serviceBuilder serviceBuilder
}
type serviceBuilder interface {
BuildHTTP(ctx context.Context, serviceName string) (http.Handler, error)
}
// NewBuilder creates a new Builder.
func NewBuilder(configs map[string]*runtime.MiddlewareInfo, serviceBuilder serviceBuilder, pluginBuilder PluginsBuilder) *Builder {
return &Builder{configs: configs, serviceBuilder: serviceBuilder, pluginBuilder: pluginBuilder}
}
// BuildMiddlewareChain creates a middleware chain.
func (b *Builder) BuildMiddlewareChain(ctx context.Context, middlewares []string) *alice.Chain {
chain := alice.New()
for _, name := range middlewares {
middlewareName := provider.GetQualifiedName(ctx, name)
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
constructorContext := provider.AddInContext(ctx, middlewareName)
if midInf, ok := b.configs[middlewareName]; !ok || midInf.Middleware == nil {
return nil, fmt.Errorf("middleware %q does not exist", middlewareName)
}
var err error
if constructorContext, err = recursion.CheckRecursion(constructorContext, "middleware", middlewareName); err != nil {
b.configs[middlewareName].AddError(err, true)
return nil, err
}
constructor, err := b.buildConstructor(constructorContext, middlewareName)
if err != nil {
b.configs[middlewareName].AddError(err, true)
return nil, err
}
handler, err := constructor(next)
if err != nil {
b.configs[middlewareName].AddError(err, true)
return nil, err
}
return handler, nil
})
}
return &chain
}
// it is the responsibility of the caller to make sure that b.configs[middlewareName].Middleware exists.
func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) (alice.Constructor, error) {
config := b.configs[middlewareName]
if config == nil || config.Middleware == nil {
return nil, fmt.Errorf("invalid middleware %q configuration", middlewareName)
}
var middleware alice.Constructor
badConf := errors.New("cannot create middleware: multi-types middleware not supported, consider declaring two different pieces of middleware instead")
// AddPrefix
if config.AddPrefix != nil {
middleware = func(next http.Handler) (http.Handler, error) {
return addprefix.New(ctx, next, *config.AddPrefix, middlewareName)
}
}
// BasicAuth
if config.BasicAuth != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return auth.NewBasic(ctx, next, *config.BasicAuth, middlewareName)
}
}
// Buffering
if config.Buffering != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return buffering.New(ctx, next, *config.Buffering, middlewareName)
}
}
// Chain
if config.Chain != nil {
if middleware != nil {
return nil, badConf
}
var qualifiedNames []string
for _, name := range config.Chain.Middlewares {
qualifiedNames = append(qualifiedNames, provider.GetQualifiedName(ctx, name))
}
config.Chain.Middlewares = qualifiedNames
middleware = func(next http.Handler) (http.Handler, error) {
return chain.New(ctx, next, *config.Chain, b, middlewareName)
}
}
// CircuitBreaker
if config.CircuitBreaker != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return circuitbreaker.New(ctx, next, *config.CircuitBreaker, middlewareName)
}
}
// Compress
if config.Compress != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return compress.New(ctx, next, *config.Compress, middlewareName)
}
}
// ContentType
if config.ContentType != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return contenttype.New(ctx, next, *config.ContentType, middlewareName)
}
}
// EncodedCharacters
if config.EncodedCharacters != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return encodedcharacters.NewEncodedCharacters(ctx, next, *config.EncodedCharacters, middlewareName), nil
}
}
// CustomErrors
if config.Errors != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return customerrors.New(ctx, next, *config.Errors, b.serviceBuilder, middlewareName)
}
}
// DigestAuth
if config.DigestAuth != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return auth.NewDigest(ctx, next, *config.DigestAuth, middlewareName)
}
}
// ForwardAuth
if config.ForwardAuth != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return auth.NewForward(ctx, next, *config.ForwardAuth, middlewareName)
}
}
// GrpcWeb
if config.GrpcWeb != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return grpcweb.New(ctx, next, *config.GrpcWeb, middlewareName), nil
}
}
// Headers
if config.Headers != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return headers.New(ctx, next, *config.Headers, middlewareName)
}
}
// IPWhiteList
if config.IPWhiteList != nil {
qualifiedName := provider.GetQualifiedName(ctx, middlewareName)
log.Warn().Msgf("Middleware %q of type IPWhiteList is deprecated, please use IPAllowList instead.", qualifiedName)
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return ipwhitelist.New(ctx, next, *config.IPWhiteList, middlewareName)
}
}
// IPAllowList
if config.IPAllowList != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return ipallowlist.New(ctx, next, *config.IPAllowList, middlewareName)
}
}
// GeoBlock — country-level header injection + access control.
// Single source of truth across all brands (Hanzo / Lux / Zoo).
// See pkg/middlewares/geoblock for the resolver chain
// (Cloudflare/X-Forwarded-Country upstream → MaxMind MMDB fallback)
// and rejection semantics (RFC 7725 451 by default).
if config.GeoBlock != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return geoblock.New(ctx, next, *config.GeoBlock, middlewareName)
}
}
// WAF (OWASP Coraza, SecLang / OWASP Core Rule Set).
// See pkg/middlewares/waf: a configuration that loads no rule is refused
// here rather than serving traffic through an empty ruleset.
if config.WAF != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return waf.New(ctx, next, *config.WAF, middlewareName)
}
}
// InFlightReq
if config.InFlightReq != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return inflightreq.New(ctx, next, *config.InFlightReq, middlewareName)
}
}
// PassTLSClientCert
if config.PassTLSClientCert != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return passtlsclientcert.New(ctx, next, *config.PassTLSClientCert, middlewareName)
}
}
// AuthTLSPassCertificateToUpstream
if config.AuthTLSPassCertificateToUpstream != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return authtlspasscertificatetoupstream.NewAuthTLSPassCertificateToUpstream(ctx, next, *config.AuthTLSPassCertificateToUpstream, middlewareName)
}
}
// RateLimit
if config.RateLimit != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return ratelimiter.New(ctx, next, *config.RateLimit, middlewareName)
}
}
// RedirectRegex
if config.RedirectRegex != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return redirect.NewRedirectRegex(ctx, next, *config.RedirectRegex, middlewareName)
}
}
// RedirectScheme
if config.RedirectScheme != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return redirect.NewRedirectScheme(ctx, next, *config.RedirectScheme, middlewareName)
}
}
// ReplacePath
if config.ReplacePath != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return replacepath.New(ctx, next, *config.ReplacePath, middlewareName)
}
}
// ReplacePathRegex
if config.ReplacePathRegex != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return replacepathregex.New(ctx, next, *config.ReplacePathRegex, middlewareName)
}
}
// Retry
if config.Retry != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
// TODO missing metrics / accessLog
return retry.New(ctx, next, *config.Retry, retry.Listeners{}, middlewareName)
}
}
// StripPrefix
if config.StripPrefix != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return stripprefix.New(ctx, next, *config.StripPrefix, middlewareName)
}
}
// StripPrefixRegex
if config.StripPrefixRegex != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return stripprefixregex.New(ctx, next, *config.StripPrefixRegex, middlewareName)
}
}
// StaticFiles
if config.StaticFiles != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return staticfiles.New(ctx, next, *config.StaticFiles, middlewareName)
}
}
// Plugin
if config.Plugin != nil && !reflect.ValueOf(b.pluginBuilder).IsNil() { // Using "reflect" because "b.pluginBuilder" is an interface.
if middleware != nil {
return nil, badConf
}
pluginType, rawPluginConfig, err := findPluginConfig(config.Plugin)
if err != nil {
return nil, fmt.Errorf("plugin: %w", err)
}
plug, err := b.pluginBuilder.Build(pluginType, rawPluginConfig, middlewareName)
if err != nil {
return nil, fmt.Errorf("plugin: %w", err)
}
middleware = func(next http.Handler) (http.Handler, error) {
return newTraceablePlugin(ctx, middlewareName, plug, next)
}
}
// Gateway API HTTPRoute filters middlewares.
if config.RequestHeaderModifier != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return headermodifier.NewRequestHeaderModifier(ctx, next, *config.RequestHeaderModifier, middlewareName), nil
}
}
if config.ResponseHeaderModifier != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return headermodifier.NewResponseHeaderModifier(ctx, next, *config.ResponseHeaderModifier, middlewareName), nil
}
}
if config.RequestRedirect != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return gapiredirect.NewRequestRedirect(ctx, next, *config.RequestRedirect, middlewareName)
}
}
if config.URLRewrite != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return urlrewrite.NewURLRewrite(ctx, next, *config.URLRewrite, middlewareName), nil
}
}
if middleware == nil {
return nil, fmt.Errorf("invalid middleware %q configuration: invalid middleware type or middleware does not exist", middlewareName)
}
return observability.WrapMiddleware(ctx, middleware), nil
}