docs: add rate limiting configuration guide and wiki deployment guides
- commit
- 2f7df3bbe6efe3857d77d68553b995a8f2455fc8
- author
- bnema · · 2026-01-18 01:59 UTC
- parent
- 912a154
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +834 -2
M docs/config/index.md +14 -0
| @@ -47,6 +47,14 @@ secrets_backend = "pass" # "pass", "sops", or "unsafe" | |||
| 47 | 47 | token_secret = "gordon/auth/token_secret" | |
| 48 | 48 | token_expiry = "30d" # Duration (1y, 30d, 2w) or 0 for never | |
| 49 | 49 | ||
| 50 | + | # API rate limiting | |
| 51 | + | [api.rate_limit] | |
| 52 | + | enabled = true # Enable rate limiting (default: true) | |
| 53 | + | global_rps = 500 # Max requests/second globally | |
| 54 | + | per_ip_rps = 50 # Max requests/second per IP | |
| 55 | + | burst = 100 # Burst size | |
| 56 | + | trusted_proxies = [] # IPs/CIDRs trusted for X-Forwarded-For | |
| 57 | + | ||
| 50 | 58 | # Deploy behavior | |
| 51 | 59 | [deploy] | |
| 52 | 60 | pull_policy = "if-tag-changed" # always, if-not-present, if-tag-changed | |
| @@ -111,6 +119,7 @@ enabled = false # Auto-create routes from image names | |||
| 111 | 119 | |---------|-------------|---------------| | |
| 112 | 120 | | `[server]` | Core server settings | [Server](./server.md) | | |
| 113 | 121 | | `[auth]` | Authentication and secrets backend | [Auth](./auth.md) | | |
| 122 | + | | `[api.rate_limit]` | Rate limiting configuration | [Rate Limiting](./rate-limiting.md) | | |
| 114 | 123 | | `[deploy]` | Deployment behavior | [Deploy](./deploy.md) | | |
| 115 | 124 | | `[logging]` | Logging configuration | [Logging](./logging.md) | | |
| 116 | 125 | | `[env]` | Environment variable settings | [Environment](./env.md) | | |
| @@ -133,6 +142,11 @@ enabled = false # Auto-create routes from image names | |||
| 133 | 142 | | `auth.type` | `"password"` | | |
| 134 | 143 | | `auth.secrets_backend` | `"unsafe"` | | |
| 135 | 144 | | `auth.token_expiry` | `"30d"` | | |
| 145 | + | | `api.rate_limit.enabled` | `true` | | |
| 146 | + | | `api.rate_limit.global_rps` | `500` | | |
| 147 | + | | `api.rate_limit.per_ip_rps` | `50` | | |
| 148 | + | | `api.rate_limit.burst` | `100` | | |
| 149 | + | | `api.rate_limit.trusted_proxies` | `[]` | | |
| 136 | 150 | | `deploy.pull_policy` | `"if-tag-changed"` | | |
| 137 | 151 | | `logging.level` | `"info"` | | |
| 138 | 152 | | `logging.format` | `"console"` | | |
| @@ -47,6 +47,14 @@ secrets_backend = "pass" # "pass", "sops", or "unsafe" | |||||
| 47 | token_secret = "gordon/auth/token_secret" | 47 | token_secret = "gordon/auth/token_secret" | ||
| 48 | token_expiry = "30d" # Duration (1y, 30d, 2w) or 0 for never | 48 | token_expiry = "30d" # Duration (1y, 30d, 2w) or 0 for never | ||
| 49 | 49 | ||||
| 50 | + | # API rate limiting | |||
| 51 | + | [api.rate_limit] | |||
| 52 | + | enabled = true # Enable rate limiting (default: true) | |||
| 53 | + | global_rps = 500 # Max requests/second globally | |||
| 54 | + | per_ip_rps = 50 # Max requests/second per IP | |||
| 55 | + | burst = 100 # Burst size | |||
| 56 | + | trusted_proxies = [] # IPs/CIDRs trusted for X-Forwarded-For | |||
| 57 | + | ||||
| 50 | # Deploy behavior | 58 | # Deploy behavior | ||
| 51 | [deploy] | 59 | [deploy] | ||
| 52 | pull_policy = "if-tag-changed" # always, if-not-present, if-tag-changed | 60 | pull_policy = "if-tag-changed" # always, if-not-present, if-tag-changed | ||
| @@ -111,6 +119,7 @@ enabled = false # Auto-create routes from image names | |||||
| 111 | |---------|-------------|---------------| | 119 | |---------|-------------|---------------| | ||
| 112 | | `[server]` | Core server settings | [Server](./server.md) | | 120 | | `[server]` | Core server settings | [Server](./server.md) | | ||
| 113 | | `[auth]` | Authentication and secrets backend | [Auth](./auth.md) | | 121 | | `[auth]` | Authentication and secrets backend | [Auth](./auth.md) | | ||
| 122 | + | | `[api.rate_limit]` | Rate limiting configuration | [Rate Limiting](./rate-limiting.md) | | |||
| 114 | | `[deploy]` | Deployment behavior | [Deploy](./deploy.md) | | 123 | | `[deploy]` | Deployment behavior | [Deploy](./deploy.md) | | ||
| 115 | | `[logging]` | Logging configuration | [Logging](./logging.md) | | 124 | | `[logging]` | Logging configuration | [Logging](./logging.md) | | ||
| 116 | | `[env]` | Environment variable settings | [Environment](./env.md) | | 125 | | `[env]` | Environment variable settings | [Environment](./env.md) | | ||
| @@ -133,6 +142,11 @@ enabled = false # Auto-create routes from image names | |||||
| 133 | | `auth.type` | `"password"` | | 142 | | `auth.type` | `"password"` | | ||
| 134 | | `auth.secrets_backend` | `"unsafe"` | | 143 | | `auth.secrets_backend` | `"unsafe"` | | ||
| 135 | | `auth.token_expiry` | `"30d"` | | 144 | | `auth.token_expiry` | `"30d"` | | ||
| 145 | + | | `api.rate_limit.enabled` | `true` | | |||
| 146 | + | | `api.rate_limit.global_rps` | `500` | | |||
| 147 | + | | `api.rate_limit.per_ip_rps` | `50` | | |||
| 148 | + | | `api.rate_limit.burst` | `100` | | |||
| 149 | + | | `api.rate_limit.trusted_proxies` | `[]` | | |||
| 136 | | `deploy.pull_policy` | `"if-tag-changed"` | | 150 | | `deploy.pull_policy` | `"if-tag-changed"` | | ||
| 137 | | `logging.level` | `"info"` | | 151 | | `logging.level` | `"info"` | | ||
| 138 | | `logging.format` | `"console"` | | 152 | | `logging.format` | `"console"` | | ||
A docs/config/rate-limiting.md +368 -0
| @@ -0,0 +1,368 @@ | |||
| 1 | + | # Rate Limiting | |
| 2 | + | ||
| 3 | + | Protect your Gordon instance from abuse with configurable rate limiting. | |
| 4 | + | ||
| 5 | + | ## Overview | |
| 6 | + | ||
| 7 | + | Rate limiting prevents: | |
| 8 | + | - **Brute force attacks** on authentication endpoints | |
| 9 | + | - **Denial of Service (DoS)** from excessive requests | |
| 10 | + | - **Resource exhaustion** from misbehaving clients | |
| 11 | + | - **Registry abuse** (e.g., automated scraping) | |
| 12 | + | ||
| 13 | + | Rate limiting applies to: | |
| 14 | + | - **Registry API** (`/v2/*`) — Global + per-IP limits | |
| 15 | + | - **Token endpoint** (`/v2/token`) — Same limiter as registry (Global + per-IP) | |
| 16 | + | - **Admin API** (`/admin/*`) — Separate per-IP limit only | |
| 17 | + | ||
| 18 | + | ## Quick Start | |
| 19 | + | ||
| 20 | + | Rate limiting is **enabled by default** with sensible defaults: | |
| 21 | + | ||
| 22 | + | ```toml | |
| 23 | + | [api.rate_limit] | |
| 24 | + | enabled = true | |
| 25 | + | global_rps = 500 | |
| 26 | + | per_ip_rps = 50 | |
| 27 | + | burst = 100 | |
| 28 | + | trusted_proxies = [] | |
| 29 | + | ``` | |
| 30 | + | ||
| 31 | + | > **Important:** Gordon does not handle TLS termination, so production deployments require a front proxy (Cloudflare, nginx, etc.) for HTTPS. You **must** configure `trusted_proxies` to ensure rate limiting sees real client IPs instead of your proxy's IP. | |
| 32 | + | ||
| 33 | + | ## Configuration | |
| 34 | + | ||
| 35 | + | ```toml | |
| 36 | + | [api.rate_limit] | |
| 37 | + | enabled = true | |
| 38 | + | global_rps = 500 | |
| 39 | + | per_ip_rps = 50 | |
| 40 | + | burst = 100 | |
| 41 | + | trusted_proxies = ["127.0.0.1", "10.0.0.0/8"] | |
| 42 | + | ``` | |
| 43 | + | ||
| 44 | + | ## Options | |
| 45 | + | ||
| 46 | + | | Option | Type | Default | Description | | |
| 47 | + | |--------|------|---------|-------------| | |
| 48 | + | | `enabled` | bool | `true` | Enable or disable rate limiting | | |
| 49 | + | | `global_rps` | float | `500` | Maximum requests per second across all clients combined | | |
| 50 | + | | `per_ip_rps` | float | `50` | Maximum requests per second per client IP address | | |
| 51 | + | | `burst` | int | `100` | Maximum burst size (requests allowed to exceed the rate temporarily) | | |
| 52 | + | | `trusted_proxies` | []string | `[]` | IP addresses or CIDR ranges trusted to set `X-Forwarded-For` | | |
| 53 | + | ||
| 54 | + | ## Architecture | |
| 55 | + | ||
| 56 | + | Gordon uses two separate rate limiters: | |
| 57 | + | ||
| 58 | + | ``` | |
| 59 | + | ┌─────────────────────────────────────────────────────────┐ | |
| 60 | + | │ Incoming Request │ | |
| 61 | + | └─────────────────────────────────────────────────────────┘ | |
| 62 | + | │ | |
| 63 | + | ┌───────────────┴───────────────┐ | |
| 64 | + | ▼ ▼ | |
| 65 | + | ┌───────────────┐ ┌───────────────┐ | |
| 66 | + | │ /v2/* routes │ │ /admin/* routes│ | |
| 67 | + | └───────────────┘ └───────────────┘ | |
| 68 | + | │ │ | |
| 69 | + | ▼ ▼ | |
| 70 | + | ┌─────────────────────────┐ ┌─────────────────────────┐ | |
| 71 | + | │ Registry Rate Limiter │ │ Admin Rate Limiter │ | |
| 72 | + | │ ┌───────────────────┐ │ │ ┌───────────────────┐ │ | |
| 73 | + | │ │ Global Limiter │ │ │ │ Per-IP Limiter │ │ | |
| 74 | + | │ │ (global_rps) │ │ │ │ (per_ip_rps) │ │ | |
| 75 | + | │ └───────────────────┘ │ │ └───────────────────┘ │ | |
| 76 | + | │ ┌───────────────────┐ │ └─────────────────────────┘ | |
| 77 | + | │ │ Per-IP Limiters │ │ | |
| 78 | + | │ │ (per_ip_rps) │ │ | |
| 79 | + | │ └───────────────────┘ │ | |
| 80 | + | └─────────────────────────┘ | |
| 81 | + | ``` | |
| 82 | + | ||
| 83 | + | **Registry Limiter** (for `/v2/*` and `/v2/token`): | |
| 84 | + | - Checks global limit first (all clients combined) | |
| 85 | + | - Then checks per-IP limit | |
| 86 | + | - Both must pass for the request to proceed | |
| 87 | + | ||
| 88 | + | **Admin Limiter** (for `/admin/*`): | |
| 89 | + | - Per-IP limit only | |
| 90 | + | - No global limit (admin traffic is typically low volume) | |
| 91 | + | - Separate from registry to avoid CI/CD traffic affecting admin access | |
| 92 | + | ||
| 93 | + | ## Option Details | |
| 94 | + | ||
| 95 | + | ### `enabled` | |
| 96 | + | ||
| 97 | + | Master switch for rate limiting. When `false`, all requests are allowed without throttling. | |
| 98 | + | ||
| 99 | + | ```toml | |
| 100 | + | [api.rate_limit] | |
| 101 | + | enabled = false # Disable rate limiting (not recommended for production) | |
| 102 | + | ``` | |
| 103 | + | ||
| 104 | + | ### `global_rps` | |
| 105 | + | ||
| 106 | + | The maximum number of requests per second that Gordon will accept from **all clients combined**. This is a hard cap that protects against distributed attacks where many IPs each send moderate traffic. | |
| 107 | + | ||
| 108 | + | ```toml | |
| 109 | + | [api.rate_limit] | |
| 110 | + | global_rps = 500 # 500 requests/second total | |
| 111 | + | ``` | |
| 112 | + | ||
| 113 | + | **Sizing guidance:** | |
| 114 | + | - **Small deployments** (1-10 apps): `100-500` RPS | |
| 115 | + | - **Medium deployments** (10-50 apps): `500-2000` RPS | |
| 116 | + | - **Large deployments** (50+ apps): `2000-10000` RPS | |
| 117 | + | ||
| 118 | + | Consider your CI/CD patterns—parallel builds pushing multiple images can generate bursts of traffic. | |
| 119 | + | ||
| 120 | + | ### `per_ip_rps` | |
| 121 | + | ||
| 122 | + | The maximum requests per second allowed from a **single client IP**. This prevents any single client from monopolizing Gordon's resources. | |
| 123 | + | ||
| 124 | + | ```toml | |
| 125 | + | [api.rate_limit] | |
| 126 | + | per_ip_rps = 50 # 50 requests/second per IP | |
| 127 | + | ``` | |
| 128 | + | ||
| 129 | + | **Sizing guidance:** | |
| 130 | + | - Normal Docker operations (pull, push) rarely exceed 10-20 RPS | |
| 131 | + | - CI/CD pipelines with parallel jobs may need 50-100 RPS | |
| 132 | + | - Automated tooling (vulnerability scanners, etc.) may need higher limits | |
| 133 | + | ||
| 134 | + | ### `burst` | |
| 135 | + | ||
| 136 | + | The token bucket burst size. Allows clients to temporarily exceed the rate limit for short bursts, which is useful for legitimate traffic patterns like: | |
| 137 | + | - Initial connection setup (multiple manifest/blob requests) | |
| 138 | + | - Parallel layer downloads | |
| 139 | + | - Health check bursts | |
| 140 | + | ||
| 141 | + | ```toml | |
| 142 | + | [api.rate_limit] | |
| 143 | + | burst = 100 # Allow bursts of up to 100 requests | |
| 144 | + | ``` | |
| 145 | + | ||
| 146 | + | A burst of `100` with `per_ip_rps = 50` means a client can send 100 requests instantly, then must wait ~2 seconds for the bucket to refill before another burst. | |
| 147 | + | ||
| 148 | + | ### `trusted_proxies` | |
| 149 | + | ||
| 150 | + | **Critical for correct IP detection in production.** | |
| 151 | + | ||
| 152 | + | Gordon is a reverse proxy that routes requests to your containers, but it **does not handle TLS termination**. For HTTPS support, you must place Gordon behind a TLS-terminating proxy like Cloudflare, nginx, or a load balancer. | |
| 153 | + | ||
| 154 | + | ``` | |
| 155 | + | Internet → [Cloudflare/nginx] → Gordon → Containers | |
| 156 | + | (HTTPS) (HTTP) | |
| 157 | + | ``` | |
| 158 | + | ||
| 159 | + | In this setup, Gordon sees all connections coming from your front proxy's IP, not the real clients. The proxy sets `X-Forwarded-For` to communicate the original client IP. | |
| 160 | + | ||
| 161 | + | **The problem:** `X-Forwarded-For` can be spoofed. If Gordon trusted this header unconditionally, attackers could: | |
| 162 | + | - Bypass per-IP rate limits by sending fake IPs | |
| 163 | + | - Create unlimited rate limiter entries (memory exhaustion) | |
| 164 | + | ||
| 165 | + | **The solution:** Gordon only honors `X-Forwarded-For` from IPs listed in `trusted_proxies`. For all other connections, it uses the direct connection IP. | |
| 166 | + | ||
| 167 | + | ```toml | |
| 168 | + | [api.rate_limit] | |
| 169 | + | trusted_proxies = ["127.0.0.1", "10.0.0.0/8"] | |
| 170 | + | ``` | |
| 171 | + | ||
| 172 | + | **Supported formats:** | |
| 173 | + | - Single IP: `"192.168.1.1"` | |
| 174 | + | - CIDR range: `"10.0.0.0/8"` | |
| 175 | + | - IPv6: `"::1"`, `"fd00::/8"` | |
| 176 | + | ||
| 177 | + | > **If `trusted_proxies` is empty or misconfigured**, all requests appear to come from your proxy's IP, making per-IP rate limiting ineffective (all clients share one limit). | |
| 178 | + | ||
| 179 | + | ## Deployment Examples | |
| 180 | + | ||
| 181 | + | ### Cloudflare (Recommended) | |
| 182 | + | ||
| 183 | + | Cloudflare provides free TLS termination and DDoS protection. This is the recommended setup for production. | |
| 184 | + | ||
| 185 | + | Cloudflare publishes their IP ranges at https://www.cloudflare.com/ips/ | |
| 186 | + | ||
| 187 | + | ```toml | |
| 188 | + | [api.rate_limit] | |
| 189 | + | # Cloudflare IPs (verified 2026-01-18 - check cloudflare.com/ips for updates) | |
| 190 | + | trusted_proxies = [ | |
| 191 | + | # IPv4 | |
| 192 | + | "173.245.48.0/20", | |
| 193 | + | "103.21.244.0/22", | |
| 194 | + | "103.22.200.0/22", | |
| 195 | + | "103.31.4.0/22", | |
| 196 | + | "141.101.64.0/18", | |
| 197 | + | "108.162.192.0/18", | |
| 198 | + | "190.93.240.0/20", | |
| 199 | + | "188.114.96.0/20", | |
| 200 | + | "197.234.240.0/22", | |
| 201 | + | "198.41.128.0/17", | |
| 202 | + | "162.158.0.0/15", | |
| 203 | + | "104.16.0.0/13", | |
| 204 | + | "104.24.0.0/14", | |
| 205 | + | "172.64.0.0/13", | |
| 206 | + | "131.0.72.0/22", | |
| 207 | + | # IPv6 | |
| 208 | + | "2400:cb00::/32", | |
| 209 | + | "2606:4700::/32", | |
| 210 | + | "2803:f800::/32", | |
| 211 | + | "2405:b500::/32", | |
| 212 | + | "2405:8100::/32", | |
| 213 | + | "2a06:98c0::/29", | |
| 214 | + | "2c0f:f248::/32", | |
| 215 | + | ] | |
| 216 | + | ``` | |
| 217 | + | ||
| 218 | + | > **Note:** Cloudflare IPs may change. Verify at https://www.cloudflare.com/ips/ periodically. | |
| 219 | + | ||
| 220 | + | ### Local Development (No TLS) | |
| 221 | + | ||
| 222 | + | For local development only, Gordon can be accessed directly without a proxy: | |
| 223 | + | ||
| 224 | + | ```toml | |
| 225 | + | [api.rate_limit] | |
| 226 | + | trusted_proxies = [] # Empty = use RemoteAddr directly | |
| 227 | + | ``` | |
| 228 | + | ||
| 229 | + | > **Warning:** Never expose Gordon directly to the internet without TLS. Use this configuration only for local development. | |
| 230 | + | ||
| 231 | + | ### Behind nginx (Same Host) | |
| 232 | + | ||
| 233 | + | When nginx runs on the same machine as Gordon: | |
| 234 | + | ||
| 235 | + | ```toml | |
| 236 | + | [api.rate_limit] | |
| 237 | + | trusted_proxies = ["127.0.0.1", "::1"] | |
| 238 | + | ``` | |
| 239 | + | ||
| 240 | + | ### Behind nginx (Separate Host) | |
| 241 | + | ||
| 242 | + | When nginx runs on a different server: | |
| 243 | + | ||
| 244 | + | ```toml | |
| 245 | + | [api.rate_limit] | |
| 246 | + | trusted_proxies = ["10.0.1.5"] # nginx server IP | |
| 247 | + | ``` | |
| 248 | + | ||
| 249 | + | ### Behind AWS Application Load Balancer (ALB) | |
| 250 | + | ||
| 251 | + | ALB terminates TLS and forwards requests to your instances. Unlike Cloudflare, ALB IPs are dynamic and change as AWS scales the load balancer. You need to trust your VPC's private IP range (CIDR) since ALB connects from within your VPC. | |
| 252 | + | ||
| 253 | + | ```toml | |
| 254 | + | [api.rate_limit] | |
| 255 | + | trusted_proxies = ["10.0.0.0/16"] # Your VPC CIDR | |
| 256 | + | ``` | |
| 257 | + | ||
| 258 | + | For detailed setup instructions, see the [AWS ALB Guide](../../wiki/guides/aws-alb.md). | |
| 259 | + | ||
| 260 | + | ### Behind Kubernetes Ingress | |
| 261 | + | ||
| 262 | + | When running Gordon in Kubernetes with an ingress controller (nginx-ingress, Traefik, etc.), you need to trust the pod network CIDR. The ingress controller must also be configured to forward client IPs via `X-Forwarded-For`. | |
| 263 | + | ||
| 264 | + | ```toml | |
| 265 | + | [api.rate_limit] | |
| 266 | + | trusted_proxies = ["10.244.0.0/16"] # Your pod network CIDR | |
| 267 | + | ``` | |
| 268 | + | ||
| 269 | + | For detailed setup instructions, see the [Kubernetes Ingress Guide](../../wiki/guides/kubernetes-ingress.md). | |
| 270 | + | ||
| 271 | + | ## Rate Limit Response | |
| 272 | + | ||
| 273 | + | When a client exceeds the rate limit, Gordon returns: | |
| 274 | + | ||
| 275 | + | ``` | |
| 276 | + | HTTP/1.1 429 Too Many Requests | |
| 277 | + | Content-Type: application/json | |
| 278 | + | Docker-Distribution-API-Version: registry/2.0 | |
| 279 | + | Retry-After: 1 | |
| 280 | + | ||
| 281 | + | { | |
| 282 | + | "errors": [ | |
| 283 | + | { | |
| 284 | + | "code": "TOOMANYREQUESTS", | |
| 285 | + | "message": "rate limit exceeded" | |
| 286 | + | } | |
| 287 | + | ] | |
| 288 | + | } | |
| 289 | + | ``` | |
| 290 | + | ||
| 291 | + | The `Retry-After` header indicates when the client can retry (in seconds). | |
| 292 | + | ||
| 293 | + | ## Monitoring | |
| 294 | + | ||
| 295 | + | Rate-limited requests are logged at `info` level: | |
| 296 | + | ||
| 297 | + | ``` | |
| 298 | + | level=info msg="rate limit exceeded" client_ip=203.0.113.50 path=/v2/myapp/manifests/latest | |
| 299 | + | ``` | |
| 300 | + | ||
| 301 | + | To see rate limiting in action, enable debug logging: | |
| 302 | + | ||
| 303 | + | ```toml | |
| 304 | + | [logging] | |
| 305 | + | level = "debug" | |
| 306 | + | ``` | |
| 307 | + | ||
| 308 | + | ## Security Considerations | |
| 309 | + | ||
| 310 | + | ### IP Spoofing Prevention | |
| 311 | + | ||
| 312 | + | If `trusted_proxies` is misconfigured, attackers can: | |
| 313 | + | 1. Send requests with fake `X-Forwarded-For: 1.2.3.4` | |
| 314 | + | 2. Each request appears to come from a different IP | |
| 315 | + | 3. Per-IP rate limiting becomes ineffective | |
| 316 | + | 4. Attackers create unbounded limiter entries (memory exhaustion) | |
| 317 | + | ||
| 318 | + | **Always verify your proxy configuration** by checking logs to ensure client IPs are detected correctly. | |
| 319 | + | ||
| 320 | + | ### Endpoint Coverage | |
| 321 | + | ||
| 322 | + | All authenticated endpoints are protected: | |
| 323 | + | ||
| 324 | + | | Endpoint | Rate Limiter | Limits Applied | | |
| 325 | + | |----------|--------------|----------------| | |
| 326 | + | | `/v2/*` (registry) | Registry limiter | Global RPS + Per-IP RPS | | |
| 327 | + | | `/v2/token` | Registry limiter | Global RPS + Per-IP RPS | | |
| 328 | + | | `/admin/*` | Admin limiter | Per-IP RPS only | | |
| 329 | + | ||
| 330 | + | The token endpoint shares the registry limiter, preventing brute force attacks on credentials. The admin API uses a separate per-IP limiter to avoid interference with registry traffic. | |
| 331 | + | ||
| 332 | + | ### Recommended Production Settings | |
| 333 | + | ||
| 334 | + | ```toml | |
| 335 | + | [api.rate_limit] | |
| 336 | + | enabled = true | |
| 337 | + | global_rps = 1000 # Adjust based on your traffic | |
| 338 | + | per_ip_rps = 30 # Stricter per-IP limit | |
| 339 | + | burst = 50 # Smaller burst for tighter control | |
| 340 | + | trusted_proxies = [] # Configure based on your proxy setup | |
| 341 | + | ``` | |
| 342 | + | ||
| 343 | + | ## Troubleshooting | |
| 344 | + | ||
| 345 | + | ### "I'm getting rate limited but traffic is low" | |
| 346 | + | ||
| 347 | + | 1. Check if `trusted_proxies` is configured correctly | |
| 348 | + | 2. All traffic may appear to come from your proxy's IP | |
| 349 | + | 3. Add your proxy to `trusted_proxies` to see real client IPs | |
| 350 | + | ||
| 351 | + | ### "Rate limiting doesn't seem to work" | |
| 352 | + | ||
| 353 | + | 1. Verify `enabled = true` | |
| 354 | + | 2. Check that requests go through Gordon (not cached by CDN) | |
| 355 | + | 3. Ensure `global_rps` and `per_ip_rps` are set appropriately | |
| 356 | + | ||
| 357 | + | ### "I need to whitelist certain IPs" | |
| 358 | + | ||
| 359 | + | Rate limiting doesn't support IP whitelisting. Consider: | |
| 360 | + | - Increasing `per_ip_rps` for legitimate high-traffic clients | |
| 361 | + | - Using a reverse proxy with its own rate limiting rules | |
| 362 | + | - Implementing IP-based access control at the network level | |
| 363 | + | ||
| 364 | + | ## Related | |
| 365 | + | ||
| 366 | + | - [Authentication](./auth.md) | |
| 367 | + | - [Configuration Reference](./reference.md) | |
| 368 | + | - [Logging](./logging.md) | |
| @@ -0,0 +1,368 @@ | |||||
| 1 | + | # Rate Limiting | |||
| 2 | + | ||||
| 3 | + | Protect your Gordon instance from abuse with configurable rate limiting. | |||
| 4 | + | ||||
| 5 | + | ## Overview | |||
| 6 | + | ||||
| 7 | + | Rate limiting prevents: | |||
| 8 | + | - **Brute force attacks** on authentication endpoints | |||
| 9 | + | - **Denial of Service (DoS)** from excessive requests | |||
| 10 | + | - **Resource exhaustion** from misbehaving clients | |||
| 11 | + | - **Registry abuse** (e.g., automated scraping) | |||
| 12 | + | ||||
| 13 | + | Rate limiting applies to: | |||
| 14 | + | - **Registry API** (`/v2/*`) — Global + per-IP limits | |||
| 15 | + | - **Token endpoint** (`/v2/token`) — Same limiter as registry (Global + per-IP) | |||
| 16 | + | - **Admin API** (`/admin/*`) — Separate per-IP limit only | |||
| 17 | + | ||||
| 18 | + | ## Quick Start | |||
| 19 | + | ||||
| 20 | + | Rate limiting is **enabled by default** with sensible defaults: | |||
| 21 | + | ||||
| 22 | + | ```toml | |||
| 23 | + | [api.rate_limit] | |||
| 24 | + | enabled = true | |||
| 25 | + | global_rps = 500 | |||
| 26 | + | per_ip_rps = 50 | |||
| 27 | + | burst = 100 | |||
| 28 | + | trusted_proxies = [] | |||
| 29 | + | ``` | |||
| 30 | + | ||||
| 31 | + | > **Important:** Gordon does not handle TLS termination, so production deployments require a front proxy (Cloudflare, nginx, etc.) for HTTPS. You **must** configure `trusted_proxies` to ensure rate limiting sees real client IPs instead of your proxy's IP. | |||
| 32 | + | ||||
| 33 | + | ## Configuration | |||
| 34 | + | ||||
| 35 | + | ```toml | |||
| 36 | + | [api.rate_limit] | |||
| 37 | + | enabled = true | |||
| 38 | + | global_rps = 500 | |||
| 39 | + | per_ip_rps = 50 | |||
| 40 | + | burst = 100 | |||
| 41 | + | trusted_proxies = ["127.0.0.1", "10.0.0.0/8"] | |||
| 42 | + | ``` | |||
| 43 | + | ||||
| 44 | + | ## Options | |||
| 45 | + | ||||
| 46 | + | | Option | Type | Default | Description | | |||
| 47 | + | |--------|------|---------|-------------| | |||
| 48 | + | | `enabled` | bool | `true` | Enable or disable rate limiting | | |||
| 49 | + | | `global_rps` | float | `500` | Maximum requests per second across all clients combined | | |||
| 50 | + | | `per_ip_rps` | float | `50` | Maximum requests per second per client IP address | | |||
| 51 | + | | `burst` | int | `100` | Maximum burst size (requests allowed to exceed the rate temporarily) | | |||
| 52 | + | | `trusted_proxies` | []string | `[]` | IP addresses or CIDR ranges trusted to set `X-Forwarded-For` | | |||
| 53 | + | ||||
| 54 | + | ## Architecture | |||
| 55 | + | ||||
| 56 | + | Gordon uses two separate rate limiters: | |||
| 57 | + | ||||
| 58 | + | ``` | |||
| 59 | + | ┌─────────────────────────────────────────────────────────┐ | |||
| 60 | + | │ Incoming Request │ | |||
| 61 | + | └─────────────────────────────────────────────────────────┘ | |||
| 62 | + | │ | |||
| 63 | + | ┌───────────────┴───────────────┐ | |||
| 64 | + | ▼ ▼ | |||
| 65 | + | ┌───────────────┐ ┌───────────────┐ | |||
| 66 | + | │ /v2/* routes │ │ /admin/* routes│ | |||
| 67 | + | └───────────────┘ └───────────────┘ | |||
| 68 | + | │ │ | |||
| 69 | + | ▼ ▼ | |||
| 70 | + | ┌─────────────────────────┐ ┌─────────────────────────┐ | |||
| 71 | + | │ Registry Rate Limiter │ │ Admin Rate Limiter │ | |||
| 72 | + | │ ┌───────────────────┐ │ │ ┌───────────────────┐ │ | |||
| 73 | + | │ │ Global Limiter │ │ │ │ Per-IP Limiter │ │ | |||
| 74 | + | │ │ (global_rps) │ │ │ │ (per_ip_rps) │ │ | |||
| 75 | + | │ └───────────────────┘ │ │ └───────────────────┘ │ | |||
| 76 | + | │ ┌───────────────────┐ │ └─────────────────────────┘ | |||
| 77 | + | │ │ Per-IP Limiters │ │ | |||
| 78 | + | │ │ (per_ip_rps) │ │ | |||
| 79 | + | │ └───────────────────┘ │ | |||
| 80 | + | └─────────────────────────┘ | |||
| 81 | + | ``` | |||
| 82 | + | ||||
| 83 | + | **Registry Limiter** (for `/v2/*` and `/v2/token`): | |||
| 84 | + | - Checks global limit first (all clients combined) | |||
| 85 | + | - Then checks per-IP limit | |||
| 86 | + | - Both must pass for the request to proceed | |||
| 87 | + | ||||
| 88 | + | **Admin Limiter** (for `/admin/*`): | |||
| 89 | + | - Per-IP limit only | |||
| 90 | + | - No global limit (admin traffic is typically low volume) | |||
| 91 | + | - Separate from registry to avoid CI/CD traffic affecting admin access | |||
| 92 | + | ||||
| 93 | + | ## Option Details | |||
| 94 | + | ||||
| 95 | + | ### `enabled` | |||
| 96 | + | ||||
| 97 | + | Master switch for rate limiting. When `false`, all requests are allowed without throttling. | |||
| 98 | + | ||||
| 99 | + | ```toml | |||
| 100 | + | [api.rate_limit] | |||
| 101 | + | enabled = false # Disable rate limiting (not recommended for production) | |||
| 102 | + | ``` | |||
| 103 | + | ||||
| 104 | + | ### `global_rps` | |||
| 105 | + | ||||
| 106 | + | The maximum number of requests per second that Gordon will accept from **all clients combined**. This is a hard cap that protects against distributed attacks where many IPs each send moderate traffic. | |||
| 107 | + | ||||
| 108 | + | ```toml | |||
| 109 | + | [api.rate_limit] | |||
| 110 | + | global_rps = 500 # 500 requests/second total | |||
| 111 | + | ``` | |||
| 112 | + | ||||
| 113 | + | **Sizing guidance:** | |||
| 114 | + | - **Small deployments** (1-10 apps): `100-500` RPS | |||
| 115 | + | - **Medium deployments** (10-50 apps): `500-2000` RPS | |||
| 116 | + | - **Large deployments** (50+ apps): `2000-10000` RPS | |||
| 117 | + | ||||
| 118 | + | Consider your CI/CD patterns—parallel builds pushing multiple images can generate bursts of traffic. | |||
| 119 | + | ||||
| 120 | + | ### `per_ip_rps` | |||
| 121 | + | ||||
| 122 | + | The maximum requests per second allowed from a **single client IP**. This prevents any single client from monopolizing Gordon's resources. | |||
| 123 | + | ||||
| 124 | + | ```toml | |||
| 125 | + | [api.rate_limit] | |||
| 126 | + | per_ip_rps = 50 # 50 requests/second per IP | |||
| 127 | + | ``` | |||
| 128 | + | ||||
| 129 | + | **Sizing guidance:** | |||
| 130 | + | - Normal Docker operations (pull, push) rarely exceed 10-20 RPS | |||
| 131 | + | - CI/CD pipelines with parallel jobs may need 50-100 RPS | |||
| 132 | + | - Automated tooling (vulnerability scanners, etc.) may need higher limits | |||
| 133 | + | ||||
| 134 | + | ### `burst` | |||
| 135 | + | ||||
| 136 | + | The token bucket burst size. Allows clients to temporarily exceed the rate limit for short bursts, which is useful for legitimate traffic patterns like: | |||
| 137 | + | - Initial connection setup (multiple manifest/blob requests) | |||
| 138 | + | - Parallel layer downloads | |||
| 139 | + | - Health check bursts | |||
| 140 | + | ||||
| 141 | + | ```toml | |||
| 142 | + | [api.rate_limit] | |||
| 143 | + | burst = 100 # Allow bursts of up to 100 requests | |||
| 144 | + | ``` | |||
| 145 | + | ||||
| 146 | + | A burst of `100` with `per_ip_rps = 50` means a client can send 100 requests instantly, then must wait ~2 seconds for the bucket to refill before another burst. | |||
| 147 | + | ||||
| 148 | + | ### `trusted_proxies` | |||
| 149 | + | ||||
| 150 | + | **Critical for correct IP detection in production.** | |||
| 151 | + | ||||
| 152 | + | Gordon is a reverse proxy that routes requests to your containers, but it **does not handle TLS termination**. For HTTPS support, you must place Gordon behind a TLS-terminating proxy like Cloudflare, nginx, or a load balancer. | |||
| 153 | + | ||||
| 154 | + | ``` | |||
| 155 | + | Internet → [Cloudflare/nginx] → Gordon → Containers | |||
| 156 | + | (HTTPS) (HTTP) | |||
| 157 | + | ``` | |||
| 158 | + | ||||
| 159 | + | In this setup, Gordon sees all connections coming from your front proxy's IP, not the real clients. The proxy sets `X-Forwarded-For` to communicate the original client IP. | |||
| 160 | + | ||||
| 161 | + | **The problem:** `X-Forwarded-For` can be spoofed. If Gordon trusted this header unconditionally, attackers could: | |||
| 162 | + | - Bypass per-IP rate limits by sending fake IPs | |||
| 163 | + | - Create unlimited rate limiter entries (memory exhaustion) | |||
| 164 | + | ||||
| 165 | + | **The solution:** Gordon only honors `X-Forwarded-For` from IPs listed in `trusted_proxies`. For all other connections, it uses the direct connection IP. | |||
| 166 | + | ||||
| 167 | + | ```toml | |||
| 168 | + | [api.rate_limit] | |||
| 169 | + | trusted_proxies = ["127.0.0.1", "10.0.0.0/8"] | |||
| 170 | + | ``` | |||
| 171 | + | ||||
| 172 | + | **Supported formats:** | |||
| 173 | + | - Single IP: `"192.168.1.1"` | |||
| 174 | + | - CIDR range: `"10.0.0.0/8"` | |||
| 175 | + | - IPv6: `"::1"`, `"fd00::/8"` | |||
| 176 | + | ||||
| 177 | + | > **If `trusted_proxies` is empty or misconfigured**, all requests appear to come from your proxy's IP, making per-IP rate limiting ineffective (all clients share one limit). | |||
| 178 | + | ||||
| 179 | + | ## Deployment Examples | |||
| 180 | + | ||||
| 181 | + | ### Cloudflare (Recommended) | |||
| 182 | + | ||||
| 183 | + | Cloudflare provides free TLS termination and DDoS protection. This is the recommended setup for production. | |||
| 184 | + | ||||
| 185 | + | Cloudflare publishes their IP ranges at https://www.cloudflare.com/ips/ | |||
| 186 | + | ||||
| 187 | + | ```toml | |||
| 188 | + | [api.rate_limit] | |||
| 189 | + | # Cloudflare IPs (verified 2026-01-18 - check cloudflare.com/ips for updates) | |||
| 190 | + | trusted_proxies = [ | |||
| 191 | + | # IPv4 | |||
| 192 | + | "173.245.48.0/20", | |||
| 193 | + | "103.21.244.0/22", | |||
| 194 | + | "103.22.200.0/22", | |||
| 195 | + | "103.31.4.0/22", | |||
| 196 | + | "141.101.64.0/18", | |||
| 197 | + | "108.162.192.0/18", | |||
| 198 | + | "190.93.240.0/20", | |||
| 199 | + | "188.114.96.0/20", | |||
| 200 | + | "197.234.240.0/22", | |||
| 201 | + | "198.41.128.0/17", | |||
| 202 | + | "162.158.0.0/15", | |||
| 203 | + | "104.16.0.0/13", | |||
| 204 | + | "104.24.0.0/14", | |||
| 205 | + | "172.64.0.0/13", | |||
| 206 | + | "131.0.72.0/22", | |||
| 207 | + | # IPv6 | |||
| 208 | + | "2400:cb00::/32", | |||
| 209 | + | "2606:4700::/32", | |||
| 210 | + | "2803:f800::/32", | |||
| 211 | + | "2405:b500::/32", | |||
| 212 | + | "2405:8100::/32", | |||
| 213 | + | "2a06:98c0::/29", | |||
| 214 | + | "2c0f:f248::/32", | |||
| 215 | + | ] | |||
| 216 | + | ``` | |||
| 217 | + | ||||
| 218 | + | > **Note:** Cloudflare IPs may change. Verify at https://www.cloudflare.com/ips/ periodically. | |||
| 219 | + | ||||
| 220 | + | ### Local Development (No TLS) | |||
| 221 | + | ||||
| 222 | + | For local development only, Gordon can be accessed directly without a proxy: | |||
| 223 | + | ||||
| 224 | + | ```toml | |||
| 225 | + | [api.rate_limit] | |||
| 226 | + | trusted_proxies = [] # Empty = use RemoteAddr directly | |||
| 227 | + | ``` | |||
| 228 | + | ||||
| 229 | + | > **Warning:** Never expose Gordon directly to the internet without TLS. Use this configuration only for local development. | |||
| 230 | + | ||||
| 231 | + | ### Behind nginx (Same Host) | |||
| 232 | + | ||||
| 233 | + | When nginx runs on the same machine as Gordon: | |||
| 234 | + | ||||
| 235 | + | ```toml | |||
| 236 | + | [api.rate_limit] | |||
| 237 | + | trusted_proxies = ["127.0.0.1", "::1"] | |||
| 238 | + | ``` | |||
| 239 | + | ||||
| 240 | + | ### Behind nginx (Separate Host) | |||
| 241 | + | ||||
| 242 | + | When nginx runs on a different server: | |||
| 243 | + | ||||
| 244 | + | ```toml | |||
| 245 | + | [api.rate_limit] | |||
| 246 | + | trusted_proxies = ["10.0.1.5"] # nginx server IP | |||
| 247 | + | ``` | |||
| 248 | + | ||||
| 249 | + | ### Behind AWS Application Load Balancer (ALB) | |||
| 250 | + | ||||
| 251 | + | ALB terminates TLS and forwards requests to your instances. Unlike Cloudflare, ALB IPs are dynamic and change as AWS scales the load balancer. You need to trust your VPC's private IP range (CIDR) since ALB connects from within your VPC. | |||
| 252 | + | ||||
| 253 | + | ```toml | |||
| 254 | + | [api.rate_limit] | |||
| 255 | + | trusted_proxies = ["10.0.0.0/16"] # Your VPC CIDR | |||
| 256 | + | ``` | |||
| 257 | + | ||||
| 258 | + | For detailed setup instructions, see the [AWS ALB Guide](../../wiki/guides/aws-alb.md). | |||
| 259 | + | ||||
| 260 | + | ### Behind Kubernetes Ingress | |||
| 261 | + | ||||
| 262 | + | When running Gordon in Kubernetes with an ingress controller (nginx-ingress, Traefik, etc.), you need to trust the pod network CIDR. The ingress controller must also be configured to forward client IPs via `X-Forwarded-For`. | |||
| 263 | + | ||||
| 264 | + | ```toml | |||
| 265 | + | [api.rate_limit] | |||
| 266 | + | trusted_proxies = ["10.244.0.0/16"] # Your pod network CIDR | |||
| 267 | + | ``` | |||
| 268 | + | ||||
| 269 | + | For detailed setup instructions, see the [Kubernetes Ingress Guide](../../wiki/guides/kubernetes-ingress.md). | |||
| 270 | + | ||||
| 271 | + | ## Rate Limit Response | |||
| 272 | + | ||||
| 273 | + | When a client exceeds the rate limit, Gordon returns: | |||
| 274 | + | ||||
| 275 | + | ``` | |||
| 276 | + | HTTP/1.1 429 Too Many Requests | |||
| 277 | + | Content-Type: application/json | |||
| 278 | + | Docker-Distribution-API-Version: registry/2.0 | |||
| 279 | + | Retry-After: 1 | |||
| 280 | + | ||||
| 281 | + | { | |||
| 282 | + | "errors": [ | |||
| 283 | + | { | |||
| 284 | + | "code": "TOOMANYREQUESTS", | |||
| 285 | + | "message": "rate limit exceeded" | |||
| 286 | + | } | |||
| 287 | + | ] | |||
| 288 | + | } | |||
| 289 | + | ``` | |||
| 290 | + | ||||
| 291 | + | The `Retry-After` header indicates when the client can retry (in seconds). | |||
| 292 | + | ||||
| 293 | + | ## Monitoring | |||
| 294 | + | ||||
| 295 | + | Rate-limited requests are logged at `info` level: | |||
| 296 | + | ||||
| 297 | + | ``` | |||
| 298 | + | level=info msg="rate limit exceeded" client_ip=203.0.113.50 path=/v2/myapp/manifests/latest | |||
| 299 | + | ``` | |||
| 300 | + | ||||
| 301 | + | To see rate limiting in action, enable debug logging: | |||
| 302 | + | ||||
| 303 | + | ```toml | |||
| 304 | + | [logging] | |||
| 305 | + | level = "debug" | |||
| 306 | + | ``` | |||
| 307 | + | ||||
| 308 | + | ## Security Considerations | |||
| 309 | + | ||||
| 310 | + | ### IP Spoofing Prevention | |||
| 311 | + | ||||
| 312 | + | If `trusted_proxies` is misconfigured, attackers can: | |||
| 313 | + | 1. Send requests with fake `X-Forwarded-For: 1.2.3.4` | |||
| 314 | + | 2. Each request appears to come from a different IP | |||
| 315 | + | 3. Per-IP rate limiting becomes ineffective | |||
| 316 | + | 4. Attackers create unbounded limiter entries (memory exhaustion) | |||
| 317 | + | ||||
| 318 | + | **Always verify your proxy configuration** by checking logs to ensure client IPs are detected correctly. | |||
| 319 | + | ||||
| 320 | + | ### Endpoint Coverage | |||
| 321 | + | ||||
| 322 | + | All authenticated endpoints are protected: | |||
| 323 | + | ||||
| 324 | + | | Endpoint | Rate Limiter | Limits Applied | | |||
| 325 | + | |----------|--------------|----------------| | |||
| 326 | + | | `/v2/*` (registry) | Registry limiter | Global RPS + Per-IP RPS | | |||
| 327 | + | | `/v2/token` | Registry limiter | Global RPS + Per-IP RPS | | |||
| 328 | + | | `/admin/*` | Admin limiter | Per-IP RPS only | | |||
| 329 | + | ||||
| 330 | + | The token endpoint shares the registry limiter, preventing brute force attacks on credentials. The admin API uses a separate per-IP limiter to avoid interference with registry traffic. | |||
| 331 | + | ||||
| 332 | + | ### Recommended Production Settings | |||
| 333 | + | ||||
| 334 | + | ```toml | |||
| 335 | + | [api.rate_limit] | |||
| 336 | + | enabled = true | |||
| 337 | + | global_rps = 1000 # Adjust based on your traffic | |||
| 338 | + | per_ip_rps = 30 # Stricter per-IP limit | |||
| 339 | + | burst = 50 # Smaller burst for tighter control | |||
| 340 | + | trusted_proxies = [] # Configure based on your proxy setup | |||
| 341 | + | ``` | |||
| 342 | + | ||||
| 343 | + | ## Troubleshooting | |||
| 344 | + | ||||
| 345 | + | ### "I'm getting rate limited but traffic is low" | |||
| 346 | + | ||||
| 347 | + | 1. Check if `trusted_proxies` is configured correctly | |||
| 348 | + | 2. All traffic may appear to come from your proxy's IP | |||
| 349 | + | 3. Add your proxy to `trusted_proxies` to see real client IPs | |||
| 350 | + | ||||
| 351 | + | ### "Rate limiting doesn't seem to work" | |||
| 352 | + | ||||
| 353 | + | 1. Verify `enabled = true` | |||
| 354 | + | 2. Check that requests go through Gordon (not cached by CDN) | |||
| 355 | + | 3. Ensure `global_rps` and `per_ip_rps` are set appropriately | |||
| 356 | + | ||||
| 357 | + | ### "I need to whitelist certain IPs" | |||
| 358 | + | ||||
| 359 | + | Rate limiting doesn't support IP whitelisting. Consider: | |||
| 360 | + | - Increasing `per_ip_rps` for legitimate high-traffic clients | |||
| 361 | + | - Using a reverse proxy with its own rate limiting rules | |||
| 362 | + | - Implementing IP-based access control at the network level | |||
| 363 | + | ||||
| 364 | + | ## Related | |||
| 365 | + | ||||
| 366 | + | - [Authentication](./auth.md) | |||
| 367 | + | - [Configuration Reference](./reference.md) | |||
| 368 | + | - [Logging](./logging.md) | |||
A wiki/guides/aws-alb.md +161 -0
| @@ -0,0 +1,161 @@ | |||
| 1 | + | # AWS Application Load Balancer (ALB) Setup | |
| 2 | + | ||
| 3 | + | This guide covers deploying Gordon behind AWS Application Load Balancer for TLS termination. | |
| 4 | + | ||
| 5 | + | ## Overview | |
| 6 | + | ||
| 7 | + | AWS ALB is a Layer 7 load balancer that: | |
| 8 | + | - Terminates TLS connections | |
| 9 | + | - Forwards requests to your EC2 instances or ECS containers | |
| 10 | + | - Automatically scales with traffic | |
| 11 | + | - Sets `X-Forwarded-For` header with the client's real IP | |
| 12 | + | ||
| 13 | + | ## Architecture | |
| 14 | + | ||
| 15 | + | ``` | |
| 16 | + | Client → ALB (HTTPS/443) → Gordon (HTTP/80) → Containers | |
| 17 | + | ↓ | |
| 18 | + | TLS terminated | |
| 19 | + | X-Forwarded-For set | |
| 20 | + | ``` | |
| 21 | + | ||
| 22 | + | ## Understanding VPC CIDR | |
| 23 | + | ||
| 24 | + | A **VPC** (Virtual Private Cloud) is your isolated network in AWS. Every VPC has a **CIDR** (Classless Inter-Domain Routing) block—the private IP range used by resources in that VPC. | |
| 25 | + | ||
| 26 | + | ALB connects to Gordon from within your VPC, so Gordon sees the ALB's private IP, not the client's public IP. The client IP is passed via `X-Forwarded-For`. | |
| 27 | + | ||
| 28 | + | **To find your VPC CIDR:** | |
| 29 | + | ||
| 30 | + | 1. Go to AWS Console → VPC → Your VPCs | |
| 31 | + | 2. Find the **IPv4 CIDR** column | |
| 32 | + | 3. Common values: `10.0.0.0/16`, `172.31.0.0/16` | |
| 33 | + | ||
| 34 | + | ## Gordon Configuration | |
| 35 | + | ||
| 36 | + | ```toml | |
| 37 | + | [api.rate_limit] | |
| 38 | + | enabled = true | |
| 39 | + | # Trust your VPC CIDR so Gordon reads X-Forwarded-For from ALB | |
| 40 | + | trusted_proxies = ["10.0.0.0/16"] # Replace with your VPC CIDR | |
| 41 | + | ``` | |
| 42 | + | ||
| 43 | + | ### Multiple VPCs or Subnets | |
| 44 | + | ||
| 45 | + | If Gordon receives traffic from multiple VPCs (e.g., via VPC peering): | |
| 46 | + | ||
| 47 | + | ```toml | |
| 48 | + | [api.rate_limit] | |
| 49 | + | trusted_proxies = [ | |
| 50 | + | "10.0.0.0/16", # Production VPC | |
| 51 | + | "10.1.0.0/16", # Staging VPC | |
| 52 | + | ] | |
| 53 | + | ``` | |
| 54 | + | ||
| 55 | + | ## ALB Configuration | |
| 56 | + | ||
| 57 | + | ### Target Group Settings | |
| 58 | + | ||
| 59 | + | When creating your ALB target group: | |
| 60 | + | ||
| 61 | + | | Setting | Value | | |
| 62 | + | |---------|-------| | |
| 63 | + | | Target type | `instance` or `ip` | | |
| 64 | + | | Protocol | HTTP | | |
| 65 | + | | Port | 80 (or your Gordon port) | | |
| 66 | + | | Health check path | `/health` or `/v2/` | | |
| 67 | + | ||
| 68 | + | ### Client IP Preservation | |
| 69 | + | ||
| 70 | + | ALB automatically sets `X-Forwarded-For`. No special configuration needed for HTTP targets. | |
| 71 | + | ||
| 72 | + | For **NLB** (Network Load Balancer) with TCP, you may need to enable Proxy Protocol v2: | |
| 73 | + | ||
| 74 | + | ```yaml | |
| 75 | + | # Kubernetes annotation for AWS Load Balancer Controller | |
| 76 | + | service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*" | |
| 77 | + | ``` | |
| 78 | + | ||
| 79 | + | ## Verifying the Setup | |
| 80 | + | ||
| 81 | + | 1. Make a request through the ALB | |
| 82 | + | 2. Check Gordon logs for the client IP: | |
| 83 | + | ||
| 84 | + | ```bash | |
| 85 | + | # Should show real client IP, not ALB IP | |
| 86 | + | journalctl -u gordon | grep "client_ip" | |
| 87 | + | ``` | |
| 88 | + | ||
| 89 | + | If you see the ALB's private IP (e.g., `10.0.x.x`) instead of the client's public IP, check that: | |
| 90 | + | - `trusted_proxies` includes your VPC CIDR | |
| 91 | + | - ALB is forwarding `X-Forwarded-For` (default behavior) | |
| 92 | + | ||
| 93 | + | ## Security Groups | |
| 94 | + | ||
| 95 | + | Ensure your security groups allow: | |
| 96 | + | ||
| 97 | + | | Source | Destination | Port | Purpose | | |
| 98 | + | |--------|-------------|------|---------| | |
| 99 | + | | `0.0.0.0/0` | ALB | 443 | HTTPS from internet | | |
| 100 | + | | ALB SG | Gordon SG | 80 | ALB to Gordon | | |
| 101 | + | ||
| 102 | + | ## Terraform Example | |
| 103 | + | ||
| 104 | + | ```hcl | |
| 105 | + | resource "aws_lb" "gordon" { | |
| 106 | + | name = "gordon-alb" | |
| 107 | + | internal = false | |
| 108 | + | load_balancer_type = "application" | |
| 109 | + | security_groups = [aws_security_group.alb.id] | |
| 110 | + | subnets = var.public_subnets | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | resource "aws_lb_listener" "https" { | |
| 114 | + | load_balancer_arn = aws_lb.gordon.arn | |
| 115 | + | port = "443" | |
| 116 | + | protocol = "HTTPS" | |
| 117 | + | ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06" | |
| 118 | + | certificate_arn = var.certificate_arn | |
| 119 | + | ||
| 120 | + | default_action { | |
| 121 | + | type = "forward" | |
| 122 | + | target_group_arn = aws_lb_target_group.gordon.arn | |
| 123 | + | } | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | resource "aws_lb_target_group" "gordon" { | |
| 127 | + | name = "gordon-tg" | |
| 128 | + | port = 80 | |
| 129 | + | protocol = "HTTP" | |
| 130 | + | vpc_id = var.vpc_id | |
| 131 | + | ||
| 132 | + | health_check { | |
| 133 | + | path = "/v2/" | |
| 134 | + | healthy_threshold = 2 | |
| 135 | + | unhealthy_threshold = 10 | |
| 136 | + | } | |
| 137 | + | } | |
| 138 | + | ``` | |
| 139 | + | ||
| 140 | + | ## Troubleshooting | |
| 141 | + | ||
| 142 | + | ### All requests show the same IP | |
| 143 | + | ||
| 144 | + | **Symptom:** Rate limiting doesn't work, logs show ALB IP for all requests. | |
| 145 | + | ||
| 146 | + | **Cause:** `trusted_proxies` doesn't include ALB's IP range. | |
| 147 | + | ||
| 148 | + | **Fix:** Add your VPC CIDR to `trusted_proxies`. | |
| 149 | + | ||
| 150 | + | ### 502 Bad Gateway | |
| 151 | + | ||
| 152 | + | **Symptom:** ALB returns 502 errors. | |
| 153 | + | ||
| 154 | + | **Cause:** Gordon health check failing or not listening. | |
| 155 | + | ||
| 156 | + | **Fix:** Verify Gordon is running and health check path is correct. | |
| 157 | + | ||
| 158 | + | ## Related | |
| 159 | + | ||
| 160 | + | - [Rate Limiting Configuration](../../docs/config/rate-limiting.md) | |
| 161 | + | - [Running Gordon in a Container](./running-in-container.md) | |
| @@ -0,0 +1,161 @@ | |||||
| 1 | + | # AWS Application Load Balancer (ALB) Setup | |||
| 2 | + | ||||
| 3 | + | This guide covers deploying Gordon behind AWS Application Load Balancer for TLS termination. | |||
| 4 | + | ||||
| 5 | + | ## Overview | |||
| 6 | + | ||||
| 7 | + | AWS ALB is a Layer 7 load balancer that: | |||
| 8 | + | - Terminates TLS connections | |||
| 9 | + | - Forwards requests to your EC2 instances or ECS containers | |||
| 10 | + | - Automatically scales with traffic | |||
| 11 | + | - Sets `X-Forwarded-For` header with the client's real IP | |||
| 12 | + | ||||
| 13 | + | ## Architecture | |||
| 14 | + | ||||
| 15 | + | ``` | |||
| 16 | + | Client → ALB (HTTPS/443) → Gordon (HTTP/80) → Containers | |||
| 17 | + | ↓ | |||
| 18 | + | TLS terminated | |||
| 19 | + | X-Forwarded-For set | |||
| 20 | + | ``` | |||
| 21 | + | ||||
| 22 | + | ## Understanding VPC CIDR | |||
| 23 | + | ||||
| 24 | + | A **VPC** (Virtual Private Cloud) is your isolated network in AWS. Every VPC has a **CIDR** (Classless Inter-Domain Routing) block—the private IP range used by resources in that VPC. | |||
| 25 | + | ||||
| 26 | + | ALB connects to Gordon from within your VPC, so Gordon sees the ALB's private IP, not the client's public IP. The client IP is passed via `X-Forwarded-For`. | |||
| 27 | + | ||||
| 28 | + | **To find your VPC CIDR:** | |||
| 29 | + | ||||
| 30 | + | 1. Go to AWS Console → VPC → Your VPCs | |||
| 31 | + | 2. Find the **IPv4 CIDR** column | |||
| 32 | + | 3. Common values: `10.0.0.0/16`, `172.31.0.0/16` | |||
| 33 | + | ||||
| 34 | + | ## Gordon Configuration | |||
| 35 | + | ||||
| 36 | + | ```toml | |||
| 37 | + | [api.rate_limit] | |||
| 38 | + | enabled = true | |||
| 39 | + | # Trust your VPC CIDR so Gordon reads X-Forwarded-For from ALB | |||
| 40 | + | trusted_proxies = ["10.0.0.0/16"] # Replace with your VPC CIDR | |||
| 41 | + | ``` | |||
| 42 | + | ||||
| 43 | + | ### Multiple VPCs or Subnets | |||
| 44 | + | ||||
| 45 | + | If Gordon receives traffic from multiple VPCs (e.g., via VPC peering): | |||
| 46 | + | ||||
| 47 | + | ```toml | |||
| 48 | + | [api.rate_limit] | |||
| 49 | + | trusted_proxies = [ | |||
| 50 | + | "10.0.0.0/16", # Production VPC | |||
| 51 | + | "10.1.0.0/16", # Staging VPC | |||
| 52 | + | ] | |||
| 53 | + | ``` | |||
| 54 | + | ||||
| 55 | + | ## ALB Configuration | |||
| 56 | + | ||||
| 57 | + | ### Target Group Settings | |||
| 58 | + | ||||
| 59 | + | When creating your ALB target group: | |||
| 60 | + | ||||
| 61 | + | | Setting | Value | | |||
| 62 | + | |---------|-------| | |||
| 63 | + | | Target type | `instance` or `ip` | | |||
| 64 | + | | Protocol | HTTP | | |||
| 65 | + | | Port | 80 (or your Gordon port) | | |||
| 66 | + | | Health check path | `/health` or `/v2/` | | |||
| 67 | + | ||||
| 68 | + | ### Client IP Preservation | |||
| 69 | + | ||||
| 70 | + | ALB automatically sets `X-Forwarded-For`. No special configuration needed for HTTP targets. | |||
| 71 | + | ||||
| 72 | + | For **NLB** (Network Load Balancer) with TCP, you may need to enable Proxy Protocol v2: | |||
| 73 | + | ||||
| 74 | + | ```yaml | |||
| 75 | + | # Kubernetes annotation for AWS Load Balancer Controller | |||
| 76 | + | service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*" | |||
| 77 | + | ``` | |||
| 78 | + | ||||
| 79 | + | ## Verifying the Setup | |||
| 80 | + | ||||
| 81 | + | 1. Make a request through the ALB | |||
| 82 | + | 2. Check Gordon logs for the client IP: | |||
| 83 | + | ||||
| 84 | + | ```bash | |||
| 85 | + | # Should show real client IP, not ALB IP | |||
| 86 | + | journalctl -u gordon | grep "client_ip" | |||
| 87 | + | ``` | |||
| 88 | + | ||||
| 89 | + | If you see the ALB's private IP (e.g., `10.0.x.x`) instead of the client's public IP, check that: | |||
| 90 | + | - `trusted_proxies` includes your VPC CIDR | |||
| 91 | + | - ALB is forwarding `X-Forwarded-For` (default behavior) | |||
| 92 | + | ||||
| 93 | + | ## Security Groups | |||
| 94 | + | ||||
| 95 | + | Ensure your security groups allow: | |||
| 96 | + | ||||
| 97 | + | | Source | Destination | Port | Purpose | | |||
| 98 | + | |--------|-------------|------|---------| | |||
| 99 | + | | `0.0.0.0/0` | ALB | 443 | HTTPS from internet | | |||
| 100 | + | | ALB SG | Gordon SG | 80 | ALB to Gordon | | |||
| 101 | + | ||||
| 102 | + | ## Terraform Example | |||
| 103 | + | ||||
| 104 | + | ```hcl | |||
| 105 | + | resource "aws_lb" "gordon" { | |||
| 106 | + | name = "gordon-alb" | |||
| 107 | + | internal = false | |||
| 108 | + | load_balancer_type = "application" | |||
| 109 | + | security_groups = [aws_security_group.alb.id] | |||
| 110 | + | subnets = var.public_subnets | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | resource "aws_lb_listener" "https" { | |||
| 114 | + | load_balancer_arn = aws_lb.gordon.arn | |||
| 115 | + | port = "443" | |||
| 116 | + | protocol = "HTTPS" | |||
| 117 | + | ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06" | |||
| 118 | + | certificate_arn = var.certificate_arn | |||
| 119 | + | ||||
| 120 | + | default_action { | |||
| 121 | + | type = "forward" | |||
| 122 | + | target_group_arn = aws_lb_target_group.gordon.arn | |||
| 123 | + | } | |||
| 124 | + | } | |||
| 125 | + | ||||
| 126 | + | resource "aws_lb_target_group" "gordon" { | |||
| 127 | + | name = "gordon-tg" | |||
| 128 | + | port = 80 | |||
| 129 | + | protocol = "HTTP" | |||
| 130 | + | vpc_id = var.vpc_id | |||
| 131 | + | ||||
| 132 | + | health_check { | |||
| 133 | + | path = "/v2/" | |||
| 134 | + | healthy_threshold = 2 | |||
| 135 | + | unhealthy_threshold = 10 | |||
| 136 | + | } | |||
| 137 | + | } | |||
| 138 | + | ``` | |||
| 139 | + | ||||
| 140 | + | ## Troubleshooting | |||
| 141 | + | ||||
| 142 | + | ### All requests show the same IP | |||
| 143 | + | ||||
| 144 | + | **Symptom:** Rate limiting doesn't work, logs show ALB IP for all requests. | |||
| 145 | + | ||||
| 146 | + | **Cause:** `trusted_proxies` doesn't include ALB's IP range. | |||
| 147 | + | ||||
| 148 | + | **Fix:** Add your VPC CIDR to `trusted_proxies`. | |||
| 149 | + | ||||
| 150 | + | ### 502 Bad Gateway | |||
| 151 | + | ||||
| 152 | + | **Symptom:** ALB returns 502 errors. | |||
| 153 | + | ||||
| 154 | + | **Cause:** Gordon health check failing or not listening. | |||
| 155 | + | ||||
| 156 | + | **Fix:** Verify Gordon is running and health check path is correct. | |||
| 157 | + | ||||
| 158 | + | ## Related | |||
| 159 | + | ||||
| 160 | + | - [Rate Limiting Configuration](../../docs/config/rate-limiting.md) | |||
| 161 | + | - [Running Gordon in a Container](./running-in-container.md) | |||
M wiki/guides/index.md +4 -2
| @@ -7,9 +7,11 @@ In-depth guides for specific Gordon setups and integrations. | |||
| 7 | 7 | - [Running Gordon in a Container](./running-in-container.md) - Deploy Gordon itself in Docker or Podman | |
| 8 | 8 | - [Podman Rootless Setup](./podman-rootless.md) - Enhanced security with rootless containers | |
| 9 | 9 | ||
| 10 | - | ## External Services | |
| 10 | + | ## TLS & Load Balancing | |
| 11 | 11 | ||
| 12 | - | - [Cloudflare DNS & Proxy Setup](./cloudflare-setup.md) - HTTPS termination with Cloudflare | |
| 12 | + | - [Cloudflare DNS & Proxy Setup](./cloudflare-setup.md) - HTTPS termination with Cloudflare (recommended) | |
| 13 | + | - [AWS Application Load Balancer](./aws-alb.md) - Deploy behind AWS ALB | |
| 14 | + | - [Kubernetes Ingress](./kubernetes-ingress.md) - Deploy behind Kubernetes ingress controller | |
| 13 | 15 | ||
| 14 | 16 | ## Remote Management | |
| 15 | 17 | ||
| @@ -7,9 +7,11 @@ In-depth guides for specific Gordon setups and integrations. | |||||
| 7 | - [Running Gordon in a Container](./running-in-container.md) - Deploy Gordon itself in Docker or Podman | 7 | - [Running Gordon in a Container](./running-in-container.md) - Deploy Gordon itself in Docker or Podman | ||
| 8 | - [Podman Rootless Setup](./podman-rootless.md) - Enhanced security with rootless containers | 8 | - [Podman Rootless Setup](./podman-rootless.md) - Enhanced security with rootless containers | ||
| 9 | 9 | ||||
| 10 | - | ## External Services | 10 | + | ## TLS & Load Balancing |
| 11 | 11 | ||||
| 12 | - | - [Cloudflare DNS & Proxy Setup](./cloudflare-setup.md) - HTTPS termination with Cloudflare | 12 | + | - [Cloudflare DNS & Proxy Setup](./cloudflare-setup.md) - HTTPS termination with Cloudflare (recommended) |
| 13 | + | - [AWS Application Load Balancer](./aws-alb.md) - Deploy behind AWS ALB | |||
| 14 | + | - [Kubernetes Ingress](./kubernetes-ingress.md) - Deploy behind Kubernetes ingress controller | |||
| 13 | 15 | ||||
| 14 | ## Remote Management | 16 | ## Remote Management | ||
| 15 | 17 | ||||
A wiki/guides/kubernetes-ingress.md +287 -0
| @@ -0,0 +1,287 @@ | |||
| 1 | + | # Kubernetes Ingress Setup | |
| 2 | + | ||
| 3 | + | This guide covers deploying Gordon behind a Kubernetes ingress controller for TLS termination. | |
| 4 | + | ||
| 5 | + | ## Overview | |
| 6 | + | ||
| 7 | + | In Kubernetes, an **Ingress Controller** handles external traffic: | |
| 8 | + | - Terminates TLS connections | |
| 9 | + | - Routes requests to services based on hostname/path | |
| 10 | + | - Sets `X-Forwarded-For` header with the client's real IP | |
| 11 | + | ||
| 12 | + | Common ingress controllers: **nginx-ingress**, **Traefik**, **Contour**, **Kong**. | |
| 13 | + | ||
| 14 | + | ## Architecture | |
| 15 | + | ||
| 16 | + | ``` | |
| 17 | + | Client → Ingress Controller (HTTPS/443) → Gordon Service (HTTP) → Gordon Pod | |
| 18 | + | ↓ | |
| 19 | + | TLS terminated | |
| 20 | + | X-Forwarded-For set | |
| 21 | + | ``` | |
| 22 | + | ||
| 23 | + | ## Understanding Pod Network CIDR | |
| 24 | + | ||
| 25 | + | Kubernetes assigns each pod an IP from the **pod network CIDR**. When the ingress controller forwards requests to Gordon, it comes from the ingress pod's IP. | |
| 26 | + | ||
| 27 | + | **To find your pod network CIDR:** | |
| 28 | + | ||
| 29 | + | ```bash | |
| 30 | + | # Method 1: Check cluster-info | |
| 31 | + | kubectl cluster-info dump | grep -m 1 cluster-cidr | |
| 32 | + | ||
| 33 | + | # Method 2: Check CNI config (varies by CNI plugin) | |
| 34 | + | kubectl get cm -n kube-system kubeadm-config -o yaml | grep podSubnet | |
| 35 | + | ||
| 36 | + | # Method 3: Check a pod's IP to infer the range | |
| 37 | + | kubectl get pods -o wide | |
| 38 | + | ``` | |
| 39 | + | ||
| 40 | + | **Common defaults by CNI:** | |
| 41 | + | ||
| 42 | + | | CNI Plugin | Default Pod CIDR | | |
| 43 | + | |------------|------------------| | |
| 44 | + | | Flannel | `10.244.0.0/16` | | |
| 45 | + | | Calico | `192.168.0.0/16` | | |
| 46 | + | | Weave | `10.32.0.0/12` | | |
| 47 | + | | Cilium | `10.0.0.0/8` | | |
| 48 | + | | GKE | `10.0.0.0/14` or `/9` | | |
| 49 | + | | EKS (VPC CNI) | VPC CIDR | | |
| 50 | + | ||
| 51 | + | ## Gordon Configuration | |
| 52 | + | ||
| 53 | + | ```toml | |
| 54 | + | [api.rate_limit] | |
| 55 | + | enabled = true | |
| 56 | + | # Trust pod network so Gordon reads X-Forwarded-For from ingress | |
| 57 | + | trusted_proxies = ["10.244.0.0/16"] # Replace with your pod CIDR | |
| 58 | + | ``` | |
| 59 | + | ||
| 60 | + | ## Ingress Controller Configuration | |
| 61 | + | ||
| 62 | + | The ingress controller must be configured to forward the real client IP. | |
| 63 | + | ||
| 64 | + | ### nginx-ingress | |
| 65 | + | ||
| 66 | + | Create or update the ConfigMap: | |
| 67 | + | ||
| 68 | + | ```yaml | |
| 69 | + | apiVersion: v1 | |
| 70 | + | kind: ConfigMap | |
| 71 | + | metadata: | |
| 72 | + | name: ingress-nginx-controller | |
| 73 | + | namespace: ingress-nginx | |
| 74 | + | data: | |
| 75 | + | # Enable X-Forwarded-For processing | |
| 76 | + | use-forwarded-headers: "true" | |
| 77 | + | # Trust these CIDRs to set X-Forwarded-For | |
| 78 | + | # Include your cloud provider's load balancer IPs | |
| 79 | + | proxy-real-ip-cidr: "0.0.0.0/0" # Or specific LB CIDR | |
| 80 | + | ``` | |
| 81 | + | ||
| 82 | + | If you have a cloud load balancer in front of nginx-ingress (common in EKS, GKE): | |
| 83 | + | ||
| 84 | + | ```yaml | |
| 85 | + | data: | |
| 86 | + | use-forwarded-headers: "true" | |
| 87 | + | use-proxy-protocol: "false" # Set to "true" if using NLB with proxy protocol | |
| 88 | + | proxy-real-ip-cidr: "10.0.0.0/8,192.168.0.0/16" | |
| 89 | + | ``` | |
| 90 | + | ||
| 91 | + | ### Traefik | |
| 92 | + | ||
| 93 | + | In your Traefik configuration: | |
| 94 | + | ||
| 95 | + | ```yaml | |
| 96 | + | # traefik.yaml or Helm values | |
| 97 | + | entryPoints: | |
| 98 | + | web: | |
| 99 | + | address: ":80" | |
| 100 | + | forwardedHeaders: | |
| 101 | + | trustedIPs: | |
| 102 | + | - "10.0.0.0/8" | |
| 103 | + | - "192.168.0.0/16" | |
| 104 | + | websecure: | |
| 105 | + | address: ":443" | |
| 106 | + | forwardedHeaders: | |
| 107 | + | trustedIPs: | |
| 108 | + | - "10.0.0.0/8" | |
| 109 | + | - "192.168.0.0/16" | |
| 110 | + | ``` | |
| 111 | + | ||
| 112 | + | ## Complete Example | |
| 113 | + | ||
| 114 | + | ### Gordon Deployment | |
| 115 | + | ||
| 116 | + | ```yaml | |
| 117 | + | apiVersion: apps/v1 | |
| 118 | + | kind: Deployment | |
| 119 | + | metadata: | |
| 120 | + | name: gordon | |
| 121 | + | spec: | |
| 122 | + | replicas: 1 | |
| 123 | + | selector: | |
| 124 | + | matchLabels: | |
| 125 | + | app: gordon | |
| 126 | + | template: | |
| 127 | + | metadata: | |
| 128 | + | labels: | |
| 129 | + | app: gordon | |
| 130 | + | spec: | |
| 131 | + | containers: | |
| 132 | + | - name: gordon | |
| 133 | + | image: your-registry/gordon:latest | |
| 134 | + | ports: | |
| 135 | + | - containerPort: 80 | |
| 136 | + | - containerPort: 5000 | |
| 137 | + | volumeMounts: | |
| 138 | + | - name: config | |
| 139 | + | mountPath: /etc/gordon | |
| 140 | + | - name: docker-sock | |
| 141 | + | mountPath: /var/run/docker.sock | |
| 142 | + | volumes: | |
| 143 | + | - name: config | |
| 144 | + | configMap: | |
| 145 | + | name: gordon-config | |
| 146 | + | - name: docker-sock | |
| 147 | + | hostPath: | |
| 148 | + | path: /var/run/docker.sock | |
| 149 | + | --- | |
| 150 | + | apiVersion: v1 | |
| 151 | + | kind: Service | |
| 152 | + | metadata: | |
| 153 | + | name: gordon | |
| 154 | + | spec: | |
| 155 | + | selector: | |
| 156 | + | app: gordon | |
| 157 | + | ports: | |
| 158 | + | - name: http | |
| 159 | + | port: 80 | |
| 160 | + | targetPort: 80 | |
| 161 | + | - name: registry | |
| 162 | + | port: 5000 | |
| 163 | + | targetPort: 5000 | |
| 164 | + | ``` | |
| 165 | + | ||
| 166 | + | ### Gordon ConfigMap | |
| 167 | + | ||
| 168 | + | ```yaml | |
| 169 | + | apiVersion: v1 | |
| 170 | + | kind: ConfigMap | |
| 171 | + | metadata: | |
| 172 | + | name: gordon-config | |
| 173 | + | data: | |
| 174 | + | gordon.toml: | | |
| 175 | + | [server] | |
| 176 | + | port = 80 | |
| 177 | + | registry_port = 5000 | |
| 178 | + | gordon_domain = "gordon.example.com" | |
| 179 | + | ||
| 180 | + | [api.rate_limit] | |
| 181 | + | enabled = true | |
| 182 | + | trusted_proxies = ["10.244.0.0/16"] | |
| 183 | + | ||
| 184 | + | [auth] | |
| 185 | + | enabled = false # Configure auth as needed | |
| 186 | + | ``` | |
| 187 | + | ||
| 188 | + | ### Ingress Resource | |
| 189 | + | ||
| 190 | + | ```yaml | |
| 191 | + | apiVersion: networking.k8s.io/v1 | |
| 192 | + | kind: Ingress | |
| 193 | + | metadata: | |
| 194 | + | name: gordon | |
| 195 | + | annotations: | |
| 196 | + | nginx.ingress.kubernetes.io/proxy-body-size: "0" # Unlimited for image uploads | |
| 197 | + | spec: | |
| 198 | + | ingressClassName: nginx | |
| 199 | + | tls: | |
| 200 | + | - hosts: | |
| 201 | + | - gordon.example.com | |
| 202 | + | secretName: gordon-tls | |
| 203 | + | rules: | |
| 204 | + | - host: gordon.example.com | |
| 205 | + | http: | |
| 206 | + | paths: | |
| 207 | + | - path: / | |
| 208 | + | pathType: Prefix | |
| 209 | + | backend: | |
| 210 | + | service: | |
| 211 | + | name: gordon | |
| 212 | + | port: | |
| 213 | + | number: 80 | |
| 214 | + | ``` | |
| 215 | + | ||
| 216 | + | ## Verifying the Setup | |
| 217 | + | ||
| 218 | + | 1. Deploy Gordon and the ingress | |
| 219 | + | 2. Make a request through the ingress | |
| 220 | + | 3. Check Gordon logs: | |
| 221 | + | ||
| 222 | + | ```bash | |
| 223 | + | kubectl logs -l app=gordon | grep "client_ip" | |
| 224 | + | ``` | |
| 225 | + | ||
| 226 | + | If you see the ingress pod's IP instead of the client's IP: | |
| 227 | + | - Verify `trusted_proxies` includes your pod CIDR | |
| 228 | + | - Verify ingress controller has `use-forwarded-headers: "true"` | |
| 229 | + | ||
| 230 | + | ## Cloud-Specific Notes | |
| 231 | + | ||
| 232 | + | ### GKE | |
| 233 | + | ||
| 234 | + | GKE uses a VPC-native pod network. Find the pod CIDR in: | |
| 235 | + | - Console → Kubernetes Engine → Cluster → Networking → Pod address range | |
| 236 | + | ||
| 237 | + | ```toml | |
| 238 | + | trusted_proxies = ["10.0.0.0/14"] # GKE default | |
| 239 | + | ``` | |
| 240 | + | ||
| 241 | + | ### EKS | |
| 242 | + | ||
| 243 | + | EKS with VPC CNI assigns pod IPs from your VPC subnets: | |
| 244 | + | ||
| 245 | + | ```toml | |
| 246 | + | trusted_proxies = ["10.0.0.0/16"] # Your VPC CIDR | |
| 247 | + | ``` | |
| 248 | + | ||
| 249 | + | ### AKS | |
| 250 | + | ||
| 251 | + | AKS default pod CIDR: | |
| 252 | + | ||
| 253 | + | ```toml | |
| 254 | + | trusted_proxies = ["10.244.0.0/16"] # Azure CNI default | |
| 255 | + | ``` | |
| 256 | + | ||
| 257 | + | ## Troubleshooting | |
| 258 | + | ||
| 259 | + | ### All requests show the same IP | |
| 260 | + | ||
| 261 | + | **Symptom:** Rate limiting ineffective, logs show ingress pod IP. | |
| 262 | + | ||
| 263 | + | **Cause:** `trusted_proxies` missing pod CIDR or ingress not forwarding headers. | |
| 264 | + | ||
| 265 | + | **Fix:** | |
| 266 | + | 1. Add pod CIDR to `trusted_proxies` | |
| 267 | + | 2. Enable `use-forwarded-headers` in ingress controller | |
| 268 | + | ||
| 269 | + | ### X-Forwarded-For contains multiple IPs | |
| 270 | + | ||
| 271 | + | **Symptom:** `X-Forwarded-For: 203.0.113.50, 10.0.1.5, 10.244.0.3` | |
| 272 | + | ||
| 273 | + | This is normal when multiple proxies are involved (cloud LB → ingress → Gordon). Gordon takes the **first** IP (leftmost), which is the original client. | |
| 274 | + | ||
| 275 | + | ### Client IP shows cloud load balancer | |
| 276 | + | ||
| 277 | + | **Symptom:** Client IP is AWS/GCP/Azure LB IP, not real client. | |
| 278 | + | ||
| 279 | + | **Cause:** Cloud LB not configured to forward client IP. | |
| 280 | + | ||
| 281 | + | **Fix:** Configure cloud LB to preserve or pass client IP (varies by provider). | |
| 282 | + | ||
| 283 | + | ## Related | |
| 284 | + | ||
| 285 | + | - [Rate Limiting Configuration](../../docs/config/rate-limiting.md) | |
| 286 | + | - [AWS ALB Guide](./aws-alb.md) | |
| 287 | + | - [Running Gordon in a Container](./running-in-container.md) | |
| @@ -0,0 +1,287 @@ | |||||
| 1 | + | # Kubernetes Ingress Setup | |||
| 2 | + | ||||
| 3 | + | This guide covers deploying Gordon behind a Kubernetes ingress controller for TLS termination. | |||
| 4 | + | ||||
| 5 | + | ## Overview | |||
| 6 | + | ||||
| 7 | + | In Kubernetes, an **Ingress Controller** handles external traffic: | |||
| 8 | + | - Terminates TLS connections | |||
| 9 | + | - Routes requests to services based on hostname/path | |||
| 10 | + | - Sets `X-Forwarded-For` header with the client's real IP | |||
| 11 | + | ||||
| 12 | + | Common ingress controllers: **nginx-ingress**, **Traefik**, **Contour**, **Kong**. | |||
| 13 | + | ||||
| 14 | + | ## Architecture | |||
| 15 | + | ||||
| 16 | + | ``` | |||
| 17 | + | Client → Ingress Controller (HTTPS/443) → Gordon Service (HTTP) → Gordon Pod | |||
| 18 | + | ↓ | |||
| 19 | + | TLS terminated | |||
| 20 | + | X-Forwarded-For set | |||
| 21 | + | ``` | |||
| 22 | + | ||||
| 23 | + | ## Understanding Pod Network CIDR | |||
| 24 | + | ||||
| 25 | + | Kubernetes assigns each pod an IP from the **pod network CIDR**. When the ingress controller forwards requests to Gordon, it comes from the ingress pod's IP. | |||
| 26 | + | ||||
| 27 | + | **To find your pod network CIDR:** | |||
| 28 | + | ||||
| 29 | + | ```bash | |||
| 30 | + | # Method 1: Check cluster-info | |||
| 31 | + | kubectl cluster-info dump | grep -m 1 cluster-cidr | |||
| 32 | + | ||||
| 33 | + | # Method 2: Check CNI config (varies by CNI plugin) | |||
| 34 | + | kubectl get cm -n kube-system kubeadm-config -o yaml | grep podSubnet | |||
| 35 | + | ||||
| 36 | + | # Method 3: Check a pod's IP to infer the range | |||
| 37 | + | kubectl get pods -o wide | |||
| 38 | + | ``` | |||
| 39 | + | ||||
| 40 | + | **Common defaults by CNI:** | |||
| 41 | + | ||||
| 42 | + | | CNI Plugin | Default Pod CIDR | | |||
| 43 | + | |------------|------------------| | |||
| 44 | + | | Flannel | `10.244.0.0/16` | | |||
| 45 | + | | Calico | `192.168.0.0/16` | | |||
| 46 | + | | Weave | `10.32.0.0/12` | | |||
| 47 | + | | Cilium | `10.0.0.0/8` | | |||
| 48 | + | | GKE | `10.0.0.0/14` or `/9` | | |||
| 49 | + | | EKS (VPC CNI) | VPC CIDR | | |||
| 50 | + | ||||
| 51 | + | ## Gordon Configuration | |||
| 52 | + | ||||
| 53 | + | ```toml | |||
| 54 | + | [api.rate_limit] | |||
| 55 | + | enabled = true | |||
| 56 | + | # Trust pod network so Gordon reads X-Forwarded-For from ingress | |||
| 57 | + | trusted_proxies = ["10.244.0.0/16"] # Replace with your pod CIDR | |||
| 58 | + | ``` | |||
| 59 | + | ||||
| 60 | + | ## Ingress Controller Configuration | |||
| 61 | + | ||||
| 62 | + | The ingress controller must be configured to forward the real client IP. | |||
| 63 | + | ||||
| 64 | + | ### nginx-ingress | |||
| 65 | + | ||||
| 66 | + | Create or update the ConfigMap: | |||
| 67 | + | ||||
| 68 | + | ```yaml | |||
| 69 | + | apiVersion: v1 | |||
| 70 | + | kind: ConfigMap | |||
| 71 | + | metadata: | |||
| 72 | + | name: ingress-nginx-controller | |||
| 73 | + | namespace: ingress-nginx | |||
| 74 | + | data: | |||
| 75 | + | # Enable X-Forwarded-For processing | |||
| 76 | + | use-forwarded-headers: "true" | |||
| 77 | + | # Trust these CIDRs to set X-Forwarded-For | |||
| 78 | + | # Include your cloud provider's load balancer IPs | |||
| 79 | + | proxy-real-ip-cidr: "0.0.0.0/0" # Or specific LB CIDR | |||
| 80 | + | ``` | |||
| 81 | + | ||||
| 82 | + | If you have a cloud load balancer in front of nginx-ingress (common in EKS, GKE): | |||
| 83 | + | ||||
| 84 | + | ```yaml | |||
| 85 | + | data: | |||
| 86 | + | use-forwarded-headers: "true" | |||
| 87 | + | use-proxy-protocol: "false" # Set to "true" if using NLB with proxy protocol | |||
| 88 | + | proxy-real-ip-cidr: "10.0.0.0/8,192.168.0.0/16" | |||
| 89 | + | ``` | |||
| 90 | + | ||||
| 91 | + | ### Traefik | |||
| 92 | + | ||||
| 93 | + | In your Traefik configuration: | |||
| 94 | + | ||||
| 95 | + | ```yaml | |||
| 96 | + | # traefik.yaml or Helm values | |||
| 97 | + | entryPoints: | |||
| 98 | + | web: | |||
| 99 | + | address: ":80" | |||
| 100 | + | forwardedHeaders: | |||
| 101 | + | trustedIPs: | |||
| 102 | + | - "10.0.0.0/8" | |||
| 103 | + | - "192.168.0.0/16" | |||
| 104 | + | websecure: | |||
| 105 | + | address: ":443" | |||
| 106 | + | forwardedHeaders: | |||
| 107 | + | trustedIPs: | |||
| 108 | + | - "10.0.0.0/8" | |||
| 109 | + | - "192.168.0.0/16" | |||
| 110 | + | ``` | |||
| 111 | + | ||||
| 112 | + | ## Complete Example | |||
| 113 | + | ||||
| 114 | + | ### Gordon Deployment | |||
| 115 | + | ||||
| 116 | + | ```yaml | |||
| 117 | + | apiVersion: apps/v1 | |||
| 118 | + | kind: Deployment | |||
| 119 | + | metadata: | |||
| 120 | + | name: gordon | |||
| 121 | + | spec: | |||
| 122 | + | replicas: 1 | |||
| 123 | + | selector: | |||
| 124 | + | matchLabels: | |||
| 125 | + | app: gordon | |||
| 126 | + | template: | |||
| 127 | + | metadata: | |||
| 128 | + | labels: | |||
| 129 | + | app: gordon | |||
| 130 | + | spec: | |||
| 131 | + | containers: | |||
| 132 | + | - name: gordon | |||
| 133 | + | image: your-registry/gordon:latest | |||
| 134 | + | ports: | |||
| 135 | + | - containerPort: 80 | |||
| 136 | + | - containerPort: 5000 | |||
| 137 | + | volumeMounts: | |||
| 138 | + | - name: config | |||
| 139 | + | mountPath: /etc/gordon | |||
| 140 | + | - name: docker-sock | |||
| 141 | + | mountPath: /var/run/docker.sock | |||
| 142 | + | volumes: | |||
| 143 | + | - name: config | |||
| 144 | + | configMap: | |||
| 145 | + | name: gordon-config | |||
| 146 | + | - name: docker-sock | |||
| 147 | + | hostPath: | |||
| 148 | + | path: /var/run/docker.sock | |||
| 149 | + | --- | |||
| 150 | + | apiVersion: v1 | |||
| 151 | + | kind: Service | |||
| 152 | + | metadata: | |||
| 153 | + | name: gordon | |||
| 154 | + | spec: | |||
| 155 | + | selector: | |||
| 156 | + | app: gordon | |||
| 157 | + | ports: | |||
| 158 | + | - name: http | |||
| 159 | + | port: 80 | |||
| 160 | + | targetPort: 80 | |||
| 161 | + | - name: registry | |||
| 162 | + | port: 5000 | |||
| 163 | + | targetPort: 5000 | |||
| 164 | + | ``` | |||
| 165 | + | ||||
| 166 | + | ### Gordon ConfigMap | |||
| 167 | + | ||||
| 168 | + | ```yaml | |||
| 169 | + | apiVersion: v1 | |||
| 170 | + | kind: ConfigMap | |||
| 171 | + | metadata: | |||
| 172 | + | name: gordon-config | |||
| 173 | + | data: | |||
| 174 | + | gordon.toml: | | |||
| 175 | + | [server] | |||
| 176 | + | port = 80 | |||
| 177 | + | registry_port = 5000 | |||
| 178 | + | gordon_domain = "gordon.example.com" | |||
| 179 | + | ||||
| 180 | + | [api.rate_limit] | |||
| 181 | + | enabled = true | |||
| 182 | + | trusted_proxies = ["10.244.0.0/16"] | |||
| 183 | + | ||||
| 184 | + | [auth] | |||
| 185 | + | enabled = false # Configure auth as needed | |||
| 186 | + | ``` | |||
| 187 | + | ||||
| 188 | + | ### Ingress Resource | |||
| 189 | + | ||||
| 190 | + | ```yaml | |||
| 191 | + | apiVersion: networking.k8s.io/v1 | |||
| 192 | + | kind: Ingress | |||
| 193 | + | metadata: | |||
| 194 | + | name: gordon | |||
| 195 | + | annotations: | |||
| 196 | + | nginx.ingress.kubernetes.io/proxy-body-size: "0" # Unlimited for image uploads | |||
| 197 | + | spec: | |||
| 198 | + | ingressClassName: nginx | |||
| 199 | + | tls: | |||
| 200 | + | - hosts: | |||
| 201 | + | - gordon.example.com | |||
| 202 | + | secretName: gordon-tls | |||
| 203 | + | rules: | |||
| 204 | + | - host: gordon.example.com | |||
| 205 | + | http: | |||
| 206 | + | paths: | |||
| 207 | + | - path: / | |||
| 208 | + | pathType: Prefix | |||
| 209 | + | backend: | |||
| 210 | + | service: | |||
| 211 | + | name: gordon | |||
| 212 | + | port: | |||
| 213 | + | number: 80 | |||
| 214 | + | ``` | |||
| 215 | + | ||||
| 216 | + | ## Verifying the Setup | |||
| 217 | + | ||||
| 218 | + | 1. Deploy Gordon and the ingress | |||
| 219 | + | 2. Make a request through the ingress | |||
| 220 | + | 3. Check Gordon logs: | |||
| 221 | + | ||||
| 222 | + | ```bash | |||
| 223 | + | kubectl logs -l app=gordon | grep "client_ip" | |||
| 224 | + | ``` | |||
| 225 | + | ||||
| 226 | + | If you see the ingress pod's IP instead of the client's IP: | |||
| 227 | + | - Verify `trusted_proxies` includes your pod CIDR | |||
| 228 | + | - Verify ingress controller has `use-forwarded-headers: "true"` | |||
| 229 | + | ||||
| 230 | + | ## Cloud-Specific Notes | |||
| 231 | + | ||||
| 232 | + | ### GKE | |||
| 233 | + | ||||
| 234 | + | GKE uses a VPC-native pod network. Find the pod CIDR in: | |||
| 235 | + | - Console → Kubernetes Engine → Cluster → Networking → Pod address range | |||
| 236 | + | ||||
| 237 | + | ```toml | |||
| 238 | + | trusted_proxies = ["10.0.0.0/14"] # GKE default | |||
| 239 | + | ``` | |||
| 240 | + | ||||
| 241 | + | ### EKS | |||
| 242 | + | ||||
| 243 | + | EKS with VPC CNI assigns pod IPs from your VPC subnets: | |||
| 244 | + | ||||
| 245 | + | ```toml | |||
| 246 | + | trusted_proxies = ["10.0.0.0/16"] # Your VPC CIDR | |||
| 247 | + | ``` | |||
| 248 | + | ||||
| 249 | + | ### AKS | |||
| 250 | + | ||||
| 251 | + | AKS default pod CIDR: | |||
| 252 | + | ||||
| 253 | + | ```toml | |||
| 254 | + | trusted_proxies = ["10.244.0.0/16"] # Azure CNI default | |||
| 255 | + | ``` | |||
| 256 | + | ||||
| 257 | + | ## Troubleshooting | |||
| 258 | + | ||||
| 259 | + | ### All requests show the same IP | |||
| 260 | + | ||||
| 261 | + | **Symptom:** Rate limiting ineffective, logs show ingress pod IP. | |||
| 262 | + | ||||
| 263 | + | **Cause:** `trusted_proxies` missing pod CIDR or ingress not forwarding headers. | |||
| 264 | + | ||||
| 265 | + | **Fix:** | |||
| 266 | + | 1. Add pod CIDR to `trusted_proxies` | |||
| 267 | + | 2. Enable `use-forwarded-headers` in ingress controller | |||
| 268 | + | ||||
| 269 | + | ### X-Forwarded-For contains multiple IPs | |||
| 270 | + | ||||
| 271 | + | **Symptom:** `X-Forwarded-For: 203.0.113.50, 10.0.1.5, 10.244.0.3` | |||
| 272 | + | ||||
| 273 | + | This is normal when multiple proxies are involved (cloud LB → ingress → Gordon). Gordon takes the **first** IP (leftmost), which is the original client. | |||
| 274 | + | ||||
| 275 | + | ### Client IP shows cloud load balancer | |||
| 276 | + | ||||
| 277 | + | **Symptom:** Client IP is AWS/GCP/Azure LB IP, not real client. | |||
| 278 | + | ||||
| 279 | + | **Cause:** Cloud LB not configured to forward client IP. | |||
| 280 | + | ||||
| 281 | + | **Fix:** Configure cloud LB to preserve or pass client IP (varies by provider). | |||
| 282 | + | ||||
| 283 | + | ## Related | |||
| 284 | + | ||||
| 285 | + | - [Rate Limiting Configuration](../../docs/config/rate-limiting.md) | |||
| 286 | + | - [AWS ALB Guide](./aws-alb.md) | |||
| 287 | + | - [Running Gordon in a Container](./running-in-container.md) | |||