Skip to main content
Was this helpful?

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:

HeaderValue
Content-Security-PolicydefaultSrc 'none'; scriptSrc 'self'; connectSrc 'self' <allowed-origins>; imgSrc 'self' data:; styleSrc 'self'; fontSrc 'self' data:; frameAncestors 'none'; objectSrc 'none'; upgradeInsecureRequests
Strict-Transport-Securitymax-age=31536000; includeSubDomains; preload
X-Frame-OptionsDENY
Referrer-Policystrict-origin-when-cross-origin
Cross-Origin-Embedder-Policyrequire-corp
Cross-Origin-Opener-Policysame-origin (when origin is HTTPS; suppressed on plain HTTP)
Permissions-Policycamera=(), microphone=(), geolocation=(), payment=(), usb=(), bluetooth=(), fullscreen=(self)
X-Powered-ByRemoved (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

ControlConfig / Action
Strong JWT secretopenssl rand -base64 32, set as JWT_SECRET
CORS locked to production originSECURITY_ALLOWED_ORIGINS=https://your-frontend.com
NODE_ENV=productionEnables production log level (info), disables verbose bootstrap logs
DB_SYNCHRONIZE=falseNever allow TypeORM to auto-alter schema
DB_SSL=falseAccurate for taseventeeen.tarhely.eu — do not change unless provider adds SSL
Rate limiting tunedAdjust RATE_LIMIT_MAX_REQUESTS and LOGIN_MAX_ATTEMPTS for your traffic
.env not in gitVerify with git ls-files backend-nestjs/.env — must return empty
Helmet headers activeVerify 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.