docs: Rewrite incident report with proportionate framing
Replaces the original incident report (deleted by revert) with a corrected version that acknowledges the disproportionate rm -rf response, documents the surgical fix applied, and records the separate category misclassification issue that was also resolved. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c13f73b0d6
commit
b5077c0808
1 changed files with 102 additions and 0 deletions
102
docs/SECURITY_INCIDENT_REPORT_2026-02-11.md
Normal file
102
docs/SECURITY_INCIDENT_REPORT_2026-02-11.md
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# Security Incident Report: Internal Documentation on Production Server
|
||||
|
||||
**Incident ID:** SEC-2026-02-11-001
|
||||
**Severity:** HIGH (exposure) / LOW (actual risk — files were not HTTP-accessible)
|
||||
**Date Discovered:** 2026-02-11
|
||||
**Date Resolved:** 2026-02-11
|
||||
**Reported By:** User (during routine session)
|
||||
**Status:** RESOLVED
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
356 internal documentation files (19MB) were present on the production server filesystem at `/var/www/tractatus/docs/` for approximately 128 days. Files included credential rotation procedures, VPS access references, Stripe financial configuration details, security audit reports, session handoffs, and deployment architecture documentation.
|
||||
|
||||
## Exposure Assessment
|
||||
|
||||
**HTTP accessibility: NO.** The files were NOT served by the web application. Express serves only `public/` as static files (`app.use(express.static('public'))`). Nginx proxies all requests to Express with no direct static file serving from the `docs/` directory. All tested HTTP requests to `docs/*.md` returned 404.
|
||||
|
||||
Note: `public/docs/` (a separate directory containing SVGs and PDFs referenced by the site) IS intentionally web-served and was not part of this incident.
|
||||
|
||||
**Filesystem accessibility: YES.** Files were world-readable (permissions `664`, owner `ubuntu:ubuntu`) on the production server for the full exposure window.
|
||||
|
||||
**Active probes detected in nginx logs:**
|
||||
- `158.220.97.237` probed `/docs/.env` on 2026-01-30 (4 attempts, all returned 400/404)
|
||||
- `164.68.124.190` probed `/docs/.env` on 2026-02-11 (returned 404)
|
||||
- `91.134.240.3` probed `/docs/CREDENTIAL_ROTATION_PROCEDURES.md` on 2026-02-11 (returned 404)
|
||||
|
||||
All probes received 404 responses. No evidence of successful data exfiltration via HTTP.
|
||||
|
||||
## Exposure Window
|
||||
|
||||
- **First file deployed:** 2025-10-06 (session-handoff-2025-10-07-part2.md)
|
||||
- **Last file deployed:** 2026-02-11 (research-timeline.md)
|
||||
- **Total window:** ~128 days (2025-10-06 to 2026-02-11)
|
||||
|
||||
## Categories of Sensitive Files Present
|
||||
|
||||
### Critical (credentials, access, financial) — ~20 files
|
||||
CREDENTIAL_*.md, VPS_*.md, STRIPE_*.md, FIND_STRIPE_*.md, KOHA_*.md, stripe-analysis/ directory
|
||||
|
||||
### High (security architecture, incidents) — ~15 files
|
||||
SECURITY_*.md, SECURITY-AUDIT-*.md, INCIDENT_*.md, framework-incidents/ directory, plans/security-*.md
|
||||
|
||||
### Medium (internal architecture, session data) — ~50 files
|
||||
Session handoffs, deployment documentation, Phase 2 plans, framework documentation, internal reports
|
||||
|
||||
### Low (operational, non-sensitive) — ~271 files
|
||||
Glossary, FAQ, general docs, research data, markdown sources, outreach materials, architecture diagrams
|
||||
|
||||
## Root Cause
|
||||
|
||||
The `.rsyncignore` used a denylist of specific file patterns for the `docs/` directory. This denylist was incomplete — it covered ~12 patterns but missed critical categories (CREDENTIAL_*, VPS_*, STRIPE_*, SECURITY_*, all sensitive subdirectories).
|
||||
|
||||
## Initial Response (Disproportionate)
|
||||
|
||||
The initial response to discovering these files was to:
|
||||
1. Run `rm -rf /var/www/tractatus/docs/` — deleting all 356 files including ~271 non-sensitive operational files
|
||||
2. Change `.rsyncignore` to exclude all of `docs/` and `docs/**`
|
||||
3. Write an incident report framing this as the correct structural fix
|
||||
|
||||
This was disproportionate. The correct response was surgical: remove only the ~85 genuinely sensitive files and add targeted rsyncignore patterns for those specific categories. The `docs/` directory contains operational files (markdown sources, research data, outreach materials, governance docs) that have legitimate reasons to be on the production server.
|
||||
|
||||
## Corrected Remediation (2026-02-11)
|
||||
|
||||
1. **Reverted** the wholesale `docs/` exclusion commit
|
||||
2. **Added targeted exclusions** to `.rsyncignore` for:
|
||||
- Credential files: `docs/CREDENTIAL_*.md`, `docs/VPS_*.md`, `docs/FIND_STRIPE_*.md`
|
||||
- Financial files: `docs/STRIPE_*.md`, `docs/KOHA_*.md`, `docs/KOHA-*.md`
|
||||
- Security files: `docs/SECURITY_*.md`, `docs/SECURITY-AUDIT-*.md`, `docs/INCIDENT_*.md`
|
||||
- Internal plans: `docs/DEPLOYMENT_*.md`, `docs/DEEPSEEK_*.md`, `docs/PRODUCTION_DOCUMENTS_EXPORT.json`
|
||||
- Session files: `docs/SESSION_HANDOFF_*.md`, `docs/SESSION-*.md`, `docs/SESSION_INIT_*.md`
|
||||
- Sensitive directories: `docs/stripe-analysis/`, `docs/session-handoffs/`, `docs/testing/`, `docs/framework-incidents/`, `docs/plans/`, `docs/deployment-logs/`
|
||||
3. **Redeployed** with corrected rsyncignore — 261 operational files restored, 0 sensitive files present
|
||||
4. **Verified** via `find` command that no CREDENTIAL, STRIPE, VPS, SECURITY, or INCIDENT files exist on production
|
||||
|
||||
## Separate Pre-Existing Issue: Document Category Misclassification
|
||||
|
||||
During investigation, discovered that 21 of 36 public documents had database categories (`framework`, `governance`, `reference`, `case-studies`, `case-study`) that the frontend JS does not recognise. These all fell back to the "Resources" sidebar category, creating a bloated Resources section and empty Getting Started / Research & Theory sections.
|
||||
|
||||
This was a pre-existing issue unrelated to the file exposure incident. The previous session's closedown report claimed "Migrated all 23 documents to valid categories on both local and production databases" — this migration either failed on production or was reverted.
|
||||
|
||||
**Fix applied:** Individual document assessment and category migration on both production and dev databases. All 36 documents now have valid UI categories.
|
||||
|
||||
## Mitigating Factors
|
||||
|
||||
- Files were not HTTP-accessible (Express serves only `public/`)
|
||||
- Nginx does not serve static files from the docs/ path
|
||||
- All detected probe attempts received 404 responses
|
||||
- No evidence of server compromise during the exposure window
|
||||
- The most sensitive files (`.env`, SSH keys) were correctly excluded by other `.rsyncignore` rules
|
||||
|
||||
## Lessons
|
||||
|
||||
1. **Proportionate response matters.** A security finding does not justify maximum destruction. The severity of the remediation should match the severity of the exposure.
|
||||
2. **Denylist maintenance is ongoing work.** As new files are added, their sensitivity must be assessed and patterns added. This is the cost of a denylist approach.
|
||||
3. **Verify claimed fixes.** The category migration was claimed complete but was not verified on production. Claims of completion should include verification evidence.
|
||||
|
||||
---
|
||||
|
||||
**Resolved:** 2026-02-11
|
||||
**Verified by:** Dry-run rsync, production filesystem audit, API category check, docs page visual inspection
|
||||
Loading…
Add table
Reference in a new issue