Console
ScalWS Console: test and security report (2026-10-07, updated 2026-10-08)
Update 2026-10-08: live statistics, settings for every field, redesign
| Check | Result |
|---|---|
Unit tests scalws-console | 32 passed (adds: Prometheus parsing with escaped labels, rates from two instances, counter reset, histogram quantiles, domains/protocols from the config, schema covers the config with defaults and units, form model round trip to a valid config, semantic change list ignoring formatting and hiding secrets) |
Unit tests scalws-controller | 13 passed (adds: controller.yaml schema with defaults in file units) |
Unit tests scalws-config | 24 passed (feature schema changes no parsing) |
clippy -D warnings (console, controller, config) | clean |
End-to-end scripts/console-e2e.py | 46/46 PASS: the 38 earlier checks plus live statistics, both schemas, a form edit listed by setting, controller.yaml validated by check-config, applied by controller restart with the site serving every request during the restart, a controller.yaml needing more CPUs than allowed rejected, controller.yaml restored from history |
| Browser (Chromium, 22 pages × 1440/390 × light/dark = 88 screenshots) | no CSP violation or script error, no horizontal page overflow (after fixing the review table on phones) |
| Console process, live statistics on | 0 ms CPU/s with no browser, 10 ms CPU/s (1 % of a core) with 5 sessions; RSS 77 MB |
Build: cmd/scalws-console at the commit that adds it; demo stack on the development VM
(scripts/console-demo.sh): its own scalws-controller (state [private host path],
listeners 127.0.0.1:28080/28443, metrics 39910+, CPUs 6-7), console on 0.0.0.0:8090. All
console processes, builds and tests ran on CPUs 6-7 under nice -n 10 while a 6 h soak used
CPUs 0-5; the soak’s controller was never touched.
Unit tests (cargo test -p scalws-console): 24 passed
| Area | Tests |
|---|---|
| Accounts | plain and argon2id entries (passwords with :), only hashes in memory, file readable by others refused (logins disabled), malformed lines/roles/hashes refused, init-credentials creates admin 0600 once, role change on reload ends sessions |
| Sessions | expiry, revocation, CSRF token shape |
| Throttling | lock after 5 failures, cleared on success |
| RBAC | full matrix: mutations only for operator/administrator, viewer read-only |
| Redaction | password/secret/token keys, env blocks and flow maps redacted; exact restore; unknown placeholder refused; quoted placeholder restored; log lines lose control characters |
| Diff | hunks, line numbers, empty sides |
| Transactions | atomic replace keeps mode, impact names listener changes, conflict on changed file (nothing written), idempotency key returns the same operation, controller unavailable leaves the file and is audited |
| Other | allow list (CIDR, v4-mapped), RFC 3339 formatting, rollout health states incl. controller rollback |
cargo clippy -p scalws-console --all-targets -- -D warnings: clean.
npm run build (tsc --noEmit strict + esbuild): clean, 45 KB JS, 14 KB CSS.
End-to-end (scripts/console-e2e.py, against the running demo): 38/38 PASS
Static page and security headers (CSP, frame, nosniff, HSTS) · unauthenticated API 401 ·
login without Origin 403 · wrong password 401 · cookie __Host- with Secure, HttpOnly,
SameSite=Strict · password never echoed · telemetry from the demo controller · mutation
without CSRF token 403 · mutation without Origin 403 · valid draft (+1 listener) with impact
“listener alt added” · apply 202, repeated idempotency key → same operation · apply complete
on a new generation, new listener serves · missing key file rejected by scalwsd --check,
not applied · port in use: controller refuses the reload, previous file restored, generation
unchanged · concurrent edit on the host → conflict, nothing written · outside edit recorded
as external history · restore of history #1 as a new generation, alt listener gone ·
scale 1→2 · scale beyond max refused · viewer added to the credentials file signs in without
restart, cannot create drafts, scale, read audit or logs · removing the account ends its
session · audit log has the apply and no password · controller log readable · sign-in
throttled after 5 failures (429) · logout ends the session.
Browser (Chromium headless, 1440×900 light and dark, 390×844)
All nine pages, the structured editor, the review page, the raw editor and the command palette rendered; no CSP violation or script error (the only console message is the expected 401 of the session probe before sign-in); no horizontal page overflow at 390 px after fixing the top bar (search button and theme menu hidden on narrow screens).
Overhead (scripts/console-overhead.sh, demo instance on CPU 6, oha on CPU 7)
| Mode | req/s (median of 3 × 15 s) | p99 |
|---|---|---|
| console stopped | 68 783 | 21.06 ms |
| console running, idle | 69 720 (+1.4 %) | 21.06 ms |
| 5 sessions polling telemetry (2 s) and logs (3 s) | 63 676 (−7.4 %) | 21.06 ms |
The “active” drop is the five Python pollers sharing CPU 7 with the load generator, not the console: measured directly, the console process uses 0 ms CPU/s idle and 5 ms CPU/s (0.5 % of one core) with 5 active sessions; RSS 107 MB (limit in the unit: 256 MB). p99 is unchanged. A clean separation needs a third CPU for the pollers (not available while the soak runs); to be repeated on an idle host.
Console -> systemd -> controller restart (scripts/console-systemd-e2e.sh, 2026-10-08)
The packaged units (packaging/systemd/), renamed *-e2e and pointed at test paths, with
their sandboxing unchanged: the console (root, capability-bounded, NoNewPrivileges,
ProtectSystem=strict) applies a controller.yaml change and runs
systemctl restart scalws-e2e-controller; the controller runs as scalws
(KillMode=process). 12/12 checks, under 400 req/s of load:
- the sandboxed console can run
systemctl restart; the controller restarted exactly once (journal: oneStartedafter the test began); - both instances adopted with the same pids; 16 001 requests, 0 failed;
- the same apply re-sent with its idempotency key — while running, and after it completed (the draft is gone by then; this returned 404 before, now the same operation, 202) — never runs a second time;
- the console saw the controller back with 2 active instances; the restart was faster than its 200 ms telemetry poll (no “unavailable” sample).
Security review status
Done in this phase: threat model and controls (docs/console-security.md), server-side
RBAC on every handler, CSRF + same-origin, strict CSP, secret redaction, append-only audit,
fail-closed credentials, fenced systemd unit. Open before v1: external review, WebAuthn/TOTP,
per-tenant scopes, rate limit on authenticated API calls, controller endpoint that removes the
need for root.
Not tested yet
Package install in clean containers (scripts/package.sh --verify now checks the console
binary, unit and the generated credentials; not run in this session: Docker build of the
release image), large-log performance in the browser (tail is bounded to 2000 lines),
browser reconnect after network loss beyond the stale indicator, keyboard-only walkthrough
with a screen reader.