Security Remediation Runbook
Companion to incident-risk-register.md. The register says what could go wrong; this says what to do about it, in order, with a definition of done for each item.
How to use it: work top to bottom. Tracks are ordered by risk reduction per hour of effort, not by risk score β a few 15-minute items beat several multi-day ones. Nothing here depends on a later track, so you can stop after any one of them and be meaningfully better off.
Legend. [you] = only you can do it (a purchase, a login, a phone call). [agent] = an agent
can do it and hand you a diff to review. [both] = agent drafts, you execute.
Progress is tracked in docs/Policies/security-remediation-progress.md β one line per task with
status, date, and a note. Create it on first run.
Track A β Identity (2 hours, do first)
Everything else assumes your accounts are yours. A phished GitHub or Workspace login makes every other control in this document irrelevant. Note the scope limit: this track defends logins, not tokens β most of your credential exposure is API keys, which Track C handles.
A1. Phishing-resistant MFA on the identity root [you] β 60β90 min
Risk #5, and indirectly #21 and #17.
What this protects, precisely. WebAuthn (a hardware key or a platform passkey) signs a challenge bound to the origin your browser is actually talking to. A lookalike domain or a real-time proxy gets a signature for the wrong origin and the login fails, with no judgement required from you. TOTP has no origin binding β six digits go into whatever page asked for them, and adversary-in-the-middle phishing kits relay them within the 30-second window, then steal the session cookie. This closes exactly one door: being tricked into authenticating to an attacker. It does nothing about leaked API tokens (see Track C), stolen session cookies, or a malicious OAuth grant you approve yourself.
Order matters more than coverage. Almost every account has βemail me a reset linkβ as its recovery path, so the Workspace mailbox is the root of the identity tree and the registrar is the root of the domain β which, with invoice billing, is a payment instrument. Do these in order:
- Google Workspace β the reset path for everything else, and where BEC (#21) starts.
- Domain registrar β controls DNS, DMARC, and mail delivery.
- GitHub β all source code, plus the
@anglinai/uipublish path (#18). - Cloudflare β DNS, Workers, Pages, R2.
- AWS β root account and your IAM user, separately.
- Supabase, Stripe, Backblaze β enroll if they support WebAuthn; several of these are TOTP-only. Donβt treat this line as a checklist, check each one.
Passkeys first, one hardware key as backup. Platform passkeys (Touch ID on the Mac, Face ID on the phone, synced via iCloud Keychain) are the same protocol with the same origin binding, cost nothing, and can be enrolled today. For a solo operator the sensible posture is passkeys as the daily factor plus one physical key stored off-site for the case where the Mac and phone are both gone, or the Apple account itself is the problem. Buying four YubiKeys is not the prerequisite β donβt let procurement delay the highest-priority track.
The step people skip: once a phishing-resistant factor is enrolled, remove SMS and TOTP as fallbacks wherever the provider allows it. Leaving the weak factor enabled means an attacker phishes that instead and the strong factor protected nothing.
- Store recovery codes on paper, off the laptop, somewhere youβd still have after a house fire. These are now the weakest link β treat them like the backup key.
- GitHub org settings: require 2FA for all members, restrict repo-visibility changes.
- Done when: Workspace, the registrar, GitHub, and Cloudflare each accept a passkey; SMS/TOTP fallback is removed where possible and noted where it isnβt; recovery codes are in a physical location you can name; and the backup key is enrolled and stored off-site.
A2. Confirm DMARC is at p=reject [both] β 20 min
Risk #17, #21. At p=none you are collecting evidence of spoofing while allowing it.
dig +short TXT _dmarc.theaccessible.orgdig +short TXT gevell.com | grep spfIf the policy is none or quarantine: first confirm every legitimate sender (Resend, Google
Workspace, anything else) passes SPF and DKIM alignment in your existing DMARC aggregate reports,
then move to reject. Moving to reject with an unaligned sender silently breaks your auth
emails β check docs/admin/security/dmarc-reports.md for whatβs currently sending.
- Done when:
p=reject, and one test send from each sending source lands in an external inbox.
A3. Register lookalike domains [you] β 20 min
Most invoice fraud uses a cousin domain rather than a true spoof, precisely because DMARC works.
Register and park at minimum: theaccessib1e.org, theaccesible.org, theaccessible.net,
theaccessible.com if available. Point them nowhere. Enable auto-renew.
- Done when: registered, auto-renew on, and no MX records (so they canβt receive mail).
A4. Registrar hardening [you] β 15 min
Registrar lock on, registry lock if the TLD offers it, hardware MFA (from A1), auto-renew with a payment method that wonβt expire, and a calendar reminder 60 days before expiry. Add CAA records restricting issuance to your CA.
Track B β Money (3 hours, do second)
Risk #21 and #22. These are the highest-likelihood incidents that cost you money directly, and the controls are mostly words on a page rather than code.
B1. Invoice anti-fraud language [both] β 30 min
Add to every invoice template and to the contract/SOW:
Our banking and remittance details never change. Any notice of changed remittance information β whether by email, letter, or phone β is fraudulent. Before acting on any such notice, verify by calling [number] using this number and not one supplied in the message.
Say the same thing verbally at contract signing so the customerβs AP office has it on file before an attacker gets there.
- Done when: the line is in the invoice template and in the standard agreement.
B2. Banking-detail handling rule [you] β 15 min
Write it down and follow it: bank details are delivered once, through the customerβs supplier portal or a signed PDF, never as email body text, and are never updated by email. Any inbound request to change details β yours or a customerβs β is verified by phone on a number you already had.
B3. Workspace mailbox-rule alerting [both] β 45 min
Auto-forwarding rules and new mailbox delegates are the standard BEC persistence mechanism and therefore the standard detection signal.
- Google Workspace Admin β disable automatic forwarding to external addresses org-wide unless you actively need it.
- Set an alert on rule/delegate creation, routed to Telegram or larry@anglin.com.
- Review existing rules and delegates on all mailboxes right now.
- Done when: external auto-forwarding is off or alerted, and youβve reviewed todayβs state.
B4. Manual entitlement grant log [agent] β 2 hours
Risk #22. Stripe was your implicit entitlement ledger; invoicing removed it and nothing replaced it.
- Append-only table (
entitlement_grants): who granted, to which org, what was granted, PO number, authorizing contact, timestamp. No updates, no deletes. - Every manual grant in the admin UI writes a row; make the PO field required.
- Monthly: reconcile grants against signed POs, and AR invoices issued against payments received.
- Done when: the migration is applied and the admin UI cannot grant without writing a row.
B5. Org-membership request rule [you], then [agent] β 30 min now, ~1 day later
Risk #22 β an attacker asking to be added to an existing customerβs org is a cross-tenant breach achieved by email alone.
- Now (policy): never add a member to an existing org from an inbound email. Route every membership change through the admin contact named in the contract, confirmed out-of-band.
- Later (product): give customers their own admin console for membership so the request never reaches your inbox, restrict self-join to verified institutional domains with approval by an existing org admin, and notify existing admins whenever a member is added.
Track C β Secrets (1 day)
Risk #1, the highest-likelihood item on the register.
C1. Secret scanning, both gates [agent] β 2 hours
gitleaksas a pre-commit hook and as a CI job β a hook alone is bypassed by--no-verify.- Enable GitHub secret scanning with push protection at the org level.
- Done when: a test commit containing a fake AWS key is blocked locally and in CI.
C2. Historical scan [agent] β 2 hours
Run gitleaks detect over full history on every repo. Anything found is already leaked β
rotate it, donβt just delete the commit. Record findings in the progress file by location and
type only; the progress file is committed to git, so never write the credential value into it.
C3. .dockerignore audit [agent] β 1 hour
Every service with a Dockerfile must exclude .env*. Add a CI assertion so a new service canβt
regress it. You already exclude the large corpora; extend the same file.
C4. Rotation runbook [both] β 3 hours
Create docs/Policies/rotation-runbook.md: every credential, where it lives, who issues it, and
the exact commands to rotate and redeploy it. Leaks become disasters because rotation is slow and
frightening; the runbook is what makes it a 20-minute chore.
Cover at least: Supabase service_role + anon (both projects), AWS access keys, Cloudflare API
token, Anthropic and Bedrock/Gemini keys, Stripe secret + webhook signing secret, Resend, GitHub
PATs + NODE_AUTH_TOKEN, Telegram bot token, B2 application keys, SSH keys on .3/.4/.17.
- Rehearse one rotation per quarter and log it.
- Done when: the document exists and one credential has actually been rotated using it.
C5. Logger redaction test [agent] β 1 hour
Assert the structured logger redacts by key name (*key*, *token*, *secret*, authorization)
before any transport. A unit test, so it canβt silently regress.
Track D β Tenancy (2β3 days)
Risk #4, the highest-impact technical item.
D1. Cross-tenant authz test suite [agent] β 2 days
For every route that accepts a resource ID, a test asserting user A cannot read, update, or delete
user Bβs resource. Wire it into the existing check-route-test-coverage.mjs gate so a new route
without a cross-tenant test fails the build.
D2. Supabase advisors in CI [agent] β 3 hours
Run the security advisor against both projects on a schedule; fail or alert on new findings.
Confirm RLS is enabled on every table and that service_role appears only in reviewed server paths.
D3. Signed-URL audit [agent] β 3 hours
Every R2/S3 object served through a short-TTL signed URL; no public r2.dev on production buckets.
Add a nightly check that an unauthenticated GET of a known key fails, alerting if it succeeds.
Track E β Recovery (4 hours)
Risk #14, #15. You called this the easy one β it is, and itβs the cheapest severity reduction on the list.
E1. B2 immutability and write-only keys [you] β 1 hour
- Object Lock on the bucket with 30-day retention.
- Replace the rclone credential on 10.1.1.3 with an application key scoped to
listFiles+writeFilesonly, nodeleteFiles, per source host. - Done when: a delete attempt from the server fails, and the nightly backup still succeeds.
E2. Git mirror to B2 [agent] β 1 hour
βIt all lives on GitHubβ holds until an account compromise deletes or rewrites it. Add a nightly
git clone --mirror of every repo into the existing B2 backup script.
E3. Confirm Supabase PITR [you] β 30 min
Verify PITR is enabled on the production project and note the retention window in the register.
Add a nightly logical pg_dump too β it covers a case PITR doesnβt, a bad migration faithfully
replicated everywhere.
E4. Test a restore [both] β 90 min
Restore the most recent backup into a scratch project and confirm itβs complete. An untested backup is a hypothesis. Log the result and set a quarterly reminder.
Track F β Spend and abuse (1 day)
Risk #3.
F1. Pre-dispatch spend caps [agent] β 4 hours
Hard per-account and per-org daily caps enforced before dispatching inference, using existing
cost_tracking figures. Fail closed.
F2. Edge rate limits [both] β 2 hours
Cloudflare Rate Limiting per IP and per account on upload endpoints, plus Turnstile on the free form. Cheaper at the edge than in the app.
F3. Budget alarms on the rate [both] β 2 hours
AWS Budgets and Anthropic/Bedrock spend alerts at 50/80/100% of plan, wired to Telegram β and alert on the rate of spend, not just the cumulative total, so a scripted abuser trips it in an hour rather than at month end.
F4. Rehearse the kill switch [you] β 30 min
docs/admin/security/anthropic-spend-killswitch.md should be runnable in under a minute. Time yourself.
Track G β Do during the EC2 migration
Risk #7, #13. These are nearly free while youβre already moving things, and expensive to retrofit.
G1. GitHub OIDC instead of static AWS keys [agent] β 4 hours
Deploy via OIDC role assumption. Otherwise you trade βCI runs on prodβ for βCI holds the keys to prod,β which is not the win it looks like.
G2. Ephemeral runners on .4 [both] β 2 hours
--ephemeral, workspace wiped per job, runner user unprivileged with no sudo and no SSH keys to
.3, .17, or EC2. Never enable fork-PR builds on a self-hosted runner.
G3. LAN firewall and exposure check [both] β 3 hours
- Default-deny inbound on .3, .4, .17; no port forwards β reach internal services over Tailscale or Cloudflare Access.
- Every internal service bound to
127.0.0.1in compose (audit Grafana especially). - Add a monthly external
nmapof the WAN IP to the nightly audit script, emailing the diff.
Track H β Output safety (1 day)
Risk #2, #20.
H1. Sanitize model-generated HTML [agent] β 4 hours
Allowlist sanitization (DOMPurify or equivalent) before any model-generated fragment is stored,
previewed, or delivered. Strip <script>, event handlers, javascript:/data: hrefs, <iframe>.
This is the one control that matters for #2.
H2. Sandboxed preview origin [agent] β 3 hours
Serve HTML previews from an origin with no authenticated session and a strict CSP.
H3. SSRF guard on the URL scanner [agent] β 4 hours
Reject RFC1918, loopback, link-local, and IPv6 ULA targets after DNS resolution; block redirects into private ranges; ensure the fetcher has no route to the LAN or to cloud metadata endpoints.
Track I β Paperwork (1 day, and it sells)
Universities will ask for most of this during procurement. Having it ready is a sales asset, not just a control.
docs/Policies/incident-response.mdβ the stolen-laptop, leaked-key, and customer-data-exposure playbooks; notification obligations and the 72-hour clock; who you call.docs/Policies/vendor-inventory.mdβ each processor, data shared, DPA status, breach-notice SLA and contact.- Reviewer confidentiality agreements and FERPA training for anyone doing human review, plus a signed determination from each customer on whether their uploads contain student records (#9).
- A one-page security summary for procurement, drawn from the above.
Suggested order
If you want a single sequence rather than tracks: A1 β A2 β A3 β B1 β B2 β B3 β E1 β C1 β C3 β E2 β E3 β F3 β then the multi-day work (D1, C4, B4) as it fits, with Track G folded into the EC2 migration.
The first eight items are roughly one working day and remove most of the likelihood-weighted risk.