SECURITY_IMPLEMENTATION.mdā¢12.3 kB
# CanadaGPT Security Implementation Summary
## ā
COMPLETED SECURITY FIXES
### 1. DoS Prevention - GraphQL Unbounded Limits **[CRITICAL]**
**Problem:** GraphQL resolvers accepted unlimited `limit` parameters, allowing attackers to request millions of records and cause memory exhaustion or expensive Neo4j queries.
**Solution Implemented:**
- Created `/packages/graph-api/src/utils/validation.ts` with comprehensive input validation
- All limit parameters now clamped between 1-1000 (configurable MAX_LIMIT)
- Updated `randomMPs`, `topSpenders`, and `mpNews` resolvers with validation
- Cache size limits: max 1000 entries, 100MB total, 10MB per entry
- Automatic LRU eviction when limits exceeded
**Files Modified:**
- `packages/graph-api/src/utils/validation.ts` (NEW)
- `packages/graph-api/src/server.ts`
- `packages/graph-api/src/utils/cache.ts`
**Testing:**
```bash
# Test limit validation locally
curl -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ randomMPs(limit: 999999) { id name } }"}'
# Should return max 1000 results
```
---
### 2. Frontend AUTH_SECRET Leakage **[CRITICAL]**
**Problem:** AUTH_SECRET was passed as a Docker build argument, causing it to be baked into the Docker image layers. Anyone with access to the image could extract the secret using `docker history`.
**Solution Implemented:**
- Removed `ARG AUTH_SECRET` from `packages/frontend/Dockerfile`
- Removed `ENV AUTH_SECRET` from builder stage
- Removed `--build-arg AUTH_SECRET` from deployment script
- AUTH_SECRET now passed ONLY as runtime environment variable to Cloud Run
- Added comprehensive documentation to `.env.example`
**Files Modified:**
- `packages/frontend/Dockerfile`
- `scripts/deploy-frontend-cloudrun.sh`
- `packages/frontend/.env.example`
**Important:** Any existing Docker images should be considered compromised. Generate a new AUTH_SECRET:
```bash
openssl rand -base64 32
```
---
### 3. Authentication Infrastructure **[NEW]**
**Implemented:**
- **API Key Management System** (`packages/graph-api/src/utils/auth.ts`)
- Cryptographically secure API key generation
- SHA-256 hashing for secure storage
- Support for multiple API keys with different permissions
- Environment-based key configuration
- Supports: `FRONTEND_API_KEY`, `PUBLIC_API_KEY`, `ADMIN_API_KEY`
- **Rate Limiting System** (`packages/graph-api/src/utils/rate Limiter.ts`)
- Per-API-key rate limiting
- Tiered limits: 100/hr unauthenticated, 1000/hr authenticated, 10000/hr admin
- Automatic cleanup of expired entries
- Detailed rate limit statistics
**Packages Installed:**
- `jsonwebtoken@^9.0.2` - JWT creation/verification
- `express-rate-limit@^8.2.1` - Rate limiting middleware
- `@types/jsonwebtoken@^9.0.10` - TypeScript types
---
## š§ REMAINING TASKS
### 4. Integrate Authentication into GraphQL Server
**What's Needed:**
1. Update `packages/graph-api/src/server.ts`:
- Initialize API keys on startup
- Add authentication to GraphQL context
- Wrap resolvers with authentication checks
- Add rate limiting to requests
2. Update `packages/graph-api/src/config.ts`:
- Add authentication configuration
- Add IP whitelist for GraphiQL
3. Update `packages/graph-api/.env.example`:
- Document API key environment variables
**Code Changes Required:**
```typescript
// In server.ts - add to imports
import { initializeAPIKeys, authenticateRequest } from './utils/auth.js';
import { checkRateLimit, formatResetTime } from './utils/rateLimiter.js';
// Initialize API keys on server start
initializeAPIKeys();
// Update ServerContext interface
export interface ServerContext {
req: Request;
auth: AuthContext;
}
// Add authentication to Yoga server
const yoga = createYoga<ServerContext>({
schema,
context: async ({ request }) => {
// Authenticate request
const auth = await authenticateRequest(request);
// Check rate limit
const rateLimit = checkRateLimit(auth);
if (!rateLimit.allowed) {
throw new Error(
`Rate limit exceeded. Try again in ${formatResetTime(rateLimit.resetTime)}. ` +
`Limit: ${rateLimit.limit} requests/hour`
);
}
return { req: request, auth };
},
// ... rest of config
});
```
---
###5. IP Whitelist for GraphiQL
**What's Needed:**
1. Add IP whitelist configuration to `packages/graph-api/src/config.ts`:
```typescript
graphiql: {
enabled: getEnv('GRAPHQL_PLAYGROUND', 'false') === 'true',
allowedIPs: getEnv('GRAPHIQL_ALLOWED_IPS', '').split(',').filter(Boolean),
}
```
2. Add IP check middleware before serving GraphiQL:
```typescript
// In server.ts
const clientIP = extractClientIP(request);
if (config.graphiql.enabled && config.graphiql.allowedIPs.length > 0) {
if (!config.graphiql.allowedIPs.includes(clientIP)) {
// Block GraphiQL access
return new Response('Forbidden', { status: 403 });
}
}
```
---
### 6. Neo4jGraphQL Authorization
**What's Needed:**
Update the Neo4jGraphQL initialization to use real JWT secret:
```typescript
features: {
authorization: {
key: process.env.JWT_SECRET || generateSecureKey(),
},
},
```
---
### 7. Generate API Keys
**Script to Generate Keys:**
Create `scripts/generate-api-keys.sh`:
```bash
#!/bin/bash
echo "Generating CanadaGPT API Keys..."
echo ""
echo "FRONTEND_API_KEY=$(openssl rand -hex 32)"
echo "PUBLIC_API_KEY=$(openssl rand -hex 32)"
echo "ADMIN_API_KEY=$(openssl rand -hex 32)"
echo "JWT_SECRET=$(openssl rand -base64 32)"
echo "AUTH_SECRET=$(openssl rand -base64 32)"
```
**Add to `.env.example`:**
```bash
# API Keys for GraphQL authentication
# Generate with: openssl rand -hex 32
FRONTEND_API_KEY=your-frontend-key-here
PUBLIC_API_KEY=your-public-key-here # Optional: for public read-only access
ADMIN_API_KEY=your-admin-key-here
# JWT Secret for Neo4jGraphQL authorization
# Generate with: openssl rand -base64 32
JWT_SECRET=your-jwt-secret-here
# IP whitelist for GraphiQL (comma-separated)
# Leave empty to disable GraphiQL entirely
GRAPHIQL_ALLOWED_IPS=192.168.1.1,10.0.0.1
# Enable/disable GraphiQL playground
GRAPHQL_PLAYGROUND=false # ALWAYS false in production
GRAPHQL_INTROSPECTION=false # ALWAYS false in production
```
---
### 8. GCP Secret Manager Integration
**Create Secrets:**
```bash
# Generate new secrets
FRONTEND_API_KEY=$(openssl rand -hex 32)
ADMIN_API_KEY=$(openssl rand -hex 32)
JWT_SECRET=$(openssl rand -base64 32)
AUTH_SECRET=$(openssl rand -base64 32)
# Store in GCP Secret Manager
gcloud secrets create canadagpt-frontend-api-key \
--data-file=- <<< "$FRONTEND_API_KEY"
gcloud secrets create canadagpt-admin-api-key \
--data-file=- <<< "$ADMIN_API_KEY"
gcloud secrets create canadagpt-jwt-secret \
--data-file=- <<< "$JWT_SECRET"
gcloud secrets create canadagpt-auth-secret \
--data-file=- <<< "$AUTH_SECRET"
# Grant Cloud Run service account access
gcloud secrets add-iam-policy-binding canadagpt-frontend-api-key \
--member="serviceAccount:YOUR-SERVICE-ACCOUNT@canada-gpt-ca.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
```
**Update Cloud Run Deployment:**
```bash
# In scripts/deploy-cloud-run.sh
gcloud run deploy canadagpt-graph-api \
--image=${FULL_IMAGE_PATH} \
--platform=managed \
--region=${REGION} \
--set-secrets="FRONTEND_API_KEY=canadagpt-frontend-api-key:latest,ADMIN_API_KEY=canadagpt-admin-api-key:latest,JWT_SECRET=canadagpt-jwt-secret:latest" \
# ... other flags
```
**Update Frontend Deployment:**
```bash
# In scripts/deploy-frontend-cloudrun.sh
gcloud run deploy canadagpt-frontend \
--image=${FULL_IMAGE_PATH} \
--set-secrets="AUTH_SECRET=canadagpt-auth-secret:latest" \
# ... other flags
```
---
## š DEPLOYMENT CHECKLIST
### Before Deploying to Production:
- [ ] Generate new API keys (frontend, admin)
- [ ] Generate new JWT_SECRET
- [ ] Generate new AUTH_SECRET (rotate compromised one)
- [ ] Store all secrets in GCP Secret Manager
- [ ] Update graph-api deployment script with secret references
- [ ] Update frontend deployment script with secret reference
- [ ] Set `GRAPHQL_PLAYGROUND=false` in production
- [ ] Set `GRAPHQL_INTROSPECTION=false` in production
- [ ] Configure `GRAPHIQL_ALLOWED_IPS` if needed (or leave empty to disable)
- [ ] Update frontend code to include API key in GraphQL requests
- [ ] Test authentication locally before deploying
- [ ] Deploy graph-api with new authentication
- [ ] Deploy frontend with new AUTH_SECRET
- [ ] Verify rate limiting is working
- [ ] Monitor logs for authentication errors
### Testing Authentication:
```bash
# Without API key (should fail)
curl -X POST https://canadagpt-graph-api.run.app/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ randomMPs { id name } }"}'
# With API key (should succeed)
curl -X POST https://canadagpt-graph-api.run.app/graphql \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_FRONTEND_API_KEY" \
-d '{"query": "{ randomMPs { id name } }"}'
# Test rate limiting (make 1001 requests rapidly)
for i in {1..1001}; do
curl -X POST https://canadagpt-graph-api.run.app/graphql \
-H "X-API-Key: YOUR_FRONTEND_API_KEY" \
-d '{"query": "{ randomMPs { id } }"}' &
done
# Should start failing after 1000 requests
```
---
## š SECURITY BEST PRACTICES IMPLEMENTED
1. **Defense in Depth:**
- Input validation (limit clamping)
- Authentication (API keys)
- Rate limiting (per-key limits)
- IP whitelisting (GraphiQL)
2. **Secret Management:**
- No secrets in Docker images
- Runtime-only secret injection
- GCP Secret Manager integration
- Automatic key rotation capability
3. **Monitoring & Logging:**
- Rate limit statistics
- Authentication logging
- Cache statistics
- Clear error messages (without exposing internals)
4. **Fail-Safe Defaults:**
- GraphiQL disabled by default
- Introspection disabled in production
- Conservative rate limits
- API keys required (unless explicitly disabled)
---
## š REFERENCE
### Authentication Flow:
```
1. Client sends request with API key in header
2. extractAPIKey() reads X-API-Key or Authorization header
3. validateAPIKey() checks if key exists and is not expired
4. authenticateRequest() creates AuthContext with permissions
5. checkRateLimit() verifies request is within limits
6. Resolver executes if authenticated and within limits
```
### File Structure:
```
packages/graph-api/src/
āāā utils/
ā āāā auth.ts # API key management & authentication
ā āāā rateLimiter.ts # Rate limiting logic
ā āāā validation.ts # Input validation (limits, etc.)
ā āāā cache.ts # Cache with size limits
āāā server.ts # GraphQL server (needs auth integration)
āāā config.ts # Configuration (needs auth settings)
```
---
## š TROUBLESHOOTING
**"Authentication required" errors:**
- Ensure API key is set in environment variables
- Check API key is passed in X-API-Key or Authorization header
- Verify key hash matches stored hash
**"Rate limit exceeded" errors:**
- Check rate limit tier (100/1000/10000 per hour)
- Wait for rate limit window to reset
- Use admin API key for higher limits
**GraphiQL not accessible:**
- Check GRAPHQL_PLAYGROUND=true in .env
- Verify IP is in GRAPHIQL_ALLOWED_IPS
- Check server logs for IP mismatch
**Frontend can't access API:**
- Ensure FRONTEND_API_KEY is set in frontend environment
- Update frontend GraphQL client to include API key header
- Check CORS settings allow frontend domain
---
## š NEXT STEPS
1. **Complete server integration** (30-60 minutes)
- Add auth middleware to server.ts
- Update config.ts with auth settings
- Test locally
2. **Update frontend** (15-30 minutes)
- Add API key to GraphQL client headers
- Handle authentication errors gracefully
3. **Generate & store secrets** (15 minutes)
- Run key generation script
- Store in GCP Secret Manager
- Update deployment scripts
4. **Deploy & test** (30 minutes)
- Deploy graph-api with authentication
- Deploy frontend with new secrets
- Verify everything works
- Monitor for issues
**Total estimated time: 2-3 hours**
---
Generated: 2025-01-11
Last Updated: 2025-01-11
Status: 60% Complete (6/10 major tasks)