caddy-ratelimit

repository·master·Indexed 19 days ago

https://github.com/mholt/caddy-ratelimit

Caddy HTTP Rate Limit Module providing internal and distributed HTTP rate limiting for the Caddy web server. It utilizes a sliding window algorithm and supports static or dynamic rate limit zones based on request attributes such as IP, host, or headers. Features include support for IPv4/IPv6 network prefix grouping, Prometheus metrics, and eventually consistent state synchronization across clusters using Caddy storage modules.

Tokens
4.9K
Snippets
9
Records
21
Agent score
65%

What's inside caddy-ratelimit

  1. Configure distributed rate limiting

    master

    Distributed rate limiting allows multiple Caddy instances to share rate limit state across a cluster.

    Requirements:

    • All instances in the cluster must have the exact same rate limit zone configurations.
    • All instances must be configured to use the same storage module.

    How it works: Instances periodically write their internal state to storage and read the state from other instances. This synchronization is eventually consistent and approximate.

    Configuration: Enable it by providing a non-null distributed object in JSON or the distributed subdirective in a Caddyfile. You can tune the following parameters:

    • read_interval: How often to read state from storage.
    • write_interval: How often to write state to storage.
    • purge_age: How long to keep old state.

    Note: Default intervals are 5s.

  2. How rate limit zones and keys work

    master

    The rate_limit HTTP handler uses zones to define rate limiting rules.

    • Zone: A named configuration representing a specific rate limit (e.g., 100 events per 1 minute). Each zone requires a window and max_events.
    • Key: A string used to identify the specific rate limiter within a zone.
      • Static Keys: If the key contains no placeholders (e.g., key: "static"), only one rate limiter is allocated for the entire zone, applying the limit globally to all requests in that zone.
      • Dynamic Keys: If the key contains placeholders (e.g., key: "{http.request.remote.host}"), a new rate limiter is allocated for every unique expanded key (e.g., per client IP). This allows for per-user or per-IP rate limiting.

    Zones can optionally use request matchers to filter which requests are subject to that specific zone's limits.

    {
    	"handler": "rate_limit",
    	"rate_limits": {
    		"<name>": {
    			"match": [],
    			"key": "",
    			"window": "",
    			"max_events": 0,
    			"ipv4_prefix": 0,
    			"ipv6_prefix": 0
    		}
    	}
    }
  3. Use placeholders in the RateLimit Key

    master

    The Key field in a RateLimit configuration determines how rate limiters are allocated.

    • Static Key: If you provide a static string like "global", exactly one rate limiter is created for the entire zone. All requests matching the MatcherSetsRaw will share this single bucket.
    • Dynamic Key: If you use Caddy placeholders, a unique rate limiter is created for every unique value produced by the placeholder. For example, using "{http.request.remote.host}" will create one rate limiter per unique client IP address.
  4. Use distributed rate limiting

    master

    To enable rate limiting across a cluster of Caddy instances, configure the distributed option.

    Requirements:

    • All instances in the cluster must share the same storage configuration.
    • Rate limit zones must have the exact same configuration across all instances to ensure consistent calculations.

    Configuration:

    • read_interval: How often to sync state from other instances (defaults to 5s).
    • write_interval: How often to sync local state to the storage (defaults to 5s).
  5. How distributed rate limiting works

    master

    Distributed rate limiting in caddy-ratelimit provides eventually consistent rate limiting across a cluster of Caddy instances. It achieves this by periodically synchronizing the state of internal rate limiters to a shared storage backend (using certmagic.Storage).

    The mechanism:

    1. Write Phase: The local instance writes its current rate limiter states (event counts and oldest event timestamps) to storage at a defined WriteInterval.
    2. Read Phase: The local instance reads the states of all other instances from storage at a defined ReadInterval.
    3. Enforcement: When a request arrives, the handler calculates the total count by summing the local count and the counts reported by all other instances in the cluster. If the combined count exceeds the allowed limit, the request is rate-limited.

    Consistency vs. Overhead: Because synchronization happens at intervals, the limiting is not exact. Lower (more frequent) sync intervals increase precision and consistency but increase I/O and CPU overhead.

  6. How metrics are handled across Caddy reloads

    master

    The module uses a package-level singleton globalMetrics to ensure metric continuity.

    When Caddy reloads, it provisions a fresh metrics registry. The module handles this by re-registering the existing collectors with the new registry via registerMetrics. This mechanism ensures that Prometheus metrics are reported continuously and that their accumulated values remain intact across configuration reloads. If a collector is already registered with a registry, the module gracefully handles the prometheus.AlreadyRegisteredError.

  7. Handle rate limit exceeded events

    master

    When a rate limit is exceeded, the handler performs several actions that allow for custom responses or logging:

    1. HTTP Error: Returns a 429 Too Many Requests status code.
    2. Retry-After Header: Sets a Retry-After header indicating how many seconds to wait (including optional jitter).
    3. Caddy Replacer: Sets the placeholder {http.rate_limit.exceeded.name} to the name of the zone that was exceeded. You can use this in your Caddyfile or JSON config to customize error responses.
    4. Events: Emits a rate_limit_exceeded event containing the zone, wait duration, and remote_ip.
  8. Group IP addresses using IPv4Prefix and IPv6Prefix

    master

    To prevent abuse from clients cycling through different IP addresses within the same network, you can use IPv4Prefix and IPv6Prefix to group rate limit keys by subnet.

    • IPv4Prefix: An integer between 0 and 32. A value of 24 will group all addresses in the same /24 subnet into a single rate limit bucket.
    • IPv6Prefix: An integer between 0 and 128. A value of 64 will group all addresses in the same /64 network into a single rate limit bucket.

    If these are set to 0 (the default), every individual IP address is treated as a unique rate limiter.

  9. Configure rate limiting via Caddyfile

    master

    The rate_limit directive allows you to define rate limiting zones and global settings within your Caddyfile. It is registered as an HTTP handler directive and is ordered to appear before basic_auth.

    rate_limit {
        zone my_zone {
            key "some_key"
            window 1m
            events 100
            match {
                path /api/*
            }
        }
        distributed {
            read_interval 10s
            write_interval 10s
            purge_age 1h
        }
        storage redis
        jitter 0.1
    }
  10. Example: Configure static and dynamic rate limit zones

    master

    This example demonstrates two zones:

    1. static_example: A global limit of 100 GET requests per minute across all clients.
    2. dynamic_example: A per-IP limit of 2 requests per 5 seconds using the {http.request.remote.host} placeholder.

    Both zones are configured with distributed rate limiting enabled.

    Caddyfile

    :80
    
    rate_limit {
    	distributed
    	zone static_example {
    		match {
    			method GET
    		}
    		key    static
    		events 100
    		window 1m
    	}
    	zone dynamic_example {
    		key    {remote_host}
    		events 2
    		window 5s
    	}
    	log_key
    }
    
    respond "I'm behind the rate limiter!"

    JSON

    {
    	"apps": {
    		"http": {
    			"servers": {
    				"demo": {
    					"listen": [":80"],
    					"routes": [
    						{
    							"handle": [
    									{
    										"handler": "rate_limit",
    										"rate_limits": {
    											"static_example": {
    												"match": [
    													"{\"method\": [\"GET\"]}"
    													]
    												],
    												"key": "static",
    												"window": "1m",
    												"max_events": 100
    											}
    											,
    											"dynamic_example": {
    												"key": "{http.request.remote.host}",
    												"window": "5s",
    												"max_events": 2
    											}
    										}
    										,
    											"distributed": {},
    											"log_key": true
    										}
    									},
    									{
    										"handler": "static_response",
    										"body": "I'm behind the rate limiter!"
    									}
    								]
    							}
    							}
    						]
    					}
    				}
    			}
    		}
    	}
    }
  11. Example: Rate limiting by IPv6 network prefix

    master

    To prevent attackers from bypassing per-IP limits by cycling through many addresses in an IPv6 prefix, use the ipv6_prefix option. This groups all addresses within the same network (e.g., a /64) into a single rate limit bucket.

    Caddyfile

    rate_limit {
    	zone per_network {
    		key         {remote_host}
    		events      100
    		window      1m
    		ipv6_prefix 64
    	}
    }

    JSON

    {
    	"handler": "rate_limit",
    	"rate_limits": {
    		"per_network": {
    			"key": "{http.request.remote.host}",
    			"window": "1m",
    			"max_events": 100,
    			"ipv6_prefix": 64
    		}
    	}
    }