Secrets Management
This document covers how secrets are managed across development and production environments.
Overview
Secrets are handled differently by environment:
| Environment | Storage | Access |
|-------------|---------|--------|
| Development | .env.local | Direct file read |
| CI/CD | GitHub Secrets | Workflow injection |
| Production | AWS Secrets Manager | Runtime fetch |
Development Secrets
Local Configuration
Store secrets in .env.local (gitignored):
# .env.local
OPENAI_API_KEY=sk-your-key
NEXTAUTH_SECRET=development-secret-min-32-chars
GH_CLIENT_SECRET=your-github-oauth-secret
Required Secrets
| Secret | Description | How to Get |
|--------|-------------|------------|
| NEXTAUTH_SECRET | Session encryption | openssl rand -base64 32 |
| OPENAI_API_KEY | OpenAI API access | OpenAI dashboard |
| GH_TOKEN | GitHub API access | GitHub settings |
Optional Secrets
| Secret | Description |
|--------|-------------|
| GH_CLIENT_SECRET | GitHub OAuth |
| GOOGLE_CLIENT_SECRET | Google OAuth |
| UPSTASH_REDIS_REST_TOKEN | Rate limiting |
| APP_JWT_PRIVATE_KEY | RSA private key for app tokens |
| APP_JWT_PUBLIC_KEY | RSA public key for app tokens |
GitHub Secrets
Repository Secrets
Configure in Settings > Secrets and variables > Actions:
| Secret | Description |
|--------|-------------|
| GH_TOKEN | GitHub PAT for API access |
| GH_CLIENT_SECRET | OAuth client secret |
| GOOGLE_CLIENT_SECRET | Google OAuth secret |
| NEXTAUTH_SECRET | Auth session secret |
| REVALIDATE_SECRET | ISR revalidation |
| UPSTASH_REDIS_REST_TOKEN | Redis auth |
| APP_JWT_PRIVATE_KEY | App token signing key |
| APP_JWT_PUBLIC_KEY | App token public key |
Environment Secrets
Different secrets per environment (production, staging):
# .github/workflows/deploy.yml
env:
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
GH_CLIENT_SECRET: ${{ secrets.GH_CLIENT_SECRET }}
AWS Secrets Manager
Secret Structure
Two secrets are used in production:
Environment Secret (portfolio/env/{stage}):
Contains per-environment secrets:
{
"OPENAI_API_KEY": "sk-...",
"REVALIDATE_SECRET": "...",
"NEXTAUTH_SECRET": "...",
"GH_CLIENT_SECRET": "...",
"GOOGLE_CLIENT_SECRET": "...",
"UPSTASH_REDIS_REST_TOKEN": "...",
"CHAT_ORIGIN_SECRET": "..."
}
Repository Secret (portfolio/repo):
Contains cross-environment secrets:
{
"GH_TOKEN": "ghp_...",
"ADMIN_EMAILS": "admin@example.com",
"APP_JWT_PRIVATE_KEY": "-----BEGIN PRIVATE KEY-----\
...\
-----END PRIVATE KEY-----",
"APP_JWT_PUBLIC_KEY": "-----BEGIN PUBLIC KEY-----\
...\
-----END PUBLIC KEY-----"
}
Creating Secrets
Via AWS Console:
- Go to AWS Secrets Manager
- Store a new secret
- Choose "Other type of secret"
- Enter key-value pairs
- Name:
portfolio/env/productionorportfolio/repo
Via AWS CLI:
aws secretsmanager create-secret \
--name portfolio/env/production \
--secret-string '{"OPENAI_API_KEY":"sk-...","NEXTAUTH_SECRET":"..."}'
Updating Secrets
aws secretsmanager update-secret \
--secret-id portfolio/env/production \
--secret-string '{"OPENAI_API_KEY":"sk-new-key",...}'
Referencing in CDK
# In environment file or GitHub vars
SECRETS_MANAGER_ENV_SECRET_ID=portfolio/env/production
SECRETS_MANAGER_REPO_SECRET_ID=portfolio/repo
Runtime Injection
How It Works
- CDK stack creates Lambda functions
- Secret IDs passed via CloudFront origin headers
- Lambda@Edge wrapper reads headers on cold start
- Fetches secrets from Secrets Manager
- Merges into
process.env
Header Flow
CloudFront Origin Headers:
x-opn-env-secret-id: portfolio/env/production
x-opn-repo-secret-id: portfolio/repo
x-opn-secrets-region: us-east-1
Lambda Wrapper
// Injected by CDK during build
async function loadSecrets() {
const envSecretId = readHeader('x-opn-env-secret-id');
const region = readHeader('x-opn-secrets-region');
const secret = await secretsManager.getSecretValue({
SecretId: envSecretId,
});
const parsed = JSON.parse(secret.SecretString);
Object.assign(process.env, parsed);
}
Syncing Secrets
To GitHub
# Sync production secrets to GitHub
pnpm sync:prod:github
Uses sync-env-to-github from @volpestyle/devops.
To AWS
# Sync production secrets to AWS Secrets Manager
pnpm sync:prod:aws
Uses sync-env-to-aws from @volpestyle/devops.
Full Production Sync
# Sync to both GitHub and AWS
pnpm sync:prod
Secret Rotation
Manual Rotation
- Generate new secret value
- Update in Secrets Manager
- Redeploy Lambda (or wait for cold start)
Recommended Rotation
| Secret | Rotation Frequency |
|--------|-------------------|
| NEXTAUTH_SECRET | Annually |
| OPENAI_API_KEY | On compromise |
| GH_TOKEN | Annually |
| REVALIDATE_SECRET | On compromise |
Security Best Practices
Never Commit Secrets
# .gitignore
.env.local
.env.*.local
.env.production
Use Environment-Specific Values
Different secrets per environment:
portfolio/env/productionportfolio/env/staging
Principle of Least Privilege
Lambda roles only have secretsmanager:GetSecretValue for specific secrets.
Secret Validation
CDK validates secret references before deployment:
cd infra/cdk
pnpm validate
Troubleshooting
Secret Not Found
Error: Secrets Manager can't find the specified secret
Solutions:
- Verify secret name/ARN is correct
- Check secret exists in correct region
- Confirm IAM permissions
Permission Denied
Error: User is not authorized to perform secretsmanager:GetSecretValue
Solutions:
- Check Lambda execution role
- Verify secret resource policy
- Confirm region configuration
Cold Start Delay
Secrets are fetched on cold start. If latency is critical:
- Use provisioned concurrency
- Keep Lambda warm
- Consider caching in Lambda layer
Related Documentation
- Environment Variables - All variables
- Deployment - Environment setup
- Infrastructure - AWS resources
