Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@
SERVER_ADDR=:8080
BASE_URL=http://localhost:8080

# Trusted Proxies (optional, SECURITY-SENSITIVE)
# Controls which upstream proxies may set forwarding headers (X-Forwarded-For /
# X-Real-IP) that AuthGate honours when resolving the client IP. The client IP
# feeds per-IP rate limiting, audit-log source IPs, and session fingerprints.
# Comma-separated list of CIDRs and/or bare IPs.
# unset / empty -> trust NONE: client IP is the direct TCP peer (RemoteAddr).
# This is the secure default — spoofed headers are ignored.
# 10.0.1.5,10.0.1.6 -> trust only these proxies' forwarding headers.
# * -> trust ALL proxies (legacy behaviour; insecure if directly
# reachable). Use only as a temporary migration escape hatch.
# Use the EXACT proxy/load-balancer addresses (or the narrowest dedicated proxy
# subnet). Avoid broad ranges like 10.0.0.0/8 — any host in a trusted range can
# set the client IP via forwarding headers and re-enable spoofing if it can
# reach AuthGate. If you run behind a reverse proxy, set this to the proxy's
# address(es) AND configure the proxy to OVERWRITE X-Forwarded-For (see
# docs/DEPLOYMENT.md).
# TRUSTED_PROXIES=10.0.1.5,10.0.1.6

# TLS / HTTPS (optional)
# When both TLS_CERT_FILE and TLS_KEY_FILE are set, AuthGate serves HTTPS on SERVER_ADDR.
# TLS_CERT_FILE should contain the full certificate chain; TLS_KEY_FILE the PEM-encoded private key.
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ Key configuration categories (see `.env.example` and `docs/CONFIGURATION.md` for

**Security Features**

- `TRUSTED_PROXIES` - Comma-separated CIDR/IP allowlist of proxies whose `X-Forwarded-For`/`X-Real-IP` headers `c.ClientIP()` will honour. Wired into the Gin engine via `SetTrustedProxies` in `bootstrap.setupTrustedProxies`. Default unset = trust none (`ClientIP()` returns `RemoteAddr`, the secure default); `*` = trust all (legacy). The resolved client IP drives rate-limit keys, audit source IPs, and session fingerprints. **Breaking change** vs prior releases (which trusted all proxies)
- `ENABLE_RATE_LIMIT`, `RATE_LIMIT_STORE` (memory/redis) - Rate limiting
- `ENABLE_AUDIT_LOGGING` - Comprehensive audit trails
- `SESSION_FINGERPRINT` - Session security (User-Agent validation)
Expand Down
37 changes: 37 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ This guide covers all configuration options for AuthGate, including environment
## Table of Contents

- [Environment Variables](#environment-variables)
- [Trusted Proxies](#trusted-proxies)
- [TLS / HTTPS](#tls--https)
- [Bootstrap and Shutdown Timeouts](#bootstrap-and-shutdown-timeouts)
- [Generate Strong Secrets](#generate-strong-secrets)
Expand All @@ -31,6 +32,10 @@ Create a `.env` file in the project root:
SERVER_ADDR=:8080 # Listen address (e.g., :8080, 0.0.0.0:8080)
BASE_URL=http://localhost:8080 # Public URL for verification_uri

# Trusted Proxies (optional, security-sensitive) — see "Trusted Proxies" below
# Use exact proxy/LB IPs (or the narrowest dedicated proxy subnet), not broad ranges.
# TRUSTED_PROXIES=10.0.1.5,10.0.1.6

# TLS / HTTPS (optional) — set both to serve HTTPS on SERVER_ADDR
# TLS_CERT_FILE=/etc/authgate/tls/fullchain.pem
# TLS_KEY_FILE=/etc/authgate/tls/privkey.pem
Expand Down Expand Up @@ -227,6 +232,38 @@ EXTRA_CLAIMS_MAX_VAL_SIZE=512 # Max bytes per value (0 disables)

---

## Trusted Proxies

AuthGate resolves the client IP from forwarding headers (`X-Forwarded-For` /
`X-Real-IP`) **only** for requests that arrive from a proxy address listed in
`TRUSTED_PROXIES`. The resolved client IP drives **per-IP rate limiting**,
**audit-log source IPs**, and **session fingerprints**, so it must not be
attacker-controllable.

```bash
TRUSTED_PROXIES=10.0.1.5,10.0.1.6 # exact proxy/LB IPs (CIDRs also allowed), comma-separated
```

Use your proxy/load-balancer's **exact addresses** (or the narrowest dedicated proxy subnet, e.g. `10.0.1.0/24`). Avoid broad ranges like `10.0.0.0/8`: every host inside a trusted range is allowed to set the client IP via forwarding headers, so a wide range can re-enable spoofing from any internal workload that can reach AuthGate.

Resolution mapping:

| `TRUSTED_PROXIES` value | Behaviour |
| ---------------------------- | -------------------------------------------------------------------------------------- |
| unset / empty (**default**) | Trust **none**. `ClientIP()` is the direct TCP peer (`RemoteAddr`); headers ignored. |
| `10.0.0.0/8,192.168.1.5` | Trust **only** these proxies — honour their forwarding headers. |
| `*` | Trust **all** proxies (legacy behaviour). Insecure if directly reachable; migration only. |

Notes:

- **Secure by default.** With `TRUSTED_PROXIES` unset, spoofed `X-Forwarded-For` headers are ignored, so a directly reachable server can't be tricked into multiplying per-IP rate-limit quotas or forging audit/session IPs.
- **Validated at startup.** Each entry must be a valid CIDR or bare IP (IPv4 or IPv6), or the single wildcard `*`. Malformed entries cause `Config.Validate()` to fail and the server refuses to start.
- **Behind a reverse proxy**, set this to the proxy's address(es) **and** configure the proxy to **overwrite** both `X-Forwarded-For` and `X-Real-IP` with the direct peer (e.g. nginx `proxy_set_header X-Forwarded-For $remote_addr;` plus `proxy_set_header X-Real-IP $remote_addr;`). AuthGate honours both headers from a trusted proxy, so overwriting only one leaves the other client-spoofable. Appending (nginx's `$proxy_add_x_forwarded_for`) leaves client-supplied values spoofable. See [DEPLOYMENT.md](DEPLOYMENT.md) and [RATE_LIMITING.md](RATE_LIMITING.md).
- **Breaking change.** Previous releases trusted all proxies. After upgrading, deployments behind a proxy will see every client IP collapse to the proxy's address until `TRUSTED_PROXIES` is set. Set it to your proxy CIDRs, or use `*` for an immediate (insecure) restore of the old behaviour.
- On startup AuthGate logs the effective mode (`Trusted proxies: none (using RemoteAddr)` / `wildcard (trust all)` / the configured list).

---

## TLS / HTTPS

AuthGate can serve HTTPS directly by setting two environment variables. When both are configured, the server listens on `SERVER_ADDR` using TLS. When both are empty (the default), it serves plain HTTP. Setting only one of the two is rejected at startup by `Config.Validate()` — this prevents silently falling back to HTTP when the operator meant to enable TLS.
Expand Down
31 changes: 30 additions & 1 deletion docs/DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,12 @@ server {
# Headers
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# OVERWRITE X-Forwarded-For with the direct peer ($remote_addr), NOT
# $proxy_add_x_forwarded_for (which APPENDS, leaving any client-supplied
# value intact and spoofable). AuthGate derives the rate-limit key,
# audit source IP, and session fingerprint from this header, so it must
# not be attacker-controllable. Pair this with TRUSTED_PROXIES below.
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
Expand Down Expand Up @@ -441,6 +446,30 @@ sudo nginx -t
sudo systemctl reload nginx
```

### Tell AuthGate to Trust the Proxy

AuthGate ignores `X-Forwarded-For` / `X-Real-IP` by default and resolves the
client IP from the direct TCP peer (the secure default — a directly reachable
server can't be tricked by spoofed headers). When running behind the Nginx
reverse proxy above, set `TRUSTED_PROXIES` to the proxy's address(es) so the
overwritten forwarding header is honoured and the real client IP reaches the
rate limiter, audit log, and session fingerprint:

```bash
# AuthGate environment (CIDRs and/or bare IPs, comma-separated)
TRUSTED_PROXIES=127.0.0.1,::1 # Nginx on the same host
# TRUSTED_PROXIES=10.0.1.5 # Nginx's exact IP on a private subnet
# TRUSTED_PROXIES=10.0.1.0/24 # or the narrowest dedicated proxy subnet
```

> Use the proxy's **exact** address(es) or the narrowest dedicated proxy subnet —
> avoid broad ranges like `10.0.0.0/8`, since every host in a trusted range may
> set the client IP via forwarding headers and could re-enable spoofing if it can
> reach AuthGate. Only the proxy must overwrite (not append) `X-Forwarded-For` —
> see the `proxy_set_header X-Forwarded-For $remote_addr;` line above.
> `TRUSTED_PROXIES=*` restores the legacy trust-all behaviour for migration only
> (insecure).

### Obtain SSL Certificate (Let's Encrypt)

```bash
Expand Down
34 changes: 32 additions & 2 deletions docs/RATE_LIMITING.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ server {
listen 80;
location / {
proxy_pass http://authgate;
# OVERWRITE (do not append) X-Forwarded-For with the direct peer so a
# client can't pre-seed the header to forge its IP. Use $remote_addr,
# NOT $proxy_add_x_forwarded_for (which appends, preserving spoofed
# values). The per-IP rate limiter keys on the resolved client IP.
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
Expand All @@ -170,8 +176,26 @@ server {
ENABLE_RATE_LIMIT=true
RATE_LIMIT_STORE=redis
REDIS_ADDR=redis.example.com:6379

# REQUIRED behind a proxy: tell AuthGate which proxy addresses may set the
# forwarding headers above. Without this, AuthGate trusts NONE and every
# request collapses to the proxy's IP (one shared rate-limit bucket).
# List the EXACT proxy/load-balancer addresses (or the narrowest dedicated
# proxy subnet) — avoid broad ranges like 10.0.0.0/8, which would let any
# internal host in the range spoof the client IP.
TRUSTED_PROXIES=10.0.1.5,10.0.1.6
```

> **Trusted proxies & client IP spoofing.** AuthGate resolves the client IP from
> `X-Forwarded-For` / `X-Real-IP` **only** for requests arriving from an address
> listed in `TRUSTED_PROXIES`. When `TRUSTED_PROXIES` is unset (the secure
> default), those headers are ignored and the rate-limit key is the direct TCP
> peer — so an attacker reaching AuthGate directly cannot rotate spoofed headers
> to evade per-IP limits. Set `TRUSTED_PROXIES` to your proxy's address(es) when
> deploying behind one, and configure that proxy to **overwrite** the forwarding
> headers as shown above. `TRUSTED_PROXIES=*` restores the legacy trust-all
> behaviour (insecure) for migration only.

## Redis Setup

### Local Development
Expand Down Expand Up @@ -267,12 +291,18 @@ REDIS_ADDR=your-redis-service:6379
LOGIN_RATE_LIMIT=10
```

2. Configure reverse proxy to preserve client IPs:
2. Configure reverse proxy to preserve client IPs (overwrite, don't append):

```nginx
proxy_set_header X-Forwarded-For $remote_addr;
```

…and tell AuthGate to trust that proxy so the header is honoured:

```bash
TRUSTED_PROXIES=10.0.1.5,10.0.1.6 # your proxy's exact address(es)
```

### Issue: Redis memory usage growing

**Cause:** Rate limiter keys accumulating
Expand Down Expand Up @@ -315,7 +345,7 @@ RATE_LIMIT_CLEANUP_INTERVAL=10m
## Security Notes

- Rate limiting is **per IP address**
- Use `X-Forwarded-For` header when behind proxy/load balancer
- Behind a proxy/load balancer, set `TRUSTED_PROXIES` to the proxy address(es) and have the proxy **overwrite** `X-Forwarded-For` with `$remote_addr`. Without `TRUSTED_PROXIES`, AuthGate ignores forwarding headers (secure default) and keys on the direct peer, so a directly reachable server can't be tricked into multiplying the quota with spoofed headers.
- Consider additional WAF rules for sophisticated attacks
- Rate limiting is one layer; combine with other security measures
- Review logs regularly for attack patterns
Expand Down
14 changes: 12 additions & 2 deletions docs/TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,12 +460,22 @@ DEVICE_CODE_RATE_LIMIT=30 # 30 req/min

3. **Check if behind proxy:**

If behind a reverse proxy, ensure real IP is forwarded:
If behind a reverse proxy, ensure the real IP is forwarded. OVERWRITE
`X-Forwarded-For` with the direct peer (`$remote_addr`), NOT
`$proxy_add_x_forwarded_for` (which appends, preserving spoofable client values),
and tell AuthGate to trust the proxy via `TRUSTED_PROXIES`:

```nginx
# Nginx configuration
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-For $remote_addr;
```

```bash
# AuthGate: trust the proxy so the forwarded IP is honoured.
# Use the proxy's exact IP(s) or the narrowest dedicated proxy subnet —
# avoid broad ranges like 10.0.0.0/8 (any host in the range could then spoof).
TRUSTED_PROXIES=10.0.1.5
```

4. **Temporarily disable for debugging:**
Expand Down
34 changes: 34 additions & 0 deletions internal/bootstrap/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ func setupRouter(
setupGinMode(cfg)
r := gin.New()

// Pin trusted proxies so client-supplied forwarding headers can't spoof
// the resolved client IP (rate-limit key, audit IP, session fingerprint).
setupTrustedProxies(r, cfg)

// Setup middleware
r.Use(metrics.HTTPMetricsMiddleware(prometheusMetrics))
r.Use(gin.Logger(), gin.Recovery())
Expand Down Expand Up @@ -443,6 +447,36 @@ func setupGinMode(cfg *config.Config) {
log.Printf("Gin mode: %s", ginModeLogMessage[cfg.IsProduction])
}

// setupTrustedProxies pins which upstream proxies Gin will trust when
// resolving c.ClientIP() from forwarding headers (X-Forwarded-For / X-Real-IP).
// The resolution mapping (validated earlier by Config.Validate):
//
// unset / empty -> SetTrustedProxies(nil) // ClientIP() == RemoteAddr (secure default)
// "*" -> SetTrustedProxies(0.0.0.0/0, ::/0) // trust all (legacy behaviour)
// CIDR / bare-IP list -> SetTrustedProxies(list) // trust only the listed proxies
//
// A failure from SetTrustedProxies is fatal, consistent with serveStaticFiles.
func setupTrustedProxies(r *gin.Engine, cfg *config.Config) {
proxies, mode := resolveTrustedProxies(cfg.TrustedProxies)
if err := r.SetTrustedProxies(proxies); err != nil {
log.Fatalf("Failed to set trusted proxies: %v", err)
}
log.Printf("Trusted proxies: %s", mode)
}

// resolveTrustedProxies maps the configured TRUSTED_PROXIES list to the slice
// passed to SetTrustedProxies plus a human-readable mode string for the startup
// log. The wildcard "*" expands to the IPv4 and IPv6 "any" ranges.
func resolveTrustedProxies(configured []string) (proxies []string, mode string) {
if len(configured) == 0 {
return nil, "none (using RemoteAddr)"
}
if config.IsWildcardProxies(configured) {
return []string{"0.0.0.0/0", "::/0"}, "wildcard (trust all)"
}
return configured, fmt.Sprintf("%v", configured)
}

var ginModeMap = map[bool]string{
true: gin.ReleaseMode,
false: gin.DebugMode,
Expand Down
Loading
Loading