Fix picomatch audit vulnerability and complete security hardening verification
Override picomatch to 4.0.4 to resolve high-severity ReDoS (GHSA-c2c7-rcm5-vvqj), verify all security hardening tasks (gitleaks, pnpm audit, Docker non-root, Caddy headers, scanner blocking), sync specs, and archive the change. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
433886772f
commit
b6908fb288
10 changed files with 604 additions and 28 deletions
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-03-25
|
||||
104
openspec/changes/archive/2026-03-26-security-hardening/design.md
Normal file
104
openspec/changes/archive/2026-03-26-security-hardening/design.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
## 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.
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
## Why
|
||||
|
||||
A security audit found several gaps: no security headers (HSTS, CSP, etc.),
|
||||
no secret scanning in CI, no dependency vulnerability scanning, Docker
|
||||
containers running as root, and bot scanners probing for `.env` files with
|
||||
no filtering. The cookie and auth setup is solid, but the infrastructure and
|
||||
CI layers need hardening.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Security headers**: HSTS, X-Content-Type-Options, X-Frame-Options,
|
||||
Referrer-Policy, Permissions-Policy via Caddyfile
|
||||
- **Content-Security-Policy**: Restrict script/style/font sources
|
||||
- **Gitleaks**: Secret scanning in CI to prevent credential leaks
|
||||
- **Dependency auditing**: `pnpm audit` in CI + Dependabot for automated updates
|
||||
- **Docker hardening**: Non-root user in all Dockerfiles
|
||||
- **Bot/scanner blocking**: Caddy matcher to reject known scanner paths
|
||||
(`.env`, `.git`, `wp-config`, etc.) with 403 before hitting the app
|
||||
- **Fail2ban or equivalent**: Rate-limit SSH brute force and scanner IPs
|
||||
at the server level
|
||||
- **SECURITY.md**: Vulnerability disclosure policy
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `security-hardening`: Security headers, CI secret/dependency scanning,
|
||||
Docker non-root, scanner blocking, server-level rate limiting
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `infrastructure`: Caddy security headers + scanner blocking, Docker non-root,
|
||||
Terraform firewall adjustments, CI scanning steps
|
||||
|
||||
## Impact
|
||||
|
||||
- **Caddyfile**: Security headers + scanner path blocking
|
||||
- **Dockerfiles**: Add non-root user (journal, planner, brouter)
|
||||
- **CI**: Add gitleaks step, pnpm audit step
|
||||
- **Repo**: Add `.gitleaks.toml`, `dependabot.yml`, `SECURITY.md`
|
||||
- **Server**: Optional fail2ban or UFW configuration
|
||||
- **Dependencies**: None for app code; gitleaks is a CI action
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Service configuration
|
||||
Each service SHALL be configured with security best practices including non-root execution and security headers.
|
||||
|
||||
#### Scenario: Caddy security headers
|
||||
- **WHEN** Caddy proxies a request
|
||||
- **THEN** it adds HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, and Permissions-Policy headers
|
||||
|
||||
#### Scenario: Caddy scanner blocking
|
||||
- **WHEN** a request matches known scanner paths (.env, .git, wp-config, etc.)
|
||||
- **THEN** Caddy returns 403 without forwarding to the application
|
||||
|
||||
### Requirement: CI/CD pipeline
|
||||
GitHub Actions SHALL include security scanning steps alongside build and test.
|
||||
|
||||
#### Scenario: Gitleaks scan
|
||||
- **WHEN** a PR is opened
|
||||
- **THEN** gitleaks scans for committed secrets
|
||||
|
||||
#### Scenario: Dependency audit
|
||||
- **WHEN** CI runs
|
||||
- **THEN** pnpm audit checks for high/critical vulnerabilities
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Security response headers
|
||||
All HTTP responses SHALL include security headers to protect against common web attacks.
|
||||
|
||||
#### Scenario: HSTS header
|
||||
- **WHEN** a browser receives a response from trails.cool
|
||||
- **THEN** the response includes `Strict-Transport-Security: max-age=31536000; includeSubDomains; preload`
|
||||
|
||||
#### Scenario: Content sniffing prevention
|
||||
- **WHEN** a browser receives a response
|
||||
- **THEN** the response includes `X-Content-Type-Options: nosniff`
|
||||
|
||||
#### Scenario: Clickjacking prevention
|
||||
- **WHEN** a browser receives a response
|
||||
- **THEN** the response includes `X-Frame-Options: DENY`
|
||||
|
||||
### Requirement: Scanner path blocking
|
||||
Known vulnerability scanner paths SHALL be blocked at the reverse proxy level before reaching the application.
|
||||
|
||||
#### Scenario: Env file scanner
|
||||
- **WHEN** a request is made to `/.env`, `/.env.local`, `/.env.prod`, or similar paths
|
||||
- **THEN** Caddy returns 403 without forwarding to the application
|
||||
|
||||
#### Scenario: Git config scanner
|
||||
- **WHEN** a request is made to `/.git/config` or `/.git/HEAD`
|
||||
- **THEN** Caddy returns 403 without forwarding to the application
|
||||
|
||||
### Requirement: Secret scanning in CI
|
||||
The CI pipeline SHALL scan commits for accidentally committed secrets.
|
||||
|
||||
#### Scenario: Secret detected
|
||||
- **WHEN** a PR contains a committed API key, token, or password
|
||||
- **THEN** the CI pipeline fails with a clear message indicating the leaked secret
|
||||
|
||||
#### Scenario: Known public values allowed
|
||||
- **WHEN** a known-public value (e.g., Sentry DSN) is committed
|
||||
- **THEN** gitleaks allows it via the `.gitleaks.toml` allowlist
|
||||
|
||||
### Requirement: Dependency vulnerability scanning
|
||||
The CI pipeline SHALL check for known vulnerabilities in dependencies.
|
||||
|
||||
#### Scenario: High severity vulnerability
|
||||
- **WHEN** a dependency has a high or critical vulnerability advisory
|
||||
- **THEN** the CI pipeline fails
|
||||
|
||||
### Requirement: Non-root Docker containers
|
||||
Application containers SHALL run as a non-root user.
|
||||
|
||||
#### Scenario: Container user
|
||||
- **WHEN** a container starts
|
||||
- **THEN** the process runs as a non-root user (not UID 0)
|
||||
|
||||
### Requirement: Vulnerability disclosure policy
|
||||
The repository SHALL include a SECURITY.md with responsible disclosure instructions.
|
||||
|
||||
#### Scenario: Security contact
|
||||
- **WHEN** a security researcher finds a vulnerability
|
||||
- **THEN** SECURITY.md provides clear instructions for reporting it
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
## 1. Caddy Security Headers
|
||||
|
||||
- [x] 1.1 Add security headers block to Caddyfile: HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy
|
||||
- [x] 1.2 Add Content-Security-Policy header (permissive initial policy allowing React Router inline scripts, OSM tiles, Sentry, WebSocket)
|
||||
- [x] 1.3 Test headers with securityheaders.com or curl after deploy
|
||||
|
||||
## 2. Scanner Blocking
|
||||
|
||||
- [x] 2.1 Add Caddy path matcher for common scanner targets (.env*, .git*, wp-*, admin*, config.*, backup*)
|
||||
- [x] 2.2 Return 403 for matched paths before reverse_proxy
|
||||
- [x] 2.3 Verify scanner paths return 403, normal paths still work
|
||||
|
||||
## 3. CI Secret Scanning
|
||||
|
||||
- [x] 3.1 Add gitleaks/gitleaks-action@v2 step to CI workflow
|
||||
- [x] 3.2 Create .gitleaks.toml with allowlist for Sentry DSNs (public values)
|
||||
- [x] 3.3 Verify gitleaks passes on current codebase
|
||||
|
||||
## 4. Dependency Auditing
|
||||
|
||||
- [x] 4.1 Add `pnpm audit --audit-level=high` step to CI workflow
|
||||
- [x] 4.2 Create .github/dependabot.yml for weekly pnpm dependency updates
|
||||
- [x] 4.3 Verify pnpm audit passes on current codebase
|
||||
|
||||
## 5. Docker Hardening
|
||||
|
||||
- [x] 5.1 Add non-root user to Journal Dockerfile runtime stage
|
||||
- [x] 5.2 Add non-root user to Planner Dockerfile runtime stage
|
||||
- [x] 5.3 Add non-root user to BRouter Dockerfile
|
||||
- [x] 5.4 Test containers start and serve correctly as non-root
|
||||
|
||||
## 6. Server Hardening
|
||||
|
||||
- [x] 6.1 Install and configure fail2ban on Hetzner server (SSH protection)
|
||||
- [x] 6.2 Configure UFW firewall (allow 22, 80, 443 only)
|
||||
- [x] 6.3 Document server hardening steps in deployment docs
|
||||
|
||||
## 7. Documentation
|
||||
|
||||
- [x] 7.1 Create SECURITY.md with vulnerability disclosure policy and security contact
|
||||
- [x] 7.2 Update privacy manifest with security practices documented
|
||||
|
||||
## 8. Verify
|
||||
|
||||
- [x] 8.1 Run securityheaders.com scan on trails.cool and planner.trails.cool
|
||||
- [x] 8.2 Verify scanner paths return 403 in production
|
||||
- [x] 8.3 Verify CI passes with all new scanning steps
|
||||
- [x] 8.4 Verify Docker containers run as non-root
|
||||
Loading…
Add table
Add a link
Reference in a new issue