Console

ScalWS Console: security and threat model

Assets

  • The running service (availability of client traffic).
  • The configuration and the files it references (certificate paths, application roots).
  • Secrets inside the configuration (application env values, passwords, tokens).
  • Console accounts and sessions; the audit trail.

Trust boundaries

browser (untrusted network) ──TLS──▶ console ──Unix socket──▶ controller ──▶ instances
                                       │
                                       └─ /etc/scalws (configuration), /var/lib/scalws-console

Threats and controls

ThreatControl
Password guessingargon2id (RustCrypto argon2, default parameters m=19 MiB, t=2, p=1); 5 failures per address or user name in 15 min lock for 30 s, doubling to 15 min; at most 2 verifications at a time; unknown users are verified against a dummy hash (no timing oracle)
Session theftrandom 256-bit token, cookie __Host-scalws_console with Secure; HttpOnly; SameSite=Strict; Path=/; only its SHA-256 is kept server-side; idle timeout 30 min (--session-idle-minutes), absolute 12 h; logout revokes; changing a user’s role or removing it in the credentials file revokes its sessions
CSRFper-session token in X-CSRF-Token (constant-time compare) on every non-GET request, plus same-origin Origin/Referer; login requires same origin and application/json
XSSstrict CSP (default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'), no inline script or style, every server-provided string inserted with textContent, log lines stripped of control characters
Clickjacking, sniffing, leaksX-Frame-Options: DENY, nosniff, Referrer-Policy: no-referrer, COOP/CORP same-origin, Permissions-Policy off, HSTS, Cache-Control: no-store on the API
Privilege escalation in the UIcapabilities checked server-side in every handler (docs/console-rbac.md); the role is re-read from the credentials file on every request
Destructive actions by a stolen, unattended sessionapply, rollback and cache purge need a password confirmation in the last 5 minutes; confirmations state impact; restore needs the word restore typed
Writing a broken configurationdraft → schema + semantic validation → scalwsd --check → diff → confirm → atomic write → controller reload → health check → automatic restore of the previous file on failure (docs/console-config-workflow.md)
Lost update between operatorsapply carries the draft’s base hash; a changed file is a conflict and nothing is written
Duplicate apply on retryidempotency key per apply (same key → same operation) and one apply at a time
Secret disclosuresecret values replaced by placeholders in every view, draft editor, diff, validation change list (including secrets nested in a changed section) and audit event, and by (secret) in validation errors, warnings and --check output; validation messages carry the position of an error but no quoted source lines; originals stay in server memory (draft) and go back into the file on save; private key files are never read
Console compromise affects trafficseparate process, no data-plane hook, controller API only; systemd fencing below
Denial of service of the consolebounded bodies (5 MiB), connections (512 in all, 16 per client address, IPv6 per /64), TLS handshake timeout (10 s), request-head timeout (10 s), request-body timeout (10 s), idle connections closed after 60 s without traffic, connections that have not presented a valid session closed after 30 s whatever their traffic, at most 8 open drafts and 30 new drafts a minute per user, at most 16 sessions per account (signing in again ends the oldest), throttled sign-in and re-authentication attempts not audited one by one, bounded log/audit reads, MemoryMax=256M
Exposureoptional --allow address list; listen address configurable (e.g. 127.0.0.1:8090 behind an SSH tunnel)

Credentials file

/etc/scalws/console/credentials (packaged installs; <state_dir>/credentials otherwise), mode 0600, one account per line:

# comments allowed
admin:administrator:<password>
alice:operator:$argon2id$v=19$m=19456,t=2,p=1$...
  • The ScalWS installer creates it with user admin and a random 24-character password, prints the address, user and password once, and leaves the password in the file. scalws-console run creates the same account if the file is missing (development fallback; the password is written to the file only, never logged).
  • Passwords may be plain text or an argon2id PHC hash (scalws-console hash-password reads one on stdin). Plain entries are hashed when the file is read; only hashes are kept in memory.
  • Trade-off of plain text: anyone who can read the file (root, backups of /etc) can read the password. The file is root-only and the console refuses it when group or others have any access (logins are disabled and the reason is logged). Replace the generated password by a hash after the first login if backups of /etc leave the host.
  • The console re-reads the file when its modification time changes: password and role changes apply without a restart; sessions of changed or removed accounts end.

Privileges

The console needs to connect to the controller socket (0600, owner scalws) and to replace /etc/scalws/scalws.yaml with its original owner and mode. The packaged unit therefore runs it as root with only CAP_DAC_OVERRIDE CAP_CHOWN CAP_FOWNER, a read-only file system except /etc/scalws and /var/lib/scalws-console, no new privileges, a system-call filter, MemoryDenyWriteExecute and address families limited to Unix, IPv4 and IPv6 (packaging/systemd/scalws-console.service). A controller endpoint that applies a configuration itself (docs/console-architecture.md, gap report) would let the console run as an unprivileged user.

Audit event schema

/var/lib/scalws-console/audit.jsonl, one JSON object per line, opened append-only (0600):

FieldMeaning
timeRFC 3339 UTC with milliseconds
actor, roleaccount and its role
actionlogin, logout, reauth, draft.create, draft.discard, config.apply, config.rollback, runtime.scale, runtime.autoscaling, tenant.limits, cache.purge
objectthe configuration file, controller, session or a draft id
resultok, failed, complete, rolled_back, conflict
operationoperation id of an apply or rollback
previous_generation, new_generationcontroller generations around the change
summaryhuman-readable outcome
diffredacted unified diff (at most 400 lines)
source_ip, user_agentwhere the request came from

Failed sign-ins are recorded with the user name that was tried. Passwords are never recorded.

The audit view (GET /api/v1/audit) reads the file backwards in 4 MiB steps until it has the requested number of matches, at most the newest 64 MiB. Routine events (sign-in, sign-out, re-authentication) are recorded at most 20 times a minute per account, failed sign-ins per client network (IPv6 per /64); the rest of the minute is counted in one audit.suppressed event. One user’s burst of events cannot push other users’ events out of the view. The whole file stays on disk.

Not done yet (C7)

WebAuthn / TOTP second factor, per-tenant scopes, external security review. The handoff’s definition of done requires the review before v1.