runbook-set: v3-ga-security-incident-response.v1
verifier: pnpm verify:v3 security-incident-response (proposed; wire into
pnpm verify:v3 operator-runbooks-on-call alongside the availability set)
These runbooks complement V3/runbooks/incident-response-v3-ga.md, which covers
availability incidents only. Every security incident below has a named incident
commander role, a first-15-minutes checklist, evidence-preservation steps that
survive legal review, and an audit event written into the Lilith Operator
Console. Severity follows the same SEV-1/2/3 scale as the availability set.
Security incidents additionally page the security on-call
(oshun-security-oncall) and the Lilith-Privacy lead when personal data may be
involved.
Shared rules for all four runbooks:
- The incident commander for the calendar day (per
V3/operations/on-call-rota-ga-week.json) owns the bridge; the security commander role below owns the technical response. - Evidence preservation always precedes remediation that would destroy state: snapshot first, rotate second.
- All notification clocks below start at confirmation of the incident, not detection of the alert — but the confirmation decision itself has a 4-hour SLA from first credible signal, so the clock cannot be parked.
- No public statement, tweet, or status-page update goes out without joint sign-off from the incident commander and the legal owner.
incident: data-breach-personal-data#
- Owner: Security incident commander (
oshun-security-oncall), joint with Lilith-Privacy lead - Severity: SEV-1 if personal data confirmed exfiltrated or exposed; SEV-2 for credible-but-unconfirmed exposure; SEV-3 for contained internal mishandling with no external exposure
- Trigger: confirmed unauthorized access to any datastore holding personal data (Postgres world/persistence DBs, consent ledgers, voice transcripts, DSAR export bundles, V1 account records), anomalous bulk-read alerts, or a credible external report (researcher, user, press).
- First action SLA: acknowledge within 5 minutes; commander assigned and breach-assessment bridge open within 15 minutes; breach-or-not determination within 4 hours of first credible signal.
First 15 minutes#
- Page
oshun-security-oncall, Lilith-Privacy lead, and the daily incident commander; open the incident bridge and a restricted evidence channel. - Freeze the suspected access path: revoke the implicated credentials or service tokens, do not yet delete accounts or wipe hosts.
- Snapshot evidence (see evidence preservation below) before any remediation that mutates state.
- Start the breach-assessment worksheet: what data classes, what row counts, what regions, what time window.
- Record
operator.security.data_breach.openedin the Operator Console.
Decision tree: is regulator notification required?#
- Is personal data involved? Personal data in V3 scope: V1 account records,
e-mail addresses, payment metadata, voice transcripts
(
VOICE_TRANSCRIPT_RETENTION.md), recordings and consent-ledger entries (RECORDING_CONSENT_FLOW.md), DSAR exports (DSAR_V1_PIPELINE.md), Pixel Streaming session logs with IP addresses. If no → SEV-3 internal security incident, document and close; no regulator clock. - Which residents are affected? Map affected rows to wave-1 regions using
the residency zones in
V3/PER_REGION_RULES.md(us-west,eu-de,uk,in,brstorage zones). A breach of aneu-dereplica is a DE/EU matter even if the attacker entry point was elsewhere. - Is there a risk to rights and freedoms? Encrypted-at-rest data with keys not compromised may be exempt under GDPR Art. 33(1) / UK GDPR — the legal owner makes this call, never engineering alone.
- If notification is required, the per-region clocks and owners below apply simultaneously. Run them in parallel, not in sequence.
Regulator notification matrix (wave-1 regions)#
| Region | Authority | Deadline (from confirmation) | Notification owner | Template |
|---|---|---|---|---|
de |
Competent German state DPA (lead SA, one-stop-shop) | 72 hours (GDPR Art. 33) | Lilith-Privacy lead + EU counsel | V3/legal/templates/breach-notice-gdpr-de.md |
gb |
ICO | 72 hours (UK GDPR Art. 33) | Lilith-Privacy lead + UK counsel | V3/legal/templates/breach-notice-ico-gb.md |
in |
CERT-In, then Data Protection Board of India | 6 hours to CERT-In for cyber incidents; DPDP Board notice without delay as prescribed | Security commander + IN counsel | V3/legal/templates/breach-notice-certin-in.md |
br |
ANPD | 3 working days (ANPD Resolution CD/ANPD 15/2024) | Lilith-Privacy lead + BR counsel | V3/legal/templates/breach-notice-anpd-br.md |
us-ca |
California AG (if >500 CA residents) + affected users | Without unreasonable delay (Cal. Civ. Code §1798.82); AG sample notice when >500 CA residents | Lilith-Privacy lead + US counsel | V3/legal/templates/breach-notice-ca-ag-us.md |
The India CERT-In 6-hour clock is the tightest deadline in the wave-1 set: if
any in-resident data may be affected, the CERT-In draft starts in the first
hour, before full scoping is complete. Partial notices are amended later; missed
clocks are not.
- Steps: contain access path → preserve evidence → scope affected data classes and regions → legal determination → regulator notices per matrix → user notices (per-region template, plain language, what happened / what data / what we did / what you should do) → forced credential rotation for affected accounts via the JWT family-revocation machinery → post-incident review with a written timeline.
- Rollback: none — breach response only moves forward. Re-enable revoked service credentials only after the entry path is closed and verified by an independent reviewer.
- Escalation: legal owner, executive on-call (mandatory for SEV-1), payment provider and cloud TAM if their surfaces are implicated, external forensics retainer if exfiltration is confirmed.
- Evidence preservation: snapshot affected Postgres instances and WAL range
(
pg_basebackupto the locked evidence buckets3://oshun-v3-evidence/<incident-id>/, object-lock compliance mode, 7-year retention), export relevant Redis keyspace (redis-cli --rdb), copy access and audit logs for the incident window ±24 h, hash every artifact (SHA-256 manifest signed by two responders), record chain of custody in the Operator Console case. - Audit event:
operator.security.data_breach.handled - Drill: tabletop required before GA; evidence
drill-data-breach-tabletop-ga(owner: Lilith-Privacy lead)
incident: account-takeover-credential-stuffing#
- Owner: Security incident commander, joint with Identity/BFF on-call
- Severity: SEV-2 for an active stuffing wave; SEV-1 if confirmed takeovers include payment actions, minors' accounts, or instructor/operator accounts
- Trigger: any of the detection signals below crossing threshold on the
v3-bff-auth-abusedashboard, or a confirmed user report of an account they did not access.
Detection signals#
| Signal | Steady-state baseline | Alert threshold |
|---|---|---|
| Login failure ratio (failed / total per 5 min) | < 5% | > 20% sustained 10 min |
| Distinct usernames attempted per source IP per hour | < 5 | > 50 |
| Login attempts from a single /24 (or /48 v6) per min | < 30 | > 300 |
| Successful logins followed by immediate e-mail/payout change | ~0 | > 5 per hour |
| Logins with credentials matching known-breach corpus | ~0 | any spike > 100/hour |
| Impossible travel (login pairs > 900 km/h apart) | < 2/hour | > 20/hour |
First 15 minutes#
- Page security on-call and Identity/BFF on-call; open the bridge.
- Confirm the wave on the dashboard; capture the current attacking IP/ASN set and the credential-list fingerprint (sorted username sample hash).
- Apply rate-limit escalation step 1 (below). Do not block legitimate-login paths wholesale — paid concert windows may be live.
- Snapshot auth logs for the window to the evidence bucket.
- Record
operator.security.account_takeover.opened.
Rate-limit escalation ladder#
Applied at the BFF auth routes (/api/v1/auth/login, /api/v3/lilith/launch),
in order, each step held 15 minutes before the next:
- Tighten per-IP login limit from 10/min to 3/min; enable CAPTCHA on failure-then-retry from the same IP.
- Block the top offending ASNs/CIDRs at the WAF (see
V3/security/ddos-waf-posture.mdrule classauth-protection); require e-mail verification code for logins from never-seen-before device fingerprints. - Enable credential-list defense: deny-and-flag any login whose credential pair matches the observed attack corpus, even when correct — route those users to forced reset.
- (Commander approval required) Temporarily require fresh re-authentication for all sensitive actions platform-wide (payout changes, e-mail changes, DSAR requests).
Forced re-authentication procedure (JWT machinery)#
Per V3/JWT_REFRESH_ROTATION.md, every session has a refresh-token family
(familyId) and one-time-use refresh tokens:
- For each compromised account, revoke all refresh-token families for that user
(
POST /api/v1/auth/admin/revoke-families {userId}). Access JWTs are short-lived, so full session death occurs within the access-token TTL. - Force password reset on next login; invalidate active password if it matched the attack corpus.
- For accounts with confirmed entry: also revoke device registrations, review
payout/e-mail mutations in the incident window, and reverse fraudulent
commerce actions via the
refund-tax-reversaloperator action. - Notify each affected user with the account-takeover template (what we saw, what we reset, payment-impact statement).
- Steps: confirm wave → escalate rate limits stepwise → identify compromised accounts (successful logins from attack infrastructure) → revoke families and force reset → reverse fraudulent mutations → user notices → feed attack corpus fingerprints back into the WAF managed rules.
- Rollback: relax rate-limit steps one at a time after the attack traffic stays below 2x baseline for 60 minutes; CAPTCHA-on-login is the last control removed.
- Escalation: Lilith-Commerce lead when payment actions are involved, Lilith-Privacy lead when the takeover set includes minors, then the data-breach runbook above if the attacker read personal data at scale.
- Evidence preservation: auth logs and WAF logs for the window, attacking IP/ASN
list, credential-corpus fingerprint, list of revoked
familyIds and affecteduserIds, all hashed and manifested in the evidence bucket. - Audit event:
operator.security.account_takeover.handled - Drill: stuffing wave replayed against staging auth before GA; evidence
drill-credential-stuffing-ga(owner: Identity/BFF on-call lead)
incident: jwt-signing-key-compromise#
- Owner: Security incident commander, joint with Realtime Gateway on-call
- Severity: SEV-1 always — a compromised signing key forges any session
- Trigger: signing key material found outside the KMS boundary (repo, log, paste site), a forged-but-valid token observed (valid signature, no matching issuance record), KMS access-policy violation alert, or insider-risk report.
First 15 minutes#
- Page security on-call, Realtime Gateway on-call, and the daily incident commander. Treat as SEV-1 immediately; do not wait for confirmation of active exploitation.
- Confirm which key id (
kid) is implicated and which surfaces trust it: BFF-issued access/refresh JWTs, gateway handshake validation, Pixel Streaming session JWTs (PIXEL_STREAMING_SESSION_START_SUCCESS.md). - Generate the replacement keypair inside KMS (never on a laptop) and stage it unpublished.
- Snapshot current JWKS, KMS audit log, and token-issuance logs to the evidence bucket before rotation mutates state.
- Record
operator.security.jwt_key_compromise.opened.
Emergency rotation — exact order of operations#
Deviating from this order either locks out all users (rotating before validators know the new key) or leaves the attacker valid (revoking issuance but not validation). Execute in order, verifying each step:
- Stage new key: create
kid=N+1in KMS; confirm signing works with a canary token in staging. - Publish dual JWKS: add
kid=N+1to the JWKS endpoint whilekid=Nis still present. Verify all validators (BFF, realtime gateway, pxstream relay) have refreshed their JWKS cache (cache TTL is the gating step — force-refresh via the admin endpoint rather than waiting). - Cut issuance over: switch the BFF and gateway token issuers to sign with
kid=N+1. From this moment no new tokens carry the compromised key. - Hold-down window: wait one access-token TTL so legitimately issued
kid=Naccess tokens age out naturally. During the hold-down, validators log everykid=Ntoken seen, building the blast-radius list. - Revoke
kid=Nvalidation: removekid=Nfrom the JWKS. Every remainingkid=Ntoken — legitimate or forged — is now dead. - Invalidate refresh state: revoke all refresh-token families issued
under
kid=N(the rotation machinery inapps/v3/lilith-realtime-gateway/src/lib.rskeys revocation onfamilyId). Forged refresh tokens have no issuance record and die with the key; legitimate ones die with the family revocation. - Verify: synthetic session-start probes on all 7 POPs and a gateway
handshake probe must succeed with
kid=N+1and fail with a held-backkid=Ntoken.
Session invalidation blast radius#
Step 5+6 logs out every active user — at steady state ~5,000 concurrent sessions
(FLEET_LOAD_VALIDATION.md), of which ~34% are Pixel Streaming sessions on the
7 POPs (TIER_ROUTER_DECISION_DISTRIBUTION.md). Plan for the reconnect
stampede: the reconnect path (RECONNECT_VALIDATION.md) requires a valid resume
token, which is also dead, so all 5,000 sessions come back through full login.
Pre-warm the BFF auth path and enable the surge queue
(V3/launch/launch-surge-plan.md) before executing step 5 if concurrency is
above 2,500. If a ticketed Saraswati concert is live, coordinate the step-5
moment with the event commander — between songs, not mid-set — unless active
exploitation forces immediate execution.
User communications#
- Status page (
/status): "We are performing an emergency security maintenance; you may be signed out and asked to sign in again." Published at step 3, before the mass logout, not after. - No public mention of "key compromise" until the post-incident review and legal sign-off; the honest-but-bounded phrasing is "precautionary credential rotation."
- If forged tokens were used to access personal data, the data-breach runbook above triggers with its notification matrix.
- Rollback: none. A compromised key is never re-trusted. If
kid=N+1proves faulty, roll forward tokid=N+2via the same procedure. - Escalation: executive on-call (mandatory), cloud/KMS provider TAM, external forensics if the leak vector is unknown after 24 hours.
- Evidence preservation: KMS audit logs, JWKS change history, the
kid=Ntoken-sighting log from the hold-down window (this is the forensic record of attacker activity), issuance-record diffs, all hashed and manifested. - Audit event:
operator.security.jwt_key_compromise.rotated - Drill: full rotation executed in staging with 1,000 synthetic sessions before
GA; evidence
drill-jwt-key-rotation-ga(owner: Realtime Gateway lead). The drill must measure JWKS cache-refresh lag on every validator.
incident: voice-recording-privacy-incident#
- Owner: Lilith-Privacy incident commander, joint with Concert capture on-call
- Severity: SEV-1 if a consent-system failure occurred during a live public concert or affected minors; SEV-2 for class/cohort sessions or pre-publication detection
- Trigger: consent-ledger writes failing or disagreeing with the active
recording state, privacy-seat or observer-tier attendees appearing in capture
output, audience-cam cues firing into privacy sections (blocked path in
RECORDING_CONSENT_FLOW.mdobserved open), minor without guardian confirmation present in a recorded session, or a redaction job silently failing.
First 15 minutes#
- Page Lilith-Privacy lead and Concert capture on-call; open the bridge.
- Stop the bleed, keep the show: pause master-capture publication and highlight publication immediately. The performance itself continues — the incident is in the capture/consent plane, not the show plane. If the consent system cannot be trusted at all, stop capture entirely (capture stop, not concert stop).
- Quarantine all segments from the suspected failure window: mark read-only,
block the editorial publication gate, and flag the C2PA signing queue to hold
(
C2PA_EVERY_EXPORT.mdpath) so no signed export leaves with tainted content. - Snapshot the consent ledger, privacy-seat ledger, and capture-worker logs for the window to the evidence bucket — the ledgers are themselves the evidence of what consent existed when.
- Record
operator.security.recording_privacy.opened.
- Steps: quarantine segments → reconstruct ground truth (cross-check consent
ledger vs privacy-seat ledger vs attendance records from the DSAR category set
in
DSAR_V1_PIPELINE.md) → classify each quarantined segment as clean / redactable / destroy → run redaction re-render from the failure timestamp for redactable segments (the mid-session opt-out machinery inRECORDING_CONSENT_FLOW.mdalready supports timestamp-anchored redaction) → destroy unredactable segments with a signed destruction record → re-run the publication gate with a second privacy reviewer → notify affected attendees whose privacy election was violated, with what was captured and what was done. If captured data already left the platform (published highlight, LL-HLS crowd band), execute the takedown cascade via thetakedown-executoroperator action and assess the data-breach runbook trigger — published voice/likeness of a privacy-electing attendee is a personal-data exposure and inherits its notification matrix. - Rollback: resume capture and publication only after the consent system passes
its drill checks (
V3/privacy/recording-consent-drills.jsonscenarios re-run green against production config) and the Privacy lead signs off in the Operator Console. - Escalation: legal owner, Lilith-Rights lead (performer consent may also be implicated), Saraswati Editorial lead for the affected event, minor-safety reviewer whenever a minor is in scope (mandatory, regardless of severity).
- Evidence preservation: consent-ledger and privacy-seat-ledger snapshots, capture-worker logs, quarantined segment hashes, redaction-job inputs and outputs, destruction records, the C2PA hold queue state — all manifested with chain of custody. Quarantined segments are retained in the locked evidence bucket even when destroyed from production, until legal releases the hold.
- Audit event:
operator.security.recording_privacy.handled - Drill: consent-ledger write failure injected during a staged rehearsal concert
before GA; evidence
drill-recording-privacy-ga(owner: Lilith-Privacy lead)
Fail-Closed Criteria#
This runbook set is GA-blocking when any of the following holds:
- any of the four incidents lacks a named commander role, first-15-minutes checklist, evidence-preservation procedure, or audit event;
- the data-breach notification matrix does not cover every wave-1 region in
V3/REGION_ROLLOUT_COMPLIANCE.mdwith an authority, deadline, owner, and template path; - the JWT rotation drill evidence is missing or the staging drill did not measure validator JWKS cache-refresh lag;
- the breach-notice templates referenced in the matrix do not exist under
V3/legal/templates/; - any drill listed above is not recorded green before the GA release-readiness decision.