Security Policy
MolTrace security controls protect regulated scientific data, raw evidence archives, analysis outputs, user identities, audit records, and customer workspaces.
Security program
Section titled “Security program”This page is the procurement-facing security template. Certification and control claims must be verified by the security owner before they are used in sales, contracts, or public trust materials.
Infrastructure and hosting
Section titled “Infrastructure and hosting”| Control area | Position |
|---|---|
| Cloud provider | Google Cloud. The backend runs on Cloud Run (service moltrace-backend, project moltrace-prod, scale-to-zero); the frontend is hosted on Vercel (moltrace.co) and reaches the API through a same-origin /api/backend proxy. Render is fully retired. (2026-07) |
| Regions | us-central1 for the Cloud Run service, the database, and the storage buckets. Single-region with no tenant region pinning — an additional or customer-selected region is a roadmap item, not something that can be contracted today. See Privacy Policy → Data residency. (2026-07) |
| Data stores | Cloud SQL for PostgreSQL 16 on a private IP with no public interface, reached from Cloud Run over Direct VPC egress. Cloud Storage buckets hold the immutable raw-FID vault (moltrace-raw-vault, versioned), the model weights, and the DVC remote. (2026-07) |
| Key and secret custody | All credentials resolve from Secret Manager; the field-encryption key-encryption key lives in Cloud KMS. Images are stored in Artifact Registry and built by Cloud Build. (2026-07) |
| Deploy identity | CI/CD deploys the backend keylessly via Workload Identity Federation — no stored service-account key — behind the fail-closed release gate described in Supply-chain integrity. (2026-07) |
| Workspace isolation | Organization-scoped projects, files, reports, approvals, and audit events. |
| Backups | Encrypted backups with tested restore procedures and documented retention windows. |
Write-once raw-evidence vault on serverless
Section titled “Write-once raw-evidence vault on serverless”Cloud Run’s filesystem is ephemeral, so the write-once ALCOA+ raw-FID vault has a Cloud Storage storage backend: writes are create-only (an if_generation_match=0 precondition, so an object can never be silently overwritten), the SHA-256 digest is verified both on reuse of an existing object and after every write, and object versioning plus the create-only precondition are the write-once mechanism today. Bucket retention / bucket lock is not configured — see Open findings from the infrastructure migration. The backend is selected with RAW_VAULT_BACKEND=gcs and RAW_VAULT_BUCKET; the local filesystem backend remains the default for self-hosted and development deployments. (2026-07)
Encryption
Section titled “Encryption”| Data state | Draft control |
|---|---|
| In transit | TLS 1.3 minimum at the edge (deployment runbook in Deployment & Hosting); HSTS with includeSubDomains + preload over HTTPS. |
| At rest | AES-256 or equivalent managed encryption for object storage, databases, and backups. |
| Field-level (sensitive secrets) | AES-256-GCM envelope encryption — a fresh per-field data key (DEK) wrapped by a key-encryption key (KEK) from a pluggable provider; the ciphertext envelope carries the algorithm + KEK key id so the KEK rotates without re-encrypting data, and customer-managed keys (BYOK) drop in behind one seam. (v0.46.0, 2026-06-15) |
| Passwords | Argon2id memory-hard KDF (per-user unique salt + optional KMS-held pepper). Pre-existing PBKDF2 hashes still verify and are transparently re-hashed to Argon2id on the next successful login (crypto-agility, no forced reset). (v0.45.0, 2026-06-15) |
| Secrets | Resolved through a single secrets provider seam (env-backed today, with a documented swap to a managed store — Vault, AWS / GCP Secrets Manager — behind one interface), and gated by a CI secret-scanning gate (gitleaks over full history; the build blocks on any committed credential). See Secrets management. |
Access controls
Section titled “Access controls”Access follows least privilege. Administrative actions, exports, approvals, and regulated workflow changes are logged and reviewable. The platform fails closed: every backend route requires an authenticated principal by default — a forgotten gate on a new endpoint surfaces as 401, not as a public hole.
Authorization model — policy-as-code
Section titled “Authorization model — policy-as-code”Authorization is decided by a single embedded policy-decision point (authz.authorize) — pure in-process logic, no sidecar to deploy. The engine is deny-by-default with forbid-overrides-permit semantics: a system api key and an admin are unrestricted; a user reads or writes only resources they own; non-owner reads return a non-leaking 404 (an unowned resource is indistinguishable from a missing one), and privilege gates render 403. A router-level baseline applies an authenticated-principal requirement to every main-router route; new endpoints inherit it automatically and a small pinned allow-list governs the public routes. (v0.44.0, 2026-06-14)
Enterprise single sign-on (SSO)
Section titled “Enterprise single sign-on (SSO)”Per-organization OpenID Connect federation: Authorization Code with PKCE (S256), JWKS-validated id_token (signature + issuer + audience + expiry + nonce), and a just-in-time user + team-membership provisioning step gated by each connection’s allow-listed email domains. The callback never returns a bearer in a browser redirect — it stamps a single-use exchange code that the SPA trades for an opaque session over a normal POST, and the redirect URI is computed server-side from settings (never client-supplied) to foreclose open-redirect / token-theft. An optional enforce-SSO mode rejects password login for any email under a governed connection (403). Client secrets are AES-256-GCM authenticated-encrypted at rest. (v0.40.0, 2026-06-13)
Automated provisioning (SCIM 2.0)
Section titled “Automated provisioning (SCIM 2.0)”An IdP can auto-provision and auto-deprovision users via SCIM 2.0 bolted onto each SSO connection (Okta, Microsoft Entra ID). Bearer-token authentication is per connection (SHA-256 digest stored, one live token per connection, plaintext returned exactly once on issue) and resolves the request to exactly one connection → one organization — the sole tenant key on every SCIM call. The connection’s email_domains allow-list is fail-closed: a connection may only provision or link an email whose domain is on its list, so a connection can’t cross-tenant a user belonging to another organization. Deprovisioning is always soft — active:false and SCIM DELETE flip the SCIM mapping + this org’s membership + revoke sessions, but no user or audit row is ever physically deleted (preserving 21 CFR Part 11 / GxP traceability); the global account is only disabled when no other org still has an active membership. Users are written with the canonical least-privilege viewer role and the canonical disabled status. (v0.41.0, 2026-06-13)
Multi-factor authentication (MFA)
Section titled “Multi-factor authentication (MFA)”Two factor types: TOTP (RFC 6238 over pyotp, with ±1-step drift window, replay-guarded by a recorded last_used_step, and AES-256-GCM at-rest secret encryption keyed separately from SSO) and phishing-resistant WebAuthn/FIDO2 passkeys (py_webauthn, server-pinned RP-ID and origin, user-verification required, single-use TTL-bounded challenges, and sign-count clone detection). One-time recovery codes are issued on the user’s first confirmed factor of any type (passkey-first users included). Enrollment, login-verify, and the credential-management surface are exposed under /auth/mfa/*.
The MFA-pending challenge lives in a separate mfa_login_challenges table that is invisible to the bearer resolver, so “no MFA, no bearer” is structural rather than checked. A wrong code can’t be retried — the pending token is consumed in its own committed transaction before the factor is verified (single-attempt). Per-organization MFA enforcement is wired through the access-context check, so a user whose org requires MFA past grace is blocked on product routes (with 403 mfa_required / mfa_enrollment_required) until MFA is proven; this applies to every session including SSO, so federation can’t bypass an MFA-required tenant’s policy. (v0.42.0, 2026-06-14)
Step-up re-authentication
Section titled “Step-up re-authentication”Sensitive operations — electronic-signature creation and admin actions — require a fresh step-up re-authentication (21 CFR Part 11 §11.200 contemporaneous re-auth). The step-up factor and Authenticator Assurance Level are captured on the session and persisted onto the signature and the audit event; the system api-key path remains the audited break-glass. (v0.42.0, 2026-06-14)
Privileged production access
Section titled “Privileged production access”- Role-based access controls.
- Just-in-time approval for privileged production access.
- Audit logging for security-relevant administrative actions.
Session management
Section titled “Session management”Each successful login mints a short-lived access bearer plus a long-lived, rotating, single-use refresh token, grouped into a login family (one family = one user agent / login chain). A normal refresh rotates the pair atomically; presenting a spent refresh is reuse and revokes the entire family (OWASP / RFC 9700). The session model carries idle + absolute timeouts, optional device binding (fingerprint pinned at mint, checked on rotation), and the MFA / step-up state is carried forward on rotation (a session doesn’t downgrade by refreshing).
Revocation is immediate — the access bearer dies on the next request, not after a TTL window: a family-revoked check is on the hot resolver path, so a logout, a password reset, or a refresh/revoke cuts a held bearer immediately. revoke_all_user_tokens is family-aware, so a global revoke (password reset) can’t be undone by a held refresh. The refresh-rotation contract is exposed at POST /auth/refresh and POST /auth/refresh/revoke; the SPA receives a typed machine-code on a 401 (token_invalid / token_expired / token_reuse_detected) so it can take the right action. Tokens are opaque, SHA-256-at-rest. (v0.43.0, 2026-06-14)
API abuse protection
Section titled “API abuse protection”An in-app token-bucket rate limiter (rate_limit.enforce) is wired into the router gates, so routes inherit it rather than opting in: the main router’s _baseline_access_gate reuses the already-resolved principal (no duplicate token decode), and rate-limit-only gates cover the SCIM and nmr2d routers so privileged provisioning and the heavy 2D-analysis routes are throttled too. Buckets are keyed system-key / admin → unlimited | user:{id}:{route} | ip:{client_ip}:{route} — the per-user key is the per-tenant key today. Unauthenticated auth endpoints carry tight limits (login 10/min; sign-up and password reset 5/min), everything else a generous default. A throttled request returns 429 with Retry-After and X-RateLimit-* (CORS-exposed) and emits a de-duplicated SecurityEvent(event_type="rate_limit"). The in-process bucket map is bounded (idle-then-LRU eviction) so key rotation cannot exhaust memory, and the store sits behind a RateLimitStore protocol so Redis drops in without call-site changes. (v0.55.0, 2026-06-21)
- Request-body size guard — non-multipart bodies over
MAX_REQUEST_BODY_BYTESare rejected with 413; multipart uploads are exempt because they carry their own raw-archive caps. (v0.55.0, 2026-06-21) - Settings, default-off —
RATE_LIMIT_ENABLED=falseandMAX_REQUEST_BODY_BYTES=0ship as the defaults so local development and the test suite are unaffected; production turns them on through its deployment environment.RATE_LIMIT_DEFAULT_PER_MINUTE,RATE_LIMIT_BURST_MULTIPLIER, andRATE_LIMIT_TRUST_FORWARDED_FORtune behaviour — the last is what makes IP keying correct when the service sits behind the edge proxy. Enforcement is fail-open: a limiter error can never 500 a request. (v0.55.0, 2026-06-21) - WAF — honestly scoped as an edge runbook (
docs/security/waf_edge_runbook.md) rather than faked in-app: managed rule sets, bot mitigation, and L7 controls are configured at the CDN / edge tier, while the rate limiter is the testable in-repo enforcement layer. An ASVS-aligned OWASP API Top-10 coverage map (docs/security/owasp_api_top10_p16.md) records which control answers which risk. (v0.55.0, 2026-06-21)
Rate limiting across multiple Cloud Run instances
Section titled “Rate limiting across multiple Cloud Run instances”Finding MT-VULN-2026-001 (Medium, open). The limiter’s buckets live in process, so the guarantee is per-instance, not per-service. Production runs Cloud Run with --max-instances 2, which has two consequences worth stating plainly:
- The effective limit can be up to 2× the configured value, because each instance keeps its own bucket map.
- Bucket state is lost on scale-to-zero, so counters reset when the service idles down.
The earlier documentation asserted single-worker consistency. That was true on the retired Render deployment and became false at the migration. Closing the finding means backing the existing RateLimitStore protocol with Redis so the buckets are shared — the seam already exists, so no call sites change. Until then, treat the limiter as abuse mitigation rather than a precise quota. (v0.62.1, 2026-06-26)
Secrets management
Section titled “Secrets management”Two layers, both fail-closed:
- CI / pre-commit secret-scanning gate. A standalone GitHub Actions workflow runs gitleaks v8.30.1 (pinned binary + verified SHA-256) over the full git history on every push and pull request, decoupled from the test workflow so a test failure can’t skip it; the build blocks on any finding. The same configuration + version runs locally as a pre-commit hook. A tight allowlist whitelists audit-confirmed dev placeholders / test fixtures / templates only — never a live secret.
- Secrets-provider seam. A single in-process read-point (
secrets_provider.resolve_secret/resolve_secret_strict) is the source of truth for every credential-class config value —DATABASE_URL,REDIS_URL,API_KEY, the SSO / MFA encryption keys, the password pepper. The env-backed default is byte-for-byte identical to the prioros.getenvreads; the documented swap path is a managed store (Vault, AWS / GCP Secrets Manager) and short-lived dynamic DB credentials behind the same interface — no call site changes. (v0.47.0, 2026-06-16)
A full-history audit confirmed zero real committed secrets before the gate landed.
In the Google Cloud deployment the platform side of that seam is filled in: every credential the service reads is sourced from Secret Manager rather than stored in the service configuration, and the field-encryption key-encryption key is held in Cloud KMS. (2026-07)
Secure development lifecycle
Section titled “Secure development lifecycle”Security scanning runs as CI gates in a standalone security-scan.yml workflow, deliberately decoupled from the test workflow (no needs: coupling) so a failing test can never skip a security gate — the same design as the gitleaks secret-scanning workflow:
- SAST — Semgrep over
p/python,p/javascript,p/typescript,p/owasp-top-ten, andp/react. - SCA — Trivy filesystem scan (vulnerabilities and licenses) over
uv.lockandpnpm-lock.yaml. - IaC — Trivy config scan over the deployment blueprints and the workflow files themselves.
Severity policy: CRITICAL findings (and Semgrep ERROR-severity findings) block — each job runs a gate pass that exits non-zero. HIGH / MEDIUM / LOW are uploaded as SARIF to the GitHub Security → Code scanning tab and tracked to closure under documented triage SLAs (critical 7 days, high 30 days, medium 90 days), so a pre-existing lower-severity advisory does not red-line the build while still being owned. All three jobs run on push, pull request, and manual dispatch; registering them as required status checks in main branch protection is what converts them from advisory to merge-blocking, and deploy only fires on a green push to main. The gate suite, severity policy, findings-to-closure flow, and branch-protection wiring are documented in docs/security_sdlc_gates.md. (v0.53.0, 2026-06-20)
Honest scope: DAST against ephemeral preview deployments is deferred — there is no isolated preview environment to scan — and the seam is documented for when a staging environment exists. Container-image scanning is not in place at all: these gates cover dependencies and Dockerfile misconfiguration, never the built image (MT-VULN-2026-002, open — see Open findings from the infrastructure migration). (v0.53.0, 2026-06-20)
Supply-chain integrity
Section titled “Supply-chain integrity”- SBOM per build. The
sbom-backendjob exports CycloneDX 1.5 fromuv.lock(uv export --format cyclonedx1.5); thesbom-frontendjob generates CycloneDX 1.7 frompnpm-lock.yaml(pnpm sbom --sbom-format cyclonedx). Both upload as per-run artifacts. (v0.54.0, 2026-06-21) - Signed build provenance. The
attestjob (push-to-mainonly) mints SLSAprovenance/v1over both SBOMs viaactions/attest-build-provenance@v4, fully keyless — Actions OIDC → Sigstore / Fulcio / Rekor, with no stored signing key and no external account. Attestations persist to the repository attestation store and are queryable per release throughgh attestation verifyor the attestations API. (v0.54.0, 2026-06-21) - Verify-at-deploy gate. A separate
verify-provenancejob downloads the exact attested SBOM artifacts from the same run and runsgh attestation verify … --signer-workflow ….deploydeclaresneeds: verify-provenance, so a verification failure blocks every deploy step. It is a gating job rather than a step insidedeployprecisely because the deploy steps useif: always()— only an unmetneeds:stops them all. (v0.54.0, 2026-06-21)
Honest boundary: the hosting platforms rebuild from source after the gate, outside CI’s signing boundary, so the attestation covers the source plus dependency closure at the gated commit, not the platform-served artifact. The gate is only effective with platform auto-deploy disabled (it is). Details in docs/supply_chain_provenance.md. (v0.54.0, 2026-06-21)
Zero-trust CI hardening and IaC posture
Section titled “Zero-trust CI hardening and IaC posture”- SHA-pinned actions and least-privilege tokens. Every GitHub Actions
uses:is pinned to a 40-character commit SHA (9 distinct actions acrossci-cd.yml,security-scan.yml, andsecret-scan.yml, with the tag kept in a trailing comment for Dependabot), so a hijacked upstream tag can no longer flow into CI.ci-cd.ymlcarries a least-privilege defaultpermissions: { contents: read }so its jobs — includingdeploy— stop inheriting the repository-default token scope, whileattestandverify-provenancekeep their narrowly scoped-up permissions. There is nopull_request_targetanywhere, anddeploy/attestare gated topush→main. (v0.57.0, 2026-06-26) - CSPM-lite posture scoring with a drift gate.
infra/cspm/score_iac_posture.pydiffs the current HIGH/CRITICAL misconfiguration set against a committed baseline (infra/cspm/iac_posture_baseline.json, currently empty — a clean posture) and fails CI on any new misconfiguration that has not already been accepted. Accepting one is a deliberate--updatewith a written justification, mirroring the.trivyignoreVEX register. This layers a continuously scored, drift-alerting signal on top of the CRITICAL-blocking Trivyiacjob. (v0.57.0, 2026-06-26) - Shared-responsibility posture.
docs/security/zero_trust_infra.mdmaps what is enforced in-repo against what the managed platforms and operations own — network segmentation, cloud IAM, host hardening, runtime protection — and records the open operational items: cloud-account CSPM with safe auto-remediation, a runtime-protection agent, and branch-protection required checks. (v0.57.0, 2026-06-26)
On the current Google Cloud footprint several of those platform-side controls are concrete: the database has no public interface (private-IP Cloud SQL reached over Direct VPC egress), every credential resolves from Secret Manager, the field-encryption KEK lives in Cloud KMS, and CI holds no long-lived service-account key because deploys authenticate through Workload Identity Federation. (2026-07)
Audit trail integrity (tamper-evident chain)
Section titled “Audit trail integrity (tamper-evident chain)”The append-only audit log is a tamper-evident hash chain: every row stores prev_hash + entry_hash over a canonical UTC-normalized serialization of its fields, so any insert, edit, delete, or reorder breaks recomputation. Chaining is enforced by a single SQLAlchemy before_flush listener, so all backend write sites are covered with no per-site code change.
Periodic HMAC-signed checkpoints (audit_checkpoints) anchor the chain — a wholesale history rewrite is infeasible without the signing key — and a signed high-water mark on a singleton row catches the otherwise-undetectable case of truncating the most-recent unanchored rows: the live MAX(chain_seq) can be lowered freely, but the signed head can’t be lowered without the key, so the gap is visible. A UNIQUE(chain_seq) constraint is the fork backstop (a raced append fails + rolls back rather than forking).
Admins can verify and anchor on demand:
GET /admin/audit/verify— full O(n) chain walk + anchor re-verify; returns chain length, anchor count, and the first break (row id + reason) if any.POST /admin/audit/anchor— record a new HMAC-signed checkpoint over the current chain head.
A scheduled reconciliation job runs the same verify and alerts + records a chained security.audit_chain.break event on any failure, and an O(1) audit_chain_check is wired into /system/status so tip tampering, anchor forgery, tail truncation, and a recorded break surface on the health endpoint. Converts “we don’t delete” into “you can prove we didn’t.” (v0.49.0, 2026-06-18)
Security monitoring and detections
Section titled “Security monitoring and detections”The immutable SecurityEvent stream and the tamper-evident audit chain feed a detection engine (detections.run_detections) built from four pure rules:
impossible_travel— the same actor produces twologin_successevents from different IPs inside a window (an IP-velocity heuristic; geo enrichment is a documented seam).privilege_escalation— anis_adminflip.cross_tenant_access—cross_tenant_deniedevents at or above a threshold inside a window, i.e. enumeration probing.audit_chain_break— ledger verification fails.
Alerts ship through a pluggable SIEM sink: a JsonStdoutSink always writes structured {"siem_alert": …} records to stdout for the platform log drain to forward into any SIEM, and a WebhookSink POSTs best-effort when SECURITY_ALERT_WEBHOOK_URL is set. Only error/critical alerts ship. Emission is wired at the real seams — login_success plus client IP at the password-login routes, cross_tenant_denied at the owner-scoped deny branches, privilege_escalation at the login admin-email auto-grant sites — gated by SECURITY_SIEM_ENABLED (default on) and best-effort, so a telemetry failure can never break the instrumented request. Admins scan and ship on demand through GET /admin/security/alerts (read-only scan view) and POST /admin/security/detections/run (scan + ship — the cron hook), both require_admin behind the default-deny gate. Honest boundary (docs/security/siem_detections.md): a hosted SIEM and a 24/7 on-call rotation are operational; the detection rules, the sink seam, the scan endpoints, and the end-to-end scenario tests are what live in the repository. (v0.58.0, 2026-06-26)
Vulnerability management
Section titled “Vulnerability management”| Activity | Cadence or SLA |
|---|---|
| Dependency and infrastructure scans | Weekly. |
| Secret-scanning (gitleaks full history) | Every push + pull request; pre-commit. |
| SAST / SCA / IaC CI gates (Semgrep + Trivy) | Every push, pull request, and manual dispatch; CRITICAL blocks. (v0.53.0, 2026-06-20) |
| IaC posture drift | Scored on every CI run against a committed baseline; any new HIGH/CRITICAL misconfiguration fails the build. (v0.57.0, 2026-06-26) |
| Finding triage-to-closure SLA (scanner and externally reported) | Critical 7 days, high 30 days, medium 90 days. (v0.53.0, 2026-06-20) |
| External penetration test | Annually and before each major release, under documented rules of engagement. (v0.56.0, 2026-06-25) |
| Critical CVE remediation target | 24 hours after validated impact assessment. |
| High CVE remediation target | 7 days after validated impact assessment. |
One severity rubric (CVSS v3.1) governs both scanner-found and externally reported issues, and every finding lands in the in-repo security findings register with its SLA date, status, and remediation evidence. The CVE remediation targets in the last two rows remain the draft procurement position and must be reconciled with the implemented triage SLAs (docs/security_sdlc_gates.md) by the security owner before they are quoted externally.
Open findings from the infrastructure migration
Section titled “Open findings from the infrastructure migration”The move from Render to Google Cloud invalidated three control claims the documentation had made. They are the first entries in the findings register, and all three are open:
| ID | Severity | Status | What is actually true |
|---|---|---|---|
MT-VULN-2026-001 | Medium | Open | The in-app rate limiter is per-instance. With --max-instances 2 the effective limit can be 2× the configured value, and bucket state is lost on scale-to-zero. Closing it needs the existing RateLimitStore (Redis) seam. See Rate limiting across multiple Cloud Run instances. |
MT-VULN-2026-002 | Medium | Open | Container-image vulnerability scanning is missing. CI scans dependencies and Dockerfile misconfiguration, never the built image. The old “N/A — no Dockerfile” record was true on buildpacks and became false once an image was built. See Deployment → Container-image vulnerability scanning gap. |
MT-VULN-2026-003 | Medium | Open | The documented ≤ 5 min RPO is not met: Cloud SQL PITR is not enabled by the deploy runbook and the instance is zonal, so real database RPO is the ≤ 24 h daily-backup window. The storage vault has versioning but no retention / bucket lock. See Deployment → Where the stated RPO is not met today. |
Listing them here is deliberate. The register exists to show what is open — with severity, status, and the medium 90-day triage SLA above — not only what has been closed.
Documentation accuracy sweep after the Google Cloud migration
Section titled “Documentation accuracy sweep after the Google Cloud migration”The in-repo security documentation (docs/security/) was originally written against the retired Render deployment, because the design passes trusted a stale render.yaml. Every security document was swept and corrected against the real Google Cloud footprint, and the three findings above were opened rather than quietly patched.
Corrections also ran the other way. The migration improved two postures the documentation had understated:
- Private networking is genuinely implemented. Cloud SQL runs on a private IP with no public interface, reached over Direct VPC egress.
- Deploy authority is keyless. Backend deploys authenticate through Workload Identity Federation rather than stored deploy-hook secrets.
(v0.62.1, 2026-06-26)
Vulnerability disclosure and penetration testing
Section titled “Vulnerability disclosure and penetration testing”GET /.well-known/security.txt— a public, unauthenticatedtext/plainRFC 9116 disclosure file.Expiresis computed at request time (clamped into the one-year window) so a served file is never stale, and the optionalPolicy/Canonical/Encryption/Acknowledgmentsfields are emitted only when configured, so the file never advertises a page that 404s or a paid bounty the program does not run. Everything is settings-driven underSECURITY_TXT_*(SECURITY_TXT_ENABLED, default on → 404 when disabled;SECURITY_TXT_CONTACTS;SECURITY_TXT_EXPIRES_DAYS; the optional URLs;SECURITY_TXT_PREFERRED_LANGUAGES), values are CR/LF-sanitized so an operator-supplied env value cannot inject a forged field line, and the path is onPUBLIC_ROUTE_PATHSso it is covered by the same default-deny gate and IP-keyed rate limiter as/health. (v0.56.0, 2026-06-25)- Vulnerability Disclosure Policy (
docs/security/vulnerability_disclosure_policy.md) — in-scope and out-of-scope surfaces, safe harbor for good-faith research, a CVSS v3.1 severity rubric, response and remediation SLAs (the same rubric as the CI gates), and coordinated-disclosure terms. Honestly scoped: coordinated disclosure with credit, not a paid bounty. (v0.56.0, 2026-06-25) - Pen-test program runbook (
docs/security/pentest_program.md) — the annual plus pre-major-release cadence, rules of engagement, and the intake → triage → register → remediate → verify → disclose findings pipeline. (v0.56.0, 2026-06-25) - Threat model (
docs/security/threat_model.md) — STRIDE per surface across auth/session, SSO/SCIM, MFA/step-up, e-signature, dossier RBAC, rate limiting, the raw vault, the audit ledger, the supply chain, share links, and admin routes, with residual-risk callouts, plus a new-surface threat-model checklist authors run before shipping an externally reachable surface. (v0.56.0, 2026-06-25) - Security findings register (
docs/security/security_findings_register.md) — the cross-source roll-up tying each finding to CVSS severity, SLA dates, status, and remediation evidence; it cross-links the GitHub code-scanning tab and the.trivyignoreVEX register. (v0.56.0, 2026-06-25)
Honest boundary: engaging a pen-test firm and launching a bounty platform are operational steps outside the repository. What ships in-repo is the policy, the safe harbor, the machine-readable intake, the threat model, and the findings process they feed. (v0.56.0, 2026-06-25)
Certifications and assurance
Section titled “Certifications and assurance”Use only verified status labels here:
- SOC 2 Type II: add report availability only when completed and approved for NDA sharing.
- ISO 27001: mark as planned or in progress only with an approved target date.
- GDPR DPA: available only after legal approval of the DPA template and subprocessors list.
Incident response
Section titled “Incident response”Security incidents should be triaged, contained, investigated, remediated, and communicated according to contractual, regulatory, and legal obligations.
Draft notification target: notify affected customers within 72 hours of confirming an incident that affects their data, then provide a written incident report within 30 days when required by contract or law. Notices should go to the customer’s security contact and billing/admin contact. The implemented deadline engine below defaults the processor-to-customer DPA SLA to 24 hours, so the contractual target must be reconciled with it by the security owner before it is quoted.
Incident-response program
Section titled “Incident-response program”- IR plan (
docs/security/incident_response_plan.md) — SEV1–SEV4 severity tiers; roles (incident commander, comms, scribe, DPO); the detect → triage → contain → eradicate → recover → notify → post-incident lifecycle; a containment-levers table mapped to real endpoints and store functions (revoke a session family,revoke_all_user_tokens, SCIM deprovision,POST /admin/users/{id}/demote, rotate the API and audit-signing keys); forensic evidence sources (audit-chain verify and search, theSecurityEventstream, debug bundles, soft-delete retention, e-signature verify); Part 11 e-signature sign-off on the record; and tabletop plus post-incident-review templates. (v0.59.0, 2026-06-26) - Runbooks (
docs/security/incident_runbooks.md) — R1 audit-chain break, R2 account takeover (impossible travel), R3 privilege escalation, R4 cross-tenant probing, R5 credential/key compromise, R6 denial of service, R7 external VDP report. Each is keyed to the matching signal in Security monitoring and detections and to the containment levers. (v0.59.0, 2026-06-26) - Notification-deadline engine (
ir_timeline, library-only — no endpoint). Given an incident’s awareness timestamp and a breach classification, it computes the notification obligations and evaluates each as met / missed / overdue / pending, which makes “notifications meet deadlines” concrete and testable. It gets the processor-versus-controller distinction right: for customer data MolTrace acts as a processor (GDPR Art. 33(2)) whose binding deadline is to notify the customer within the DPA SLA (default 24 hours), while the customer owns the 72-hour supervisory-authority clock; for MolTrace’s own data it wears the controller hat (72-hour supervisory-authority notice plus the Art. 34 data-subject notice). Decision-support only — it computes timing, not legal conclusions, and it sends nothing. (v0.59.0, 2026-06-26) - Breach-notification workflow (
docs/security/breach_notification.md) — the processor/controller table and the trap to avoid, the GDPR “awareness” clock, a decision tree, howir_timelineis used, the Art. 33(3) notice content, and the Art. 34(3) exemptions. Framed as designed to support GDPR breach-notification obligations, never as a held attestation. (v0.59.0, 2026-06-26)
Honest boundaries stated throughout the program docs: sending the notices and staffing a 24/7 on-call rotation are operational, and the notifiable-breach and high-risk determinations are human judgments (DPO), not automated. (v0.59.0, 2026-06-26)
Contact
Section titled “Contact”Security reports and responsible disclosure should route to security@moltrace.com. Acknowledge valid reports within two business days. Provide PGP details on request until a public key is posted.
The machine-readable pointer to the same intake is served at /.well-known/security.txt (RFC 9116); the terms, scope, safe harbor, and severity rubric that govern a report are in the vulnerability disclosure policy. (v0.56.0, 2026-06-25)