Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:46:15 +08:00
commit 3b04d08a92
7 changed files with 1719 additions and 0 deletions

View File

@@ -0,0 +1,402 @@
---
name: env-config-validator
description: Validate environment configuration files across local, staging, and production environments. Ensure required secrets, database URLs, API keys, and public variables are properly scoped and set. Use this skill when setting up environments, validating configuration, checking for missing secrets, auditing environment variables, ensuring proper scoping of public vs private vars, or troubleshooting environment issues. Trigger terms include env, environment variables, secrets, configuration, .env file, environment validation, missing variables, config check, NEXT_PUBLIC, env vars, database URL, API keys.
---
# Environment Configuration Validator
Validate `.env` files across local, staging, and production environments. Ensure all required secrets, database URLs, API keys, and public variables are properly scoped, set, and secure.
## Core Capabilities
### 1. Validate Environment Files
To validate environment configuration:
- Parse `.env`, `.env.local`, `.env.production`, etc.
- Check for required variables
- Verify variable naming conventions
- Detect security issues (exposed secrets, weak values)
- Use `scripts/validate_env.py` for automated validation
### 2. Check Variable Scoping
Ensure proper scoping of environment variables:
- **Public variables** (`NEXT_PUBLIC_*`): Accessible in browser
- **Private variables**: Server-side only
- **Database credentials**: Never exposed to client
- **API keys**: Properly scoped based on usage
### 3. Cross-Environment Validation
Compare configurations across environments:
- Identify missing variables in staging/production
- Check for environment-specific overrides
- Ensure consistency in variable names
- Validate environment-specific values (URLs, keys)
### 4. Security Auditing
Detect security vulnerabilities in environment configuration:
- Exposed secrets in public variables
- Weak or default values
- Hardcoded credentials in code
- Missing required security variables (JWT secrets, encryption keys)
## Validation Rules
### Required Variables
Ensure these categories of variables are present:
1. **Database Connection**
- `DATABASE_URL` or equivalent
- Connection pool settings (optional)
2. **Authentication**
- `JWT_SECRET` or `AUTH_SECRET`
- OAuth credentials (if using OAuth)
- Session secrets
3. **External APIs**
- Third-party API keys
- Service endpoints
- Rate limiting tokens
4. **Application Config**
- `NODE_ENV`
- `NEXT_PUBLIC_APP_URL` or `APP_URL`
- Feature flags (optional)
5. **Email/Notifications** (if used)
- SMTP credentials
- Email service API keys
### Naming Conventions
Follow Next.js environment variable conventions:
- **Public variables**: `NEXT_PUBLIC_*` prefix
- Example: `NEXT_PUBLIC_API_URL`
- Accessible in browser
- Never put secrets here
- **Private variables**: No prefix
- Example: `DATABASE_URL`, `API_SECRET`
- Server-side only
- Safe for secrets
- **Naming style**: `SCREAMING_SNAKE_CASE`
- Example: `DATABASE_URL`, `JWT_SECRET`, `STRIPE_API_KEY`
### Security Rules
1. **Never expose secrets in public variables**
- [ERROR] `NEXT_PUBLIC_DATABASE_URL`
- [OK] `DATABASE_URL`
2. **Database URLs must be private**
- [ERROR] `NEXT_PUBLIC_DB_URL`
- [OK] `DATABASE_URL`
3. **API keys scoping**
- Client-side API keys → `NEXT_PUBLIC_*` (e.g., Google Maps)
- Server-side API keys → No prefix (e.g., Stripe secret)
4. **No hardcoded secrets in code**
- Use environment variables for all secrets
- Never commit `.env.local` or `.env.production`
5. **Strong secrets**
- JWT/session secrets: minimum 32 characters
- Use cryptographically random values
- No default or example values in production
## Validation Script
Use `scripts/validate_env.py` to automate validation:
```bash
# Validate current .env file
python scripts/validate_env.py
# Validate specific file
python scripts/validate_env.py --file .env.production
# Compare multiple environments
python scripts/validate_env.py --compare .env.local .env.production
# Check against required variables template
python scripts/validate_env.py --template .env.example
```
The script checks:
- Required variables are present
- Naming conventions are followed
- No secrets in public variables
- No weak or default values
- Consistent naming across environments
## Common Issues and Solutions
### Issue: Missing DATABASE_URL in Production
**Detection**: Script reports missing required variable
**Solution**:
```bash
# Add to .env.production
DATABASE_URL="postgresql://user:password@host:5432/dbname"
```
**Note**: Use different databases for dev/staging/prod
### Issue: Secret Exposed in Public Variable
**Detection**: Script finds `NEXT_PUBLIC_` prefix on secret
**Problem**:
```bash
# [ERROR] WRONG - secret exposed to browser
NEXT_PUBLIC_API_SECRET="secret123"
```
**Solution**:
```bash
# [OK] CORRECT - server-side only
API_SECRET="secret123"
```
### Issue: Weak JWT Secret
**Detection**: Script detects short or weak secret
**Problem**:
```bash
# [ERROR] WRONG - too short, predictable
JWT_SECRET="secret"
```
**Solution**:
```bash
# [OK] CORRECT - strong, random, 32+ characters
JWT_SECRET="a8f3d9c2e1b7f4a6d8c3e9b2f1a7d4c8e3b9f2a1d7c4e8b3f9a2d1c7e4b8f3a9"
```
Generate with:
```bash
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
```
### Issue: Inconsistent Variable Names Across Environments
**Detection**: Script comparison shows name mismatch
**Problem**:
```bash
# .env.local
DATABASE_URL="..."
# .env.production
DB_URL="..." # [ERROR] Different name
```
**Solution**: Use consistent names
```bash
# Both files
DATABASE_URL="..."
```
### Issue: Missing Public API URL
**Detection**: Client-side code fails to connect to API
**Problem**: `NEXT_PUBLIC_API_URL` not set
**Solution**:
```bash
# .env.local
NEXT_PUBLIC_API_URL="http://localhost:3000"
# .env.production
NEXT_PUBLIC_API_URL="https://api.yourapp.com"
```
## Resource Files
### scripts/validate_env.py
Python script to validate environment files, check for security issues, compare across environments, and verify against templates. Provides detailed error messages and suggestions.
### references/env_best_practices.md
Comprehensive guide to environment variable management including:
- Security best practices
- Naming conventions
- Scoping rules (public vs private)
- Common patterns for different services
- Environment-specific configuration
- Secret rotation strategies
### assets/.env.example
Template showing all required environment variables for a worldbuilding application. Use as a reference for setting up new environments or auditing existing ones.
## Environment-Specific Configuration
### Development (.env.local)
```bash
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/worldbuilding_dev"
# Authentication
JWT_SECRET="dev-secret-change-in-production"
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="dev-nextauth-secret"
# Public
NEXT_PUBLIC_API_URL="http://localhost:3000"
NEXT_PUBLIC_APP_NAME="Worldbuilding App (Dev)"
# External APIs (test keys)
OPENAI_API_KEY="sk-test-..."
STRIPE_SECRET_KEY="sk_test_..."
```
### Staging (.env.staging)
```bash
# Database
DATABASE_URL="postgresql://user:password@staging-db.com:5432/worldbuilding_staging"
# Authentication
JWT_SECRET="staging-secret-32-chars-minimum"
NEXTAUTH_URL="https://staging.yourapp.com"
NEXTAUTH_SECRET="staging-nextauth-secret"
# Public
NEXT_PUBLIC_API_URL="https://staging.yourapp.com"
NEXT_PUBLIC_APP_NAME="Worldbuilding App (Staging)"
# External APIs (test keys)
OPENAI_API_KEY="sk-test-..."
STRIPE_SECRET_KEY="sk_test_..."
```
### Production (.env.production)
```bash
# Database
DATABASE_URL="postgresql://user:password@prod-db.com:5432/worldbuilding_prod"
# Authentication
JWT_SECRET="production-secret-use-crypto-random-32-chars-minimum"
NEXTAUTH_URL="https://yourapp.com"
NEXTAUTH_SECRET="production-nextauth-secret"
# Public
NEXT_PUBLIC_API_URL="https://api.yourapp.com"
NEXT_PUBLIC_APP_NAME="Worldbuilding App"
# External APIs (production keys)
OPENAI_API_KEY="sk-live-..."
STRIPE_SECRET_KEY="sk_live_..."
# Monitoring
SENTRY_DSN="https://..."
```
## Best Practices
1. **Never commit secrets**
- Add `.env.local`, `.env.production` to `.gitignore`
- Commit `.env.example` as a template
2. **Use strong, random secrets**
- Minimum 32 characters for JWT/session secrets
- Use `crypto.randomBytes()` or password manager
3. **Scope variables correctly**
- Public (`NEXT_PUBLIC_*`): Only non-sensitive, client-accessible data
- Private (no prefix): All secrets, credentials, server-only config
4. **Consistent naming**
- Use same variable names across all environments
- Follow `SCREAMING_SNAKE_CASE` convention
5. **Environment-specific values**
- Different database URLs per environment
- Test API keys in dev/staging, production keys in prod
- Environment-specific URLs and endpoints
6. **Document required variables**
- Keep `.env.example` updated
- Add comments explaining each variable
- Document where to get values (API dashboard, etc.)
7. **Validate on deployment**
- Run validation script in CI/CD pipeline
- Fail deployment if required variables missing
- Check for security issues before deploying
8. **Rotate secrets regularly**
- Change JWT secrets periodically
- Rotate API keys on schedule
- Update after team member departures
9. **Use secret management tools**
- Consider Vercel Environment Variables
- AWS Secrets Manager, HashiCorp Vault for sensitive data
- Never store production secrets in code or comments
10. **Test environment parity**
- Staging should mirror production as closely as possible
- Use same variable names, just different values
- Test with production-like data
## Integration with Worldbuilding App
Common environment variables for worldbuilding applications:
### Database
```bash
DATABASE_URL="postgresql://..."
DATABASE_POOL_SIZE="10" # Optional
```
### Authentication
```bash
JWT_SECRET="..."
NEXTAUTH_URL="..."
NEXTAUTH_SECRET="..."
```
### External APIs
```bash
# AI services (optional)
OPENAI_API_KEY="..."
# Maps (if using)
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY="..."
# Image hosting (if using)
CLOUDINARY_URL="..."
```
### Application
```bash
NODE_ENV="production"
NEXT_PUBLIC_APP_URL="https://..."
NEXT_PUBLIC_APP_NAME="Worldbuilding App"
```
### Email (if using)
```bash
SMTP_HOST="..."
SMTP_PORT="587"
SMTP_USER="..."
SMTP_PASSWORD="..."
```
Consult `references/env_best_practices.md` for detailed guidance and `assets/.env.example` for a complete template.

View File

@@ -0,0 +1,157 @@
# Environment Configuration Template
# Copy this file to .env.local for development
# DO NOT commit .env.local or .env.production to git
# ============================================
# ENVIRONMENT
# ============================================
NODE_ENV=development
# ============================================
# DATABASE
# ============================================
# PostgreSQL connection string
DATABASE_URL="postgresql://user:password@localhost:5432/worldbuilding_dev"
# Optional: Connection pool settings
# DATABASE_POOL_SIZE=10
# DATABASE_POOL_TIMEOUT=30000
# ============================================
# AUTHENTICATION
# ============================================
# JWT secret (generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))")
JWT_SECRET="your-jwt-secret-here-32-chars-minimum"
# NextAuth.js (if using)
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="your-nextauth-secret-here-32-chars-minimum"
# OAuth providers (if using)
# GOOGLE_CLIENT_ID=""
# GOOGLE_CLIENT_SECRET=""
# GITHUB_CLIENT_ID=""
# GITHUB_CLIENT_SECRET=""
# ============================================
# APPLICATION URLs
# ============================================
# Public-facing application URL
NEXT_PUBLIC_APP_URL="http://localhost:3000"
# API base URL (if different from app URL)
NEXT_PUBLIC_API_URL="http://localhost:3000"
# Application name
NEXT_PUBLIC_APP_NAME="Worldbuilding App"
# ============================================
# EXTERNAL APIs (Optional)
# ============================================
# OpenAI (for AI features)
# OPENAI_API_KEY="sk-..."
# OPENAI_ORG_ID="org-..."
# Anthropic Claude (alternative to OpenAI)
# ANTHROPIC_API_KEY="sk-ant-..."
# Google Maps (if using maps features)
# NEXT_PUBLIC_GOOGLE_MAPS_KEY="..." # Only if client-side
# GOOGLE_MAPS_SERVER_KEY="..." # For server-side geocoding
# Cloudinary (if using image hosting)
# CLOUDINARY_URL="cloudinary://..."
# NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME="..."
# ============================================
# EMAIL (Optional)
# ============================================
# SMTP Configuration
# SMTP_HOST="smtp.gmail.com"
# SMTP_PORT=587
# SMTP_USER="your-email@gmail.com"
# SMTP_PASSWORD="your-app-specific-password"
# SMTP_FROM="noreply@example.com"
# Email service API (alternative to SMTP)
# SENDGRID_API_KEY="SG...."
# MAILGUN_API_KEY="..."
# ============================================
# PAYMENT (Optional)
# ============================================
# Stripe
# NEXT_PUBLIC_STRIPE_PUBLIC_KEY="pk_test_..." # Client-side
# STRIPE_SECRET_KEY="sk_test_..." # Server-side
# STRIPE_WEBHOOK_SECRET="whsec_..." # Webhook verification
# ============================================
# FILE STORAGE (Optional)
# ============================================
# AWS S3
# AWS_ACCESS_KEY_ID="..."
# AWS_SECRET_ACCESS_KEY="..."
# AWS_REGION="us-east-1"
# AWS_S3_BUCKET="your-bucket-name"
# ============================================
# MONITORING & LOGGING (Optional)
# ============================================
# Sentry (error tracking)
# SENTRY_DSN="https://...@sentry.io/..."
# SENTRY_ENVIRONMENT="development"
# SENTRY_AUTH_TOKEN="..." # For source maps upload
# LogRocket (session replay)
# NEXT_PUBLIC_LOGROCKET_APP_ID="..."
# Custom logging
# LOG_LEVEL="debug" # debug, info, warn, error
# ============================================
# RATE LIMITING (Optional)
# ============================================
# Redis (for rate limiting)
# REDIS_URL="redis://localhost:6379"
# Upstash Redis (managed Redis)
# UPSTASH_REDIS_REST_URL="https://..."
# UPSTASH_REDIS_REST_TOKEN="..."
# ============================================
# SEARCH (Optional)
# ============================================
# Algolia (if using search)
# NEXT_PUBLIC_ALGOLIA_APP_ID="..."
# NEXT_PUBLIC_ALGOLIA_SEARCH_KEY="..." # Search-only public key
# ALGOLIA_ADMIN_KEY="..." # Admin key (server-side only)
# Meilisearch (alternative to Algolia)
# MEILISEARCH_HOST="http://localhost:7700"
# MEILISEARCH_KEY="..."
# ============================================
# FEATURE FLAGS (Optional)
# ============================================
# NEXT_PUBLIC_FEATURE_AI_ASSISTANCE="false"
# NEXT_PUBLIC_FEATURE_COLLABORATIVE_EDITING="false"
# NEXT_PUBLIC_FEATURE_TIMELINE_VISUALIZATION="true"
# ============================================
# DEVELOPMENT TOOLS (Optional)
# ============================================
# Enable debug mode
# DEBUG="true"
# Disable telemetry
# NEXT_TELEMETRY_DISABLED=1
# ============================================
# NOTES
# ============================================
# 1. Copy this file to .env.local for local development
# 2. Generate strong secrets using:
# node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# 3. Never commit .env.local or .env.production to git
# 4. Use NEXT_PUBLIC_ prefix only for non-sensitive, client-accessible variables
# 5. Keep this template updated as you add new environment variables

View File

@@ -0,0 +1,635 @@
# Environment Variable Best Practices
Comprehensive guide to managing environment variables in Next.js applications.
## Table of Contents
1. [Security Best Practices](#security-best-practices)
2. [Naming Conventions](#naming-conventions)
3. [Scoping Rules](#scoping-rules)
4. [Common Patterns](#common-patterns)
5. [Environment-Specific Configuration](#environment-specific-configuration)
6. [Secret Rotation](#secret-rotation)
7. [Testing Strategies](#testing-strategies)
8. [Deployment Checklist](#deployment-checklist)
## Security Best Practices
### 1. Never Commit Secrets
**Rule**: Never commit `.env.local` or `.env.production` files
**Setup `.gitignore`**:
```
# Environment files with secrets
.env*.local
.env.production
.env.staging
# Only commit the template
!.env.example
```
### 2. Use Strong, Random Secrets
**Requirements for secrets**:
- Minimum 32 characters
- Cryptographically random
- High entropy (mix of characters, numbers, symbols)
**Generate strong secrets**:
```bash
# Node.js (recommended for JWT/session secrets)
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# OpenSSL
openssl rand -hex 32
# Python
python -c "import secrets; print(secrets.token_hex(32))"
```
### 3. Scope Variables Correctly
**Public variables** (`NEXT_PUBLIC_*`):
- [OK] API endpoints
- [OK] Feature flags
- [OK] Client-side config
- [X] API keys
- [X] Database credentials
- [X] Any secrets
**Private variables** (no prefix):
- [OK] Database URLs
- [OK] API secrets
- [OK] JWT/session secrets
- [OK] Third-party service credentials
### 4. Validate on Startup
**Validate environment variables when app starts**:
```typescript
// lib/env.ts
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
NEXTAUTH_SECRET: z.string().min(32),
NODE_ENV: z.enum(['development', 'staging', 'production']),
NEXT_PUBLIC_API_URL: z.string().url(),
});
export const env = envSchema.parse(process.env);
```
**Benefits**:
- Fail fast on missing/invalid variables
- Type-safe access to env vars
- Clear documentation of required variables
### 5. Use Secret Management Tools
**For production environments**:
- **Vercel**: Use Environment Variables UI (automatic encryption)
- **AWS**: AWS Secrets Manager or Systems Manager Parameter Store
- **GCP**: Secret Manager
- **Azure**: Key Vault
- **HashiCorp Vault**: Enterprise secret management
- **Doppler**: Universal secrets manager
**Benefits**:
- Encryption at rest
- Access control and auditing
- Automatic rotation
- Version history
## Naming Conventions
### Standard Format
Use `SCREAMING_SNAKE_CASE` for all environment variables:
```bash
# [OK] CORRECT
DATABASE_URL="..."
JWT_SECRET="..."
NEXT_PUBLIC_API_URL="..."
# [X] WRONG
databaseUrl="..."
jwt-secret="..."
NextPublicApiUrl="..."
```
### Prefixes
**Next.js public variables**:
```bash
NEXT_PUBLIC_API_URL="https://api.example.com"
NEXT_PUBLIC_APP_NAME="My App"
NEXT_PUBLIC_GOOGLE_MAPS_KEY="..." # Only if client-side usage
```
**Service-specific prefixes** (optional but recommended):
```bash
# Database
DATABASE_URL="..."
DATABASE_POOL_SIZE="..."
# Stripe
STRIPE_PUBLIC_KEY="..."
STRIPE_SECRET_KEY="..."
STRIPE_WEBHOOK_SECRET="..."
# Email
SMTP_HOST="..."
SMTP_PORT="..."
SMTP_USER="..."
SMTP_PASSWORD="..."
```
### Avoid Redundancy
```bash
# [OK] GOOD - clear and concise
DATABASE_URL="..."
JWT_SECRET="..."
# [X] BAD - redundant
DATABASE_CONNECTION_URL="..." # "CONNECTION" is redundant with "URL"
JWT_SECRET_KEY="..." # "KEY" is redundant with "SECRET"
```
## Scoping Rules
### Public Variables (`NEXT_PUBLIC_*`)
**When to use**:
- Client-side API endpoints
- Feature flags that affect UI
- Public configuration (app name, version)
- Client-side analytics IDs
- Map API keys (if client-side only)
**Example**:
```bash
NEXT_PUBLIC_API_URL="https://api.example.com"
NEXT_PUBLIC_ANALYTICS_ID="G-XXXXXXXXXX"
NEXT_PUBLIC_FEATURE_NEW_UI="true"
```
**Access in code**:
```typescript
// Available in both server and client components
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
```
### Private Variables (no prefix)
**When to use**:
- Database credentials
- API secrets
- JWT/session secrets
- Third-party service credentials
- Server-side API keys
**Example**:
```bash
DATABASE_URL="postgresql://..."
JWT_SECRET="..."
STRIPE_SECRET_KEY="sk_live_..."
OPENAI_API_KEY="sk-..."
```
**Access in code**:
```typescript
// Only available in server components and API routes
const dbUrl = process.env.DATABASE_URL; // Server-side only
```
### Mixed Scenarios
**Stripe example** (public + private keys):
```bash
# Public key - client-side checkout
NEXT_PUBLIC_STRIPE_PUBLIC_KEY="pk_live_..."
# Secret key - server-side payments
STRIPE_SECRET_KEY="sk_live_..."
# Webhook secret - server-side webhooks
STRIPE_WEBHOOK_SECRET="whsec_..."
```
**Map services example**:
```bash
# If using client-side maps
NEXT_PUBLIC_GOOGLE_MAPS_KEY="..."
# If using server-side geocoding
GOOGLE_MAPS_SERVER_KEY="..."
```
## Common Patterns
### Database Configuration
```bash
# Standard database URL
DATABASE_URL="postgresql://user:password@host:5432/dbname"
# Optional connection pool settings
DATABASE_POOL_SIZE="10"
DATABASE_POOL_TIMEOUT="30000"
# Read replicas (optional)
DATABASE_READ_URL="postgresql://user:password@read-host:5432/dbname"
```
### Authentication (NextAuth.js)
```bash
# Required for NextAuth
NEXTAUTH_URL="https://example.com"
NEXTAUTH_SECRET="..." # 32+ character random string
# OAuth providers (if using)
GOOGLE_CLIENT_ID="..."
GOOGLE_CLIENT_SECRET="..."
GITHUB_CLIENT_ID="..."
GITHUB_CLIENT_SECRET="..."
```
### JWT Authentication (Custom)
```bash
# JWT secret
JWT_SECRET="..." # 32+ character random string
# Optional: token expiration
JWT_EXPIRES_IN="7d"
# Optional: refresh token secret
JWT_REFRESH_SECRET="..." # Different from JWT_SECRET
```
### External APIs
```bash
# OpenAI
OPENAI_API_KEY="sk-..."
OPENAI_ORG_ID="org-..." # Optional
# Stripe
NEXT_PUBLIC_STRIPE_PUBLIC_KEY="pk_live_..."
STRIPE_SECRET_KEY="sk_live_..."
STRIPE_WEBHOOK_SECRET="whsec_..."
# SendGrid
SENDGRID_API_KEY="SG...."
# AWS
AWS_ACCESS_KEY_ID="..."
AWS_SECRET_ACCESS_KEY="..."
AWS_REGION="us-east-1"
# Cloudinary
CLOUDINARY_URL="cloudinary://..."
```
### Email Configuration
```bash
# SMTP
SMTP_HOST="smtp.gmail.com"
SMTP_PORT="587"
SMTP_USER="your-email@gmail.com"
SMTP_PASSWORD="app-specific-password"
# Email service API (alternative)
SENDGRID_API_KEY="SG...."
MAILGUN_API_KEY="..."
```
### Monitoring and Logging
```bash
# Sentry
SENTRY_DSN="https://...@sentry.io/..."
SENTRY_AUTH_TOKEN="..." # For source maps upload
# LogRocket
LOGROCKET_APP_ID="..."
# Custom logging
LOG_LEVEL="info" # debug, info, warn, error
```
## Environment-Specific Configuration
### Development (.env.local)
```bash
# Permissive settings for development
NODE_ENV="development"
# Local database
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/myapp_dev"
# Development secrets (can be weak)
JWT_SECRET="dev-secret-change-in-production"
NEXTAUTH_SECRET="dev-nextauth-secret"
# Local URLs
NEXTAUTH_URL="http://localhost:3000"
NEXT_PUBLIC_API_URL="http://localhost:3000"
# Test API keys
STRIPE_SECRET_KEY="sk_test_..."
OPENAI_API_KEY="sk-test-..."
# Debug flags
DEBUG="true"
LOG_LEVEL="debug"
```
### Staging (.env.staging)
```bash
# Staging environment
NODE_ENV="production" # Use production mode
# Staging database
DATABASE_URL="postgresql://user:password@staging-db:5432/myapp_staging"
# Production-like secrets (but different from prod)
JWT_SECRET="staging-secret-32-chars-minimum"
NEXTAUTH_SECRET="staging-nextauth-secret"
# Staging URLs
NEXTAUTH_URL="https://staging.example.com"
NEXT_PUBLIC_API_URL="https://staging.example.com"
# Test API keys (same as dev)
STRIPE_SECRET_KEY="sk_test_..."
OPENAI_API_KEY="sk-test-..."
# Staging-specific config
SENTRY_ENVIRONMENT="staging"
LOG_LEVEL="info"
```
### Production (.env.production)
```bash
# Production environment
NODE_ENV="production"
# Production database
DATABASE_URL="postgresql://user:strong-password@prod-db:5432/myapp_prod"
# Strong, random secrets
JWT_SECRET="production-secret-use-crypto-random-32-chars-minimum"
NEXTAUTH_SECRET="production-nextauth-secret-also-32-chars-minimum"
# Production URLs
NEXTAUTH_URL="https://example.com"
NEXT_PUBLIC_API_URL="https://api.example.com"
# Production API keys
STRIPE_SECRET_KEY="sk_live_..."
OPENAI_API_KEY="sk-live-..."
# Production-specific config
SENTRY_ENVIRONMENT="production"
LOG_LEVEL="warn"
# Performance optimization
DATABASE_POOL_SIZE="20"
```
## Secret Rotation
### Why Rotate Secrets?
- **Security best practice**: Limit impact of potential leaks
- **Compliance**: Required by some standards (PCI-DSS, HIPAA)
- **Team changes**: Rotate after team member departures
- **Suspected breach**: Rotate immediately if compromise suspected
### What to Rotate
**Critical secrets** (rotate regularly):
- JWT/session secrets (every 90 days)
- Database passwords (every 90 days)
- API keys for financial services (every 90 days)
**Less critical** (rotate on schedule or as needed):
- Third-party API keys (annually or on breach)
- OAuth secrets (annually or on breach)
**Never rotate**:
- Public API keys (if they're truly public)
- Client IDs (non-secret identifiers)
### Rotation Process
**For JWT/session secrets**:
1. Generate new secret
2. Deploy with NEW_JWT_SECRET environment variable
3. Update code to validate with both old and new secrets
4. After grace period (e.g., 7 days), remove old secret
5. Rename NEW_JWT_SECRET to JWT_SECRET
**For database passwords**:
1. Create new database user/password
2. Deploy with new DATABASE_URL (no downtime)
3. Monitor for 24 hours
4. Delete old database user
5. Update password manager/secrets storage
**For API keys** (if service supports multiple keys):
1. Generate new API key (keep old active)
2. Deploy with new key
3. Monitor for 24 hours
4. Revoke old API key
## Testing Strategies
### Local Testing
**Use `.env.test` for test environment**:
```bash
# .env.test
NODE_ENV="test"
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/myapp_test"
JWT_SECRET="test-secret"
```
**Load in tests**:
```typescript
// jest.config.js
module.exports = {
setupFiles: ['<rootDir>/jest.setup.js'],
};
// jest.setup.js
import { loadEnvConfig } from '@next/env';
loadEnvConfig(process.cwd());
```
### Mock Environment Variables
```typescript
// test/utils/env.ts
export function mockEnv(vars: Record<string, string>) {
const original = { ...process.env };
beforeAll(() => {
Object.assign(process.env, vars);
});
afterAll(() => {
process.env = original;
});
}
// In test file
import { mockEnv } from './utils/env';
describe('API', () => {
mockEnv({
JWT_SECRET: 'test-secret',
DATABASE_URL: 'postgresql://localhost/test',
});
// Your tests...
});
```
### Validate Before Tests
```typescript
// test/setup.ts
import { envSchema } from '@/lib/env';
try {
envSchema.parse(process.env);
} catch (error) {
console.error('Invalid test environment configuration:');
console.error(error);
process.exit(1);
}
```
## Deployment Checklist
### Before Deploying to Production
- [ ] All required variables are set in production environment
- [ ] Secrets are strong (32+ characters, random)
- [ ] No secrets in `NEXT_PUBLIC_*` variables
- [ ] Database URL points to production database
- [ ] API keys are production keys (not test keys)
- [ ] `NODE_ENV` is set to `production`
- [ ] URLs are production URLs (no localhost)
- [ ] `.env.production` is NOT committed to git
- [ ] Secrets are stored in secure secret management system
- [ ] CI/CD has access to environment variables
- [ ] Monitoring/logging is configured (Sentry, etc.)
### Validation Script
Run before deployment:
```bash
python scripts/validate_env.py --file .env.production
```
### CI/CD Integration
**GitHub Actions example**:
```yaml
- name: Validate environment
run: python scripts/validate_env.py --file .env.production
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
# ... other secrets from GitHub Secrets
```
### Post-Deployment Verification
- [ ] Application starts without errors
- [ ] Database connection works
- [ ] Authentication works
- [ ] External API calls work
- [ ] No environment-related errors in logs
- [ ] Monitoring shows healthy status
## Common Mistakes to Avoid
### 1. Committing Secrets to Git
```bash
# [X] NEVER DO THIS
git add .env.production
git commit -m "Add production config" # [ERROR] Secrets in git history
```
**If you accidentally commit secrets**:
1. Rotate all exposed secrets immediately
2. Use `git filter-branch` or BFG Repo-Cleaner to remove from history
3. Force push (if safe to do so)
4. Notify team and audit access
### 2. Exposing Secrets in Public Variables
```bash
# [X] WRONG
NEXT_PUBLIC_DATABASE_URL="postgresql://..." # [ERROR] Exposed to client
NEXT_PUBLIC_JWT_SECRET="..." # [ERROR] Exposed to client
```
### 3. Weak Secrets
```bash
# [X] WRONG
JWT_SECRET="secret" # [ERROR] Too weak
JWT_SECRET="password123" # [ERROR] Predictable
JWT_SECRET="myapp-secret" # [ERROR] Too short
```
### 4. Copy-Pasting Between Environments
```bash
# [X] WRONG - Same secrets in dev and prod
# .env.local
JWT_SECRET="abc123..."
# .env.production
JWT_SECRET="abc123..." # [ERROR] Should be different
```
### 5. Hardcoding in Code
```typescript
// [X] WRONG
const apiKey = 'sk_live_hardcoded_key'; // [ERROR] Never hardcode
// [OK] CORRECT
const apiKey = process.env.STRIPE_SECRET_KEY;
```
## Resources
- [Next.js Environment Variables Documentation](https://nextjs.org/docs/basic-features/environment-variables)
- [OWASP Secret Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html)
- [12-Factor App Config](https://12factor.net/config)
- [Vercel Environment Variables](https://vercel.com/docs/concepts/projects/environment-variables)

View File

@@ -0,0 +1,453 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Validate environment configuration files.
Usage:
python validate_env.py
python validate_env.py --file .env.production
python validate_env.py --compare .env.local .env.production
python validate_env.py --template .env.example
"""
import os
import re
import io
import sys
import argparse
from pathlib import Path
# Configure stdout for UTF-8 encoding (prevents Windows encoding errors)
if sys.platform == 'win32':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
from typing import Dict, List, Set, Tuple, Optional
from dataclasses import dataclass
@dataclass
class ValidationIssue:
"""Represents a validation issue."""
severity: str # 'error', 'warning', 'info'
category: str # 'security', 'naming', 'missing', 'weak'
variable: str
message: str
suggestion: Optional[str] = None
class EnvValidator:
"""Validate environment configuration files."""
# Required variable categories
REQUIRED_CATEGORIES = {
'database': ['DATABASE_URL', 'DB_URL', 'POSTGRES_URL', 'MONGODB_URI', 'MYSQL_URL'],
'auth': ['JWT_SECRET', 'AUTH_SECRET', 'NEXTAUTH_SECRET', 'SESSION_SECRET'],
}
# Common secrets that should NOT be public
SECRET_PATTERNS = [
'SECRET', 'PASSWORD', 'KEY', 'TOKEN', 'CREDENTIAL',
'DATABASE', 'DB_URL', 'CONNECTION', 'PRIVATE',
]
# Weak or default values
WEAK_VALUES = [
'secret', 'password', 'test', 'example', 'changeme',
'123456', 'admin', 'default', 'your-secret-here',
]
def __init__(self, env_file: str = '.env'):
self.env_file = Path(env_file)
self.variables: Dict[str, str] = {}
self.issues: List[ValidationIssue] = []
def load_env_file(self) -> bool:
"""Load and parse environment file."""
if not self.env_file.exists():
self.issues.append(ValidationIssue(
severity='error',
category='missing',
variable='',
message=f'Environment file not found: {self.env_file}',
suggestion='Create the file or check the path'
))
return False
try:
content = self.env_file.read_text(encoding='utf-8')
except Exception as e:
self.issues.append(ValidationIssue(
severity='error',
category='parsing',
variable='',
message=f'Failed to read file: {e}',
))
return False
# Parse environment variables
for line in content.split('\n'):
line = line.strip()
# Skip comments and empty lines
if not line or line.startswith('#'):
continue
# Parse KEY=VALUE
match = re.match(r'^([A-Z_][A-Z0-9_]*)\s*=\s*(.*)$', line)
if match:
key = match.group(1)
value = match.group(2).strip()
# Remove quotes if present
if value.startswith('"') and value.endswith('"'):
value = value[1:-1]
elif value.startswith("'") and value.endswith("'"):
value = value[1:-1]
self.variables[key] = value
return True
def validate(self) -> List[ValidationIssue]:
"""Run all validation checks."""
self.issues = []
if not self.load_env_file():
return self.issues
# Run validation checks
self._check_required_variables()
self._check_naming_conventions()
self._check_public_secrets()
self._check_weak_values()
self._check_secret_strength()
return self.issues
def _check_required_variables(self):
"""Check for required variables."""
for category, possible_vars in self.REQUIRED_CATEGORIES.items():
found = any(var in self.variables for var in possible_vars)
if not found:
self.issues.append(ValidationIssue(
severity='error',
category='missing',
variable=category,
message=f'Missing required {category} configuration',
suggestion=f'Add one of: {", ".join(possible_vars)}'
))
def _check_naming_conventions(self):
"""Check variable naming conventions."""
for key in self.variables.keys():
# Check for lowercase or mixed case
if not key.isupper():
self.issues.append(ValidationIssue(
severity='warning',
category='naming',
variable=key,
message=f'Variable should use SCREAMING_SNAKE_CASE',
suggestion=f'Rename to: {key.upper()}'
))
# Check for invalid characters
if not re.match(r'^[A-Z_][A-Z0-9_]*$', key):
self.issues.append(ValidationIssue(
severity='warning',
category='naming',
variable=key,
message='Variable name contains invalid characters',
suggestion='Use only uppercase letters, numbers, and underscores'
))
def _check_public_secrets(self):
"""Check for secrets in public variables."""
for key, value in self.variables.items():
# Check if variable is public
if not key.startswith('NEXT_PUBLIC_'):
continue
# Check if it looks like a secret
for pattern in self.SECRET_PATTERNS:
if pattern in key.upper():
self.issues.append(ValidationIssue(
severity='error',
category='security',
variable=key,
message='Secret exposed in public variable',
suggestion=f'Remove NEXT_PUBLIC_ prefix to make it private'
))
break
# Check for database URLs in public vars
if any(db in key.upper() for db in ['DATABASE', 'DB_URL', 'POSTGRES', 'MONGODB', 'MYSQL']):
self.issues.append(ValidationIssue(
severity='error',
category='security',
variable=key,
message='Database credentials exposed in public variable',
suggestion='Remove NEXT_PUBLIC_ prefix - database URLs must be private'
))
def _check_weak_values(self):
"""Check for weak or default values."""
for key, value in self.variables.items():
value_lower = value.lower()
# Check for weak values
for weak in self.WEAK_VALUES:
if weak in value_lower:
# Only error for secrets, warn for others
severity = 'error' if any(p in key.upper() for p in self.SECRET_PATTERNS) else 'warning'
self.issues.append(ValidationIssue(
severity=severity,
category='weak',
variable=key,
message=f'Weak or default value detected',
suggestion='Use a strong, random value'
))
break
# Check for empty values in required vars
if not value:
self.issues.append(ValidationIssue(
severity='warning',
category='missing',
variable=key,
message='Variable is defined but has no value',
suggestion='Provide a value or remove the variable'
))
def _check_secret_strength(self):
"""Check strength of secret values."""
secret_keys = [
'JWT_SECRET', 'AUTH_SECRET', 'NEXTAUTH_SECRET',
'SESSION_SECRET', 'ENCRYPTION_KEY', 'SECRET_KEY'
]
for key in secret_keys:
if key not in self.variables:
continue
value = self.variables[key]
# Check minimum length (32 characters recommended)
if len(value) < 32:
self.issues.append(ValidationIssue(
severity='error',
category='weak',
variable=key,
message=f'Secret is too short ({len(value)} characters, minimum 32 recommended)',
suggestion='Generate a stronger secret with: node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"'
))
# Check for low entropy (repeating characters, patterns)
if self._is_low_entropy(value):
self.issues.append(ValidationIssue(
severity='warning',
category='weak',
variable=key,
message='Secret appears to have low entropy',
suggestion='Use a cryptographically random value'
))
def _is_low_entropy(self, value: str) -> bool:
"""Check if a value has low entropy."""
if len(value) < 10:
return True
# Check for repeating characters
unique_chars = len(set(value))
if unique_chars < len(value) / 3:
return True
# Check for sequential patterns
if re.search(r'(.)\1{5,}', value): # Same char repeated 5+ times
return True
if re.search(r'(abc|123|xyz|789)', value.lower()): # Sequential patterns
return True
return False
def compare_with(self, other_env_file: str) -> List[ValidationIssue]:
"""Compare with another environment file."""
other = EnvValidator(other_env_file)
other.load_env_file()
comparison_issues = []
# Variables in this file but not in other
missing_in_other = set(self.variables.keys()) - set(other.variables.keys())
for var in missing_in_other:
comparison_issues.append(ValidationIssue(
severity='warning',
category='missing',
variable=var,
message=f'Variable exists in {self.env_file} but missing in {other_env_file}',
suggestion=f'Add to {other_env_file} if required'
))
# Variables in other but not in this file
missing_in_this = set(other.variables.keys()) - set(self.variables.keys())
for var in missing_in_this:
comparison_issues.append(ValidationIssue(
severity='warning',
category='missing',
variable=var,
message=f'Variable exists in {other_env_file} but missing in {self.env_file}',
suggestion=f'Add to {self.env_file} if required'
))
# Check for same values (potential copy-paste error)
for var in set(self.variables.keys()) & set(other.variables.keys()):
# Skip checking NODE_ENV and similar environment-specific vars
skip_vars = ['NODE_ENV', 'NEXT_PUBLIC_APP_NAME']
if var in skip_vars:
continue
if self.variables[var] == other.variables[var]:
# Only warn for secrets or URLs (should differ per environment)
if any(p in var.upper() for p in ['SECRET', 'URL', 'KEY', 'TOKEN']):
if not var.startswith('NEXT_PUBLIC_'): # Public URLs might be same
comparison_issues.append(ValidationIssue(
severity='warning',
category='security',
variable=var,
message=f'Same value in both {self.env_file} and {other_env_file}',
suggestion='Ensure environment-specific values are different'
))
return comparison_issues
def validate_against_template(self, template_file: str) -> List[ValidationIssue]:
"""Validate against a template file (.env.example)."""
template = EnvValidator(template_file)
template.load_env_file()
template_issues = []
# Check for missing required variables
for var in template.variables.keys():
if var not in self.variables:
template_issues.append(ValidationIssue(
severity='error',
category='missing',
variable=var,
message=f'Required variable (from {template_file}) is missing',
suggestion=f'Add {var} to {self.env_file}'
))
return template_issues
def format_issues(issues: List[ValidationIssue]) -> str:
"""Format issues for display."""
if not issues:
return "[OK] No issues found!"
lines = []
# Group by severity
errors = [i for i in issues if i.severity == 'error']
warnings = [i for i in issues if i.severity == 'warning']
info = [i for i in issues if i.severity == 'info']
if errors:
lines.append("\n=== ERRORS ===\n")
for issue in errors:
lines.append(f"[X] [{issue.category.upper()}] {issue.variable}")
lines.append(f" {issue.message}")
if issue.suggestion:
lines.append(f" [TIP] {issue.suggestion}")
lines.append("")
if warnings:
lines.append("\n=== WARNINGS ===\n")
for issue in warnings:
lines.append(f"[WARN] [{issue.category.upper()}] {issue.variable}")
lines.append(f" {issue.message}")
if issue.suggestion:
lines.append(f" [TIP] {issue.suggestion}")
lines.append("")
if info:
lines.append("\n=== INFO ===\n")
for issue in info:
lines.append(f"[INFO] [{issue.category.upper()}] {issue.variable}")
lines.append(f" {issue.message}")
if issue.suggestion:
lines.append(f" [TIP] {issue.suggestion}")
lines.append("")
# Summary
lines.append("\n=== SUMMARY ===")
lines.append(f"Errors: {len(errors)}")
lines.append(f"Warnings: {len(warnings)}")
lines.append(f"Info: {len(info)}")
return '\n'.join(lines)
def main():
parser = argparse.ArgumentParser(
description='Validate environment configuration files'
)
parser.add_argument(
'--file',
default='.env',
help='Environment file to validate (default: .env)'
)
parser.add_argument(
'--compare',
nargs=2,
metavar=('FILE1', 'FILE2'),
help='Compare two environment files'
)
parser.add_argument(
'--template',
help='Validate against a template file (.env.example)'
)
args = parser.parse_args()
# Comparison mode
if args.compare:
file1, file2 = args.compare
print(f"Comparing {file1} and {file2}...\n")
validator = EnvValidator(file1)
validator.load_env_file()
comparison_issues = validator.compare_with(file2)
print(format_issues(comparison_issues))
return 1 if any(i.severity == 'error' for i in comparison_issues) else 0
# Template validation mode
if args.template:
print(f"Validating {args.file} against template {args.template}...\n")
validator = EnvValidator(args.file)
issues = validator.validate()
template_issues = validator.validate_against_template(args.template)
all_issues = issues + template_issues
print(format_issues(all_issues))
return 1 if any(i.severity == 'error' for i in all_issues) else 0
# Standard validation mode
print(f"Validating {args.file}...\n")
validator = EnvValidator(args.file)
issues = validator.validate()
print(format_issues(issues))
return 1 if any(i.severity == 'error' for i in issues) else 0
if __name__ == '__main__':
exit(main())