# Packet Station security This document is rewritten for the server build. The old browser build's security document is in the old repo (`C:\Users\spencerm\Coding\PacketStation\docs\SECURITY.md`). The design comes from [BRIEF.md](BRIEF.md) and [PLAN.md](PLAN.md). Status: phase 0. Most controls below are designed, not built. Each one names the phase that builds it. Contents: - Part 1: the 20 old layers and where each one lives now. - Part 2: the new build's controls. - Part 3: the production requirement IDs and the phase that closes each. ## Part 1: the old layers, moved to the server The M: drive build had 20 layers of protection, most of them enforced in the browser. Every layer that still applies moves to the server. The rest are dropped or replaced. | # | Old layer | Now | Phase | |---|---|---|---| | 1 | PESCO network and firewall | Kept. The app runs on `docker-1` inside the PESCO network, reached only through Nginx Proxy Manager. It publishes no port. PostgreSQL is on each stack's internal network only. | 0 | | 2 | Windows folder rights on the M: folder | Dropped. Replaced by database roles: the app role cannot change the schema or update or delete the audit; migrations run as a separate role. Files sit on a volume only the containers see. | 0 | | 3 | Employee ID + PIN sign-in | Replaced by AD sign-in with the domain password, plus an optional Packet Station PIN checked on the server, for registered shop devices only. | 1 | | 4 | Department and level screens | A static role-to-screen map in the browser, plus server policy on every request. The browser only decides what to show. | 1, 2 | | 5 | Per-PC access log | Replaced by the server audit. Each row carries the AD account, session, client IP and user agent. No per-PC browser ids. | 1 | | 6 | Filing seals and filed-packet lock | HMAC seals computed on the server with a server key. The filed lock is a server policy rule. | 7 | | 7 | Record integrity check | A worker job (seals, asset hashes, audit hash chain) with an alert. | 9 | | 8 | Self-hosted libraries | Kept: no CDN. The CSP allows nothing external. | 0 | | 9 | Output escaping | React escapes output by default. The PDF export escapes on the server. | 1, 7 | | 10 | Central permission policy | Server policy, checked on every write, including writes that do not come from the screens. | 2 | | 11 | PIN sign-off (electronic signature) | Server-verified signature with the PIN or the domain password, stored with who, when, what, factor and a content hash. | 1 | | 12 | IT edit reasons | Kept. Admin writes need a reason and are marked as IT edits in the packet history and the audit. | 2 | | 13 | Screen lock | Server-enforced sessions. The browser reports idle and background state; the server records it and refuses an idle or locked session. Unlock is a step-up check on the server. | 1 | | 14 | Input and upload validation | On the server: field limits on every write, uploads typed by content, size limits, sanitised names. | 2, 4 | | 15 | Second-admin approval | Superseded. Roles come only from AD groups and the group-to-role map is derived, so there is nothing to grant in the app. | 1 | | 16 | Security alerts and automatic integrity check | Worker jobs and the Notifications tab (email through app-03). | 9 | | 17 | Controlled-copy stamp on data books | Applied to server-built PDFs and signed with the seal key. | 7 | | 18 | CSV formula guard | Applied to every server export. | 7 | | 19 | Update lock and stale-version block | Replaced by a build version endpoint. A screen on an older build shows a banner and reloads. | 0 (endpoint), 10 (banner) | | 20 | VISUAL helper write guard | Dropped. There is no helper. VisualKit and MFilesKit are read-only by design and connect as read-only accounts. | 3 | ## Part 2: the new build's controls ### Sessions (phase 1) - HttpOnly, SameSite=Strict session cookie. The session row on the server decides. - Password sessions: idle sign-out after 15 minutes, absolute 12 hours. PIN shop sessions: absolute 10 hours, idle 15 minutes. All are settings. - Idle and background lock are reported by the browser and enforced by the server. Unlock is a step-up check with the PIN or the domain password. - Origin check on every state-changing request. - Admin is a password session only. A PIN session cannot open Admin or change Account settings. - Roles are resolved from AD at sign-in, cached on the session, and re-resolved every 15 minutes and nightly. A disabled AD account is always refused. ### PIN (phase 1) - Optional, per user, set by the user in Account settings after a password sign-in. Setting or changing it needs the domain password in the same request. - 6 to 12 digits, hashed with Argon2id on the server. Never visible to Admin. Admin can clear a PIN (audited), not set one. - PIN sign-in uses the employee ID and works only on registered devices. Devices are registered by an Admin or a shop department Supervisor in a password session. - Five wrong PIN attempts in a row, across sign-in, unlock and signature, disable the PIN. Three wrong attempts at unlock or signature end the session. A guessed PIN never touches AD. ### Rate limits (phase 1) - Checked before any bind to AD, so the app can never lock a domain account out. - Defaults: 5 failed password sign-ins per account per 5 minutes, then a 10-minute cooldown. 30 failed attempts per client address per 5 minutes. Successful sign-ins never count. - The domain's own lockout (20 attempts in 5 minutes) stays the last line. - The client address comes from `X-Forwarded-For` only when the request comes from `npm_app_1` itself, re-resolved on change. Other containers on `npm_default` are not trusted. ### Policy (phase 2) - One policy in Python on the server. Every write goes: session, policy, transition, validation, version check, audit row, in one transaction. - Every packet read returns `can: [...]` and `blocked: {...}` so the browser enables controls from the server's answer. - A workflow step taken twice is refused with 409 and the current state. - Details in [POLICY.md](POLICY.md). ### Audit (phase 1) - Append-only `audit_events`, with a hash chain (`prev_hash`, `row_hash`). - The app role may insert and select, never update or delete. A trigger also refuses updates and deletes. Migrations run as a separate role. - `packetstation doctor` checks that an audit UPDATE fails. ### Seals (phase 7) - Filing writes an HMAC seal with the server seal key. A reopen by Admin needs a reason and keeps the old seal in history. - The integrity job checks seals and asset hashes (phase 9). ### Headers (phase 0) - The app sets HSTS, CSP and the other security headers itself. NPM's own HSTS is off. - The CSP allows nothing external. It is tuned again in phase 10. ### Secrets (phase 0) - Docker secrets only, files under `/etc/packetstation//secrets`, owner `root:10001`, mode `0440`. - Read through `*_PASSWORD_FILE` settings. MFilesKit's password is exported by the entrypoint from its secret file. - Never in the repo, `.env`, logs or chat. See [DEPLOY.md](DEPLOY.md). ## Part 3: requirement IDs The IDs come from Part 3 of the old SECURITY.md. The status follows the approved plan. | ID | Status | |---|---| | H1, H2 | Closed, phase 0 (docker-1, internal DNS, NPM) | | H3 | Closed, phase 0 (NPM TLS, HSTS from the app) | | H4 | Closed, phase 0 (two stacks) | | H5 | IT's (host patching); documented in DEPLOY.md | | I1 | Partly closed, phase 1 (AD sign-in, no SSO); Entra SSO deferred | | I2 | Deferred (MFA with Entra) | | I3 | Closed for "PIN on the server", phase 1; device certificate deferred | | I4 | Closed, phase 1 | | I5 | Superseded (no in-app admin grant); separate IT admin accounts remain IT's | | I6 | Largely closed, phase 9 (groups plus reconcile); quarterly review IT's | | I7 | Closed, phase 1 | | I8 | App part closed, phase 1; kiosk lock policy IT's (Intune or GPO) | | A1, A2, A3 | Closed, phase 2 | | A4 | Closed, phase 0 | | D1, D2, D4 | Deferred (envelope encryption, Key Vault) | | D3 | IT's (host disk encryption on docker-1) | | D5 | Closed, phase 4 (optional ClamAV) | | D6, D7 | Deferred (retention period and customer copy need a decision from Seth and Q.C.) | | M1 | Closed, phase 2 | | M2 | Closed, phase 7 | | M3 | Closed, phase 9 | | M4, M5 | Deferred (logs to stdout now; SIEM shipping is IT's) | | S1 | Closed, phase 0, tuned phase 10 | | S2 | Closed, phase 1 | | S3 | Closed, phase 9 (CI scanning) | | S4 | Deferred to IT (pentest before go-live) | | S5 | Closed, phase 2 | | S6, S7, S8 | Closed, phase 7 | | O1, O2 | Closed, phase 9 (pg_dump timer plus PBS; one rehearsed restore); quarterly restore IT's | | O3, O4 | Closed, phase 10 | | O5 | Superseded (no migration; M: retired read-only) | The brief's wording for the identity IDs, which the table above follows: - **I4 and I7** (PIN lockout, PIN signatures verified on the server) are closed by this build. - **I8** is closed for the app's part. The kiosk device lock policy in I8 is IT's, via Intune or GPO. - **I6** (joiner, mover, leaver) is largely carried by the AD groups plus the nightly reconcile. The quarterly access review is still IT's. - **I3** is closed for "PIN checked on the server" and open for "Intune-managed, device certificate". Registered devices use a device cookie for now. - **I1** is partly closed by AD sign-in. There is no SSO yet. - **I5** is superseded. No in-app admin grant exists, so second-admin approval has nothing to approve. Separate IT admin accounts with MFA remain IT's. Out of scope for this build, with hooks left: writing to VISUAL or M-Files (never), Entra ID sign-in and MFA (I1, I2), envelope encryption with Key Vault (D1, D2), true `.xlsx` exports, scheduled report emails.