Security

On this page
NOTE

The canonical security operations content — incident response, key rotation, breach notification, data-flow diagrams, and the full Security Configuration Reference — lives in Security Operations & Runbooks. The universal security baseline (Kerckhoffs’s principle + public visibility enforcement) is distributed as a synced standard at docs/modules/standards/pages/security-baseline.adoc. This page is a bounded safety-critical quick-reference; update Security Operations, not this page, when security configuration changes.

Safety-critical quick-reference

  • Kerckhoffs’s principle: security must never rely on source-code secrecy — the repository is public; assume the attacker has read it. Secrets live in env / SOPS, never in source.

  • Never weaken the CSP on canopy-web / canopy-portal (no 'unsafe-inline', no 'unsafe-eval', no inline scripts/styles/handlers). See Content Security Policy (BFF).

  • Federal data tenancy (ADR-004): FTI → canopy-tanf + canopy-medicaid only; IEVS → canopy-snap only; SSA SOLQ/BINDEX → per CMA; FDSH → canopy-medicaid. No restricted data in event-bus payloads. Enforced in CI by cargo xtask compliance audit-data-tenancy against compliance/data-tenancy-authorisation.toml.

  • Secrets: env vars (CANOPY_{SERVICE}__*) + SOPS-encrypted secrets/dev.yaml (fake values only). Never hardcode; never commit .env; CI secrets are masked
    protected GitLab variables.

  • Placeholder-control guards (#1265, external-audit finding W2): outside CANOPY_ENV=development a noop-adapters build of canopy-verification refuses to boot — every adapter it can wire (Noop AND Scripted) serves fabricated IEVS/SAVE/SSA data. The refusal is a fail-closed default, not a hard deny: CANOPY_VERIFICATION__ALLOW_FABRICATED_VERIFICATION=true is the per-control accountable override (loud, auditable startup warning; the deployment owns the risk — the ADR-041 doctrine). The remaining W2 placeholders land bundled with their real controls: NoopScanner → #1006, NoopCaptcha → #663.

  • Auth: Keycloak OIDC, RS256 JWT, JWKS hourly refresh. RBAC role guards in canopy-auth (applicant / caseworker / eligibility_specialist / supervisor / admin, plus the specialist roles — fti_auditor / data_steward / auditor / analyst / studio_admin). Least privilege on handlers. Service-to-service calls authenticate with ADR-019 client_credentials service tokens behind the ADR-043 receiver contracts; user-context calls ride RFC 8693 exchanged bearers. One classified residual: canopy-verification’s /internal/v1 IEVS/SAVE adapter callbacks sit OUTSIDE the JWT router on a shared X-Service-Api-Key header (S-verification #1434 classified it; its retirement to service tokens is tracked follow-on work — see the verification API page).

  • Worker program scope is a required claim (ADR-044, #1515): an access token whose primary_programs claim is absent, empty or unrecognized is refused at admission — at the OAuth callback and at the slow-path refresh alike. There is no canopy-side override and no role-tier bypass (supervisors, admins and auditors are scoped by their claim like anyone else); the deployment’s override is the IdP claim mapper. The claim parses once into a structurally non-empty WorkerProgramScope, so the former "empty claim ⇒ see and do everything" branches have nothing left to branch on. Scope changes take effect within one access-token lifetime; the emergency path is a session-store purge. This is a BFF control — it composes with, rather than supersedes, the per-request identity story (user-context upstream writes ride RFC 8693 exchanged bearers naming the worker — ADR-043; the ADR-019 actor-claim channel is retired, #1443) and network isolation.

  • Pub 1075 citations: the revision in force is IRS Publication 1075 (Rev. 11-2021), whose control catalog is NIST SP 800-53-derived — least privilege is AC-6 (§4.1), audit-record retention is AU-11. Earlier unversioned §9.3.1 citations were swept to AC-6 in #1515; frozen records (CHANGELOG.adoc, archived plans, the roadmap’s delivery log) keep their original text, so a §9.3.1 grep hit outside those is a defect.

  • Redaction/expungement (T2-6 #687, ADR-036 Decision M): the dedicated data_steward realm role gates the privileged, irreversible crypto-shred operations (e.g. POST /v1/determinations/{id}/redact destroys a determination snapshot’s per-value key). It is separate from admin — admins grant/revoke data_steward but do not themselves hold redaction authority (separation of duties, mirroring the fti_auditor Pub-1075 separation).

  • Determinations are ECDSA P-256 detached JWS (ADR-002); invalid or missing signatures are quarantined by the orchestrator. SSN is AES-256-GCM at rest (CANOPY_ENCRYPTION_KEY); the API exposes the last 4 digits only.

  • Encryption at rest (crypto-shred stores): canopy-persons (SSN/DOB + fact versions), the five program services + persons (determination snapshots), and — since #1256 (ADR-004 A8a) — canopy-reporting's T-MSIS eligibility extracts, whose restricted attributes are sealed in one per-row restricted_payload envelope under a per-report-generation DEK (engine-evaluated keys stay plaintext per ADR-004 Amendment 3). All use the ADR-036 SealedValue envelope keyed off CANOPY_ENCRYPTION_KEY; canopy-reporting refuses to boot without it.

  • Dependencies: OpenSSL is banned (deny.toml, rustls only); cargo audit
    cargo deny are blocking CI jobs. CVE SLA: Critical 24h / High 1wk / Medium 1mo / Low next update.

  • Sessions: PostgreSQL-backed (tower-sessions-sqlx-store); MemoryStore is banned (ADR-009).

  • Broker identity (#1093, epic &72): every service authenticates to RabbitMQ with its own principal (canopy-<service>) whose topic-permission write regex on canopy.events enumerates exactly the routing keys its tree publishes — the broker refuses a forged foreign key at publish time, so a compromised service cannot mint another service’s rights-bearing events. EventEnvelope.source_service remains a diagnostic label, never an authorization input. Devstack credentials are public by design (Kerckhoffs; dev-canopy-<service>-mq-not-for-production); production injects real secrets at deploy. The canopy admin principal is reserved for tests, seed tooling, and operator surgery. Enforcement is pinned by crates/canopy-mq/tests/acl_test.rs; the full model lives in the event-delivery protocol.

Content Security Policy (BFF)

canopy-web and canopy-portal serve HTML and must apply a strict CSP:

default-src 'self'; script-src 'self'; style-src 'self';
img-src 'self' data:; frame-ancestors 'none'; form-action 'self'

Non-negotiable: no 'unsafe-inline', no 'unsafe-eval', no inline <script>/<style> blocks, no inline style="", no inline event handlers (onclick="", inline @click="", hx-on::*). All JS/CSS in external files; the Alpine CSP build (@alpinejs/csp) only; axe-core WCAG 2.1 AA must pass with zero critical violations. Full rules and implementation guidance: Security Operations › Content Security Policy.

Upload content safety (ADR-042, #1006)

Applicant uploads are quarantined until a real scan verdict: every insert lands scan_status='pending', the clamd-backed promotion worker settles verdicts bound to content identity (sha256+size re-verified at scan AND at serve — a replaced object is unservable), and content/accept/reject refuse 409 unless viewable (clean, or skipped with the audited supervisor override; DB CHECKs make acceptance-of-unscanned unrepresentable). Content responses carry Cache-Control: no-store end to end. Worker-facing scan provenance (scan_detail signature names, backend/version) never reaches applicant browsers — the portal BFF projects the wire shape onto an applicant-view allowlist. clamd itself is unauthenticated TCP: service-network only, loopback-published in devstack solely for host-lane tests (runbook). The noop backend outside development refuses boot without the accountable override (CANOPY_APPLICATIONS__ALLOW_INSECURE_SCANNER, ADR-041 doctrine).

Vulnerability reporting

See SECURITY.adoc in the repository root for the public-facing vulnerability reporting process.

Edit this page · default