20 KiB
Incident-Response Runbook
This runbook is the canonical incident-response (IR) playbook for the platform. It covers severity classification, the six-phase IR workflow, the GDPR 72-hour breach-notification workflow, and the communication templates used during and after an incident.
It complements (does not replace) the existing runbook.md, which documents the security-header and CORS contract. When runbook.md references "the IR runbook" or "the security-team", this document is what it means.
Cross-references:
threat-model.md— STRIDE matrix referenced from "Detection"vulnerability-disclosure.md— researcher-side flowpen-test-scope.md— engagement contractsbom.md— dependency-CVE forensics
1. IR-team roster and contacts
The operator maintains the live roster in their secure password manager and on the operator status page. The roles below are required for every incident:
| Role | Responsibility | Channel |
|---|---|---|
| IR Lead | Owns the incident end-to-end; approves all external communication | security@<your-domain>, on-call paging |
| Engineering Lead | Drives containment + eradication; owns the technical fix | oncall@<your-domain> |
| Communications Lead | Owns customer + regulator + press messaging | comms@<your-domain> |
| Data-Protection Officer (DPO) | Owns GDPR Article 33 / 34 decisions and filings | dpo@<your-domain> |
| Legal Counsel | Reviews customer + regulator + press communication; signs the breach-decision record | legal@<your-domain> |
| Scribe | Captures timeline, decisions and evidence into the incident channel | rotated per incident |
For P0 / Sev-1 incidents the IR Lead and Engineering Lead must both be paged within 15 minutes of detection. For P1 / Sev-2 within 1 hour. P2 within 4 hours. P3 next business day.
2. Severity classification
| Severity | Examples | Response SLA | Pages |
|---|---|---|---|
| P0 / Sev-1 | Confirmed data breach with PII exfiltration. Production-down across all tenants. Active exploit of a known vulnerability. Cryptographic-key compromise. Backup-restore failure during a recovery operation. | Acknowledge ≤ 15 min, IR-team assembled ≤ 30 min | IR Lead + Engineering Lead + DPO + Comms |
| P1 / Sev-2 | Confirmed credential compromise scoped to one tenant. Strong suspicion of breach with intact-but-elevated-risk evidence. Service degradation affecting > 25 % of tenants. Audit-verifier chain failure on a recent window. Outbound webhook leaking to an unintended URL. | Acknowledge ≤ 1 h, IR-team assembled ≤ 2 h | IR Lead + Engineering Lead |
| P2 / Sev-3 | Suspicious activity that has not yet escalated to "compromise". Failed audit check on an isolated record. Permission-matrix regression caught in staging. Single-tenant performance regression without data risk. | Acknowledge ≤ 4 h, IR-team assembled within 1 business day | IR Lead |
| P3 / Sev-4 | Low-risk finding from a security researcher (no exploit path). Documentation gap. Operator-procedural drift. | Acknowledge ≤ 1 business day | IR Lead (async) |
Severity is set by the IR Lead based on the evidence at detection time and may be raised during the incident; downgrades require Engineering Lead + DPO concurrence.
3. Six-phase IR workflow
Every incident, regardless of severity, follows the same six phases. P2/P3 incidents may compress steps but must produce the same record.
Phase 1 — Detection
Sources of detection (non-exhaustive):
- Sentry alert (especially
severity >= errorwithpii.scrubbed=falsetags) - Audit-verifier nightly job (
compliance-audit-exportchain-validation) - Metrics alert (login failure spike, queue depth spike, AI cost spike, webhook-delivery dead-letter spike)
- User / customer report via support or status-page
- Researcher report via
vulnerability-disclosure.md - Provider notification (Stripe / Mailjet / OpenAI / Bunny / Sentry / Authentik)
- IR team review of the
threat-model.mdresidual-risk list
Action: open an incident in the operator's incident-tracker, page the IR Lead, set provisional severity, and start the timeline log.
Phase 2 — Triage
The IR Lead, within the severity SLA:
- Confirms or revises the severity
- Confirms the affected surface (use
threat-model.mdcomponent list) - Identifies whether GDPR Article 33 may apply (see §5)
- Identifies whether customer notification is required (Article 34)
- Identifies the on-call IR-team members and pages them
- Starts the dedicated incident Slack channel (
#incident-<YYYY-MM-DD>-<short-id>) - Locks down evidence: snapshot Sentry events, AuditEvent rows, metrics windows, relevant logs
Output: triage record with severity, scope, GDPR-relevance, IR-team roster, evidence pointers.
Phase 3 — Containment
Goal: stop the bleeding without destroying evidence.
Tactical levers (in roughly increasing impact):
- Disable affected feature flag (see
packages/feature-flags) — narrowest blast radius - Disable affected route at API or Web layer via temporary middleware (operator change)
- Disable affected worker queue group (
docs/ops/worker-queues.md) - Disable outbound webhook delivery (operator killswitch documented in
docs/architecture/outbox-pattern.md) - Invalidate sessions for affected users (mass-revoke via DB script; documented in
docs/operations/database-rollback.md) - Rotate API keys / JWT secret / Stripe webhook secret (rotation runbook in operator password manager)
- Block IP ranges at WAF / Coolify edge
- Take service offline behind status-page maintenance — last resort
For each lever applied: record actor, timestamp, lever, scope, expected impact, observed impact in the incident channel.
Phase 4 — Eradication
Goal: remove the root cause.
- Root-cause analysis (RCA) using AuditEvent hash-chain forensics — when
compliance-audit-exportlands, the verifier is the canonical forensics source; until then the operator uses Sentry traces + DB audit rows + metrics - Code fix authored on a feature branch, reviewed by Engineering Lead
- Tests added (regression + non-regression) — coverage gate enforced
- Patch deployed (Coolify auto-deploy or manual
prisma:migrate:deploy) - If the root cause is a leaked secret: rotate, force re-issue, document in the rotation log
Phase 5 — Recovery
Goal: return to normal operation, verified.
- Re-enable feature flags / routes / queue groups disabled in containment
- Validate via metrics + Sentry that the error signal is gone
- Validate via synthetic / E2E (where the operator has explicitly approved E2E for this session) that the customer-facing path works
- Update the status page to "resolved" with the timeline summary
- For credential rotations: confirm 0 active sessions still bound to the rotated secret
Phase 6 — Lessons learned
Goal: institutional memory.
Within 5 business days of recovery, the IR Lead publishes a post-mortem at:
docs/security/incidents/<YYYY-MM-DD>-<short-id>.md
(See §9 for the template. The incidents/ directory is created lazily — first incident is responsible for creating it with a .gitkeep.)
The post-mortem is reviewed at the next IR-team weekly. Action items are tracked in the improvement plan with phase + commit category. The threat-model.md is updated if a new control class was added or a residual risk needs to be re-classified.
4. Forensics
Forensics relies on three sources of truth, in order of preference:
- AuditEvent hash-chain (canonical; tamper-evident once
compliance-audit-exportlands its verifier) - Sentry events + breadcrumbs (PII-scrubbed; subject to retention)
- Metrics + logs (high-volume; subject to retention + sampling)
Evidence rules:
- Snapshot before changing anything. The triage step creates the evidence pointer record.
- Do not paste raw secrets / customer PII / tenant identifiers / production tokens into the incident channel. Use generic labels per the rules in
SECURITY.md"Safe evidence". - Preserve operator audit-trail (admin actions during the incident are themselves auditable).
- For DB-level evidence: prefer
pg_dump --tableof a single table + WHERE clause overSELECTinto the channel. Encrypt and attach to the incident record.
5. GDPR 72-hour breach-notification workflow
This workflow applies whenever a confirmed or suspected breach involves personal data of EU / UK / EEA data subjects.
5.1 Decision flow
flowchart TD
Detection["Confirmed breach<br/>(Article 4(12))"]
Q1{"Is risk to rights and<br/>freedoms unlikely?"}
Q2{"Is risk high?"}
NoNotify["Document decision;<br/>no notification required"]
Notify33["Notify supervisory authority<br/>within 72 h (Art. 33)"]
Notify34["Notify data subjects<br/>without undue delay (Art. 34)"]
Detection --> Q1
Q1 -- "yes" --> NoNotify
Q1 -- "no" --> Notify33
Notify33 --> Q2
Q2 -- "yes" --> Notify34
Q2 -- "no" --> Done["Done — keep record"]
Notify34 --> Done
The DPO owns the decision at each branch and records the rationale in writing. The Legal Counsel signs off before any external notification leaves the operator.
5.2 72-hour clock
The 72-hour clock starts when the operator becomes aware of a breach (Article 33(1)). "Aware" means reasonable certainty that a breach affecting personal data has occurred — not certainty about scope or attribution.
| Hour | Required state |
|---|---|
| H+0 | Incident opened, IR-team paged, DPO notified |
| H+4 | Initial scope assessment — affected categories of data subjects, categories of data, approximate volume |
| H+24 | Containment in place or documented infeasibility |
| H+48 | Article 33 notification drafted and Legal-Counsel reviewed |
| H+72 | Article 33 notification filed with the competent supervisory authority |
If the operator cannot meet H+72 with complete information, file an initial notification with the available facts and state the reason for the delay (Article 33(4)).
5.3 Supervisory-authority filing
For German tenants the default supervisory authority is the competent state DPA (LDI) of the tenant's registered location, or the Federal Commissioner for Data Protection and Freedom of Information (BfDI) when federal jurisdiction applies. The DPO maintains the current authority mapping in the operator password manager.
The filing must include (Article 33(3)):
- Nature of the breach, categories and approximate numbers of data subjects and records concerned
- Name and contact details of the DPO or other contact point
- Likely consequences
- Measures taken or proposed to address the breach and mitigate adverse effects
5.4 Article 34 data-subject notification
Required when the breach is likely to result in a high risk to data subjects (Article 34(1)). Not required if:
- Encryption / pseudonymisation renders the data unintelligible (Article 34(3)(a))
- Subsequent measures ensure the high risk is no longer likely (Article 34(3)(b))
- It would involve disproportionate effort — in which case a public communication is made instead (Article 34(3)(c))
Notification template: see §7.2.
5.5 Breach-decision record (BDR)
Every breach — notified or not — produces a BDR stored at:
docs/security/incidents/<YYYY-MM-DD>-<short-id>/breach-decision-record.md
The BDR captures: scope assessment, Article 33 decision + rationale, Article 34 decision + rationale, DPO signature, Legal-Counsel signature, IR-Lead signature, supervisory-authority filing reference number (if filed), customer-notification dispatch reference (if dispatched). The BDR is not published; it is retained for the operator's GDPR-compliance retention period.
6. Mass customer notification workflow
When Article 34 (or operator policy) requires mass notification:
- The Comms Lead drafts the message from the template (§7.2)
- Legal Counsel reviews
- IR Lead approves
- The Engineering Lead enqueues a dedicated mail batch through the worker mail queue with the CRITICAL priority (see
docs/architecture/job-priority.md) - The mail batch is sent to the affected data-subject cohort only — selection script reviewed by Engineering Lead, peer-reviewed by IR Lead, stored as evidence
- The send is monitored: per-second send rate kept within Mailjet reputation thresholds; bounce + complaint webhooks watched live
- Status-page notice published before send-start
For breaches affecting > 10 000 data subjects, the operator may instead publish a public communication on the operator's security page (Article 34(3)(c)) — Legal Counsel signs off.
7. Communication templates
Templates are intentionally generic — they are filled in by the Comms Lead at incident time. Do not include real customer names, tenant identifiers, or production credentials in the live message.
7.1 Internal team — incident open
Subject: [INCIDENT][P<severity>] <short-headline>
Severity: P<severity>
Incident channel: #incident-<YYYY-MM-DD>-<short-id>
IR Lead: <name>
Engineering Lead: <name>
DPO involved: <yes/no>
Status: triage
Summary
- What we know:
- What we don't know:
- What we're doing now:
- Next checkpoint: <time>
If you are on the IR roster, join the channel. If you are not on the roster but
have data relevant to this incident, post in the channel.
Do not paste customer data, tokens, or credentials. Use the generic-label rules
in docs/security/runbook.md.
7.2 Customer / data-subject breach notification
Subject: Important security notice regarding your <product> account
Dear <Customer / Data subject>,
We are writing to inform you of a security incident that may have affected
information associated with your <product> account.
What happened
On <date>, our security team identified <short factual description>. We took
the following steps to contain the incident: <bullet list>.
What information was involved
The incident may have involved the following categories of information about
your account: <bullet list of categories — e.g. "email address", "encrypted
password hash", "tenant configuration metadata">. We have no evidence that
<categories that were not involved, if known>.
What we are doing
- <containment step>
- <eradication step>
- <recovery step>
- We have <notified / not notified> the competent supervisory authority under
Article 33 GDPR.
What you can do
- <action 1 — e.g. "rotate any API keys associated with your account">
- <action 2 — e.g. "review your audit log at <URL>">
- <action 3 — e.g. "contact us with any questions at security@<your-domain>">
For more information, please see our security advisory at
https://app.<your-domain>/security/advisories/<advisory-id>.
We are sorry for this incident and for the disruption it may cause. If you have
questions, please reply to this email or write to security@<your-domain>.
The <product> security team
7.3 Status-page post
[INVESTIGATING] <short-headline>
We are currently investigating <generic description>. Customer impact is
<scope description>. We will post an update by <time>.
[IDENTIFIED] <short-headline>
We have identified the cause as <generic description>. We are <action>.
Estimated time to resolution: <time>.
[MONITORING] <short-headline>
A fix has been deployed. We are monitoring metrics. We will post a final
update when we are confident the incident is resolved.
[RESOLVED] <short-headline>
The incident is resolved. A post-mortem will be published at
https://app.<your-domain>/security/advisories/<advisory-id> within 5 business
days.
7.4 Press statement (P0 only, CEO-approved)
On <date>, <company> identified a security incident affecting <generic
description of scope>. We took immediate steps to contain the incident,
notified affected customers, and reported the incident to the competent
supervisory authority as required by GDPR.
We are committed to the security of our customers' data. We are conducting
a full post-incident review and will publish a public advisory at
https://app.<your-domain>/security/advisories/<advisory-id> within five
business days.
For media enquiries: press@<your-domain>.
For customer enquiries: security@<your-domain>.
7.5 Supervisory-authority filing (Article 33)
The DPO uses the supervisory authority's own form (e.g. BfDI online portal). The form maps to the Article 33(3) requirements listed in §5.3. The filing reference number is recorded in the BDR.
8. Drills
The operator runs IR drills at least every six months. A drill consists of:
- A synthetic detection event (e.g. a planted Sentry alert against a
pentest:*tag) - Real-time response with the full IR-team roster
- Tabletop walk-through of the six-phase workflow
- Article 33 / 34 decision exercise (using a synthetic data-subject cohort)
- Communication-template fill-in (no actual send)
- Drill post-mortem at
docs/security/incidents/drills/<YYYY-MM-DD>-drill.md
A drill is not a substitute for a real incident's post-mortem; it is preparation.
9. Post-incident review template
A post-mortem is published at docs/security/incidents/<YYYY-MM-DD>-<short-id>.md within 5 business days of recovery. The template:
# Incident: <short-headline>
- **Date:** <YYYY-MM-DD>
- **Severity:** P<n>
- **Duration (detection → resolved):** <h:mm>
- **IR Lead:** <name>
- **Engineering Lead:** <name>
- **DPO involved:** <yes/no>
- **Customer impact:** <scope>
- **GDPR Article 33 filed:** <yes/no — reference if filed>
- **GDPR Article 34 sent:** <yes/no — count if sent>
## Summary
<2–3 sentences>
## Timeline
| Time (UTC) | Event |
| --- | --- |
## Detection
How did we find out? Which signal fired? Could we have found out earlier?
## Root cause
Technical root cause + contributing factors. Reference STRIDE component from
threat-model.md if applicable.
## Containment
What did we do, in what order? What worked, what did not?
## Eradication
Code/config change(s) that closed the root cause. Link to commits + PRs.
## Recovery
How did we verify normal operation?
## What went well
- …
## What went badly
- …
## Action items
| # | Action | Owner | Due |
| --- | --- | --- | --- |
## Threat-model updates
Did this incident reveal a new control class or move a residual risk into
"accepted" or "mitigated"? Link to the threat-model.md edit.
## Customer communication
- Article 34 notification: <yes/no, dispatch reference>
- Status-page post: <link>
- Public advisory: <link>
10. Break-glass procedures
Some operations require break-glass access (e.g. direct DB write, secret rotation outside the rotation cadence, IdP fallback). These are always P1+ by definition and trigger:
- Page IR Lead + Engineering Lead + DPO simultaneously
- Open incident channel before the break-glass action
- Two-person approval recorded in the channel
- AuditEvent emitted from the operator-tooling executing the action
- Post-action review within 24 hours
- Break-glass-credential rotation within 24 hours
Documented break-glass procedures:
- Direct DB write — see
docs/operations/database-rollback.md - Stripe webhook-secret rotation — operator password-manager runbook
- JWT-secret rotation — operator password-manager runbook (mass-revoke required)
- IdP fallback (Authentik down) — fall back to local admin credential; documented in
infra/authentik/README.md - Backup restore in production —
docs/ops/backup-cli.mdrestoresection - Tenant-snapshot restore —
docs/ops/tenant-snapshot.mdrestoresection