Skip to content

Commit e7444f3

Browse files
committed
feat(dashboard): add secure dashboard with CSRF and rate limits
1 parent 517c08e commit e7444f3

14 files changed

Lines changed: 1400 additions & 1 deletion

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ wait0 is an ultra-fast cache-first HTTP reverse proxy written in Go that serves
2626
│ ├── *_runtime_adapter.go # Root adapters that inject Service deps into modules
2727
│ ├── auth/ # Shared bearer authentication
2828
│ ├── invalidation/ # /wait0/invalidate API + async workers
29+
│ ├── dashboard/ # /wait0/dashboard HTML + stats/invalidation bridge handlers
2930
│ ├── proxy/ # Request handling/origin fetch/response headers
3031
│ ├── revalidation/ # Revalidate and warmup orchestration
3132
│ ├── discovery/ # Sitemap discovery and URL normalization
@@ -48,6 +49,7 @@ wait0 is an ultra-fast cache-first HTTP reverse proxy written in Go that serves
4849
|------|---------|
4950
| cmd/wait0/main.go | Bootstraps config/service, starts HTTP server, handles graceful shutdown |
5051
| internal/wait0/service_core.go | Root composition: constructs module controllers and adapters |
52+
| internal/wait0/dashboard/controller.go | Dashboard HTTP controller (Basic Auth + bridge endpoints + HTML shell) |
5153
| internal/wait0/proxy/controller.go | Main request handling path and cache hit/miss/bypass flow |
5254
| internal/wait0/revalidation/controller.go | Async revalidation and warmup orchestration |
5355
| internal/wait0/cache_ram.go / cache_disk.go | Root cache facades wrapping `internal/wait0/cache` |

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ It is designed for Next.js/Nuxt.js and other dynamic origins where latency and o
1414
- Built for dynamic pages, not static asset caching
1515
- RAM + LevelDB cache tiers for low latency and persistence
1616
- Async invalidation API with bearer auth and tag support
17+
- Built-in Basic-Auth dashboard for stats, charts, and invalidation
1718
- Warmup and sitemap discovery to reduce cold-start misses
1819
- Explicit response marker `X-Wait0` (`hit`, `miss`, `bypass`, etc.)
1920

@@ -95,6 +96,28 @@ curl -i \
9596
Returns `200 OK` with cache/memory/refresh/sitemap metrics snapshot.
9697
Authorization is scope-based (`stats:read`).
9798

99+
## Dashboard Example
100+
101+
Set dashboard credentials in environment:
102+
103+
```bash
104+
export WAIT0_DASHBOARD_USERNAME='ops'
105+
export WAIT0_DASHBOARD_PASSWORD='change-me'
106+
```
107+
108+
Open:
109+
110+
```text
111+
http://localhost:8082/wait0/dashboard
112+
```
113+
114+
Notes:
115+
116+
- The dashboard is Basic-Auth protected and disabled when either env variable is missing.
117+
- The dashboard reads stats and triggers invalidation through server-side bridge handlers.
118+
- Bearer tokens are not exposed to browser JavaScript.
119+
- Dashboard invalidation is CSRF-protected (same-origin + token check) and dashboard routes are rate-limited per IP.
120+
98121
## Documentation
99122

100123
| Guide | Description |

debug/debug-compose.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ services:
1010
- origin
1111
ports:
1212
- "8082:8082"
13+
environment:
14+
WAIT0_DASHBOARD_USERNAME: admin
15+
WAIT0_DASHBOARD_PASSWORD: admin
1316
volumes:
1417
- ./wait0.yaml:/wait0.yaml:ro
1518
- wait0-data:/data

debug/wait0.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ server:
1818

1919
auth:
2020
tokens:
21+
# Used by debug dashboard bridge endpoints:
22+
# - GET /wait0/dashboard/stats (stats:read)
23+
# - POST /wait0/dashboard/invalidate (invalidation:write)
2124
- id: debug-backoffice
2225
token: debug-token
2326
scopes:

docs/api-endpoints.md

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Complete HTTP endpoint reference for `wait0`.
1111
- A reverse-proxy data path for regular client requests.
1212
- A control endpoint for asynchronous cache invalidation.
1313
- A control endpoint for read-only runtime/cache statistics.
14+
- A Basic-Auth dashboard route with stats polling and invalidation form.
1415

1516
Base URL examples:
1617

@@ -157,7 +158,68 @@ The table below explains each field in the stats payload, including what it mean
157158
| `403` | `forbidden` | Token exists but lacks `stats:read` scope |
158159
| `405` | `method not allowed` | Non-GET request |
159160

160-
## 3) Invalidation API
161+
## 3) Dashboard
162+
163+
## Routes
164+
165+
- `GET /wait0/dashboard`
166+
- `GET /wait0/dashboard/`
167+
- `GET /wait0/dashboard/stats`
168+
- `POST /wait0/dashboard/invalidate`
169+
170+
## Auth
171+
172+
- HTTP Basic Auth required on all dashboard routes.
173+
- Credentials are loaded from:
174+
- `WAIT0_DASHBOARD_USERNAME`
175+
- `WAIT0_DASHBOARD_PASSWORD`
176+
177+
If either credential env variable is missing, dashboard routes are not registered and return `404`.
178+
179+
## Behavior
180+
181+
- `GET /wait0/dashboard` serves a lightweight HTML page with:
182+
- parsed stats cards,
183+
- simple charts over time (client-side in-memory history),
184+
- invalidation form.
185+
- `GET /wait0/dashboard/stats` bridges to `GET /wait0` server-side.
186+
- `POST /wait0/dashboard/invalidate` bridges to `POST /wait0/invalidate` server-side.
187+
- `POST /wait0/dashboard/invalidate` applies CSRF checks:
188+
- same-origin via `Origin` (or `Referer` fallback),
189+
- CSRF token header (`X-Wait0-CSRF`) must match dashboard CSRF cookie.
190+
- Bridge calls use server-side bearer tokens from `auth.tokens[]`, scoped by:
191+
- stats: `stats:read`
192+
- invalidation: `invalidation:write`
193+
- Bearer tokens are never sent to browser code.
194+
- Dashboard routes are rate-limited per IP (default `120` requests/minute).
195+
- Dashboard responses disable caching (`Cache-Control: no-store, max-age=0`, `Pragma: no-cache`, `Expires: 0`).
196+
197+
### Token availability behavior
198+
199+
- Missing `stats:read` token: dashboard is disabled (`404`).
200+
- Missing `invalidation:write` token: dashboard works in stats-only mode; invalidate action returns `503`.
201+
202+
## Example
203+
204+
```bash
205+
curl -i \
206+
-u "${WAIT0_DASHBOARD_USERNAME}:${WAIT0_DASHBOARD_PASSWORD}" \
207+
"http://localhost:8082/wait0/dashboard/stats"
208+
```
209+
210+
## Error responses
211+
212+
| HTTP | Body `error` | Cause |
213+
|------|--------------|-------|
214+
| `401` | `unauthorized` | Missing/invalid basic auth credentials |
215+
| `403` | `csrf origin check failed` | Cross-origin state-changing request |
216+
| `403` | `csrf token check failed` | Missing/invalid CSRF token for invalidate action |
217+
| `405` | `method not allowed` | Unsupported method for route |
218+
| `429` | `rate limit exceeded` | Dashboard per-IP rate limit exceeded |
219+
| `503` | `dashboard stats are unavailable` | Internal stats bridge unavailable |
220+
| `503` | `dashboard invalidation is unavailable` | No `invalidation:write` scoped token configured |
221+
222+
## 4) Invalidation API
161223

162224
## Route
163225

docs/for-developers.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ Coverage threshold default: `80%` (`COVERAGE_THRESHOLD` in `Makefile`).
6161
| `WAIT0_CONFIG` | `/wait0.yaml` | Default value for `-config` |
6262
| `WAIT0_INVALIDATE_DISK_CACHE_ON_START` | `true` | If `true`, LevelDB cache directory is cleared on process start |
6363
| `WAIT0_SEND_REVALIDATE_MARKERS` | `true` | Controls sending revalidation marker headers during background revalidation |
64+
| `WAIT0_DASHBOARD_USERNAME` | unset | Basic Auth username for `GET /wait0/dashboard` and dashboard API routes |
65+
| `WAIT0_DASHBOARD_PASSWORD` | unset | Basic Auth password for `GET /wait0/dashboard` and dashboard API routes |
66+
| `WAIT0_DASHBOARD_RATE_LIMIT_RPM` | `120` | Per-IP fixed-window rate limit for `/wait0/dashboard*` routes |
67+
| `WAIT0_DASHBOARD_TRUST_PROXY_HEADERS` | `false` | If `true`, dashboard rate limiter client IP extraction trusts `X-Forwarded-For` (first hop) |
68+
| `WAIT0_DASHBOARD_TRUSTED_PROXY_CIDRS` | unset | Comma-separated CIDRs of trusted proxy source IPs allowed to supply `X-Forwarded-For` |
6469

6570
## Configuration Reference (`wait0.yaml`)
6671

@@ -106,6 +111,15 @@ For invalidation API, at least one token must have scope `invalidation:write` wh
106111

107112
For stats API (`GET /wait0`), tokens need scope `stats:read`.
108113

114+
For dashboard:
115+
116+
- `stats:read` token is required to enable dashboard routes.
117+
- `invalidation:write` token is optional; without it, dashboard is stats-only and invalidate action is disabled.
118+
- Built-in dashboard rate limiting exists (`WAIT0_DASHBOARD_RATE_LIMIT_RPM`), but for internet-exposed deployments reverse-proxy/WAF rate limiting is still mandatory.
119+
- Pre-auth rate-limit key is `client IP` only.
120+
- Post-auth rate-limit key is `(client IP, verified auth principal)`.
121+
- With trusted proxies enabled, `client IP` uses `X-Forwarded-For` first hop only when `RemoteAddr` belongs to `WAIT0_DASHBOARD_TRUSTED_PROXY_CIDRS`.
122+
109123
## `rules[]`
110124

111125
| Field | Required | Notes |

0 commit comments

Comments
 (0)