Skip to content

Security

Authentication & Access Control

Cloudflare Access (Zero Trust)

All application routes (except /api/health) are protected by Cloudflare Access.

User Request → Cloudflare → JWT Validation → App Runner

How it works:

  1. User navigates to the application URL
  2. Cloudflare Access intercepts the request
  3. User authenticates via email OTP or Google
  4. Cloudflare issues a signed JWT
  5. JWT is validated by middleware in the application

Allowed users:

  • @luminarium.ai email addresses
  • @fastmarkets.com email addresses

JWT Validation Middleware

The application validates Cloudflare JWTs in src/middleware.ts:

// Validates JWT on all routes except /api/health
export const config = {
  matcher: ['/((?!api/health).*)'],
};

Why embedded keys?

The App Runner VPC connector blocks outbound internet access. Cloudflare public keys are fetched at build time and embedded in the application.

Network Security

Private Database

RDS SQL Server is in a private subnet:

  • No public IP address
  • Not accessible from the internet
  • Only App Runner can reach port 1433 via VPC Connector

App Runner

  • Runs in AWS managed VPC
  • Connects to RDS via VPC Connector
  • Only exposes port 3000 to Cloudflare

Secrets Management

GitHub Secrets

Used by CI/CD pipeline:

Secret Purpose
AWS_CI_ROLE_ARN OIDC role for AWS access
DB_PASSWORD RDS admin password
CLOUDFLARE_* Cloudflare API access

AWS Secrets Manager

Future phases will store application secrets (API keys) in AWS Secrets Manager, accessed via App Runner's IAM role.

Coding Standards

See .rules/security.md for detailed security coding standards.

Key Principles

  1. Never commit secrets - Use environment variables
  2. Validate all input - Use Zod schemas at API boundaries
  3. Use Prisma - Prevents SQL injection
  4. React escapes by default - Prevents XSS
  5. Check authentication - Verify session on protected routes
  6. Check authorization - Verify user owns resources

Example: Input Validation

import { z } from 'zod';

const InputSchema = z.object({
  name: z.string().min(1).max(100).trim(),
  email: z.string().email().toLowerCase(),
});

const parsed = InputSchema.safeParse(body);
if (!parsed.success) {
  return Response.json({ error: 'Invalid input' }, { status: 400 });
}

Security Checklist

When adding new features:

  • [ ] Input validated with Zod
  • [ ] No raw SQL queries (use Prisma)
  • [ ] No dangerouslySetInnerHTML with user content
  • [ ] Protected routes check authentication
  • [ ] Resources check authorization (user owns resource)
  • [ ] Secrets in environment variables, not code
  • [ ] No sensitive data in logs

Reporting Security Issues

If you discover a security vulnerability:

  1. Do not open a public GitHub issue
  2. Contact Paul directly
  3. Provide details of the vulnerability
  4. Allow time for a fix before disclosure