Rate Limiting
Protect your Gordon instance from abuse with configurable rate limiting.
Overview
Rate limiting prevents:
- Brute force attacks on authentication endpoints
- Denial of Service (DoS) from excessive requests
- Resource exhaustion from misbehaving clients
- Registry abuse (e.g., automated scraping)
Rate limiting applies to:
- Registry API (
/v2/*) — Global + per-IP limits
- Token endpoint (
/v2/token) — Same limiter as registry (Global + per-IP)
- Admin API (
/admin/*) — Separate per-IP limit only
Quick Start
Rate limiting is enabled by default with sensible defaults:
[api.rate_limit]
enabled = true
global_rps = 500
per_ip_rps = 50
burst = 100
trusted_proxies = []
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.
Configuration
[api.rate_limit]
enabled = true
global_rps = 500
per_ip_rps = 50
burst = 100
trusted_proxies = ["127.0.0.1", "10.0.0.0/8"]
Options
| Option |
Type |
Default |
Description |
enabled |
bool |
true |
Enable or disable rate limiting |
global_rps |
float |
500 |
Maximum requests per second across all clients combined |
per_ip_rps |
float |
50 |
Maximum requests per second per client IP address |
burst |
int |
100 |
Maximum burst size (requests allowed to exceed the rate temporarily) |
trusted_proxies |
[]string |
[] |
IP addresses or CIDR ranges trusted to set X-Forwarded-For |
Architecture
Gordon uses two separate rate limiters:
┌─────────────────────────────────────────────────────────┐
│ Incoming Request │
└─────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ /v2/* routes │ │ /admin/* routes│
└───────────────┘ └───────────────┘
│ │
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ Registry Rate Limiter │ │ Admin Rate Limiter │
│ ┌───────────────────┐ │ │ ┌───────────────────┐ │
│ │ Global Limiter │ │ │ │ Per-IP Limiter │ │
│ │ (global_rps) │ │ │ │ (per_ip_rps) │ │
│ └───────────────────┘ │ │ └───────────────────┘ │
│ ┌───────────────────┐ │ └─────────────────────────┘
│ │ Per-IP Limiters │ │
│ │ (per_ip_rps) │ │
│ └───────────────────┘ │
└─────────────────────────┘
Registry Limiter (for /v2/* and /v2/token):
- Checks global limit first (all clients combined)
- Then checks per-IP limit
- Both must pass for the request to proceed
Admin Limiter (for /admin/*):
- Per-IP limit only
- No global limit (admin traffic is typically low volume)
- Separate from registry to avoid CI/CD traffic affecting admin access
Option Details
enabled
Master switch for rate limiting. When false, all requests are allowed without throttling.
[api.rate_limit]
enabled = false # Disable rate limiting (not recommended for production)
global_rps
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.
[api.rate_limit]
global_rps = 500 # 500 requests/second total
Sizing guidance:
- Small deployments (1-10 apps):
100-500 RPS
- Medium deployments (10-50 apps):
500-2000 RPS
- Large deployments (50+ apps):
2000-10000 RPS
Consider your CI/CD patterns—parallel builds pushing multiple images can generate bursts of traffic.
per_ip_rps
The maximum requests per second allowed from a single client IP. This prevents any single client from monopolizing Gordon's resources.
[api.rate_limit]
per_ip_rps = 50 # 50 requests/second per IP
Sizing guidance:
- Normal Docker operations (pull, push) rarely exceed 10-20 RPS
- CI/CD pipelines with parallel jobs may need 50-100 RPS
- Automated tooling (vulnerability scanners, etc.) may need higher limits
burst
The token bucket burst size. Allows clients to temporarily exceed the rate limit for short bursts, which is useful for legitimate traffic patterns like:
- Initial connection setup (multiple manifest/blob requests)
- Parallel layer downloads
- Health check bursts
[api.rate_limit]
burst = 100 # Allow bursts of up to 100 requests
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.
trusted_proxies
Critical for correct IP detection in production.
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.
Internet → [Cloudflare/nginx] → Gordon → Containers
(HTTPS) (HTTP)
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.
The problem: X-Forwarded-For can be spoofed. If Gordon trusted this header unconditionally, attackers could:
- Bypass per-IP rate limits by sending fake IPs
- Create unlimited rate limiter entries (memory exhaustion)
The solution: Gordon only honors X-Forwarded-For from IPs listed in trusted_proxies. For all other connections, it uses the direct connection IP.
[api.rate_limit]
trusted_proxies = ["127.0.0.1", "10.0.0.0/8"]
Supported formats:
- Single IP:
"192.168.1.1"
- CIDR range:
"10.0.0.0/8"
- IPv6:
"::1", "fd00::/8"
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).
Deployment Examples
Cloudflare (Recommended)
Cloudflare provides free TLS termination and DDoS protection. This is the recommended setup for production.
Cloudflare publishes their IP ranges at https://www.cloudflare.com/ips/
[api.rate_limit]
# Cloudflare IPs (verified 2026-01-18 - check cloudflare.com/ips for updates)
trusted_proxies = [
# IPv4
"173.245.48.0/20",
"103.21.244.0/22",
"103.22.200.0/22",
"103.31.4.0/22",
"141.101.64.0/18",
"108.162.192.0/18",
"190.93.240.0/20",
"188.114.96.0/20",
"197.234.240.0/22",
"198.41.128.0/17",
"162.158.0.0/15",
"104.16.0.0/13",
"104.24.0.0/14",
"172.64.0.0/13",
"131.0.72.0/22",
# IPv6
"2400:cb00::/32",
"2606:4700::/32",
"2803:f800::/32",
"2405:b500::/32",
"2405:8100::/32",
"2a06:98c0::/29",
"2c0f:f248::/32",
]
Note: Cloudflare IPs may change. Verify at https://www.cloudflare.com/ips/ periodically.
Local Development (No TLS)
For local development only, Gordon can be accessed directly without a proxy:
[api.rate_limit]
trusted_proxies = [] # Empty = use RemoteAddr directly
Warning: Never expose Gordon directly to the internet without TLS. Use this configuration only for local development.
Behind nginx (Same Host)
When nginx runs on the same machine as Gordon:
[api.rate_limit]
trusted_proxies = ["127.0.0.1", "::1"]
Behind nginx (Separate Host)
When nginx runs on a different server:
[api.rate_limit]
trusted_proxies = ["10.0.1.5"] # nginx server IP
Behind AWS Application Load Balancer (ALB)
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.
[api.rate_limit]
trusted_proxies = ["10.0.0.0/16"] # Your VPC CIDR
For detailed setup instructions, see the AWS ALB Guide.
Behind Kubernetes Ingress
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.
[api.rate_limit]
trusted_proxies = ["10.244.0.0/16"] # Your pod network CIDR
For detailed setup instructions, see the Kubernetes Ingress Guide.
Rate Limit Response
When a client exceeds the rate limit, Gordon returns:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Docker-Distribution-API-Version: registry/2.0
Retry-After: 1
{
"errors": [
{
"code": "TOOMANYREQUESTS",
"message": "rate limit exceeded"
}
]
}
The Retry-After header indicates when the client can retry (in seconds).
Monitoring
Rate-limited requests are logged at info level:
level=info msg="rate limit exceeded" client_ip=203.0.113.50 path=/v2/myapp/manifests/latest
To see rate limiting in action, enable debug logging:
[logging]
level = "debug"
Security Considerations
IP Spoofing Prevention
If trusted_proxies is misconfigured, attackers can:
- Send requests with fake
X-Forwarded-For: 1.2.3.4
- Each request appears to come from a different IP
- Per-IP rate limiting becomes ineffective
- Attackers create unbounded limiter entries (memory exhaustion)
Always verify your proxy configuration by checking logs to ensure client IPs are detected correctly.
Endpoint Coverage
All authenticated endpoints are protected:
| Endpoint |
Rate Limiter |
Limits Applied |
/v2/* (registry) |
Registry limiter |
Global RPS + Per-IP RPS |
/v2/token |
Registry limiter |
Global RPS + Per-IP RPS |
/admin/* |
Admin limiter |
Per-IP RPS only |
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.
Recommended Production Settings
[api.rate_limit]
enabled = true
global_rps = 1000 # Adjust based on your traffic
per_ip_rps = 30 # Stricter per-IP limit
burst = 50 # Smaller burst for tighter control
trusted_proxies = [] # Configure based on your proxy setup
Troubleshooting
"I'm getting rate limited but traffic is low"
- Check if
trusted_proxies is configured correctly
- All traffic may appear to come from your proxy's IP
- Add your proxy to
trusted_proxies to see real client IPs
"Rate limiting doesn't seem to work"
- Verify
enabled = true
- Check that requests go through Gordon (not cached by CDN)
- Ensure
global_rps and per_ip_rps are set appropriately
"I need to whitelist certain IPs"
Rate limiting doesn't support IP whitelisting. Consider:
- Increasing
per_ip_rps for legitimate high-traffic clients
- Using a reverse proxy with its own rate limiting rules
- Implementing IP-based access control at the network level
source
1# Rate Limiting
2
3Protect your Gordon instance from abuse with configurable rate limiting.
4
5## Overview
6
7Rate 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
13Rate 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
20Rate limiting is **enabled by default** with sensible defaults:
21
22```toml
23[api.rate_limit]
24enabled = true
25global_rps = 500
26per_ip_rps = 50
27burst = 100
28trusted_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]
37enabled = true
38global_rps = 500
39per_ip_rps = 50
40burst = 100
41trusted_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
56Gordon 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
97Master switch for rate limiting. When `false`, all requests are allowed without throttling.
98
99```toml
100[api.rate_limit]
101enabled = false # Disable rate limiting (not recommended for production)
102```
103
104### `global_rps`
105
106The 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]
110global_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
118Consider your CI/CD patterns—parallel builds pushing multiple images can generate bursts of traffic.
119
120### `per_ip_rps`
121
122The 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]
126per_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
136The 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]
143burst = 100 # Allow bursts of up to 100 requests
144```
145
146A 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
152Gordon 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```
155Internet → [Cloudflare/nginx] → Gordon → Containers
156 (HTTPS) (HTTP)
157```
158
159In 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]
169trusted_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
183Cloudflare provides free TLS termination and DDoS protection. This is the recommended setup for production.
184
185Cloudflare 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)
190trusted_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
222For local development only, Gordon can be accessed directly without a proxy:
223
224```toml
225[api.rate_limit]
226trusted_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
233When nginx runs on the same machine as Gordon:
234
235```toml
236[api.rate_limit]
237trusted_proxies = ["127.0.0.1", "::1"]
238```
239
240### Behind nginx (Separate Host)
241
242When nginx runs on a different server:
243
244```toml
245[api.rate_limit]
246trusted_proxies = ["10.0.1.5"] # nginx server IP
247```
248
249### Behind AWS Application Load Balancer (ALB)
250
251ALB 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]
255trusted_proxies = ["10.0.0.0/16"] # Your VPC CIDR
256```
257
258For detailed setup instructions, see the [AWS ALB Guide](../../wiki/guides/aws-alb.md).
259
260### Behind Kubernetes Ingress
261
262When 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]
266trusted_proxies = ["10.244.0.0/16"] # Your pod network CIDR
267```
268
269For detailed setup instructions, see the [Kubernetes Ingress Guide](../../wiki/guides/kubernetes-ingress.md).
270
271## Rate Limit Response
272
273When a client exceeds the rate limit, Gordon returns:
274
275```
276HTTP/1.1 429 Too Many Requests
277Content-Type: application/json
278Docker-Distribution-API-Version: registry/2.0
279Retry-After: 1
280
281{
282 "errors": [
283 {
284 "code": "TOOMANYREQUESTS",
285 "message": "rate limit exceeded"
286 }
287 ]
288}
289```
290
291The `Retry-After` header indicates when the client can retry (in seconds).
292
293## Monitoring
294
295Rate-limited requests are logged at `info` level:
296
297```
298level=info msg="rate limit exceeded" client_ip=203.0.113.50 path=/v2/myapp/manifests/latest
299```
300
301To see rate limiting in action, enable debug logging:
302
303```toml
304[logging]
305level = "debug"
306```
307
308## Security Considerations
309
310### IP Spoofing Prevention
311
312If `trusted_proxies` is misconfigured, attackers can:
3131. Send requests with fake `X-Forwarded-For: 1.2.3.4`
3142. Each request appears to come from a different IP
3153. Per-IP rate limiting becomes ineffective
3164. 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
322All 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
330The 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]
336enabled = true
337global_rps = 1000 # Adjust based on your traffic
338per_ip_rps = 30 # Stricter per-IP limit
339burst = 50 # Smaller burst for tighter control
340trusted_proxies = [] # Configure based on your proxy setup
341```
342
343## Troubleshooting
344
345### "I'm getting rate limited but traffic is low"
346
3471. Check if `trusted_proxies` is configured correctly
3482. All traffic may appear to come from your proxy's IP
3493. Add your proxy to `trusted_proxies` to see real client IPs
350
351### "Rate limiting doesn't seem to work"
352
3531. Verify `enabled = true`
3542. Check that requests go through Gordon (not cached by CDN)
3553. Ensure `global_rps` and `per_ip_rps` are set appropriately
356
357### "I need to whitelist certain IPs"
358
359Rate 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)