Migration 0004 (VISUAL and drawing caches, go-live ignored list, resource map, job tables, order-derived columns with provenance; work order key includes the operation sequence). The real worker on APScheduler with settings re-read each fire, pause, run now, advisory locks, job_runs and a heartbeat. The visual-feed job (five VisualKit reads, COPY and MERGE cache, suspect-batch guard, the apply in one transaction: drafts, change orders, retire-moved, work order gone, closed and cancelled observed, projects and units from order links, auto-release) and the fingerprint. The drawing-lookup job (queue, budget, download on version change, sha256 into assets, decision table, staging for rev approval, drawing-origin change orders). Clerical From VISUAL panel, flags, rev approval, hand uploads, change-order review; Admin Feed settings and resource map. Synthetic fixture generator and a fixture-driven worker in CI with a Playwright scenario. Docs updated. Verified: 3249 server tests and 13 Playwright scenarios on the docker-1 CI stack, 76 frontend tests, doctor at 0004; one feed and one drawing run against VISUAL DEV and the vault from the developer machine (counts in the changelog). Co-Authored-By: Claude Fable 5.1 <[email protected]>
171 lines
16 KiB
Markdown
171 lines
16 KiB
Markdown
# 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 3. The identity controls (sessions, PIN, rate limits, audit, Origin check, derived group map) and the policy controls (server policy on every write, workflow steps as compare-and-set, server-side validation and limits, IT edit reasons, the filed lock, the upload pipeline for drawings and documents) are built and tested. Phase 3 adds the worker controls (read-only VISUAL and M-Files reads, stack guard, advisory locks, redacted errors, audited system writes). No requirement ID closes in phase 3. Every other control 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. Built. | 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: the `Origin` header (or the `Referer` origin) must match the request host, and `X-Requested-With: PacketStation` must be present. Otherwise 403.
|
|
- Admin is a password session only. A PIN session cannot open Admin or change Account settings.
|
|
- Roles are resolved from AD at sign-in (nested groups through `LDAP_MATCHING_RULE_IN_CHAIN`), cached on the session, and re-resolved every 15 minutes on the session and by the AD refresh. A disabled AD account is always refused. A session whose user has lost every role is ended at the next refresh.
|
|
- The session cookie token is stored only as an HMAC with the session key. The AD password is used for the bind and discarded.
|
|
- The group-to-role map is derived from the department table and the `PS-` naming convention at startup. No screen edits it, so nobody can promote a group or themselves inside the app.
|
|
- The browser reports hidden tabs and idle time; the server records `locked_since` and refuses a locked session with 423 until a step-up unlock. The server also locks a session itself when no request arrived for the lock interval (3 minutes, as the old build's screen lock), and when a tab stayed hidden for over a minute (the old build's background lock). A reload or an in-app navigation never locks.
|
|
|
|
### 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.
|
|
- A PIN sign-in still checks AD with the service account: a disabled account, or one removed from every group, is refused.
|
|
- The device cookie is a random token stored as an HMAC; revoking a device ends every PIN session on it.
|
|
|
|
### 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.
|
|
- A refused attempt answers 429 with `Retry-After`, is recorded as `rate_limited` and never counts toward the limits itself.
|
|
- Failed sign-ins answer one message for an unknown account, a wrong password and a disabled account, so accounts cannot be enumerated. The audit and `signin_attempts` keep the real reason.
|
|
|
|
### Policy (phase 2)
|
|
|
|
- One policy in Python on the server (`server/packetstation/policy/`), pure functions with no I/O. Every write goes: session, policy, transition, validation, version check, packet log row, audit row, NOTIFY, in one transaction with the packet row locked. Nothing in the browser decides.
|
|
- Every packet read returns `can: [...]`, `blocked: {...}` and `holder`, so the browser enables controls from the server's answer. The server re-checks on the write.
|
|
- A workflow step taken twice is refused with 409 and the current state (compare-and-set on the expected status and department). Child rows carry a row version checked with If-Match. Weld, heat tag, page and change order numbers are assigned by the server under the lock.
|
|
- Preconditions (heat map answered, drawing attached, BOM pulled or short, checklist complete, welds recorded, Q.C. items complete, closed in VISUAL before filing) run on the server and are never bypassed, Admin included.
|
|
- Admin edits outside custody need an IT reason of at least 8 characters, recorded in the packet log and the audit row (`it_reason`). An Admin cannot write a filed packet except through the reopen action, which needs a reason and a signature and breaks the seal.
|
|
- Signatures (PIN or domain password, verified in the same transaction) on submit, send to Q.C., Q.C. accept and reject, route, file, reopen, change order issue, approve, reject and verify, the revision approval and hand drawing uploads.
|
|
- Input: every string is cleaned (control and invisible characters removed, NFC, trimmed) and cut to its field limit; heat numbers, stamps, project numbers and lots are checked against the old patterns; JSON bodies over 200 KB are refused.
|
|
- Uploads: typed by content (PDF, PNG, JPEG), 25 MB limit read in chunks, sha256, stored by id under the assets volume, served only with a session and with `Content-Disposition` and `X-Content-Type-Options: nosniff`.
|
|
- The filed lock: a filed packet accepts only the data book date and the Admin reopen. Filing writes an HMAC-SHA256 seal with the server seal key; a reopen keeps the old seal with who broke it and why.
|
|
- Details in [POLICY.md](POLICY.md).
|
|
|
|
### Worker (phase 3)
|
|
|
|
- The worker publishes no port and joins no proxy network. It reaches out to VISUAL and M-Files only; nothing reaches in. Admin and Clerical reach it only through rows in PostgreSQL (`job_requests`, the `feed` settings row) and NOTIFY.
|
|
- It reads VISUAL through VisualKit as `visualkit_ro` and M-Files through MFilesKit as `mfileskitRO`. Both libraries are read-only in their own code. Nothing in Packet Station writes to either.
|
|
- Stack guard at start and again before every feed run: the test stack refuses a production VISUAL target, production refuses an implicit one, and production refuses a fixture source.
|
|
- One run per job at a time, by a PostgreSQL advisory lock per job name. Feed writes run in one transaction with a lock timeout and skip rows a person has locked.
|
|
- Job errors are redacted (connection strings, `password=`, `Authorization:` and the VISUAL and M-Files passwords themselves) and cut to 20 entries of 500 characters before they are stored or shown.
|
|
- Every feed write goes through the server policy as the system principal, which can take only the `vis_*` actions. Each one writes a packet log row and an audit row with role `system`. Each run writes a `feed.run` audit row with its counts, and each drawing fetch a `drawing.fetched` row with the sha256. Feed settings and resource map changes are audited with before and after.
|
|
- Downloaded drawings go through the same checks as uploads: PDF magic, size limit, streamed sha256, stored by id.
|
|
|
|
### 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.
|
|
- Every identity event is a row: sign-in, failed sign-in with a reason, sign-out, lock, unlock, session ended, PIN set, disabled or cleared, device registered or revoked, signature, AD refresh. Each carries the user, session, roles, client address and a small detail object, never a secret.
|
|
- `signin_attempts` and `signatures` are append-only in the same way.
|
|
|
|
### CI identity fixtures (phase 1)
|
|
|
|
- CI runs a throwaway Samba Active Directory domain controller (`PSTEST.LOCAL`) built from `deploy/samba/`, with a generated CA so the LDAPS path is real. It is seeded with the 50 groups (one missing and one extra on purpose) and test users for every role, including nested, disabled, multi-role, missing and duplicate employee ID cases. Passwords are random per run and exported to a volume the tests read. PESCO's AD is never used by CI.
|
|
|
|
### 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/<stack>/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 (server policy on every call, workflow steps as compare-and-set, validation and limits on the server) |
|
|
| 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 (append-only audit and packet log written only by the server; every packet write carries both) |
|
|
| 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 (React escapes output; server messages are plain text; PDF export escaping comes with phase 7) |
|
|
| 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.
|