Project ROUNDTABLE Docs

Security and compliance

Current controls mapped to implementation files and future boundaries

This page describes controls visible in the repository; it is not a certification or authorization decision. A beginner should read it as “where does the code try to protect data?” and “what is still future work?” The current pilot uses email/password authentication and a single-node in-memory rate limiter.

Control map

ControlImplementation locationWhat it does
Password hashingsrc/lib/auth.ts, prisma/seed.tsUses bcrypt comparison; seed hashes demo password with 12 rounds.
Session expirysrc/lib/auth.tsJWT strategy with 15-minute max age.
Authorization refreshsrc/lib/auth.tsRe-reads role, command, and organization on each JWT callback.
Audience gatessrc/lib/auth.tsrequireGovSession and requireIndustrySession.
Command scopesrc/lib/command-hierarchy.tsExact command scope and enterprise admin policy.
Role gatessrc/app/api/gov/_lib/helpers.tsAdmin/system-admin/writer checks and generic errors.
Input validationRoute route.ts filesZod schemas bound lengths, enums, dates, IDs, and formats.
Upload validationPortal submissions routeExtension, 15 MB size, and magic-byte checks.
Object encryptionsrc/lib/s3.tsRequests ServerSideEncryption per UPLOADS_S3_SSE, AES256 by default; the explicit value none omits it (local stores without KMS) and logs a warning.
Registration limitingsrc/lib/rate-limit.ts and register routeFive attempts per IP per hour in the pilot.
Password-change limitingsrc/lib/rate-limit.ts and password routeFive attempts per authenticated user per 15 minutes; excess attempts return 429 and audit password_change_rate_limited.
Generic client errorsRoute handlers/helpersAvoids returning exception details to clients.
AuditabilityAuditLog writes across auth/routes/libsRecords security and administrative events.
Operational loggingsrc/lib/logger.tsEmits one JSON stdout line per request with method, pathname only, status, and duration; never query strings, headers, or bodies. This operational log is separate from the database AuditLog compliance trail.
Export restrictionExport route handlersOnly GOV_ADMIN and GOV_SYSTEM_ADMIN.

Authentication and authorization

Credentials are lowercased and trimmed before lookup; bcrypt verifies the supplied password. A missing or bad credential returns no user and authentication failure is audited when an existing user is found. A signed-in user still needs an audience role, and a government user may still fail a narrower command or admin gate.

The system distinguishes enterprise visibility from hierarchy authority. GOV_ADMIN can see enterprise records but cannot reparent commands. GOV_SYSTEM_ADMIN can reorganize the command tree. GOV_COMMAND_LEAD is own-command only, with no downward inheritance.

Input and upload safety

Zod schemas reject oversized strings, unknown enum values, malformed URLs, invalid dates, weak passwords, and missing required fields. Upload validation does not trust the filename alone:

  • PDF must begin %PDF-.
  • DOCX/PPTX must begin the ZIP signature PK\x03\x04.
  • TXT must not contain null bytes in its first 8,192 bytes.
  • Every allowed file is at most 15 MB.

S3 objects are private and stored below uploads/; downloads require the owning industry organization. Server-side encryption is requested for object writes by default (AES256); a deployment that sets UPLOADS_S3_SSE=none — intended for local stores without KMS — writes objects without requesting it.

Audit events

Audit events provide durable accountability for authentication, authorization, data access, publication, routing, and administrative actions. AuditLog.detail stores JSON text for operational context such as counts and IDs. See the canonical AuditLog event inventory for the current labels.

Audit logging does not make data immutable in the database by itself; it creates a durable event trail. Database permissions, backups, retention, and operational monitoring remain deployment responsibilities.

Rate limiting

Registration allows five attempts per IP per hour. Authenticated password changes allow five attempts per user per 15 minutes, keyed to the user rather than the IP; excess attempts return 429 and create a password_change_rate_limited audit event.

rateLimit() uses an in-memory fixed window. It is appropriate for the single-node pilot and opportunistically removes expired buckets after the map exceeds 10,000 entries. Multiple app instances would each have their own map, so a scaled deployment should replace it with a shared store such as ElastiCache or another coordinated limiter.

Encryption and transport

The AWS runbook describes encrypted RDS, an SSE-S3 bucket, private subnets, and S3 gateway access. The application container itself relies on App Runner or a reverse proxy for HTTPS termination. The portable runbook requires TLS in front of the container and an HTTPS NEXTAUTH_URL.

Future compliance boundary

CAC (Common Access Card), FlankSpeed identity federation, and IL5 (Impact Level 5) hosting hardening are future work. The current repository does not prove CAC authentication, IL5 authorization, or a complete compliance package. Do not infer those properties from the presence of AWS, S3 encryption, or a government-themed seed dataset.

Review questions for a change

When reviewing a pull request, ask:

  1. Does every new route establish audience and role before reading the database?
  2. Does it scope URL IDs to the session instead of trusting the browser?
  3. Are all body, query, and multipart fields validated?
  4. Are errors generic to the client and detailed only in server logs?
  5. Does a security-sensitive mutation create an audit event?
  6. Does an upload validate bytes as well as extension and size?
  7. Does a new notification respect settings and avoid duplicate fan-out?
  8. Does an export have an explicit enterprise-admin gate?
  9. Does infrastructure keep secrets out of images and public buckets?
  10. Is the documentation clear about current behavior versus roadmap?

Known pilot limitations

The in-memory rate limiter does not coordinate across multiple instances. The current credentials provider does not establish CAC or federated identity. Audit logs do not by themselves enforce immutability or retention. S3 SSE-S3 and encrypted RDS are deployment controls, not proof of an authorization boundary. These limitations should be carried into threat modeling and operational runbooks.

Control verification walkthrough

For a release review, trace controls from request to persistence rather than relying on a checklist label:

  1. Enter a malformed title and confirm the Zod route returns 400.
  2. Sign in as an industry user and request a government mutation; confirm 403.
  3. Sign in as a command lead and request an unrelated command; confirm scope denial.
  4. Upload a renamed non-PDF file; confirm magic-byte validation rejects it.
  5. Upload a file over 15 MB; confirm the size limit rejects it before storage.
  6. Create a submission and inspect that filePath is a private key, not a public URL.
  7. Trigger a successful and failed login in a disposable database.
  8. Inspect AuditLog for authentication and authorization events.
  9. Disable a notification kind and verify a future event is suppressed.
  10. Attempt export as a command lead and confirm the enterprise gate.

These checks validate application behavior, not a complete authorization to operate. They should be paired with infrastructure review, dependency scanning, incident response, and records-management decisions.

Data minimization guidance

Use the smallest export and search result that answers the operational question. Keep downloaded CSV, JSON, and iCalendar files in controlled locations, avoid copying submission summaries into chat or issue descriptions, and delete temporary files according to the local handling policy. The app's generic error messages reduce accidental disclosure to clients, but operators can still disclose data through logs, screenshots, or manually copied records.