Day 85 — Configuration & Hot Reload
Learning Objectives
- Build a unified config system covering WAF, rate limits, CORS, and auth
- Hot-reload on
SIGHUPwith 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.