Access Control
Peer Access Control (ACL)
Restricts access to a route to selected WireGuard peers. Only devices connected through an allowed peer can reach the route — all other requests are blocked.
How does ACL work?
Without ACL:
Internet (any IP) → Caddy → nas.example.com → Backend ✓
VPN Peer 10.8.0.3 → Caddy → nas.example.com → Backend ✓
VPN Peer 10.8.0.5 → Caddy → nas.example.com → Backend ✓
With ACL (only peer "Laptop" allowed):
Internet (any IP) → Caddy → nas.example.com → BLOCKED ✕
VPN Peer 10.8.0.3 → Caddy → nas.example.com → Backend ✓ (Laptop)
VPN Peer 10.8.0.5 → Caddy → nas.example.com → BLOCKED ✕ (iPhone)
ACL effectively makes the route only accessible via VPN and additionally only for selected peers.
Technical Implementation
Caddy checks the client's IP address with the remote_ip matcher on every request:
- VPN requests have a WireGuard IP (e.g.
10.8.0.3) - Internet requests have a public IP (e.g.
203.0.113.50) - The ACL list only contains WireGuard IPs of allowed peers
- Public IPs are never in the list → always blocked
Use Cases
Admin Panel for Laptop Only
| Source | IP at Caddy | Access |
|---|---|---|
| Office Laptop (VPN active) | 10.8.0.3 | Allowed |
| iPhone (VPN active) | 10.8.0.5 | Blocked |
| Any internet user | 203.0.113.x | Blocked |
| Hacker / Bot | 45.33.x.x | Blocked |
Dev Server for the Dev Team Only
Route staging.example.com → Dev server on port 3000. ACL: Only peers "Dev-Laptop-1", "Dev-Laptop-2", "Dev-Laptop-3" allowed.
Different Services for Different Groups
| Route | Allowed Peers | Purpose |
|---|---|---|
nas.example.com | Laptop, Smartphone | File access for admin |
admin.example.com | Laptop only | Sensitive admin panel |
public-api.example.com | No ACL | Public API without restrictions |
Combination with Other Features
| Combination | Effect |
|---|---|
| ACL + Route Auth | VPN IP check first, then login with password/2FA |
| ACL + Rate Limiting | VPN-only access with request limiting |
| ACL + IP Access Control | ACL for VPN peers + IP filter for additional restriction |
| ACL alone | Simplest option: only selected VPN devices get through |
Setup
Via UI
- Create or edit route
- Enable Peer Access Control toggle
- Select peers from checklist (multi-select possible)
- Save
Via API
# Enable ACL with selected peers (IDs: 1 and 3)
curl -X PUT https://gatecontrol.example.com/api/v1/routes/1 \
-H "Authorization: Bearer gc_..." \
-H "Content-Type: application/json" \
-d '{"acl_enabled":true,"acl_peers":[1,3]}'
Important Notes on ACL
- ACL only blocks access through Caddy (HTTP/HTTPS routes). Direct VPN access between peers is not restricted.
- When a peer is disabled or deleted, it's automatically removed from the ACL.
- ACL only works for HTTP routes, not L4 (TCP/UDP).
- The ACL check is the first check Caddy performs — before auth, rate limiting, or other handlers.
IP Access Control (IP Filter)
Controls access to a route based on client IP address — via whitelist or blacklist with support for individual IPs, CIDR ranges, and country codes.
How does it work?
IP Access Control filters requests based on the client's IP address. Unlike Peer ACL (which only filters VPN peer IPs), the IP filter works with any IP address.
Whitelist mode (only allow these IPs):
203.0.113.50 (Office IP) → Caddy → Backend ✓ (in whitelist)
198.51.100.10 (Home IP) → Caddy → BLOCKED ✕ (not in whitelist)
45.33.32.1 (Bot) → Caddy → BLOCKED ✕ (not in whitelist)
Blacklist mode (block these IPs):
203.0.113.50 (Office IP) → Caddy → Backend ✓ (not in blacklist)
198.51.100.10 (Attacker) → Caddy → BLOCKED ✕ (in blacklist)
45.33.32.1 (CN Bot) → Caddy → BLOCKED ✕ (Country CN in blacklist)
Three Rule Types
| Type | Example | Check |
|---|---|---|
| Single IP | 203.0.113.50 | Exact match |
| CIDR range | 10.0.0.0/8 | Bitmask |
| Country code | DE, US, CN | GeoIP lookup via ip2location.io API |
Flow for Each Request
- Caddy forwards request to GateControl forward-auth
- GateControl extracts client IP (strips
::ffff:IPv6 prefix) - Checks each rule sequentially (IP → CIDR → Country)
- Country lookup: API call to ip2location.io with 24h cache (max 10,000 entries)
- Whitelist: match → allowed, no match → blocked
- Blacklist: match → blocked, no match → allowed
Use Cases
Allow Office Network Only
Route internal.example.com → Internal app. Whitelist with CIDR 185.10.20.0/24 (office IP range).
Block Bot Traffic from Specific Countries
Route shop.example.com → Webshop. Blacklist with country codes CN, RU, KP.
Block Known Attacker IPs
Route api.example.com → API. Blacklist with individual IPs identified as attackers in logs.
Setup
Via UI
- Create or edit route
- Enable IP Access Control toggle
- Select mode: Whitelist or Blacklist
- Add rules: select type (IP, CIDR, Country), enter value
- Save
For country-based filtering: enter Settings → Advanced → ip2location.io API Key.
Via API
# Enable IP filter with whitelist
curl -X PUT https://gatecontrol.example.com/api/v1/routes/1 \
-H "Authorization: Bearer gc_..." \
-H "Content-Type: application/json" \
-d '{"ip_filter_enabled":true,"ip_filter_mode":"whitelist","ip_filter_rules":[{"type":"cidr","value":"185.10.20.0/24"},{"type":"ip","value":"203.0.113.50"}]}'
# IP filter with country blacklist
curl -X PUT https://gatecontrol.example.com/api/v1/routes/1 \
-H "Authorization: Bearer gc_..." \
-H "Content-Type: application/json" \
-d '{"ip_filter_enabled":true,"ip_filter_mode":"blacklist","ip_filter_rules":[{"type":"country","value":"CN"},{"type":"country","value":"RU"}]}'
Important Notes on IP Filter
- Difference from Peer ACL: ACL only filters WireGuard peer IPs (10.8.0.x). IP Access Control filters any IP address.
- Country lookup requires an ip2location.io API Key. Without a key, country rules are ignored.
- The GeoIP cache stores up to 10,000 entries for 24 hours.
- IPv6-mapped IPv4 addresses (
::ffff:192.168.1.1) are automatically reduced to IPv4. - IP Access Control only works with Route Auth or as a standalone forward-auth check. With Basic Auth, the IP filter is not available.
- An empty whitelist allows nobody. An empty blacklist blocks nobody.
- Only available for HTTP routes, not L4.
Rate Limiting
Limits the number of requests per IP address within a time window — protects backends from overload, brute-force, and scraping.
How does it work?
Without Rate Limiting:
Bot sends 10,000 requests/minute → Backend processes all → Server overloaded
With Rate Limiting (100 requests/minute):
Bot sends 100 requests → Backend processes all ✓
Bot sends request #101 → Caddy: 429 Too Many Requests ✕
... after 1 minute ...
Bot sends request #1 → Backend processes ✓ (new window)
Technical Details
{
"handler": "rate_limit",
"rate_limits": {
"static": {
"key": "{http.request.remote.host}",
"window": "1m",
"max_events": 100
}
}
}
Configurable Values
| Parameter | Range | Default | Description |
|---|---|---|---|
| Requests | 1 – 100,000 | 100 | Maximum requests per time window |
| Window | 1s, 1m, 5m, 1h | 1m | Duration of time window |
Recommended Values
| Use Case | Requests | Window |
|---|---|---|
| Login page | 10–20 | 1m |
| REST API | 500–1000 | 5m |
| Webshop / Website | 60–120 | 1m |
| Static assets | 1000–5000 | 1m |
| Webhook endpoint | 50–100 | 1m |
Combination with Other Features
| Combination | Effect |
|---|---|
| Rate Limit + Route Auth | Rate limit after auth check — protects backend, not the login page |
| Rate Limit + Basic Auth | Rate limit before auth — also protects against brute-force on Basic Auth |
| Rate Limit + ACL | Only VPN peers get through, then rate-limited |
| Rate Limit + IP Filter | IP filter blocks known IPs, rate limit slows down the rest |
Setup
Via UI
- Create or edit route
- Enable Rate Limiting toggle
- Enter Requests (e.g. 100)
- Select Window (1s, 1m, 5m, 1h)
- Save
Via API
# Enable rate limiting: 100 requests per minute
curl -X PUT https://gatecontrol.example.com/api/v1/routes/1 \
-H "Authorization: Bearer gc_..." \
-H "Content-Type: application/json" \
-d '{"rate_limit_enabled":true,"rate_limit_requests":100,"rate_limit_window":"1m"}'
Important Notes on Rate Limiting
- Rate limiting is per IP address, not global. 100 requests/minute means each individual IP can make 100 requests.
- Behind a NAT router, all clients share the same IP — the limit applies to all of them together.
- Allowed window values:
1s,1m,5m,1h. Other values are normalized to1m. - HTTP 429 does not include a
Retry-Afterheader. - Only available for HTTP routes, not L4.
- WebSocket connections only count the initial HTTP upgrade as one request.