Two new OpenSpec changes: - observability (30 tasks): health endpoints, Prometheus, Grafana+Loki, structured logging, dashboards, alerting - security-hardening (24 tasks): Caddy headers, scanner blocking, gitleaks, pnpm audit, dependabot, non-root Docker, fail2ban Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
104 lines
3.8 KiB
Markdown
104 lines
3.8 KiB
Markdown
## Context
|
|
|
|
Security audit findings:
|
|
- ✅ Solid: cookies (httpOnly/secure/sameSite), auth (passkeys/JWT), SQL
|
|
injection safe (Drizzle), XSS safe (React), rate limiting on Planner API
|
|
- ❌ Missing: security headers, CSP, secret scanning, dependency auditing,
|
|
non-root Docker, scanner blocking, vulnerability disclosure policy
|
|
- ⚠️ Weak dev-secret fallbacks in code (`"dev-jwt-secret-change-in-production"`)
|
|
|
|
## Goals / Non-Goals
|
|
|
|
**Goals:**
|
|
- Security headers on all responses via Caddy
|
|
- Block known scanner paths at Caddy level (before hitting Node)
|
|
- Gitleaks in CI to catch committed secrets
|
|
- pnpm audit in CI to catch vulnerable dependencies
|
|
- Dependabot for automated dependency updates
|
|
- Non-root user in all Docker containers
|
|
- SECURITY.md for responsible disclosure
|
|
|
|
**Non-Goals:**
|
|
- WAF (web application firewall) — overkill for current scale
|
|
- CSRF tokens — current SameSite + JSON API pattern is sufficient
|
|
- Container image scanning (Trivy etc.) — add later
|
|
- mTLS between containers — Docker network is trusted
|
|
- Removing dev-secret fallbacks — they only activate when env vars are missing,
|
|
which is a deployment bug, not a security issue
|
|
|
|
## Decisions
|
|
|
|
### D1: Security headers in Caddyfile
|
|
|
|
Add a `header` block to both site configs:
|
|
```
|
|
header {
|
|
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
|
X-Content-Type-Options "nosniff"
|
|
X-Frame-Options "DENY"
|
|
Referrer-Policy "strict-origin-when-cross-origin"
|
|
Permissions-Policy "camera=(), microphone=(), geolocation=()"
|
|
}
|
|
```
|
|
|
|
CSP is tricky with React Router's inline scripts. Use a permissive initial
|
|
policy: `default-src 'self'; script-src 'self' 'unsafe-inline'; style-src
|
|
'self' 'unsafe-inline'; img-src 'self' data: https://*.tile.openstreetmap.org;
|
|
connect-src 'self' wss: https://*.sentry.io https://*.ingest.de.sentry.io;`
|
|
|
|
### D2: Scanner path blocking in Caddy
|
|
|
|
Add a `respond` directive before `reverse_proxy` that returns 403 for
|
|
common scanner paths:
|
|
```
|
|
@scanners path /.env* /.git* /wp-* /admin* /config.* /backup* /api/v1*
|
|
respond @scanners 403
|
|
```
|
|
|
|
This prevents the app from even seeing these requests — no Sentry noise,
|
|
no wasted compute.
|
|
|
|
### D3: Gitleaks via GitHub Action
|
|
|
|
Add `gitleaks/gitleaks-action@v2` as a CI step. It scans commits for secrets
|
|
(API keys, tokens, passwords). Runs on every PR. Add `.gitleaks.toml` for
|
|
any false-positive allowlists (e.g., Sentry DSNs are public).
|
|
|
|
### D4: pnpm audit in CI
|
|
|
|
Add `pnpm audit --audit-level=high` as a CI step. Fail only on high/critical
|
|
vulnerabilities to avoid noise from low-severity advisories. Run alongside
|
|
lint/typecheck.
|
|
|
|
### D5: Dependabot for pnpm
|
|
|
|
Create `.github/dependabot.yml` targeting pnpm ecosystem. Weekly schedule,
|
|
grouped by update type. Auto-creates PRs for outdated dependencies.
|
|
|
|
### D6: Non-root user in Dockerfiles
|
|
|
|
Add to the runtime stage of each Dockerfile:
|
|
```dockerfile
|
|
RUN addgroup --system app && adduser --system --ingroup app app
|
|
USER app
|
|
```
|
|
|
|
This limits the impact of container escape vulnerabilities. BRouter needs
|
|
read access to segments, which works since the volume is readable.
|
|
|
|
### D7: Fail2ban on the server
|
|
|
|
Install `fail2ban` on the Hetzner server to block IPs after repeated SSH
|
|
failures or scanner patterns. Configuration via Terraform user-data or
|
|
manual setup documented in deployment docs.
|
|
|
|
## Risks / Trade-offs
|
|
|
|
- **CSP with inline scripts** → React Router uses inline scripts for hydration.
|
|
Must use `'unsafe-inline'` for `script-src`. Can tighten with nonce-based
|
|
CSP later if React Router supports it.
|
|
- **Scanner blocking is path-based** → Won't catch all bots, but eliminates
|
|
the most common credential-harvesting patterns.
|
|
- **Non-root may break file permissions** → Test that the app can still read
|
|
build output and node_modules. Should work since COPY sets root ownership
|
|
by default and files are world-readable.
|