detached 2f7df3b

docs: add rate limiting configuration guide and wiki deployment guides

commit
2f7df3bbe6efe3857d77d68553b995a8f2455fc8
author
bnema · · 2026-01-18 01:59 UTC
parent
912a154
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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"
4747 token_secret = "gordon/auth/token_secret"
4848 token_expiry = "30d" # Duration (1y, 30d, 2w) or 0 for never
4949
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+
5058 # Deploy behavior
5159 [deploy]
5260 pull_policy = "if-tag-changed" # always, if-not-present, if-tag-changed
@@ -111,6 +119,7 @@ enabled = false # Auto-create routes from image names
111119 |---------|-------------|---------------|
112120 | `[server]` | Core server settings | [Server](./server.md) |
113121 | `[auth]` | Authentication and secrets backend | [Auth](./auth.md) |
122+| `[api.rate_limit]` | Rate limiting configuration | [Rate Limiting](./rate-limiting.md) |
114123 | `[deploy]` | Deployment behavior | [Deploy](./deploy.md) |
115124 | `[logging]` | Logging configuration | [Logging](./logging.md) |
116125 | `[env]` | Environment variable settings | [Environment](./env.md) |
@@ -133,6 +142,11 @@ enabled = false # Auto-create routes from image names
133142 | `auth.type` | `"password"` |
134143 | `auth.secrets_backend` | `"unsafe"` |
135144 | `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` | `[]` |
136150 | `deploy.pull_policy` | `"if-tag-changed"` |
137151 | `logging.level` | `"info"` |
138152 | `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 never48 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 behavior58 # Deploy behavior
51 [deploy]59 [deploy]
52 pull_policy = "if-tag-changed" # always, if-not-present, if-tag-changed60 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.
77 - [Running Gordon in a Container](./running-in-container.md) - Deploy Gordon itself in Docker or Podman
88 - [Podman Rootless Setup](./podman-rootless.md) - Enhanced security with rootless containers
99
10-## External Services
10+## TLS & Load Balancing
1111
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
1315
1416 ## Remote Management
1517
@@ -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 Podman7 - [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 containers8 - [Podman Rootless Setup](./podman-rootless.md) - Enhanced security with rootless containers
9 9
10-## External Services10+## TLS & Load Balancing
11 11
12-- [Cloudflare DNS & Proxy Setup](./cloudflare-setup.md) - HTTPS termination with Cloudflare12+- [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 Management16 ## 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)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close