Security Hardening
This page covers the deployment-level security controls that are already implemented in PrimeCalendar and the configuration knobs available to operators. For the overall security roadmap, see SECURITY.md in the repo root.
HTTP Security Headers (Helmet)
The backend uses the helmet npm package (v8) configured in backend-nestjs/src/common/security/security.config.ts. The following headers are set on every response:
| Header | Value |
|---|---|
Content-Security-Policy | defaultSrc 'none'; scriptSrc 'self'; connectSrc 'self' <allowed-origins>; imgSrc 'self' data:; styleSrc 'self'; fontSrc 'self' data:; frameAncestors 'none'; objectSrc 'none'; upgradeInsecureRequests |
Strict-Transport-Security | max-age=31536000; includeSubDomains; preload |
X-Frame-Options | DENY |
Referrer-Policy | strict-origin-when-cross-origin |
Cross-Origin-Embedder-Policy | require-corp |
Cross-Origin-Opener-Policy | same-origin (when origin is HTTPS; suppressed on plain HTTP) |
Permissions-Policy | camera=(), microphone=(), geolocation=(), payment=(), usb=(), bluetooth=(), fullscreen=(self) |
X-Powered-By | Removed (hidePoweredBy: true) |
CSP Report URI
You can configure a CSP violation report endpoint via:
SECURITY_CSP_REPORT_URI=https://your-report-endpoint/csp
The backend also exposes a built-in receiver at POST /api/security/reports/csp that logs violations. Set your CSP report-uri to this endpoint if you want violations captured in the backend logs.
CORS Configuration
CORS is configured from the following environment variables. The allowed origins list is built at startup:
SECURITY_ALLOWED_ORIGINS=https://your-frontend.domain.com
FRONTEND_URL=https://your-frontend.domain.com
The CORS implementation (in security.config.ts) logs a warning to stdout for every blocked origin. In development, localhost:8080 and several other local origins are allowed by default.
In production, always set SECURITY_ALLOWED_ORIGINS explicitly. Leaving it empty allows the development defaults (including localhost) through.
Allowed HTTP methods: GET, POST, PUT, PATCH, DELETE, OPTIONS.
Allowed request headers: Authorization, Content-Type, X-Requested-With, X-Organisation-Id, X-Idempotency-Key, X-API-Key, X-CSRF-Token, X-PrimeCal-Client.
JWT Configuration
JWT_SECRET=<minimum-32-char-random-value>
JWT_ISSUER=cal3-backend
JWT_AUDIENCE=cal3-users
JWT_ACCESS_TTL=900s # 15 minutes
JWT_REFRESH_TTL=1209600s # 14 days
Access tokens (15 min TTL) include iss, aud, and jti claims. Refresh tokens are hashed and stored in the auth_refresh_tokens table and rotated on use. Tokens are delivered via HttpOnly, Secure, SameSite cookies — not localStorage.
Generate a strong secret:
openssl rand -base64 32
A weak or guessable JWT_SECRET compromises all user sessions. This is the single most important secret to protect.
Rate Limiting
Global rate limiting uses @nestjs/throttler. Configure via:
RATE_LIMIT_WINDOW_SEC=60 # Sliding window duration
RATE_LIMIT_MAX_REQUESTS=120 # Max requests per window per IP
LOGIN_MAX_ATTEMPTS=5 # Failed login attempts before lockout
LOGIN_BLOCK_SECONDS=900 # Lockout duration (15 minutes)
The login endpoint has an additional adaptive lockout using LoginAttemptService. After LOGIN_MAX_ATTEMPTS failures from the same IP, further login attempts are blocked for LOGIN_BLOCK_SECONDS.
Input Validation
A global ValidationPipe is registered with whitelist: true, forbidNonWhitelisted: true, and transform: true. This means:
- Unknown properties are stripped from request bodies
- Requests with properties not declared in the DTO are rejected (400)
- DTOs are automatically transformed to their declared types
This is set at the NestJS application level and applies to all controllers.
CSRF Protection
The frontend uses secureFetch from frontend/src/services/authErrorHandler.ts, which injects an X-CSRF-Token header on mutating requests. The backend validates this header when cookies are used. The token is derived from the session rather than a separate token store.
Cross-Origin Opener Policy
Cross-Origin-Opener-Policy: same-origin is set automatically when the configured origins use HTTPS. On plain HTTP (development), the header is suppressed with a warning log:
Cross-Origin-Opener-Policy header suppressed (origin not trustworthy). Use HTTPS or set SECURITY_ENABLE_COOP=true to override.
To force the header even on HTTP (e.g. for testing):
SECURITY_ENABLE_COOP=true
Production Checklist
| Control | Config / Action |
|---|---|
| Strong JWT secret | openssl rand -base64 32, set as JWT_SECRET |
| CORS locked to production origin | SECURITY_ALLOWED_ORIGINS=https://your-frontend.com |
NODE_ENV=production | Enables production log level (info), disables verbose bootstrap logs |
DB_SYNCHRONIZE=false | Never allow TypeORM to auto-alter schema |
DB_SSL=false | Accurate for taseventeeen.tarhely.eu — do not change unless provider adds SSL |
| Rate limiting tuned | Adjust RATE_LIMIT_MAX_REQUESTS and LOGIN_MAX_ATTEMPTS for your traffic |
.env not in git | Verify with git ls-files backend-nestjs/.env — must return empty |
| Helmet headers active | Verify with curl -I http://your-backend/api/feature-flags — look for x-frame-options, content-security-policy |
Known Gaps
The following controls are not yet implemented (tracked in SECURITY.md):
- PostgreSQL Row Level Security — app-layer guards are in place but DB-level RLS is not yet enabled
- GitHub Actions CI security workflow (CodeQL, Semgrep,
npm audit, OWASP ZAP) - Docker hardening (non-root user, read-only filesystem, distroless images)
- Secrets manager integration (Azure Key Vault or equivalent)
- Automated dependency updates (Dependabot)
See SECURITY.md sections 5 and 6 for the implementation roadmap.