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
| Control | Implementation location | What it does |
|---|---|---|
| Password hashing | src/lib/auth.ts, prisma/seed.ts | Uses bcrypt comparison; seed hashes demo password with 12 rounds. |
| Session expiry | src/lib/auth.ts | JWT strategy with 15-minute max age. |
| Authorization refresh | src/lib/auth.ts | Re-reads role, command, and organization on each JWT callback. |
| Audience gates | src/lib/auth.ts | requireGovSession and requireIndustrySession. |
| Command scope | src/lib/command-hierarchy.ts | Exact command scope and enterprise admin policy. |
| Role gates | src/app/api/gov/_lib/helpers.ts | Admin/system-admin/writer checks and generic errors. |
| Input validation | Route route.ts files | Zod schemas bound lengths, enums, dates, IDs, and formats. |
| Upload validation | Portal submissions route | Extension, 15 MB size, and magic-byte checks. |
| Object encryption | src/lib/s3.ts | Requests ServerSideEncryption per UPLOADS_S3_SSE, AES256 by default; the explicit value none omits it (local stores without KMS) and logs a warning. |
| Registration limiting | src/lib/rate-limit.ts and register route | Five attempts per IP per hour in the pilot. |
| Password-change limiting | src/lib/rate-limit.ts and password route | Five attempts per authenticated user per 15 minutes; excess attempts return 429 and audit password_change_rate_limited. |
| Generic client errors | Route handlers/helpers | Avoids returning exception details to clients. |
| Auditability | AuditLog writes across auth/routes/libs | Records security and administrative events. |
| Operational logging | src/lib/logger.ts | Emits 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 restriction | Export route handlers | Only 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:
- Does every new route establish audience and role before reading the database?
- Does it scope URL IDs to the session instead of trusting the browser?
- Are all body, query, and multipart fields validated?
- Are errors generic to the client and detailed only in server logs?
- Does a security-sensitive mutation create an audit event?
- Does an upload validate bytes as well as extension and size?
- Does a new notification respect settings and avoid duplicate fan-out?
- Does an export have an explicit enterprise-admin gate?
- Does infrastructure keep secrets out of images and public buckets?
- 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:
- Enter a malformed title and confirm the Zod route returns 400.
- Sign in as an industry user and request a government mutation; confirm 403.
- Sign in as a command lead and request an unrelated command; confirm scope denial.
- Upload a renamed non-PDF file; confirm magic-byte validation rejects it.
- Upload a file over 15 MB; confirm the size limit rejects it before storage.
- Create a submission and inspect that
filePathis a private key, not a public URL. - Trigger a successful and failed login in a disposable database.
- Inspect
AuditLogfor authentication and authorization events. - Disable a notification kind and verify a future event is suppressed.
- 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.