Security Remediation Progress
Companion tracker for security-remediation-runbook.md. One line per task. Status is one of: done / unverified / skipped / blocked. βunverifiedβ means the work may have happened but the runbookβs βDone whenβ criterion was not demonstrated. Never upgrade a line to βdoneβ without the stated evidence.
Working order (from the runbookβs Suggested order): A1 β A2 β A3 β B1 β B2 β B3 β E1 β C1 β C3 β E2 β E3 β F3 β then D1, C4, B4; Track G folds into the EC2 migration.
| Task | Status | Date | Note |
|---|---|---|---|
| A1 | in progress | 2026-09-05 | Passkeys enabled on Google Workspace (the reset-path root β highest-value single account). Remaining: Cloudflare (which is also the registrar β see A4, so double weight), GitHub, AWS (new IAM admin and the root login, which still has a password even with no access keys). Verify, donβt assume: enabling passkeys is not the same as removing the weak fallback β confirm 2SV enforcement is βOnly security keyβ, applied to the super-admin account, with device-trust off, or an attacker just uses the password+SMS path. Consider Google Advanced Protection Program for the super-admin (free, enforces phishing-resistant factors, closes the app-password escape hatch). Recovery codes on paper, off the laptop. |
| A2 | partial | 2026-09-05 | DMARC already published at p=quarantine (not none), pct=100, adkim=s, aspf=r, fo=1. SPF include:_spf.google.com include:_spf.resend.com ~all. resend._domainkey and google._domainkey both published at the apex, so strict DKIM alignment should pass for both senders. [email protected] resolves (Cloudflare NS + email-routing MX) so reports are being delivered. Remaining: read the aggregate reports to confirm both senders pass aligned, then move p=quarantine β p=reject. Superseded in part by the domain change β the new domain needs SPF/DKIM/DMARC built from scratch, ramped none β quarantine β reject over a few weeks of aggregate reports, since a new domain has no sending reputation. Keep the gevell.com record at reject permanently. |
| A3 | deferred β new domain | 2026-09-05 | Do NOT register lookalikes of gevell.com β the org is changing domains. Re-target to the new domain once chosen. (Checked anyway: theaccessib1e.org, theaccesible.org, theaccessible.net were unregistered; theaccessible.com parked on HugeDomains.) |
| A4 | mostly done | 2026-09-05 | Registrar is Cloudflare; clientTransferProhibited already set, so registrar lock is on. Expiry 2027-03-02. Consequence: the Cloudflare account IS the registrar account β hardening it in A1 covers both, and makes Cloudflare the highest-value remaining enrollment. Gaps: no CAA records (any CA may issue for gevell.com) and DNSSEC unsigned β both one-click on Cloudflare, low urgency. Re-do all of this on the new domain. |
| G1 | done | 2026-09-05 | AWS CI auth switched from static keys to GitHub OIDC (#2679, c74313e31). Removes long-lived AWS credentials from the self-hosted runner ahead of the EC2 migration β the main thing to get right while CI still shares a box with other services. |
| β | done | 2026-09-05 | AWS root access keys removed, replaced with an IAM admin. Local CLI assumes AWSReservedSSO_AdministratorAccess via Identity Center (short-lived, scoped) instead of root keys. Found outside the runbook while investigating Textract; folded into Track A. Removes the worst case under #8 (stolen laptop) and unblocks the OIDC work in Track G. |
Domain migration (decided 2026-09-05)
The org is moving off gevell.com. This reorders the runbook.
B1/B2 move ahead of A1. A domain change is the strongest possible pretext for invoice fraud (#21): you are about to send university AP offices a legitimate βour domain has changedβ message, which makes a fraudulent βand our remittance details have changedβ follow-up far more credible than it would ever be otherwise. Get the anti-fraud language into the invoice template and contract before announcing the change, and announce it to anyone with an open invoice through a channel that is not email.
Keep the old domain forever. gevell.com stays registered, locked, and auto-renewing
indefinitely β not just through the transition. If it lapses, whoever picks it up receives mail as
us, and every stale customer AP record, password-reset flow, and historical invoice pointing at it
becomes theirs. Hold DMARC at p=reject on it permanently; drop MX or forward to a monitored box.
Budget it as a permanent line item.
SAML is not a blocker β verified. tenant_sso_config, tenant_sso_logins, and sso_intake all
hold zero rows; no SAML login has ever occurred. api-stack.ts pins PUBLIC_BASE_URL for the SP
entity ID and warns that changing it fails every audience check, but with no IdP registered there is
nothing to renegotiate. It becomes a one-line config change at cutover rather than a multi-week
customer dependency. Re-check this before cutover if an SSO customer signs in the meantime.
Cutover surface (all under our own control): PUBLIC_BASE_URL, Resend sending domain + DKIM,
Stripe webhook endpoints, OAuth/SSO redirect URIs, CORS origins, R2 signed-URL hosts, Cloudflare
Workers routes, the @anglinai/ui registry config, and every *.gevell.com subdomain in the
product registry (packages/accessible-org-ui/src/products.ts).
Findings
Record where a problem was found and what to do about it β never the value itself. This file is committed to git, so a leaked credential written down here is a leaked credential twice.
Format: <repo/path or system> β <credential type> β <rotated? y/n> β <date>. For a secret-scanning
hit, the only facts needed are which credential, where it surfaced, and whether it has been rotated.
Anything found in history is already leaked: rotate it, donβt just delete the commit.
(none yet)