Frontend Labs
⋯

Day 85 — Configuration & Hot Reload

Learning Objectives

  • Build a unified config system covering WAF, rate limits, CORS, and auth
  • Hot-reload on SIGHUP with validation before swap
  • Reject invalid config without disrupting service

Exercise: Hot-Reload Config System

package config

import (
    "encoding/json"
    "fmt"
    "os"
    "os/signal"
    "sync"
    "syscall"
    "time"
)

type GatewayConfig struct {
    WAF struct {
        Mode      string         `json:"mode"`
        Threshold int            `json:"threshold"`
    } `json:"waf"`
    RateLimit struct {
        PerIPBurst    float64 `json:"per_ip_burst"`
        PerIPRate     float64 `json:"per_ip_rate"`
        PerUserBurst  float64 `json:"per_user_burst"`
        PerUserRate   float64 `json:"per_user_rate"`
    } `json:"rate_limit"`
    CORS struct {
        AllowedOrigins []string `json:"allowed_origins"`
    } `json:"cors"`
    TrustedCIDRs []string `json:"trusted_cidrs"`
}

func (c *GatewayConfig) Validate() error {
    switch c.WAF.Mode {
    case "off", "detection", "blocking":
    default:
        return fmt.Errorf("waf.mode must be off|detection|blocking, got %q", c.WAF.Mode)
    }
    if c.RateLimit.PerIPRate <= 0 {
        return fmt.Errorf("rate_limit.per_ip_rate must be > 0")
    }
    if len(c.CORS.AllowedOrigins) == 0 {
        return fmt.Errorf("cors.allowed_origins must not be empty")
    }
    return nil
}

type ConfigStore struct {
    mu       sync.RWMutex
    current  *GatewayConfig
    path     string
    onChange []func(*GatewayConfig)
}

func Load(path string) (*ConfigStore, error) {
    s := &ConfigStore{path: path}
    if err := s.reload(); err != nil {
        return nil, err
    }
    return s, nil
}

func (s *ConfigStore) Get() *GatewayConfig {
    s.mu.RLock()
    defer s.mu.RUnlock()
    return s.current
}

func (s *ConfigStore) OnChange(fn func(*GatewayConfig)) {
    s.onChange = append(s.onChange, fn)
}

func (s *ConfigStore) reload() error {
    data, err := os.ReadFile(s.path)
    if err != nil { return fmt.Errorf("read config: %w", err) }

    var cfg GatewayConfig
    if err := json.Unmarshal(data, &cfg); err != nil {
        return fmt.Errorf("parse config: %w", err)
    }

    // Validate before swap — never apply invalid config
    if err := cfg.Validate(); err != nil {
        return fmt.Errorf("invalid config: %w", err)
    }

    s.mu.Lock()
    s.current = &cfg
    s.mu.Unlock()

    // Notify all listeners (WAF, rate limiter, CORS)
    for _, fn := range s.onChange {
        go fn(&cfg)
    }

    fmt.Printf("[CONFIG] reloaded from %s (waf.mode=%s)\n", s.path, cfg.WAF.Mode)
    return nil
}

// WatchSignal reloads config on SIGHUP
func (s *ConfigStore) WatchSignal(stop <-chan struct{}) {
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, syscall.SIGHUP)
    defer signal.Stop(sigCh)

    for {
        select {
        case <-stop:
            return
        case sig := <-sigCh:
            fmt.Printf("[CONFIG] received %v, reloading...\n", sig)
            if err := s.reload(); err != nil {
                fmt.Printf("[CONFIG] reload error (keeping current): %v\n", err)
                // Critically: keep current config on error
            }
        }
    }
}

// WatchFile polls for file changes (alternative to SIGHUP)
func (s *ConfigStore) WatchFile(interval time.Duration, stop <-chan struct{}) {
    var lastMod time.Time
    ticker := time.NewTicker(interval)
    defer ticker.Stop()

    for {
        select {
        case <-stop:
            return
        case <-ticker.C:
            info, err := os.Stat(s.path)
            if err != nil { continue }
            if info.ModTime().After(lastMod) {
                lastMod = info.ModTime()
                if err := s.reload(); err != nil {
                    fmt.Printf("[CONFIG] auto-reload error: %v\n", err)
                }
            }
        }
    }
}

Test hot-reload:

# Start server
go run . -config gateway.json &

# Change WAF mode to detection
sed -i 's/"blocking"/"detection"/' gateway.json

# Send SIGHUP to reload
kill -HUP $(pgrep -f 'go run')
# Server logs: [CONFIG] reloaded from gateway.json (waf.mode=detection)

# Test: malicious request now goes through (detection mode)
curl "http://localhost:8080/search?q=' UNION SELECT * FROM users--"
# Returns 200 (logged but not blocked)

Real-World Context

Emergency WAF disable without deployment: your WAF is blocking legitimate traffic from a new client. Instead of deploying code, kill -HUP $(pidof proxy) switches to detection mode in seconds. The validation step ensures you can't accidentally deploy an invalid config (empty CORS origins, typo in WAF mode) that breaks all CORS.


Generated from week12.md · 8 Oct 2026 16:36