Skip to content

Commit b85c195

Browse files
committed
proxy: document and verify rate limiter enforcement scope
1 parent d33e0fa commit b85c195

2 files changed

Lines changed: 40 additions & 1 deletion

File tree

README.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,8 @@ unauthenticated requests.
180180
regular expressions.
181181
* **Multiple rules**: All matching rules are evaluated; if any rule denies the
182182
request, it is rejected. This allows layering global and endpoint-specific
183-
limits.
183+
limits, or multiple time horizons for the same path expression. A rejected
184+
request does not consume capacity from any matching rule.
184185
* **Protocol-aware responses**: Returns HTTP 429 with `Retry-After` header for
185186
REST requests, and gRPC `ResourceExhausted` status for gRPC requests.
186187

@@ -228,3 +229,29 @@ request is rejected. This allows clients to make quick bursts of requests (up to
228229
| `requests` | Number of requests allowed per time window. | Yes |
229230
| `per` | Time window duration (e.g., `1s`, `1m`, `1h`). | Yes |
230231
| `burst` | Maximum burst size. Defaults to `requests` if not set. | No |
232+
233+
### Enforcement Scope
234+
235+
Rate limiting is an in-process abuse control rather than a distributed quota:
236+
237+
* Each Aperture process keeps independent buckets. Deploying multiple replicas
238+
multiplies the effective allowance unless requests use consistent routing.
239+
* Buckets are held in an LRU cache that defaults to 10,000 entries. Buckets are
240+
not persisted. Restarting Aperture, updating a service, or evicting a
241+
client-rule entry creates a fresh bucket with full burst capacity. Each
242+
matching rule consumes one cache entry per client. A request that matches
243+
more rules than the configured cache capacity is rejected because its state
244+
cannot be retained safely.
245+
* Limits are per client; a rule without `pathregexp` covers every path but is
246+
still not an aggregate limit shared by all clients.
247+
* Authenticated L402 requests are keyed by token ID. Auth-whitelisted,
248+
authentication-disabled, zero-priced, and other unauthenticated requests are
249+
keyed by the masked direct peer IP. Payment/MPP credentials currently also
250+
fall back to that IP key.
251+
* Aperture uses the connection's direct peer address and does not trust
252+
forwarded-IP headers. Deployments behind a load balancer should preserve the
253+
original source address or account for clients sharing the load balancer's
254+
bucket.
255+
* Authentication is validated before the authenticated token bucket is
256+
checked. The limiter protects backend request handling, but does not bound
257+
authentication or challenge-generation work.

proxy/ratelimiter_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,18 @@ func TestExtractRateLimitKeyUnauthenticatedIgnoresL402(t *testing.T) {
875875
require.Equal(t, "ip:192.168.1.0", key)
876876
}
877877

878+
// TestExtractRateLimitKeyAuthenticated makes sure a validated L402 request is
879+
// keyed by its token ID instead of the peer IP.
880+
func TestExtractRateLimitKeyAuthenticated(t *testing.T) {
881+
mac, tokenID := newTestMacaroon(t)
882+
req := httptest.NewRequest("GET", "/api/test", nil)
883+
req.Header.Set("Authorization", authHeaderForMacaroon(t, mac))
884+
ip := net.ParseIP("192.168.1.100")
885+
886+
key := ExtractRateLimitKey(req, ip, true)
887+
require.Equal(t, "token:"+tokenID, key)
888+
}
889+
878890
// TestRateLimitConfigRate tests the Rate() calculation.
879891
func TestRateLimitConfigRate(t *testing.T) {
880892
tests := []struct {

0 commit comments

Comments
 (0)