Security Guide - striae-org/striae GitHub Wiki
Overview
This guide documents security controls currently implemented in Striae.
It focuses on security boundaries, current behavior, and developer guardrails. Endpoint payloads and deep workflow internals are documented in specialized guides.
Table of Contents
- Security Model
- Authentication Controls
- Authorization and Permissions
- Worker and API Security
- Data Protection and Integrity
- Audit and Security Monitoring
- Configuration and Secret Handling
- Error Handling and Exposure
- Current Limitations and Hardening Priorities
- Developer Security Checklist
- Related Documentation
Security Model
Striae uses layered security boundaries:
- Identity boundary: Firebase Authentication controls user identity and MFA lifecycle.
- Authorization boundary: app-side permission utilities gate case/data operations.
- Gateway boundary: Cloudflare Pages Functions verify Firebase bearer tokens and enforce user-scope checks on
/api/*routes. - Service boundary: Cloudflare Workers enforce per-worker shared-secret or bearer headers from trusted proxy calls.
- Integrity boundary: exports/imports rely on hashes plus server-issued signatures.
- Accountability boundary: audit events persist to a dedicated audit storage path.
Authentication Controls
Firebase identity flow
- Email/password sign-in and registration are implemented in the auth route flow.
- Email verification is required before entering the main app experience.
- Failed auth attempts are mapped to safe user-facing error messages via centralized auth error handling.
Registration gateway
- New account registration can be restricted to an email allowlist via the
REGISTRATION_EMAILSPages secret. - The allowlist is populated from the
app/config/members.emailsfile (one entry per line) and deployed withnpm run deploy-members. - Each entry is either an exact email address or a domain wildcard (
@domain.com). - The
GET /api/auth/can-register?email=...Pages Function (functions/api/auth/can-register.ts) checks the allowlist before the client attempts Firebase account creation. It returns{ allowed: false }(HTTP 403) for non-allowlisted addresses, displaying the message: "Registration is limited to Striae membership. You may join at https://join.striae.org." - A second layer of defense is enforced in the user Pages proxy (
functions/api/user/[path](/striae-org/striae/wiki/path).ts):PUTrequests for accounts that do not yet exist in the user store are blocked ifREGISTRATION_EMAILSis set and the email is not on the list. Existing user profile updates are always allowed through. - Both layers fail open on network error; they are additive security controls, not availability-critical gates.
- When
REGISTRATION_EMAILSis unset or empty, registration is unrestricted (backward-compatible default). - The
REGISTRATION_EMAILSvalue is never exposed in the client bundle; it is a server-only Pages secret.
MFA controls
- MFA uses Firebase multi-factor APIs. Supported factors: SMS (phone) and TOTP (authenticator app).
- Login supports
auth/multi-factor-auth-requiredand resolves second-factor challenges throughMultiFactorResolver. - When multiple factors are enrolled, the login challenge UI allows the user to select which method to use.
- SMS path: reCAPTCHA challenge, Firebase
PhoneAuthProvider,PhoneMultiFactorGenerator.assertion. - TOTP path: 6-digit code entry,
TotpMultiFactorGenerator.assertionForSignIn— no reCAPTCHA required. - Users without enrolled factors are routed through mandatory MFA enrollment before normal app usage continues.
- Enrollment flow (
MFAEnrollment) presents a method choice: SMS or Authenticator App. - TOTP enrollment (
MfaTotpEnrollment): generates aTotpSecret, renders a QR code for authenticator app scanning (with optional base32 key fallback), and verifies a 6-digit code before callingmultiFactor(user).enroll(assertion, 'Authenticator App'). - MFA enrollment and verification failures log security-violation audit events for both SMS and TOTP paths.
- Enrolled factor management (listing, unenrolling, updating) is available in the user profile via
MfaEnrolledFactors,MfaTotpSection, andMfaPhoneUpdateSection.
Recent-login protection for sensitive MFA updates
- MFA phone updates and TOTP enrollment in profile flows enforce
auth/requires-recent-login. - Reauthentication supports password reauth and, when required, MFA resolver challenges (SMS or TOTP).
- If provider constraints block inline reauth, users are instructed to sign out and sign in again.
Inactivity timeout
- Inactivity settings are configured in
app/config/inactivity.ts(TIMEOUT_MINUTES=60,WARNING_MINUTES=5). - Timeout warning and sign-out are managed through the auth provider and inactivity hook.
- Current implementation scope: inactivity timeout is enabled only when an authenticated user is on routes starting with
/auth.
Authorization and Permissions
Centralized permission checks live in app/utils/permissions.ts and must be used before case operations. See the Utilities Guide — Permissions for full API detail and usage patterns.
Primary checks:
canCreateCase(user)canAccessCase(user, caseNumber)— grants access for owned and read-only shared casescanModifyCase(user, caseNumber)— requirespermitted=truefor owned cases; read-only case entries remain modifiable for review workflows
Data operations (app/utils/data-operations.ts) enforce session and access checks by default.
skipValidation exists in data-operation options for controlled internal flows. Use it only when equivalent validation has already been completed in the same call path.
Worker and API Security
Public API gateway model
- Browser clients call same-origin Pages APIs (
/api/user,/api/data,/api/image,/api/audit,/api/pdf). - Pages Functions verify Firebase identity tokens before forwarding requests.
- The image proxy requires Firebase bearer validation for
/api/image/*requests; GET requests carrying a?st=query parameter bypass Firebase verification at the Pages layer and are served directly via signed token validation in the Image Worker. - Pages Functions route to workers via Cloudflare Service Bindings; no shared-secret auth headers are added at the Pages proxy layer.
- Direct browser calls to worker endpoints are not part of the normal app transport path.
Worker authentication model
Workers are called exclusively via Cloudflare Service Bindings from Pages Functions. The service binding channel is the trust boundary; workers no longer validate shared-secret headers from the Pages proxy.
- Image Worker: validates HMAC
sttokens for signed-read flows viaIMAGE_SIGNED_URL_SECRET. - PDF Worker (outbound): Browser Rendering request uses
Authorization: Bearer {BROWSER_API_TOKEN}for/accounts/{ACCOUNT_ID}/browser-rendering/pdf.
Auth failure semantics
- Pages API returns
401when Firebase bearer auth is missing/invalid. - Pages API returns
403on user-scope violations. - Worker auth failures generally return
403. - Method mismatches return
405. - Upstream/proxy connectivity failures from Pages API return
502.
Public key distribution boundary
- No dedicated key-broker worker remains in the current architecture.
- The browser app only receives non-secret signing and export-encryption public keys through runtime config.
- Private signing, export-decryption, and data-at-rest keys remain in Pages/worker secret bindings and are never fetched by the browser.
Data Protection and Integrity
Data segregation
- User profile/permission data: Cloudflare KV (
USER_DB). - Case and annotation data: Cloudflare R2 data bucket (
STRIAE_DATA). - Audit entries: Cloudflare R2 audit bucket (
STRIAE_AUDIT). - File and image binaries: Cloudflare R2 files bucket (
STRIAE_FILES).
Encryption at rest
- User Worker stores
USER_DBrecords as encrypted envelopes (RSA-OAEP-AES-256-GCM) and decrypts only in trusted worker execution paths. - User Worker rejects plaintext/legacy unencrypted KV records and records encrypted with mismatched key ids.
- User Worker, Data Worker, Audit Worker, and Image Worker support key-registry fallback (
record keyId-> configuredactiveKeyId-> remaining registry keys) for rotation-safe decryption. - Data Worker writes case/annotation payloads with mandatory data-at-rest encryption envelopes.
- Audit Worker writes and reads audit payloads using the same mandatory encryption-at-rest model.
- Image Worker encrypts uploaded file/image blobs before writing to
STRIAE_FILESand decrypts only on authenticated reads or signed-token reads. - Encryption metadata (algorithm/version/key-id/IV/wrapped key) is stored as object metadata; plaintext is not stored in R2.
For detailed implementation, key registry patterns, key rotation, and per-worker encryption behavior, see Data-at-Rest Encryption.
PDF report handling boundary
- PDF Worker does not persist report payloads or generated PDFs in worker-managed storage.
- It renders in-memory HTML and forwards requests to Cloudflare Browser Rendering using scoped worker credentials.
- Browser-side image payloads are converted to embeddable data URLs before PDF generation so rendering does not depend on browser-local blob URLs.
Signed forensic artifacts
Server-side signing in the Data Worker protects export integrity:
POST /api/forensic/sign-manifestPOST /api/forensic/sign-confirmationPOST /api/forensic/sign-audit-export
Imported artifacts are validated with hash and signature checks in workflow code paths. Missing or invalid signature metadata is treated as a blocking validation failure in these secured import flows.
Encrypted export packages are handled separately through POST /api/forensic/decrypt-export, which unwraps and decrypts encrypted package contents for trusted Striae import workflows. See Export Encryption.
Encryption is mandatory for case package, confirmation package, and archive package workflows. These flows are fail-closed when encryption material is missing or package decryption fails.
Designated reviewer enforcement
- Original examiners may optionally embed a
designatedReviewerEmailin the case export metadata at export time. - Self-designation is blocked at the export UI level before the package is generated.
- If
designatedReviewerEmailis present, the importing user's account email must match exactly (case-insensitive) to proceed. - Import is blocked at the preview stage; the same check is re-enforced in the actual import path before any writes occur, so it cannot be bypassed by skipping preview or submitting a modified client request.
- Users whose account has no email address are also blocked when a designated reviewer is set.
Confirmation immutability guard
- Annotation updates are blocked when existing annotation data already contains
confirmationData. - This prevents post-confirmation mutation of confirmed image annotations through normal save paths.
Audit and Security Monitoring
Audit architecture
auditService.logEventstandardizes audit event creation and persistence.- Audit persistence is best effort and non-blocking for primary user workflows.
- Audit Worker endpoint contract is
/audit/?userId={userId}forGETandPOST.
Security-relevant audit coverage
Current code logs security-relevant events including:
- User login/logout, registration, and email verification lifecycle events.
- MFA enrollment/authentication outcomes for both SMS and TOTP factors.
- Failed authentication/MFA attempts (SMS and TOTP) as security violations.
- Case/file/annotation and export/import workflow events.
- Account deletion attempts and outcomes.
Retrieval behavior
- Worker-side filters:
userId(required), optionalstartDateandendDate. - Additional filters (action/result/case/workflow/offset/limit) are applied client-side in
audit.service.ts.
Configuration and Secret Handling
Worker secrets
- Secrets are configured via worker environment bindings and deployment scripts.
- Required secret values are defined by the relevant Pages/worker config and deployment scripts.
- User Worker requires
USER_KV_ENCRYPTION_PRIVATE_KEY,USER_KV_ENCRYPTION_PUBLIC_KEY, andUSER_KV_ENCRYPTION_KEY_IDto access profile records. - Worker domain values are treated as routing secrets for Pages proxy flows and worker fallbacks, not as Wrangler custom-domain replacement fields.
IMAGE_SIGNED_URL_SECRETis scoped to the Image Worker.
App runtime configuration
- The app runtime config (
app/config/config.json) includes the deployed app URL, manifest-signing public keys, export-encryption public keys, and review-limit settings used by browser workflows. - Treat runtime config delivery and deployment pipelines as security-sensitive.
- Rotate shared-secret values when compromised or during regular hardening cycles.
Development hygiene
- Do not commit real
.envsecrets. - Use centralized auth/data/permission utilities rather than ad-hoc worker calls.
- Avoid logging secrets or sensitive validation material.
Error Handling and Exposure
- Firebase auth errors are normalized to user-safe messages in
app/services/firebase/errors.ts. - Worker endpoints generally return generic error shapes, but some worker paths can include low-level error text.
- For externally exposed behavior, prefer sanitized error responses and keep sensitive diagnostics in secure logs only.
Current Limitations and Hardening Priorities
Current implementation considerations:
- Worker auth is shared-secret based, not end-user-token validation at each worker boundary.
- Cloudflare Access JWT enforcement is not enabled as part of the default deployment scripts.
- Request rate limiting/throttling is not implemented across worker APIs.
- Inactivity timeout is currently scoped to authenticated sessions on
/authroutes. - Runtime config values (for example signing public keys and review-limit settings) are public by design and should remain non-secret.
Developer Security Checklist
Before shipping security-sensitive changes:
- Confirm
canAccessCase/canModifyCase/canCreateCasechecks exist in the call path. - Avoid
skipValidationunless pre-validated in the same flow and documented. - Ensure worker calls handle
response.okand fail safely. - Add audit logging for security-sensitive operations and outcomes.
- Keep artifact signing/verification logic aligned with canonical payload rules.
- Keep mandatory package encryption/decryption flows aligned with the encryption workflow contract.
- Verify auth headers are intentional for new worker endpoints.
- Sanitize user-facing error messages.
- Do not log secrets or sensitive hash comparison values.