# Security statement

**Doc type:** Public security overview. Shared with enterprise procurement on request; summarised in [`/privacy.html`](../privacy.html) § 9.
**Last reviewed:** 2026-05-26
**Owner:** Controller / founder.
**Scope:** All systems operated by win2linux that process personal data on behalf of learners, managers, or enterprise customers.

## 1. Posture summary

| Domain | Status |
|---|---|
| **TLS in transit** | Enforced everywhere. HSTS preload. TLS 1.2+. Certificate via Let's Encrypt, auto-renewed |
| **Encryption at rest** | AES-256 across Supabase Postgres + Supabase Storage + Netlify build artefacts |
| **Authentication** | Email + bcrypt password (cost factor 12) + optional TOTP MFA (RFC 6238) |
| **Session management** | HttpOnly + Secure + SameSite=Lax JWTs; server-side session table for revocation; 24 h default TTL |
| **Authorisation** | Postgres Row-Level Security policies (defence in depth) + application-level customer-scope enforcement in every endpoint |
| **Secrets management** | All credentials in Netlify environment variables; never in repo; rotated quarterly |
| **Backups** | Supabase Pro tier — Point-in-Time Recovery enabled; daily snapshots retained 30 days |
| **Monitoring** | Sentry for application errors; Supabase Logs for DB; Netlify Functions logs; cron-driven daily sanity checks |
| **Incident response** | 1-hour escalation, 12-hour severity classification, 72-hour CNIL notification for HIGH-severity breaches |
| **Vulnerability management** | Dependabot weekly; npm audit on every CI run; manual quarterly review of OWASP Top 10 |
| **Access control (operator)** | Single-operator org. Hardware-key 2FA enforced on Supabase, Netlify, Stripe, Resend, GitHub, domain registrar. No shared credentials. Recovery codes stored offline |

## 2. Application security controls

### 2.1 Input validation
All Netlify Function endpoints validate input shape before any database call:
- Email format (regex + length cap)
- SIRET (14-digit Luhn checksum, verified against INSEE)
- Numeric ranges (seat_count between 5 and 100,000)
- Free-text length caps (billing_address ≤ 2000 chars, etc.)
- File upload size cap (10 MB) + content-type whitelist (`application/pdf`)

### 2.2 Output escaping
HTML output uses the canonical `escHtml()` helper (enforced by static-scan test `tests/static-scan.js` — every page that builds HTML strings must use it). React-style JSX-free templates use parameterised query placeholders for SQL.

### 2.3 SQL injection
All database access goes through `_db.mjs` (postgres-js) using tagged-template queries — parameters are bound, never interpolated. Where positional `unsafe()` is used, parameters are passed as an array, never concatenated.

### 2.4 CSRF
- Manager portal: SameSite=Lax session cookie + origin check on every POST.
- Devis flow: anonymous endpoints rely on per-ref bearer-soft tokens + rate-limiting; no cross-origin write attack surface exists (no shared browser session).

### 2.5 Rate-limiting
- Sign-up + login + password-reset: 5 attempts per email per hour + 20 per IP per hour
- Devis creation: 5 per email per hour + 20 per IP per hour
- PO upload: 10 per ref per hour
- All rate limits backed by `rate_limits` table; daily prune cron

### 2.6 Password hardening
- bcrypt cost factor 12
- Minimum 12 chars (signup form-level + server-level)
- Strength meter feedback (zxcvbn-style)
- No password reuse check (would require third-party HaveIBeenPwned API — roadmap)
- Failed-login count tracked; account temp-locked at 5 failures (15-min cooldown)

### 2.7 MFA
- Optional TOTP for individual learners
- Mandatory TOTP for all manager portal accounts (enforced at first login)
- Secret stored encrypted; recovery codes printable at enrolment
- No SMS — TOTP only (avoids SIM-swap risk)

### 2.8 Magic links
- 32-byte cryptographically-random token, SHA-256 hashed on the server
- 24-hour expiry for the initial provisioning link; 30-minute for one-shot reset links
- Single-use semantics (consumed on first verify)

## 3. Infrastructure security

### 3.1 Hosting
- Static site + serverless functions: Netlify (EU edge regions where supported)
- Database: Supabase Postgres in Frankfurt (eu-central-1)
- Object storage: Supabase Storage in the same region
- Email: Resend, EU residency configured
- Payments: Stripe Payments Europe Ltd, Ireland

### 3.2 Network
- TLS 1.2+ enforced; TLS 1.0 / 1.1 disabled
- HSTS with `max-age=63072000; includeSubDomains; preload`
- Content-Security-Policy: locked-down `default-src 'self'`, with explicit allow-list for Stripe.js, Plausible, course assets
- X-Frame-Options: DENY (course pages); SAMEORIGIN (legal pages)
- Referrer-Policy: strict-origin-when-cross-origin
- Permissions-Policy: deny camera, microphone, geolocation, payment (except where Stripe.js requires)

### 3.3 Database
- Connection via PgBouncer (Supabase pooler) on port 6543, transaction mode, with `sslmode=require`
- Row-Level Security enabled on every table reachable from a browser (see `db/migrations/0004_rls.sql`)
- Service-role key restricted to Netlify Function environment; never exposed to browser
- Daily automated backups + Point-in-Time Recovery (Supabase Pro tier)
- Cross-region read replica: not enabled (single-region by design, EU-only)

### 3.4 Object storage
- Two private buckets (`devis-pdfs`, `po-uploads`)
- No public read access
- Signed URLs minted on-demand with 1-hour TTL
- Service-role JWT required for write

## 4. Operational security

### 4.1 Code change discipline
- All production changes via Git commits to `master`
- Static-scan test (`tests/static-scan.js`) gates every commit on: escHtml usage, parsable inline scripts, tabnabbing-safe `target="_blank"`, no inline event handlers
- Unit + Playwright tests run before push
- No direct DB edits in production — all changes via migration files in `db/migrations/` reviewed before apply

### 4.2 Secrets handling
- All secrets in Netlify environment variables, scoped per-environment
- Rotated quarterly + on suspected exposure
- Repo scanned by `git-secrets` pre-commit hook + GitHub secret scanning
- `.env` files never committed (`.gitignore` enforced)

### 4.3 Logging
- Application errors: Sentry, retained 90 days
- Function execution logs: Netlify, retained per Netlify default
- Database query logs: Supabase Logs, retained per Supabase tier
- Audit logs: customer-facing actions (export CSV, code revoke, manager invite) logged with actor + timestamp + scope

### 4.4 Monitoring
- Sentry alert on any 5xx in production
- Supabase alert on connection-pool saturation
- Cron-driven sanity checks (e.g. zero-quote-yesterday alert, stale-session prune)
- Operator pager: email + SMS via Twilio for HIGH-severity alerts (roadmap)

## 5. Vulnerability disclosure

Security researchers can report vulnerabilities via `security@win2linux.org`. We commit to:
- Acknowledge within 5 working days
- Publish a fix or workaround for verified vulnerabilities within 30 days, or sooner for HIGH severity
- Credit the reporter (with their consent) once a fix is in place
- No legal action for good-faith research

## 6. Standards alignment

We are not currently certified against ISO 27001, SOC 2, or SecNumCloud. We do follow the relevant controls in spirit, and we are open to a formal audit at the request of a customer who needs it for their own compliance. The roadmap (doctrine gate #5 — ANSSI hardening) includes a formal ANSSI guide d'hygiène compliance review and, for Tier 2 customers, SecNumCloud-qualified hosting via OVH.

## 7. Sub-processor security

See [`sub-processors.md`](sub-processors.md). Each sub-processor's own certifications (Supabase: SOC 2 + ISO 27001; Netlify: SOC 2; Stripe: PCI DSS Level 1; Resend: SOC 2; Anthropic: SOC 2) are part of our risk acceptance.

## 8. Incident response

| Severity | Definition | Notification target | Notification window |
|---|---|---|---|
| **HIGH** | Confirmed breach affecting personal data or active exploitation | CNIL + affected data subjects | CNIL within 72 h; subjects within 5 working days |
| **MEDIUM** | Suspected breach; service compromise without confirmed personal-data exposure | Operator + affected enterprise customers | Within 24 h of confirmation |
| **LOW** | Misconfiguration, near-miss, control failure with no exposure | Operator only | Internal review only |

Incident log retained 5 years (linked to RGPD breach evidence requirement).

## 9. Customer-side controls

Enterprise customers should:
- Use a strong password + enforce MFA on their manager accounts (we enforce TOTP at first login)
- Treat exported CSV files as confidential — they contain learner-identifiable progress data
- Limit the number of authorised managers to the minimum required
- Notify us at `support@win2linux.org` if a manager leaves the organisation (we revoke access promptly)

## Changelog

| Date | Change |
|---|---|
| 2026-05-26 | Initial production statement covering Supabase EU cutover. |
