Day 82 — Middleware Ordering & Pipeline Design
Learning Objectives
- Understand why middleware order is a security architecture decision
- Build a configurable middleware pipeline with load-from-JSON ordering
- Log which middleware rejected each request (for incident response)
Correct Middleware Order (Security-First)
Request flow (each layer can short-circuit):
1. PanicRecoverer — outermost: catch panics from ALL other middleware
2. RequestID — inject before any logging
3. CORS Preflight — handle before auth (CORS is pre-auth by design)
4. Global Connection Limit — protect server resources
5. Per-IP Rate Limit — DDoS mitigation before auth (before you know who it is)
6. TLS/mTLS Check — verify transport security
7. Bot/Scanner Check — reject known scanners early
8. Authentication — verify identity
9. Per-User Rate Limit — quota enforcement (after you know who)
10. WAF — inspect payload (after auth so identity appears in WAF logs)
11. JSON Schema Validation — structural validation
12. Input Validation — semantic validation
13. Security Headers — always set on responses
14. PII Scrubber — mask before logging
15. Request Logger — log after all decisions made (includes final status)
16. Application Handler
Why Order Matters
Wrong order: WAF before Auth
Problem: WAF blocks legitimate traffic before user can authenticate
Worse: WAF logs don't include user identity → harder to investigate false positives
Wrong order: Rate limit after Auth
Problem: Unauthenticated requests can exhaust rate limit budget for real users
Attack: Attacker sends 10,000 unauthenticated requests → legitimate users get 429
Wrong order: Security headers inside handler
Problem: Panicked handler never sets security headers → XSS unprotected
Correct:
IP rate limit → Auth → User rate limit
WAF after Auth → WAF logs include user identity
PanicRecoverer outermost → Security headers always set
Exercise: Configurable Pipeline
package pipeline
import (
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
)
type PipelineConfig struct {
Middleware []string `json:"middleware"` // names in order
Required []string `json:"required"` // must be present
}
type Registry struct {
middleware map[string]func(http.Handler) http.Handler
}
func NewRegistry() *Registry { return &Registry{middleware: make(map[string]func(http.Handler) http.Handler)} }
func (r *Registry) Register(name string, mw func(http.Handler) http.Handler) {
r.middleware[name] = mw
}
func (r *Registry) Build(configFile string, handler http.Handler) (http.Handler, error) {
data, err := os.ReadFile(configFile)
if err != nil { return nil, err }
var cfg PipelineConfig
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("invalid pipeline config: %w", err)
}
// Validate required middleware are present
nameSet := make(map[string]bool)
for _, name := range cfg.Middleware { nameSet[name] = true }
for _, req := range cfg.Required {
if !nameSet[req] {
return nil, fmt.Errorf("required middleware %q not in pipeline", req)
}
}
// Build chain (reverse order — first in config = outermost)
chain := handler
for i := len(cfg.Middleware) - 1; i >= 0; i-- {
name := cfg.Middleware[i]
mw, ok := r.middleware[name]
if !ok {
return nil, fmt.Errorf("unknown middleware: %q", name)
}
inner := chain
mwName := name
chain = mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Track which middleware handles each request
ctx := context.WithValue(r.Context(), pipelineStageKey, mwName)
inner.ServeHTTP(w, r.WithContext(ctx))
}))
}
return chain, nil
}
pipeline-config.json:
{
"middleware": [
"panic_recoverer",
"request_id",
"cors",
"ip_rate_limit",
"auth",
"user_rate_limit",
"waf",
"schema_validation",
"security_headers",
"pii_scrubber",
"request_logger"
],
"required": [
"panic_recoverer",
"auth",
"request_logger"
]
}