feat(config): add unified API rate limit configuration
- commit
- 7ec476ba7e653bb16f8f03fcc6c37bb0239aa68f
- author
- bnema · · 2026-01-17 21:05 UTC
- committer
- bnema · · 2026-01-17 21:06 UTC
- parent
- d4ff823
- signature
- unsigned
2 files · +40 -2
M docs/config/reference.md +13 -0
| @@ -26,6 +26,15 @@ password_hash = "" # Path in secrets backend to bcrypt | |||
| 26 | 26 | token_secret = "" # Path in secrets backend to JWT signing key (REQUIRED for token type) | |
| 27 | 27 | token_expiry = "720h" # Token expiry duration (720h = 30 days) | |
| 28 | 28 | ||
| 29 | + | # ============================================================================= | |
| 30 | + | # API (applies to both Registry and Admin endpoints) | |
| 31 | + | # ============================================================================= | |
| 32 | + | [api.rate_limit] | |
| 33 | + | enabled = true # Enable rate limiting (default: true) | |
| 34 | + | global_rps = 500 # Max requests/second globally | |
| 35 | + | per_ip_rps = 50 # Max requests/second per client IP | |
| 36 | + | burst = 100 # Burst size for rate limiters | |
| 37 | + | ||
| 29 | 38 | # ============================================================================= | |
| 30 | 39 | # LOGGING | |
| 31 | 40 | # ============================================================================= | |
| @@ -119,6 +128,10 @@ preserve = true # Keep volumes when containers are | |||
| 119 | 128 | | `auth.type` | `"password"` | Auth type | | |
| 120 | 129 | | `auth.secrets_backend` | `"unsafe"` | Secrets storage | | |
| 121 | 130 | | `auth.token_expiry` | `"720h"` | 30 days | | |
| 131 | + | | `api.rate_limit.enabled` | `true` | Enable rate limiting | | |
| 132 | + | | `api.rate_limit.global_rps` | `500` | Global requests/second | | |
| 133 | + | | `api.rate_limit.per_ip_rps` | `50` | Per-IP requests/second | | |
| 134 | + | | `api.rate_limit.burst` | `100` | Burst size | | |
| 122 | 135 | | `logging.level` | `"info"` | Log level | | |
| 123 | 136 | | `logging.format` | `"console"` | Log format | | |
| 124 | 137 | | `logging.file.enabled` | `false` | File logging disabled | | |
| @@ -26,6 +26,15 @@ password_hash = "" # Path in secrets backend to bcrypt | |||||
| 26 | token_secret = "" # Path in secrets backend to JWT signing key (REQUIRED for token type) | 26 | token_secret = "" # Path in secrets backend to JWT signing key (REQUIRED for token type) | ||
| 27 | token_expiry = "720h" # Token expiry duration (720h = 30 days) | 27 | token_expiry = "720h" # Token expiry duration (720h = 30 days) | ||
| 28 | 28 | ||||
| 29 | + | # ============================================================================= | |||
| 30 | + | # API (applies to both Registry and Admin endpoints) | |||
| 31 | + | # ============================================================================= | |||
| 32 | + | [api.rate_limit] | |||
| 33 | + | enabled = true # Enable rate limiting (default: true) | |||
| 34 | + | global_rps = 500 # Max requests/second globally | |||
| 35 | + | per_ip_rps = 50 # Max requests/second per client IP | |||
| 36 | + | burst = 100 # Burst size for rate limiters | |||
| 37 | + | ||||
| 29 | # ============================================================================= | 38 | # ============================================================================= | ||
| 30 | # LOGGING | 39 | # LOGGING | ||
| 31 | # ============================================================================= | 40 | # ============================================================================= | ||
| @@ -119,6 +128,10 @@ preserve = true # Keep volumes when containers are | |||||
| 119 | | `auth.type` | `"password"` | Auth type | | 128 | | `auth.type` | `"password"` | Auth type | | ||
| 120 | | `auth.secrets_backend` | `"unsafe"` | Secrets storage | | 129 | | `auth.secrets_backend` | `"unsafe"` | Secrets storage | | ||
| 121 | | `auth.token_expiry` | `"720h"` | 30 days | | 130 | | `auth.token_expiry` | `"720h"` | 30 days | | ||
| 131 | + | | `api.rate_limit.enabled` | `true` | Enable rate limiting | | |||
| 132 | + | | `api.rate_limit.global_rps` | `500` | Global requests/second | | |||
| 133 | + | | `api.rate_limit.per_ip_rps` | `50` | Per-IP requests/second | | |||
| 134 | + | | `api.rate_limit.burst` | `100` | Burst size | | |||
| 122 | | `logging.level` | `"info"` | Log level | | 135 | | `logging.level` | `"info"` | Log level | | ||
| 123 | | `logging.format` | `"console"` | Log format | | 136 | | `logging.format` | `"console"` | Log format | | ||
| 124 | | `logging.file.enabled` | `false` | File logging disabled | | 137 | | `logging.file.enabled` | `false` | File logging disabled | | ||
M internal/app/run.go +27 -2
| @@ -100,6 +100,15 @@ type Config struct { | |||
| 100 | 100 | TokenSecret string `mapstructure:"token_secret"` // path in secrets backend | |
| 101 | 101 | TokenExpiry string `mapstructure:"token_expiry"` // e.g., "720h", "30d" | |
| 102 | 102 | } `mapstructure:"auth"` | |
| 103 | + | ||
| 104 | + | API struct { | |
| 105 | + | RateLimit struct { | |
| 106 | + | Enabled bool `mapstructure:"enabled"` | |
| 107 | + | GlobalRPS float64 `mapstructure:"global_rps"` | |
| 108 | + | PerIPRPS float64 `mapstructure:"per_ip_rps"` | |
| 109 | + | Burst int `mapstructure:"burst"` | |
| 110 | + | } `mapstructure:"rate_limit"` | |
| 111 | + | } `mapstructure:"api"` | |
| 103 | 112 | } | |
| 104 | 113 | ||
| 105 | 114 | // services holds all the services used by the application. | |
| @@ -863,6 +872,15 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha | |||
| 863 | 872 | middleware.RequestLogger(log), | |
| 864 | 873 | } | |
| 865 | 874 | ||
| 875 | + | // Add rate limiting middleware | |
| 876 | + | rateLimitConfig := registry.RateLimitConfig{ | |
| 877 | + | Enabled: cfg.API.RateLimit.Enabled, | |
| 878 | + | GlobalRPS: cfg.API.RateLimit.GlobalRPS, | |
| 879 | + | PerIPRPS: cfg.API.RateLimit.PerIPRPS, | |
| 880 | + | Burst: cfg.API.RateLimit.Burst, | |
| 881 | + | } | |
| 882 | + | registryMiddlewares = append(registryMiddlewares, registry.RateLimitMiddleware(rateLimitConfig)) | |
| 883 | + | ||
| 866 | 884 | if cfg.Auth.Enabled && svc.authSvc != nil { | |
| 867 | 885 | internalAuth := middleware.InternalRegistryAuth{ | |
| 868 | 886 | Username: svc.internalRegUser, | |
| @@ -895,8 +913,11 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha | |||
| 895 | 913 | } | |
| 896 | 914 | // Add admin auth middleware if auth is enabled | |
| 897 | 915 | if svc.authSvc != nil { | |
| 898 | - | // Rate limiter for admin API: 10 req/s with burst of 20 | |
| 899 | - | adminLimiter := rate.NewLimiter(rate.Limit(10), 20) | |
| 916 | + | // Rate limiter for admin API uses per-IP rate from config | |
| 917 | + | var adminLimiter *rate.Limiter | |
| 918 | + | if cfg.API.RateLimit.Enabled { | |
| 919 | + | adminLimiter = rate.NewLimiter(rate.Limit(cfg.API.RateLimit.PerIPRPS), cfg.API.RateLimit.Burst) | |
| 920 | + | } | |
| 900 | 921 | adminMiddlewares = append(adminMiddlewares, admin.AuthMiddleware(svc.authSvc, adminLimiter, log)) | |
| 901 | 922 | } | |
| 902 | 923 | adminWithMiddleware := middleware.Chain(adminMiddlewares...)(svc.adminHandler) | |
| @@ -1230,6 +1251,10 @@ func loadConfig(v *viper.Viper, configPath string) error { | |||
| 1230 | 1251 | v.SetDefault("auth.type", "password") | |
| 1231 | 1252 | v.SetDefault("auth.secrets_backend", "unsafe") | |
| 1232 | 1253 | v.SetDefault("auth.token_expiry", "720h") | |
| 1254 | + | v.SetDefault("api.rate_limit.enabled", true) | |
| 1255 | + | v.SetDefault("api.rate_limit.global_rps", 500) | |
| 1256 | + | v.SetDefault("api.rate_limit.per_ip_rps", 50) | |
| 1257 | + | v.SetDefault("api.rate_limit.burst", 100) | |
| 1233 | 1258 | v.SetDefault("auto_route.enabled", false) | |
| 1234 | 1259 | v.SetDefault("network_isolation.enabled", false) | |
| 1235 | 1260 | v.SetDefault("network_isolation.network_prefix", "gordon") | |
| @@ -100,6 +100,15 @@ type Config struct { | |||||
| 100 | TokenSecret string `mapstructure:"token_secret"` // path in secrets backend | 100 | TokenSecret string `mapstructure:"token_secret"` // path in secrets backend | ||
| 101 | TokenExpiry string `mapstructure:"token_expiry"` // e.g., "720h", "30d" | 101 | TokenExpiry string `mapstructure:"token_expiry"` // e.g., "720h", "30d" | ||
| 102 | } `mapstructure:"auth"` | 102 | } `mapstructure:"auth"` | ||
| 103 | + | ||||
| 104 | + | API struct { | |||
| 105 | + | RateLimit struct { | |||
| 106 | + | Enabled bool `mapstructure:"enabled"` | |||
| 107 | + | GlobalRPS float64 `mapstructure:"global_rps"` | |||
| 108 | + | PerIPRPS float64 `mapstructure:"per_ip_rps"` | |||
| 109 | + | Burst int `mapstructure:"burst"` | |||
| 110 | + | } `mapstructure:"rate_limit"` | |||
| 111 | + | } `mapstructure:"api"` | |||
| 103 | } | 112 | } | ||
| 104 | 113 | ||||
| 105 | // services holds all the services used by the application. | 114 | // services holds all the services used by the application. | ||
| @@ -863,6 +872,15 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha | |||||
| 863 | middleware.RequestLogger(log), | 872 | middleware.RequestLogger(log), | ||
| 864 | } | 873 | } | ||
| 865 | 874 | ||||
| 875 | + | // Add rate limiting middleware | |||
| 876 | + | rateLimitConfig := registry.RateLimitConfig{ | |||
| 877 | + | Enabled: cfg.API.RateLimit.Enabled, | |||
| 878 | + | GlobalRPS: cfg.API.RateLimit.GlobalRPS, | |||
| 879 | + | PerIPRPS: cfg.API.RateLimit.PerIPRPS, | |||
| 880 | + | Burst: cfg.API.RateLimit.Burst, | |||
| 881 | + | } | |||
| 882 | + | registryMiddlewares = append(registryMiddlewares, registry.RateLimitMiddleware(rateLimitConfig)) | |||
| 883 | + | ||||
| 866 | if cfg.Auth.Enabled && svc.authSvc != nil { | 884 | if cfg.Auth.Enabled && svc.authSvc != nil { | ||
| 867 | internalAuth := middleware.InternalRegistryAuth{ | 885 | internalAuth := middleware.InternalRegistryAuth{ | ||
| 868 | Username: svc.internalRegUser, | 886 | Username: svc.internalRegUser, | ||
| @@ -895,8 +913,11 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha | |||||
| 895 | } | 913 | } | ||
| 896 | // Add admin auth middleware if auth is enabled | 914 | // Add admin auth middleware if auth is enabled | ||
| 897 | if svc.authSvc != nil { | 915 | if svc.authSvc != nil { | ||
| 898 | - | // Rate limiter for admin API: 10 req/s with burst of 20 | 916 | + | // Rate limiter for admin API uses per-IP rate from config |
| 899 | - | adminLimiter := rate.NewLimiter(rate.Limit(10), 20) | 917 | + | var adminLimiter *rate.Limiter |
| 918 | + | if cfg.API.RateLimit.Enabled { | |||
| 919 | + | adminLimiter = rate.NewLimiter(rate.Limit(cfg.API.RateLimit.PerIPRPS), cfg.API.RateLimit.Burst) | |||
| 920 | + | } | |||
| 900 | adminMiddlewares = append(adminMiddlewares, admin.AuthMiddleware(svc.authSvc, adminLimiter, log)) | 921 | adminMiddlewares = append(adminMiddlewares, admin.AuthMiddleware(svc.authSvc, adminLimiter, log)) | ||
| 901 | } | 922 | } | ||
| 902 | adminWithMiddleware := middleware.Chain(adminMiddlewares...)(svc.adminHandler) | 923 | adminWithMiddleware := middleware.Chain(adminMiddlewares...)(svc.adminHandler) | ||
| @@ -1230,6 +1251,10 @@ func loadConfig(v *viper.Viper, configPath string) error { | |||||
| 1230 | v.SetDefault("auth.type", "password") | 1251 | v.SetDefault("auth.type", "password") | ||
| 1231 | v.SetDefault("auth.secrets_backend", "unsafe") | 1252 | v.SetDefault("auth.secrets_backend", "unsafe") | ||
| 1232 | v.SetDefault("auth.token_expiry", "720h") | 1253 | v.SetDefault("auth.token_expiry", "720h") | ||
| 1254 | + | v.SetDefault("api.rate_limit.enabled", true) | |||
| 1255 | + | v.SetDefault("api.rate_limit.global_rps", 500) | |||
| 1256 | + | v.SetDefault("api.rate_limit.per_ip_rps", 50) | |||
| 1257 | + | v.SetDefault("api.rate_limit.burst", 100) | |||
| 1233 | v.SetDefault("auto_route.enabled", false) | 1258 | v.SetDefault("auto_route.enabled", false) | ||
| 1234 | v.SetDefault("network_isolation.enabled", false) | 1259 | v.SetDefault("network_isolation.enabled", false) | ||
| 1235 | v.SetDefault("network_isolation.network_prefix", "gordon") | 1260 | v.SetDefault("network_isolation.network_prefix", "gordon") | ||