Security posture
Application-, container-, and CI/CD-layer security practices for the Digital Sovereignty Heatmap.
Security Posture
This project follows security best practices across application, container, and CI/CD layers.
Current Measures
HTTP Security Headers
Applied via Next.js middleware (src/middleware.ts) to all routes:
| Header | Value |
|---|---|
| Content-Security-Policy | default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' fonts.googleapis.com; font-src 'self' fonts.gstatic.com; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self' |
| X-Frame-Options | DENY |
| X-Content-Type-Options | nosniff |
| Referrer-Policy | strict-origin-when-cross-origin |
| Permissions-Policy | camera=(), microphone=(), geolocation=() |
API routes additionally receive Cache-Control: no-store.
Container Security
- Multi-stage Docker build — production image contains only runtime dependencies
- Non-root user (
nextjs, UID 1001) in the runtime stage - Read-only root filesystem (
read_only: truein docker-compose) - Resource limits: 512 MB memory, 1 CPU
- Restrictive data directory permissions (750)
- Alpine-based images for minimal attack surface
- OCI image labels for provenance
- Healthcheck built into the container
CI/CD Security
- Trivy — container image vulnerability scanning (CRITICAL + HIGH)
- gitleaks — secret detection in git history
- pnpm audit — dependency vulnerability scanning
- Minimal GitHub Actions permissions (
contents: readfor CI) - Release workflow scans image before pushing to GHCR
Application Security
- Environment-based configuration (no secrets in code)
.envfiles excluded from version control- Request body size limits: 16 KB (memo), 2 KB (telemetry)
- Rate limiting on POST endpoints (in-memory token bucket, per client IP)
- Zod schema validation on all POST request bodies
- No prompt payloads logged in Claude integration
Telemetry & Privacy
Telemetry is disabled by default (TELEMETRY_ENABLED=false). When enabled, the following privacy guarantees apply:
| Guarantee | Implementation |
|---|---|
| No raw events stored | Server increments aggregated counters only (metrics_counters table) |
| Strict allowlists | Every field is validated against a fixed enum — no free text accepted |
| No IP addresses stored | Client IP is used for rate limiting only (in-memory, never persisted) |
| No personal data | No usernames, emails, sessions, or device fingerprints |
| Double opt-in | Server must have TELEMETRY_ENABLED=true AND user must opt in via UI toggle |
| k-anonymity | Public /trends page only shows buckets with count >= K_ANON_THRESHOLD (default 25); smaller buckets are grouped as "Other" |
| Score bands only | Raw scores (0-100) are bucketed into 3 bands (0-33, 34-66, 67-100) before transmission |
What is collected (when both server and user opt in):
- Event type:
assessment_completed,vendor_selected,memo_generated - Score band (not raw score)
- Category and vendor slug (from curated allowlist only)
- Context enum values (sector, data classification, criticality, EU residency)
What is NOT collected:
- IP addresses, user agents, or device identifiers
- Raw assessment scores
- Free-text input of any kind
- Timing data, session identifiers, or referrer information
Reporting Vulnerabilities
If you discover a security vulnerability, please report it responsibly by opening a private security advisory on the GitHub repository.
Future Considerations
- Authentication & authorisation strategy
- CORS policy for cross-origin API access
- Dependency update automation (Dependabot / Renovate)