This document outlines the security measures implemented in the Solstice application.
The application implements multiple layers of security:
- Security Headers - Applied via CloudFront Response Headers Policy (SST/AWS)
- Secure Cookie Configuration - Enhanced Better Auth settings
- Rate Limiting - Protection against brute force attacks
- Password Validation - Strong password requirements
- Content Security Policy (CSP) - Protection against XSS attacks
Security headers are applied at the CDN layer via a CloudFront Response Headers
Policy configured in sst.config.ts. Local development responses (Vite on
localhost) do not include these headers.
- Content-Security-Policy: Static policy applied at CloudFront. Currently allows
unsafe-inlinefor scripts/styles; follow-up work should tighten this with a nonce-based policy. - X-Frame-Options: DENY - Prevents clickjacking attacks
- X-Content-Type-Options: nosniff - Prevents MIME type sniffing
- Referrer-Policy: strict-origin-when-cross-origin - Controls referrer information
- Frame-Ancestors:
'none'(via CSP) - Blocks embedding in iframes - Permissions-Policy: Restricts browser features
- Strict-Transport-Security: Enforces HTTPS with preloading
Better Auth cookies are configured with enhanced security settings:
{
secure: true, // HTTPS only in production
sameSite: "lax", // CSRF protection
httpOnly: true, // No JavaScript access
path: "/", // Available site-wide
domain: process.env.COOKIE_DOMAIN // Optional domain restriction
}The application implements both client-side (TanStack Pacer) and server-side rate limiting for sensitive endpoints. Server-side limits use Redis-backed sliding windows with fallback to in-memory limits when Redis is unavailable.
- Window: 15 minutes
- Max Requests: 5 per window
- Endpoints: Login, registration, password reset, MFA verification
- Window: 15 minutes
- Max Requests: 100 per window
- Endpoints: All API routes
- Exports: BI and audit exports (stricter window, lower max)
- Admin: Role assignment, membership changes, delegated access
Usage example (server-side):
import { enforceRateLimit } from "~/lib/security";
await enforceRateLimit({
bucket: "auth",
route: "auth:sign-in",
userId,
});- Client: Uses node-redis (
redispackage) with TLS and auth token support. - Fail-open default: If Redis is unavailable, rate limits fall back to in-memory counters and caches return uncached data.
- Fail-closed option: Set
REDIS_REQUIRED=true(prod/perf defaults) to return 429s when Redis is down; the app logs arate_limit_unavailablesecurity event. - Key isolation:
REDIS_PREFIXscopes keys by stage to avoid cross-environment collisions.
Strong password validation is enforced:
- Minimum Length: 8 characters
- Required: Uppercase, lowercase, numbers, and special characters
- Strength Meter: 0-5 scale for user feedback
Usage example:
import { validatePassword, getPasswordStrength } from "~/lib/security";
const result = validatePassword(password);
if (!result.isValid) {
// Show errors to user
console.error(result.errors);
}
const strength = getPasswordStrength(password);
// Display strength indicatorThe CSP is configured to:
- Allow self-hosted resources by default
- Allow inline scripts/styles temporarily (
unsafe-inline) until nonce support lands - Explicitly block embedding via
frame-ancestors 'none' - Allow only the external origins required for Square
- Prevent object/embed elements
- Enforce HTTPS upgrades
- CSP reporting is not yet enabled
Nonce-based CSP is not implemented yet. The router already reads a nonce when it is present in the document, but the app does not currently generate or inject one. To move to a strict CSP, we will need per-request nonce generation and a server-side injection path (for example via Lambda@Edge or app-level HTML templating).
Add these optional security-related environment variables:
# Cookie domain restriction (optional)
COOKIE_DOMAIN=.yourdomain.com
# OAuth allowed email domains (comma-separated)
OAUTH_ALLOWED_DOMAINS=yourdomain.com,trusted-partner.comWhen OAUTH_ALLOWED_DOMAINS is set, Google OAuth sign-ins are limited to the specified domains. Users attempting to authenticate with an email outside the allowlist receive a friendly error explaining that an approved organizational address is required. Leave this variable unset (the parser returns an empty array) to allow OAuth sign-ins from any domain during testing.
Security features that differ between environments:
- Cookies use HTTP (not HTTPS-only)
- Email verification not required
- CSP may be more permissive
- CloudFront security headers are not applied to localhost
- Cookies are HTTPS-only
- Email verification required
- CSP enforced at the CDN layer
- HSTS header with preloading
- Headers: Use browser dev tools or
curl -I https://yoursite.com - CSP: Check browser console for violations
- Cookies: Inspect in browser dev tools
- Rate Limiting: Test with rapid requests
- Password Validation: Test with various password combinations
Consider implementing:
- Web Application Firewall (WAF) - Additional protection layer
- Security Monitoring - Log and alert on security events
- 2FA/MFA - Two-factor authentication
- CORP for Signed Downloads - Apply CORP headers to signed S3 downloads
- API Key Management - For service-to-service auth