0010: Console request path, CSRF and sandbox credentials¶
Date: 2026-10-07 · Status: accepted
Context¶
The design requires p99 page render under 50 ms with 1,000 targets and 100,000 backups, no client-side framework, every write audited, and a query console against sandboxes only. keeper-api has no RBAC in sandbox namespaces.
Decision¶
- Reads never list from the API server. Informer events feed an in-process index of BackupTargets, Backups, Restores and Sandboxes. Requests read the index and the in-memory catalog. All target views are built once per change (catalog version + index version, at most every 2 s) and shared by requests, which only filter them by the caller's org/project scope.
- The overview's 1,000-row table is rendered by a direct HTML writer. It escapes like html/template. All other markup uses html/template. Measured (test/ui TestConsoleLatency, 1,000 targets, 100,000 backups): overview p99 ≈ 16 ms, JSON target list p99 ≈ 34 ms.
- Writes go through one path for the console (form posts, HTMX) and the API (JSON):
- Validate, check the role (mode-dependent for restores), create or patch a Keeper resource, audit.
- Restores are created in the target's namespace. Pins create or patch a Backup object that mirrors the stored
backup (
spec.backupID). - Backup-now and verify-now set annotations that the controller turns into objects with predictable names.
- CSRF. Form posts must be same-origin (
Sec-Fetch-Site/Origin). The JSON API needsContent-Type: application/json. Behind Cloudflare Access both also need a valid Access token. - No inline scripts. CSP
default-src 'self'. HTMX and its SSE extension are embedded and served with immutable caching. - Sandbox credentials. The controller writes a copy of each sandbox's credentials to
keeper-sbx-<name>in Keeper's namespace. keeper-api reads that copy for the query proxy and the audited credentials endpoint (owner or restorer-admin). keeper-api gets no access to sandbox namespaces. - Query console. Postgres runs
BEGIN READ ONLYwithstatement_timeout30 s. MySQL runsSTART TRANSACTION READ ONLYwithmax_execution_time30 s. Writes are possible only with an explicit flag (sandboxes are disposable). Results are capped at 1,000 rows and cells at 4 KiB. The target'smaskpatterns are applied. Every query is audited. Per-user history is kept in memory; the audit log is the durable record.
Consequences¶
Pages reflect cluster changes within an informer round trip. A replica serves only what its own informers saw, and
replicas converge. The in-memory audit view covers recent events on that replica; the store's audit/ objects
are the complete log.