Sigurnost i izolacija klinika
Izolaciju klinika provodi sam PostgreSQL: svaka tablica klinike prisilno primjenjuje sigurnost na razini redaka prema identifikatoru klinike postavljenom iz prijavljene sesije, nikad iz zahtjeva. Statičke provjere dokazuju pokrivenost svake tablice, a napadački test između klinika izvodi se u CI-ju pri svakoj promjeni.
Tehnička specifikacija vodi se na engleskom jeziku; naslovi i sažeci poglavlja prevedeni su na hrvatski.
Tenant isolation
- Authenticated sessionclinic id from server context
- Scoped transactionSET LOCAL clinic id
- FORCE ROW LEVEL SECURITYpolicy on every tenant table
- Explicit clinic predicatesjoint ownership checks
- Static lintsRLS coverage · scoped access
- Adversarial CI suitecross-tenant CRUD denied
Connections (2)
- Authenticated session → Scoped transaction
- Scoped transaction → FORCE ROW LEVEL SECURITY
ALTER TABLE <tenant_table> ENABLE ROW LEVEL SECURITY;
ALTER TABLE <tenant_table> FORCE ROW LEVEL SECURITY;
-- one policy per command
CREATE POLICY tenant_isolation_select ON <tenant_table> FOR SELECT USING (clinic_id = current_clinic_id());
CREATE POLICY tenant_isolation_insert ON <tenant_table> FOR INSERT WITH CHECK (clinic_id = current_clinic_id());
CREATE POLICY tenant_isolation_update ON <tenant_table> FOR UPDATE
USING (clinic_id = current_clinic_id()) WITH CHECK (clinic_id = current_clinic_id());
CREATE POLICY tenant_isolation_delete ON <tenant_table> FOR DELETE USING (clinic_id = current_clinic_id());- Clinic id comes from the server
- How it is enforced
- Derived from the authenticated session, never from request data (TENANT-01)
- Every transaction is scoped
- How it is enforced
withClinicScopesets the clinic id transaction-locally; cross-tenant work uses an explicit, audited platform scope
- Every table is covered
- How it is enforced
- The RLS-coverage lint fails on a tenant table without a forced policy; its exemption snapshot is empty
- Every route uses a scoped connection
- How it is enforced
- A lint rejects unscoped database access in routes; remaining service-level uses are frozen in a snapshot that may only shrink
- Isolation is attacked in CI
- How it is enforced
- An adversarial integration suite proves foreign-clinic SELECT, INSERT, UPDATE and DELETE are denied for every tenant table (TENANT-04)
- Objects are owned jointly
- How it is enforced
- Patient, encounter, session, artifact and fact ids are checked together; one valid id cannot unlock another patient’s object (TENANT-05)
- Exceptions are written down
- How it is enforced
- A tenant-isolation exception register, enforced by a test, lists every accepted exception
| Control | How it is enforced |
|---|---|
| Clinic id comes from the server | Derived from the authenticated session, never from request data (TENANT-01) |
| Every transaction is scoped | withClinicScope sets the clinic id transaction-locally; cross-tenant work uses an explicit, audited platform scope |
| Every table is covered | The RLS-coverage lint fails on a tenant table without a forced policy; its exemption snapshot is empty |
| Every route uses a scoped connection | A lint rejects unscoped database access in routes; remaining service-level uses are frozen in a snapshot that may only shrink |
| Isolation is attacked in CI | An adversarial integration suite proves foreign-clinic SELECT, INSERT, UPDATE and DELETE are denied for every tenant table (TENANT-04) |
| Objects are owned jointly | Patient, encounter, session, artifact and fact ids are checked together; one valid id cannot unlock another patient’s object (TENANT-05) |
| Exceptions are written down | A tenant-isolation exception register, enforced by a test, lists every accepted exception |
Identity, sessions and devices
- Access tokens are short-lived JWTs signed with PS256 by a key in Cloud KMS; public keys are published as JWKS and rotated with overlap.
- Refresh tokens are hashed, single-use and rotate on every use; replaying a used token revokes the whole token family.
- TOTP multi-factor authentication with backup codes; an idle-session policy shared by client and server.
- A lost device can be revoked by its owner from any other device, killing its token families and closing its sessions in one transaction.
- WebSockets authenticate with 60-second signed tickets and are re-checked against revocation every 15 seconds.
Perimeter and application hardening
- Edge
- Control
- Cloud Armor web application firewall and rate limiting; bot challenge on public forms
- Headers
- Control
- Helmet with a per-request CSP nonce;
no-storeon every route carrying clinical data
- Rate limits
- Control
- Per route class: authentication, data, AI per clinic, admin
- Uploads
- Control
- ClamAV scanning and magic-byte checks before any processing
- Input
- Control
- Zod schemas at every ingress; body limits of 1 MB by default
- Output
- Control
- PII scanning of AI replies; logs and errors carry correlation ids, never request bodies or clinical state (TENANT-11)
- Cron and internal calls
- Control
- OIDC-verified service identity on every scheduled endpoint
- Supply chain
- Control
- CodeQL and Semgrep in CI, a dependency audit gate, a software bill of materials per build
| Layer | Control |
|---|---|
| Edge | Cloud Armor web application firewall and rate limiting; bot challenge on public forms |
| Headers | Helmet with a per-request CSP nonce; no-store on every route carrying clinical data |
| Rate limits | Per route class: authentication, data, AI per clinic, admin |
| Uploads | ClamAV scanning and magic-byte checks before any processing |
| Input | Zod schemas at every ingress; body limits of 1 MB by default |
| Output | PII scanning of AI replies; logs and errors carry correlation ids, never request bodies or clinical state (TENANT-11) |
| Cron and internal calls | OIDC-verified service identity on every scheduled endpoint |
| Supply chain | CodeQL and Semgrep in CI, a dependency audit gate, a software bill of materials per build |
Threat model
THREAT_MODEL.md is a STRIDE catalogue over the system’s trust boundaries and data flows, extended for every surface added since — voice, imaging, the native apps, the Data Room, the local integration bridge — with a residual-risk register and penetration-test priorities. Imaging and the local bridge have their own threat models. A bug-bounty policy and a security contact are published, and a workflow tracks the breach-notification clock.
server/db/rls-migration.sqlserver/db/connection.tsscripts/lint-rls-coverage.tsscripts/lint-getdb-routes.tstests/integration/rlsCrossTenant.test.tsdocs/clinical-invariants/tenant-isolation.mdTHREAT_MODEL.mdSECURITY.md