Security Incident Risk Register — AnglinAI / Gevell
Scope: the systems this org actually runs — Supabase (converter + ACR projects), Cloudflare
(Workers/Pages/R2), AWS (Lambda, SQS, ECR), the LAN boxes 10.1.1.3 / 10.1.1.4 / 10.1.1.17,
self-hosted GitHub Actions runners, GitHub Packages (@anglinai/ui), Stripe, Resend,
Anthropic/Bedrock/Gemini, Backblaze B2, and one primary Mac workstation.
Data at risk: customer-uploaded PDFs and documents from universities (advising worksheets, org charts, catalogs — some FERPA-adjacent and containing named students/employees), customer account + billing records, and generated HTML/ACR artifacts.
Likelihood = probability of at least one occurrence in the next 3 years, given the current setup. Disaster = severity if it happens with no additional controls.
| L | Likelihood | D | Disaster | |
|---|---|---|---|---|
| 5 | Near-certain (>80%) | 5 | Existential — business-ending or contract-ending | |
| 4 | Likely (50–80%) | 4 | Severe — breach notification, customer loss, weeks of recovery | |
| 3 | Possible (20–50%) | 3 | Serious — days of downtime or material cost, recoverable | |
| 2 | Unlikely (5–20%) | 2 | Contained — hours of cleanup, no customer impact | |
| 1 | Rare (<5%) | 1 | Nuisance |
Ranked summary
| # | Incident | L | D | Risk |
|---|---|---|---|---|
| 1 | Secret committed to git or pasted into a log/issue/AI context | 5 | 4 | 20 |
| 4 | Cross-tenant data exposure (IDOR / missing org filter / RLS gap) | 4 | 5 | 20 |
| 5 | Phishing or OAuth-consent phish of the GitHub / Cloudflare / Google account | 4 | 5 | 20 |
| 6 | npm / PyPI dependency supply-chain compromise | 4 | 4 | 16 |
| 21 | Invoice fraud / business email compromise on an AR or AP thread | 4 | 4 | 16 |
| 17 | Email-sender spoofing, DNS, or registrar hijack | 3 | 5 | 15 |
| 3 | Cost-abuse / billing DoS of the free converter tier | 4 | 3 | 12 |
| 8 | Lost or stolen Mac with live keys, SSH keys, and session cookies | 3 | 4 | 12 |
| 9 | Human reviewer / student-labor insider exposure of customer documents | 3 | 4 | 12 |
| 10 | Vendor breach (Supabase, Cloudflare, Stripe, Resend, Anthropic, B2) | 3 | 4 | 12 |
| 11 | Public or over-permissive R2 / S3 bucket | 3 | 4 | 12 |
| 12 | Customer account takeover via email (magic link, password reset, mail scanners) | 3 | 4 | 12 |
| 13 | LAN service unintentionally exposed to the internet (Grafana, Loki, Redis, Postgres) | 3 | 4 | 12 |
| 19 | Destructive operator error by an agent or script (dropped table, force-push, rm -rf) | 3 | 4 | 12 |
| 22 | Fraudulent account or org-membership provisioning (manual entitlement path) | 3 | 4 | 12 |
| 18 | GitHub Packages / @anglinai/ui publish-token abuse — your own supply chain | 2 | 5 | 10 |
| 2 | Model-output poisoning — hostile document steers a generated deliverable | 3 | 3 | 9 |
| 14 | Ransomware / destructive malware on the Mac or a LAN server | 2 | 4 | 8 |
| 15 | Recovery gap on the systems of record (accounts, billing, code) | 2 | 4 | 8 |
| 20 | Injection / SSRF / file-parser exploit in the PDF or URL-scan pipeline | 2 | 4 | 8 |
| 7 | Self-hosted GitHub Actions runner compromise | 2 | 3 | 6 |
| 16 | Stripe webhook forgery or replay (workshop checkout only) | 2 | 2 | 4 |
Section numbers below are stable IDs, not ranks. Scores reflect controls already in place or committed to as of 2026-09-05: production moving to EC2 with runners left isolated on 10.1.1.4, a deliberate no-retention policy for customer documents, and invoice-based billing for all non-workshop customers.
1. Secret committed to git, or pasted into a log, issue, or AI context
L5 / D4. You handle a dozen live credential types daily and routinely paste config into
terminals, issues, and agent sessions. A .env in a Dockerfile COPY ., a service_role key in
a debug log, an AWS key in a CI echo — all are near-inevitable at this cadence. A leaked Supabase
service_role key bypasses RLS across every customer’s documents.
Controls
- Pre-commit secret scanning (
gitleaksortrufflehog) as a hook and as a CI gate — a hook alone is bypassed by--no-verify. - Turn on GitHub secret scanning + push protection on every repo (works on private repos with GitHub Advanced Security; enable at org level).
.dockerignoremust exclude.env*in every service; assert it in CI.- Never bake secrets into images. Cloudflare Workers →
wrangler secret; Lambda → SSM Parameter Store or Secrets Manager with a KMS key, injected at runtime, never inenv:of the CDK stack. - Structured logger must redact by key name (
*key*,*token*,*secret*,authorization) before any transport — add a unit test that asserts redaction. - Rotation drill: write
docs/Policies/rotation-runbook.mdlisting every credential, where it lives, and the exact commands to rotate it. Rehearse rotating one per quarter. The reason leaks become disasters is that rotation is scary and slow. - Prefer short-lived credentials: GitHub OIDC → AWS role assumption instead of long-lived
AWS_ACCESS_KEY_IDin Actions secrets.
2. Model-output poisoning — a hostile document steers a generated deliverable
L3 / D3. Revised down from L4/D5. The classic prompt-injection disaster requires the injected text to reach a model that holds tools or credentials. Yours does not: Bedrock/Anthropic calls in the conversion pipeline are stateless, tool-less inference, and customer documents are not opened in local agent sessions where MCP servers are attached. The database-exfiltration scenario is not live for this architecture.
What remains is injection-to-output, which needs no tools at all. A PDF carrying hidden
instructions (white-on-white text, off-page content, an adversarial image) can steer what your
model emits: alt text, HTML, an ACR narrative, a FormSpec. That output is then published on a
university’s website under your name, or rendered in your own admin and reviewer UIs. Two concrete
harms: (a) reputational — your compliance product emits defamatory or absurd alt text into a public
page; (b) technical — model-generated HTML containing a script or an href/javascript: payload
becomes stored XSS in the preview, the reviewer UI, or the customer’s site.
Controls
- Sanitize every model-generated HTML fragment through an allowlist (DOMPurify or equivalent)
before it is stored, previewed, or delivered. Strip
<script>, event handlers,javascript:,data:hrefs, and<iframe>. This is the single control that matters. - Serve HTML previews from a sandboxed origin with a strict CSP, never same-origin with an authenticated app session.
- Continue validating structured model output against a strict schema before use — the formspec “validated before render” pattern is the right one; apply it to edit-ops and ACR generation too.
- Never interpolate model output into SQL, shell, file paths, or R2 keys. Audit for this once.
- Length- and character-class-cap alt text; flag outputs containing imperative phrasing aimed at a model (“ignore”, “instead”, “system prompt”) for human review.
- Keep the boundary you already have: customer content stays out of credentialed agent sessions.
Note the soft edge — corpora and extracted-text reports committed under
tools/pdfpolish/reports,TestFiles, and the census output are customer-derived, and agents do read those. Treat them as untrusted when a session has write-capable MCP servers attached.
3. Cost-abuse / billing DoS of the free tier
L4 / D3. Anonymous or cheaply-created accounts can upload large PDFs that trigger expensive
vision inference. You already have a docs/admin/security/anthropic-spend-killswitch.md, which says you’ve
felt this. A scripted abuser can run four figures a day before you notice; Bedrock and Lambda have
no natural ceiling.
Controls
- Per-IP, per-account, and per-org rate limits at the Cloudflare edge (Rate Limiting rules + Turnstile on the free upload form) — cheaper than limiting inside the app.
- Hard per-account and per-org daily spend caps enforced before dispatch, using your existing
cost_trackingfigures. Fail closed. - AWS Budgets + Anthropic/Bedrock spend alarms at 50/80/100% of monthly plan, wired to Telegram and email — alert on the rate, not just the total.
- Cap input size and page count on the free tier; require a verified email + payment method above a threshold.
- Keep the kill switch documented, tested, and runnable in under a minute.
4. Cross-tenant data exposure (IDOR, missing org filter, RLS gap)
L4 / D5. You ship fast across ~15 apps sharing Supabase projects, with multiple key systems
(tap_ settings keys vs tac_ audit keys) and hand-rolled org scoping. One route that takes a
fileId and forgets AND org_id = $me exposes another university’s documents. For a company
selling compliance to public institutions, this is contract-ending, and student data drags in FERPA.
Controls
- RLS on by default, on every table, with
service_roleused only in narrow, reviewed server paths. Runget_advisors(Supabase security advisor) in CI and fail on new findings. - Add an automated authz test suite: for every resource route, a test that user A cannot read, update, or delete user B’s row. Wire it into the existing route-test-coverage CI check so a new route without a cross-tenant test fails the build.
- Use opaque, unguessable IDs (UUIDv4) everywhere customer-facing — you mostly do; verify no sequential IDs leak in URLs or R2 keys.
- Signed, short-TTL URLs for every R2 object; never a stable public path.
- Quarterly manual review: enumerate every route that accepts an ID and confirm the org filter.
5. Phishing / OAuth-consent phish of your GitHub, Cloudflare, or Google account
L4 / D5. You are a single-admin org: one account owns GitHub, Cloudflare, AWS, Supabase,
Stripe, and email. Targeted phishing at developers is now routine (fake npm/GitHub security
notices, fake “your Actions run failed” mails, malicious OAuth apps requesting repo scope).
Compromise of the GitHub or Cloudflare account is total compromise of the business.
Controls
- Phishing-resistant MFA only — hardware security keys (two YubiKeys, one off-site) or platform passkeys on GitHub, Cloudflare, AWS root + IAM, Google, Supabase, Stripe, and the domain registrar. Remove TOTP and SMS as fallbacks where the provider allows it; they are the thing that gets phished.
- Store recovery codes offline, in a safe, not in the password manager you’d lose with the laptop.
- Audit connected OAuth apps and GitHub PATs quarterly; delete anything unrecognized; prefer fine-grained PATs with expiry over classic tokens.
- Enable GitHub org settings: require 2FA, restrict who can change repo visibility, require
signed commits on
main. - Registrar: enable registrar lock and 2FA; the domain is the root of your identity.
6. npm / PyPI dependency supply-chain compromise
L4 / D4. You run weekly Dependabot merges across a large monorepo, plus Python services. A compromised transitive package executes an install script on the machine holding every credential you have — and inside CI on your prod box.
Controls
npm ci --ignore-scriptswherever feasible; allowlist the few packages that genuinely need install scripts (sharp, esbuild).- Pin with a committed lockfile (you do) and enable
minimumReleaseAge/ cooldown so Dependabot won’t pull a package published in the last 3–7 days — this defeats most publish-hijack windows. - Review Dependabot diffs for what changed, not just the version bump. Do not auto-merge.
- Run
npm audit+osv-scannerin CI; treat new critical/high as a blocking failure. - Do dependency installs and CI in containers or on a machine that doesn’t hold prod credentials (see #7).
7. Self-hosted GitHub Actions runner compromise
L2 / D3. Revised down from L3/D5. Production is moving to EC2 with theaccessible-ci /
theaccessible-runner-4b staying isolated on 10.1.1.4, which removes the original finding: CI no
longer executes on the production host. Residual risk is lateral movement — a compromised runner
sits on the LAN alongside 10.1.1.3 (backups, B2 credentials) and 10.1.1.17 (staging), and holds
whatever deploy credentials the workflows use.
Controls
- Deploy to EC2/AWS via GitHub OIDC role assumption, not long-lived
AWS_ACCESS_KEY_IDsecrets resident on the runner. This is the main thing to get right during the migration. - Ephemeral runners (
--ephemeral, workspace wiped per job) so one poisoned job doesn’t persist. - Runner user unprivileged, no sudo, no SSH keys to 10.1.1.3 / .17 / EC2.
- Never enable fork-PR builds on the self-hosted runner; require approval for outside contributors.
- Firewall .4 outbound-only toward the other LAN hosts; it should not be able to reach the backup box or staging at all.
8. Lost or stolen Mac
L3 / D4. One workstation carries SSH keys to 10.1.1.3/.4/.17, gh tokens, AWS creds in
~/.aws, dozens of .env files across worktrees, browser sessions for Cloudflare/Stripe/Supabase,
and the password manager.
Controls
- FileVault on, with a strong password and a short auto-lock (5 min). At rest a locked FileVault Mac is effectively fine; a sleeping unlocked one is not.
- Enable Find My + remote wipe; know how to trigger it.
- SSH keys with passphrases, loaded into the agent with a timeout (
ssh-add -t 3600), and hardware-backed where possible (ed25519-skwith a YubiKey — then a stolen laptop yields nothing usable). - Keep the incident playbook written and reachable from your phone: rotate GitHub PATs, revoke GitHub sessions, rotate AWS keys, rotate Cloudflare tokens, rotate the Supabase service key, revoke SSH authorized_keys on all three servers, reset Stripe/Resend keys. Practice it once.
- Don’t leave long-lived prod secrets in plaintext
.envfiles on the laptop — pull them from 1Password/op runor SSM at need.
9. Insider exposure via human reviewers / student labor
L3 / D4. Your pricing model uses US student labor for human review of customer documents. Every reviewer sees real university documents, potentially containing student names and records. This is a people-and-process risk that no firewall touches, and it’s the one a university procurement officer will ask about.
Controls
- Written confidentiality + acceptable-use agreement signed by every reviewer; FERPA training if any customer content is student records (get a signed determination from customers on whether their uploads contain them).
- Reviewers work in a purpose-built review UI that shows only assigned documents — never a shared admin login, never bulk download, never a shared Drive folder.
- Individual accounts, MFA, per-document access logging, and no local file storage; disable download where review can be done in-browser.
- Offboarding checklist executed same-day; quarterly access review of who still has an account.
- Rate-limit and alert on anomalous access volume (one reviewer opening 500 documents).
10. Vendor breach
L3 / D4. With ~10 SaaS dependencies, the chance that at least one has a reportable incident in three years is high (recent history: Okta, LastPass, Snowflake-adjacent credential attacks, npm, CI/CD vendors). Your exposure varies: Stripe holds cardholder data (their problem), Supabase holds your customers’ documents (your problem, contractually).
Controls
- Maintain a vendor inventory in
docs/Policies/— vendor, data shared, DPA status, breach-notice SLA, contact. You already have ZDR/DPA work for Bedrock; extend it to every processor. - Subscribe to each vendor’s status/security advisories; don’t learn from Twitter.
- Assume vendor credentials will leak: scope every vendor key to least privilege and rotate on a schedule, not just on incident.
- Keep an exportable copy of critical data you’d need if a vendor vanished (nightly Supabase dump → B2, which you have — verify it covers all projects, not just UISP).
- Know your own notification obligations: if Supabase is breached, you notify the universities.
11. Public or over-permissive R2 / S3 bucket
L3 / D4. Customer PDFs live in R2 (accessible-pdf-files) and S3. A bucket made public for a
one-off debug, or a flipbook/export path served without signing, exposes documents by URL — and
they are indexable.
Controls
- Every bucket private by default; access exclusively via signed URLs with minutes-scale TTL.
- Cloudflare: no public r2.dev domain enabled on production buckets; serve through a Worker that authorizes the request.
- An automated check in the nightly audit: attempt an unauthenticated GET of a known object key and alert if it succeeds.
- Object keys should be unguessable (UUID prefixes), so even a leaked path is one document.
- Lifecycle rules: delete customer source documents after the retention period you promise them.
12. Customer account takeover via email
L3 / D4. Auth is magic-link / OTP over Resend. You’ve already been bitten by mail scanners prefetching links (fixed with confirm-on-gesture). Remaining vectors: link interception, a customer’s compromised mailbox, or an OTP phish.
Controls
- Keep the POST-on-gesture confirm page; never a GET-verified link. (Done — don’t regress it.)
- Short OTP/link TTL (≤10 min), single use, invalidated on use and on a new request.
- Bind the link to the originating browser where UX allows; warn loudly on cross-device use.
- Rate-limit OTP requests per address and per IP; lock after N failures.
- Email the account on every new-device login and on any email-address change, to the old address too.
- Offer (and require, for admin accounts) a second factor — passkeys are a good fit for your customer base.
13. LAN service unintentionally exposed to the internet
L3 / D4. You run Grafana, Loki, Redis, Postgres, and app servers on 10.1.1.x. Exposure happens
via a router port-forward added for a demo, UPnP, a Tailscale/Cloudflare Tunnel misconfiguration,
or a Docker ports: binding to 0.0.0.0 instead of 127.0.0.1. Redis and Grafana with default
credentials are scanned within minutes of exposure.
Controls
- Bind every internal service to
127.0.0.1indocker-compose.yml(you do for Loki/Redis — audit the rest, Grafana especially). - No port forwards. Reach internal services over a VPN / Tailscale / Cloudflare Access, never a raw forward.
- Change every default credential; put Grafana behind SSO or Cloudflare Access.
- Monthly external scan of your WAN IP (
nmapfrom outside, or a Shodan alert on the IP) — add it to the nightly audit script and email the diff. - Host firewall (ufw / pf) default-deny inbound on all three boxes.
14. Ransomware or destructive malware
L2 / D5. Lower likelihood for a security-aware single-operator shop, but if it lands on the Mac or a LAN server with mounted shares and SSH keys, it takes the servers with it.
Controls
- Backups must be immutable and out-of-band (see #15) — that is the entire defense.
- macOS: keep Gatekeeper/XProtect on, don’t run unsigned binaries from the internet, be
disciplined about
curl | sh(a real exposure in agentic development). - Segment: the Mac should not have passwordless root SSH to prod; use per-purpose keys.
- Servers: unattended security upgrades, no desktop software, minimal exposed surface.
15. Recovery gap on the systems of record
L2 / D4. Reframed. Customer documents are deliberately not retained, which is a genuine control, not a gap — it removes the single largest blast radius from every backup, ransomware, and bucket-exposure scenario, and it’s a strong answer for university procurement. Backblaze holds internal documents and the UISP database; its compromise is embarrassing, not existential.
The question that replaces it: what happens to the things you can’t re-derive?
- The Supabase systems of record — accounts, orgs, API keys, credits/balances, Stripe linkage,
job history,
cost_tracking. Losing these ends the business just as surely as a document breach. Confirm PITR is enabled on the production project (vuvwmfxssjosfphzpzim) and that the retention window is longer than your realistic detection time for a silent corruption — days, not hours. A logicalpg_dumpto B2 on the nightly job protects against a class of failure PITR does not: a schema change or bad migration replicated into every replica. - Code. “It all lives on GitHub” is true right up until #5 — a single-account compromise can
delete or rewrite repositories, and GitHub’s own restore window is limited. A nightly
git clone --mirrorof every repo to B2 is ~an hour of work and closes it. You already have the backup script and the bucket. - Infrastructure state — CDK stacks, wrangler config, Supabase migrations. These live in git, so the mirror covers them. Secrets do not; keep an encrypted copy outside the laptop.
Controls
- B2 Object Lock with 30-day retention and write-only application keys (
listFiles+writeFiles, nodeleteFiles), one key per source host. You called this an easy fix — it is, and it also closes #14. - Add the git mirror and a Supabase logical dump to
~/.claude/scripts/server-backup.sh. - Verify Supabase PITR is on and note the window in this document.
- Test a restore quarterly and log the result.
16. Stripe webhook forgery or replay
L2 / D2. Revised down from L2/D3. Stripe now covers workshop registrations only; the institutional business bills by invoice. A forged webhook grants at most a workshop seat. Keep the basic hygiene, but this no longer warrants engineering attention.
Controls
- Verify
Stripe-Signaturebefore parsing; storeevent.idand no-op on repeats. - Re-fetch the session from Stripe before granting a seat rather than trusting the payload.
- Note the deployment gotcha that already bit you: workshop checkout and the Stripe webhook run on
the Cloudflare worker (
worker.gevell.com) and deploy manually — a signature-verification fix merged tomainis not live untilcd workers/api && npm run deploy.
17. Email-sender spoofing, DNS, or registrar hijack
L3 / D5. Likelihood revised up from L2. Once you invoice universities, your domain is a
payment instrument. gevell.com is both the sender of your auth mail and the from-address
on every invoice; anyone who can spoof it can redirect payments (see #21). Registrar takeover or a
lapsed renewal remains the tail risk, but spoofing is now the likely one, and it costs an attacker
nothing to try.
Controls
- DMARC at
p=reject, with SPF and DKIM aligned for every sending source — Resend, Google Workspace, and anything else that sends as your domain. You havedocs/admin/security/dmarc-reports.md; confirm the published policy is actuallyreject, notnoneorquarantine. Atnoneyou are collecting evidence of spoofing while permitting it. - Watch the aggregate reports for unfamiliar sending sources — that’s your spoofing early warning.
- Register and park the obvious lookalikes (
theaccessib1e.org,theaccessible.net,theaccesible.org); most invoice fraud uses a cousin domain, not a true spoof, precisely because DMARC works. - Consider a domain-monitoring alert for newly registered lookalikes.
- Registrar lock, registry lock if offered, hardware MFA on the registrar, auto-renew plus a calendar reminder, CAA records, and Telegram alerts on any Cloudflare DNS change.
18. GitHub Packages / @anglinai/ui publish-token abuse
L2 / D5. You publish a private package that every one of your apps installs and that runs in
every customer’s browser. A stolen NODE_AUTH_TOKEN with write:packages lets an attacker publish
a malicious @anglinai/ui patch release; your next npm install ships it to production.
Controls
- Publish only from CI, via OIDC/short-lived tokens — never from a laptop with a long-lived PAT.
- Require 2FA for publishing; use fine-grained tokens scoped to the single package, with expiry.
- Pin
@anglinai/uito an exact version (not^) in consuming apps and bump deliberately. - Enable provenance/attestation on publish and verify it at install time.
- Alert on every package publish event to Telegram — you should recognize each one.
19. Destructive operator or agent error
L3 / D4. Not an attacker, but it produces an identical incident: a dropped table, a
supabase db reset against prod, a force-push over main, an rm -rf in the wrong worktree, an
agent running apply_migration on the production project. You run agents with production MCP
credentials daily, which makes this markedly more likely than for a typical shop.
Controls
- Separate prod and dev Supabase projects and AWS accounts; make the prod one visually obvious in every tool and prompt.
- Prod MCP connections should use a read-only database role by default; require an explicit, separate step to get write access.
- Branch protection on
main: no force-push, no deletion, required checks. - Supabase PITR enabled on the production project (not just daily backups).
- Migrations reviewed and applied through CI, never ad-hoc from an agent session.
- Keep the “destructive actions need confirmation” discipline in your agent configs.
20. Injection / SSRF / parser exploit in the PDF or URL-scan pipeline
L2 / D4. You run untrusted PDFs through pikepdf, veraPDF, Ghostscript-adjacent tooling, and
headless Chrome, and you fetch arbitrary user-supplied URLs in the audit scanner. That’s a large
native-code attack surface plus a textbook SSRF vector (a customer submitting
http://169.254.169.254/ or http://10.1.1.4:3000/ to the scanner).
Controls
- SSRF defense in the URL scanner: resolve the hostname, reject RFC1918/loopback/link-local/IPv6 ULA targets after DNS resolution, disable redirects to private ranges, and run the fetcher in a network namespace with no route to the LAN or to cloud metadata endpoints (IMDSv2 at minimum).
- Run document parsers in the most isolated context available — Lambda with no VPC access to anything sensitive, minimal IAM, egress restricted, short timeouts, memory caps.
- Keep parser dependencies patched; they’re the CVE-rich part of your stack.
- Resource limits on every job (page count, file size, wall clock) to bound zip-bomb and billion-laughs style inputs.
- Never render untrusted HTML output on the same origin as an authenticated app; serve previews from a sandboxed origin with a strict CSP.
21. Invoice fraud / business email compromise
L4 / D4. New — added 2026-09-05 with the shift to invoice billing. This is now the most likely incident that costs you money directly. Small vendors invoicing large institutions are the standard target, and it runs in both directions:
- Against you (AR): an attacker spoofs or compromises your mail, intercepts an invoice thread with a university’s accounts-payable office, and sends “updated remittance details.” The university pays the attacker. You are out the money and in an awkward conversation with a customer about whose email was compromised. Payment terms of net-30/60 mean weeks pass before anyone notices.
- Against your customer’s AP (thread hijack): the attacker is inside the university’s mailbox, not yours, and replies within a genuine thread. Nothing on your side looks wrong.
- Against you (AP): a fake vendor or a fake “Larry” instructs your bookkeeper to pay something. Small surface today, but it grows the moment anyone else handles finances.
- Vendor-onboarding harvest: universities will require W-9s, EINs, and ACH details through supplier portals or email. That packet is exactly what an attacker needs to impersonate you convincingly, and those portal credentials become another account worth protecting.
Controls
- DMARC
p=rejectand lookalike-domain registration (#17) — this is the technical half. - Put a fixed line on every invoice: “Our banking details never change. Any notice of changed
remittance information is fraudulent — verify by phone at
before paying.” State it at contract signing too, so the customer’s AP office has it on file. - Never send bank details in email body text. Deliver them once, through the customer’s supplier portal or a signed PDF, and refuse to “update” them over email under any circumstance.
- Verify any inbound change request — to your details or a customer’s — by phone, using a number you already had, never one supplied in the message.
- Hardware MFA on Google Workspace and every supplier portal (#5). Most AR fraud starts with a mailbox, not a mail server.
- Watch for auto-forwarding rules and new mailbox delegates in Workspace; that is the standard BEC persistence mechanism and the standard detection signal. Alert on rule creation.
- Reconcile AR monthly: invoices issued vs. payments received, with anything past terms chased by phone. The detection window is the whole ballgame here.
- If invoicing volume grows, move to a supplier portal or an invoicing platform with an audit trail rather than PDF-by-email.
22. Fraudulent account or org-membership provisioning
L3 / D4. New. Invoicing means entitlements are granted by hand: someone emails a PO, you
enable an org or add a seat in the admin UI. Two failure modes. The benign one is free service. The
serious one is that an attacker asks to be added to an existing university’s organization —
“I’m the new digital accessibility coordinator at
Controls
- Never add a member to an existing org on the strength of an inbound email. Route every membership change through a designated admin contact named in the contract, and confirm out-of-band.
- Restrict org self-join to verified institutional email domains, and even then require approval from an existing org admin rather than from you.
- Give every customer an admin console for their own membership so the request never reaches your inbox — the durable fix.
- Log and retain every manual entitlement grant (who, when, on what authority, PO number) in an append-only table. With Stripe out of the loop there is no automatic ledger; this replaces the reconciliation control that Stripe was providing.
- Alert on manual grants and review them monthly against signed POs.
- Notify a customer’s existing admins whenever a member is added to their org.
The seven changes that buy the most risk reduction
- DMARC at
p=rejectplus the invoice anti-fraud language and lookalike domains. (#17, #21) - Hardware security keys on Google Workspace, GitHub, Cloudflare, AWS, Supabase, registrar, and every university supplier portal. (#5, #21)
- Secret scanning with push protection + a rehearsed rotation runbook. (#1)
- Automated cross-tenant authz tests, plus an out-of-band rule for org-membership requests — the technical and human halves of the same risk. (#4, #22)
- Dependency cooldown +
--ignore-scripts, and installs off the credential-bearing host. (#6) - Spend caps and edge rate limits enforced before dispatch, not after billing. (#3)
- Immutable B2 keys, a git mirror, confirmed Supabase PITR, and GitHub OIDC for the EC2 migration. (#14, #15, #7)
Things you should write down before you need them
docs/Policies/rotation-runbook.md— every credential, where it lives, how to rotate it.docs/Policies/incident-response.md— who to call, how to notify customers, the 72-hour clock, and the “stolen laptop” / “leaked key” / “customer data exposed” playbooks.docs/Policies/vendor-inventory.md— processors, data shared, DPA, breach-notice contact.docs/Policies/invoicing-controls.md— the AR fraud language, the verification procedure, and the manual-entitlement grant log.- A one-page security summary for university procurement — they will ask, and having it ready is a sales advantage as much as a control.