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

CheckResult
Unit tests scalws-console32 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-controller13 passed (adds: controller.yaml schema with defaults in file units)
Unit tests scalws-config24 passed (feature schema changes no parsing)
clippy -D warnings (console, controller, config)clean
End-to-end scripts/console-e2e.py46/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 on0 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

AreaTests
Accountsplain 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
Sessionsexpiry, revocation, CSRF token shape
Throttlinglock after 5 failures, cleared on success
RBACfull matrix: mutations only for operator/administrator, viewer read-only
Redactionpassword/secret/token keys, env blocks and flow maps redacted; exact restore; unknown placeholder refused; quoted placeholder restored; log lines lose control characters
Diffhunks, line numbers, empty sides
Transactionsatomic 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
Otherallow 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)

Modereq/s (median of 3 × 15 s)p99
console stopped68 78321.06 ms
console running, idle69 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: one Started after 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.