Vercel Hardening Guide
Comprehensive platform security for authentication, WAF, deployment protection, secrets, network isolation, security headers, and monitoring
Overview
Vercel is a frontend cloud platform providing deployment, hosting, and serverless compute. Its attack surface includes REST API tokens, deployment secrets, Git integrations, serverless functions, edge middleware, DNS management, and third-party marketplace integrations. Compromised access exposes deployment secrets, environment variables, source code, and enables malicious deployments or supply chain attacks.
Shared Responsibility Model
Vercel operates under a shared responsibility model (source):
Vercel manages: Infrastructure security, DDoS mitigation (L3/L4/L7), TLS encryption (automatic HTTPS with TLS 1.2/1.3), platform patching, compute isolation, data encryption at rest (AES-256), certificate management, and edge network operations across 126 PoPs globally.
Customer must configure: Application-level authentication, security headers (CSP, X-Frame-Options, etc.), environment variable scoping and access controls, WAF custom rules, deployment protection settings, RBAC and team access policies, log drain forwarding to SIEM, OIDC federation for CI/CD, and domain/DNS security.
Intended Audience
- Security engineers managing deployment platforms
- DevOps and platform engineering teams
- GRC professionals assessing deployment security posture
- Third-party risk managers evaluating hosting integrations
How to Use This Guide
- L1 (Baseline): Essential controls for all organizations
- L2 (Hardened): Enhanced controls for security-sensitive environments
- L3 (Maximum Security): Strictest controls for regulated industries
Scope
This guide covers Vercel platform security configurations including authentication and RBAC, deployment protection, Web Application Firewall, network security and DDoS mitigation, security headers, secrets management, domain security, and monitoring and detection. Application-level security (e.g., Next.js framework hardening) is out of scope but referenced where relevant.
Table of Contents
- Authentication & Access Controls
- Deployment Security
- Web Application Firewall
- Network Security
- Security Headers
- Secrets Management
- Domain & Certificate Security
- Monitoring & Detection
1. Authentication & Access Controls
1.1 Enforce SSO with SAML
Profile Level: L1 (Baseline)
NIST 800-53: IA-2(1), IA-8
Description
Configure SAML Single Sign-On to centralize authentication through your identity provider and eliminate password-based Vercel logins.
Rationale
Why This Matters:
- Centralizes authentication policy enforcement through your IdP
- Enables MFA enforcement at the IdP level rather than relying on individual user compliance
- Provides single point of revocation when employees leave
Attack Prevented: Credential stuffing, password reuse, unauthorized access after employee departure
Prerequisites
- Vercel Enterprise plan (or Pro with SSO add-on)
- SAML-compatible IdP (Okta, Entra ID, Google, OneLogin, etc. – 24+ supported)
- Team Owner access in Vercel
ClickOps Implementation
Step 1: Configure SAML IdP
- Navigate to: Team Settings → Security → SAML Single Sign-On
- Select your identity provider from the 24+ supported providers
- Configure the SAML connection following your IdP’s instructions
- Map IdP groups to Vercel roles (vercel-role-owner, vercel-role-member, etc.)
Step 2: Enforce SAML
- After confirming SSO works: Toggle Enforce SAML to ON
- Distribute custom login URL:
https://vercel.com/login?saml=<team_id> - Verify session duration is 24 hours (default – re-authentication required after)
Time to Complete: ~30 minutes
Code Pack: Terraform
resource "vercel_team_config" "saml_enforcement" {
id = var.vercel_team_id
saml = {
enforced = var.saml_enforced
}
}
Validation & Testing
- Attempt login without SAML – should be blocked when enforcement is ON
- Login via IdP – should succeed and land on team dashboard
- Remove user from IdP group – should lose Vercel access within sync interval
Expected result: Only IdP-authenticated users can access the Vercel team
Monitoring & Maintenance
- Monthly: Review SAML configuration and IdP group mappings
- Quarterly: Audit active sessions and SAML enforcement status
- On event: Re-verify after IdP changes or Vercel plan changes
Operational Impact
| Aspect | Impact Level | Details |
|---|---|---|
| User Experience | Medium | Users must authenticate via IdP; custom login URL required |
| System Performance | None | No performance impact |
| Maintenance Burden | Low | Managed by IdP; Vercel config rarely changes |
| Rollback Difficulty | Easy | Toggle enforcement OFF in Team Settings |
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.1 | Logical and physical access controls |
| NIST 800-53 | IA-2(1) | Multi-factor authentication |
| ISO 27001 | A.9.4.2 | Secure log-on procedures |
| PCI DSS | 8.3.1 | Multi-factor authentication for all access |
1.2 Configure Directory Sync (SCIM)
Profile Level: L2 (Hardened)
NIST 800-53: AC-2, IA-5(1)
Description
Enable SCIM-based directory synchronization to automatically provision and deprovision team members from your identity provider.
Rationale
Why This Matters:
- Eliminates manual user lifecycle management
- Ensures immediate deprovisioning when employees leave
- Enforces consistent role assignments across the organization
Attack Prevented: Orphaned accounts, delayed deprovisioning, unauthorized persistent access
Prerequisites
- Vercel Enterprise plan
- SAML SSO configured (Section 1.1)
- IdP supports SCIM (Okta, Entra ID, etc.)
ClickOps Implementation
Step 1: Enable Directory Sync
- Navigate to: Team Settings → Security → Directory Sync
- Generate SCIM endpoint URL and bearer token
- Configure your IdP with the SCIM endpoint
Step 2: Map Groups to Roles
- Create IdP groups matching Vercel roles:
vercel-role-owner,vercel-role-member,vercel-role-developer,vercel-role-security,vercel-role-billing - Map IdP groups to Access Groups for project-level permissions
- Ensure at least one owner mapping exists to prevent lockout
Time to Complete: ~45 minutes
Code Pack: API Script
# --- Verify team SAML/SCIM configuration ---
echo "=== Directory Sync Configuration ==="
curl -s -H "Authorization: Bearer ${VERCEL_TOKEN}" \
"https://api.vercel.com/v2/teams/${VERCEL_TEAM_ID}" | \
jq '{
name: .name,
saml: .saml,
remoteCaching: .remoteCaching,
membership: .membership
}'
# --- List current team members and their roles ---
echo ""
echo "=== Current Team Members ==="
curl -s -H "Authorization: Bearer ${VERCEL_TOKEN}" \
"https://api.vercel.com/v2/teams/${VERCEL_TEAM_ID}/members?limit=100" | \
jq '.members[] | {uid, email, role, joinedFrom}'
# --- Audit members for role compliance ---
echo ""
echo "=== Members with Owner Role (should be minimal) ==="
curl -s -H "Authorization: Bearer ${VERCEL_TOKEN}" \
"https://api.vercel.com/v2/teams/${VERCEL_TEAM_ID}/members?limit=100" | \
jq '.members[] | select(.role == "OWNER") | {uid, email}'
# --- Verify Access Groups exist (Enterprise) ---
echo ""
echo "=== Access Groups ==="
curl -s -H "Authorization: Bearer ${VERCEL_TOKEN}" \
"https://api.vercel.com/v1/access-groups?teamId=${VERCEL_TEAM_ID}" | \
jq '.accessGroups[]? | {name, membersCount, projectsCount}'
Validation & Testing
- Add a test user in IdP – should appear in Vercel team within sync interval
- Remove test user from IdP group – should lose Vercel access
- Change user role in IdP – should reflect in Vercel
Expected result: Team membership mirrors IdP directory state
Operational Impact
| Aspect | Impact Level | Details |
|---|---|---|
| User Experience | Low | Transparent to end users |
| System Performance | None | No performance impact |
| Maintenance Burden | Low | Fully automated after setup |
| Rollback Difficulty | Moderate | Must manually manage members if disabled |
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.2 | Prior to issuing system credentials, verify identity |
| NIST 800-53 | AC-2 | Account management |
| ISO 27001 | A.9.2.1 | User registration and de-registration |
| PCI DSS | 8.1.3 | Immediately revoke access for terminated users |
1.3 Enforce Least-Privilege RBAC
Profile Level: L1 (Baseline)
NIST 800-53: AC-3, AC-6
Description
Configure team and project-level role-based access control using Vercel’s granular role system and Access Groups.
Rationale
Why This Matters:
- Prevents over-privileged access to production environments
- Developers cannot modify production environment variables without explicit elevation
- Security role enables firewall management without deployment access
Attack Prevented: Insider threat, privilege escalation, unauthorized production modifications
Vercel Role Summary:
| Role | Deploy | Prod Env Vars | Billing | Firewall | Members |
|---|---|---|---|---|---|
| Owner | Yes | Yes | Yes | Yes | Yes |
| Member | Yes | Yes | No | No | No |
| Developer | Yes | No | No | No | No |
| Security | No | No | No | Yes | No |
| Billing | No | No | Yes | No | No |
| Viewer | No | No | No | No | No |
| Contributor | Per-project | Per-project | No | No | No |
ClickOps Implementation
Step 1: Audit Current Roles
- Navigate to: Team Settings → Members
- Review all members and their assigned roles
- Identify over-privileged accounts (Owners who should be Members, etc.)
Step 2: Implement Least Privilege
- Downgrade accounts to minimum required role
- Use Contributor role + project-level assignments for granular access
- Create Access Groups for team-based project permissions
- Assign Permission Groups additively (Create Project, Full Production Deployment, etc.)
Step 3: Configure Access Groups (Enterprise)
- Navigate to: Team Settings → Access Groups
- Create groups aligned to team structure (e.g., “Frontend Team”, “Platform Team”)
- Assign projects with appropriate roles (Admin, Developer, Viewer)
- Link to Directory Sync groups if SCIM is configured
Time to Complete: ~20 minutes
Code Pack: Terraform
# --- L1: Manage team members with least-privilege roles ---
resource "vercel_team_member" "members" {
for_each = var.team_members
team_id = var.vercel_team_id
email = each.value.email
role = each.value.role
}
# --- L2: Create Access Groups for project-level permissions (Enterprise) ---
resource "vercel_access_group" "groups" {
for_each = var.profile_level >= 2 ? var.access_groups : {}
team_id = var.vercel_team_id
name = each.key
}
# --- L2: Link Access Groups to projects ---
resource "vercel_access_group_project" "assignments" {
for_each = var.profile_level >= 2 ? var.access_group_projects : {}
team_id = var.vercel_team_id
access_group_id = vercel_access_group.groups[each.value.group_name].id
project_id = each.value.project_id
role = each.value.role
}
Validation & Testing
- Developer role cannot modify production environment variables
- Security role can manage firewall but cannot deploy
- Viewer role has read-only access with no deploy capability
- Contributor role has no access until explicitly assigned to a project
Expected result: Each team member has minimum required permissions
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.1, CC6.3 | Logical access controls, role-based access |
| NIST 800-53 | AC-3, AC-6 | Access enforcement, least privilege |
| ISO 27001 | A.9.1.2 | Access to networks and network services |
| PCI DSS | 7.1 | Limit access to system components |
1.4 Harden API Token Lifecycle
Profile Level: L1 (Baseline)
NIST 800-53: IA-5, IA-4
Description
Enforce scoped, time-limited API tokens and replace long-lived credentials with OIDC federation where possible.
Rationale
Why This Matters:
- Vercel now enforces 90-day maximum lifetime on granular tokens
- Classic tokens have been revoked platform-wide
- OIDC federation eliminates static credentials entirely for cloud provider access
- 2FA is required by default for token creation
Attack Prevented: Token theft, credential leakage in CI/CD logs, unauthorized API access
ClickOps Implementation
Step 1: Audit Existing Tokens
- Navigate to: Account Settings → Tokens
- Review all active tokens for scope and expiration
- Delete unused or overly-scoped tokens
Step 2: Create Scoped Tokens
- Create new tokens with minimum required scopes
- Set expiration to shortest practical duration (max 90 days)
- Use descriptive names indicating purpose (e.g., “github-actions-deploy”)
Step 3: Implement OIDC Federation (Preferred)
- Navigate to: Team Settings → OIDC Federation
- Set issuer mode to Team (recommended over Global)
- Configure cloud provider trust policies (AWS, GCP, Azure)
- Replace static credentials in environment variables with OIDC token references
Time to Complete: ~30 minutes
Code Pack: CLI Script
# --- Audit existing tokens via Vercel API ---
echo "=== Auditing Vercel API Tokens ==="
curl -s -H "Authorization: Bearer ${VERCEL_TOKEN}" \
"https://api.vercel.com/v5/user/tokens" | \
jq '.tokens[] | {id, name, activeAt, expiresAt, type}'
# --- List tokens with no expiration (security risk) ---
echo ""
echo "=== Tokens Without Expiration (ACTION REQUIRED) ==="
curl -s -H "Authorization: Bearer ${VERCEL_TOKEN}" \
"https://api.vercel.com/v5/user/tokens" | \
jq '.tokens[] | select(.expiresAt == null) | {id, name, createdAt}'
# --- Create a scoped token with 90-day max expiration ---
echo ""
echo "=== Creating Scoped Token (example) ==="
# Uncomment and customize:
# curl -s -X POST -H "Authorization: Bearer ${VERCEL_TOKEN}" \
# -H "Content-Type: application/json" \
# "https://api.vercel.com/v5/user/tokens" \
# -d '{
# "name": "github-actions-deploy",
# "expiresAt": '"$(($(date +%s) + 7776000))000"',
# "type": "oauth2-token"
# }'
# --- Verify OIDC federation status ---
echo ""
echo "=== OIDC Federation Status ==="
curl -s -H "Authorization: Bearer ${VERCEL_TOKEN}" \
"https://api.vercel.com/v1/teams/${VERCEL_TEAM_ID}" | \
jq '{oidcTokenConfig: .oidcTokenConfig}'
Validation & Testing
- No tokens exist with unlimited expiration
- OIDC federation provides short-lived credentials (60-min TTL)
- All CI/CD pipelines use scoped tokens or OIDC
- Token creation requires 2FA
Expected result: No long-lived, overly-scoped tokens; OIDC for cloud provider access
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.1 | Logical access controls |
| NIST 800-53 | IA-5 | Authenticator management |
| ISO 27001 | A.9.2.4 | Management of secret authentication information |
| PCI DSS | 8.2.4 | Change user passwords/passphrases at least every 90 days |
2. Deployment Security
2.1 Configure Deployment Protection
Profile Level: L1 (Baseline)
NIST 800-53: CM-3, AC-3
Description
Enable multi-layered deployment protection using Vercel Authentication, password protection, and trusted IPs to prevent unauthorized access to preview and production deployments.
Rationale
Why This Matters:
- Preview deployments can expose unreleased features, staging credentials, and internal APIs
- Unprotected preview URLs are indexed by search engines and discoverable by attackers
- Production environment variables can leak through unprotected preview deployments
Attack Prevented: Unauthorized access to staging environments, information disclosure via preview URLs, credential harvesting from preview deployments
Attack Scenario: Attacker discovers *.vercel.app preview URL via DNS enumeration, accesses unprotected preview with staging database credentials exposed in client-side code.
ClickOps Implementation
Step 1: Enable Standard Protection (All Plans)
- Navigate to: Project Settings → Deployment Protection
- Set protection level to Standard Protection (protects all except production custom domains)
- Enable Vercel Authentication – requires team login for preview access
Step 2: Add Password Protection (L2 – Enterprise/Pro Add-on)
- Enable Password Protection for preview deployments
- Set a strong, unique password and distribute via secure channel
- Consider enabling for all deployments in sensitive projects
Step 3: Configure Trusted IPs (L3 – Enterprise)
- Add office and VPN egress IP ranges as trusted IPs
- Set protection mode to Trusted IP Required
- Apply to All Deployments for maximum protection
Step 4: Harden Protection Bypass
- Review Protection Bypass for Automation settings
- If used: ensure
VERCEL_AUTOMATION_BYPASS_SECRETis a strong 32-character random value - L3: Disable automation bypass entirely if not required
Time to Complete: ~15 minutes
Code Pack: Terraform
# --- L1: Production branch protection and deployment settings ---
resource "vercel_project" "hardened" {
name = data.vercel_project.current.name
git_repository = var.git_repository != "" ? {
type = var.git_provider
repo = var.git_repository
production_branch = var.production_branch
} : null
# Block deployments from forked repositories
git_fork_protection = var.git_fork_protection_enabled
# Disable preview deployments for tighter control (L2+)
preview_deployments_disabled = var.profile_level >= 2
# Enable skew protection to prevent version mismatches (L2+)
skew_protection = var.profile_level >= 2 ? "12 hours" : null
# Prioritize production builds over preview builds
prioritise_production_builds = true
# Git provider security options
git_provider_options = {
create_deployments = var.profile_level >= 2 ? "only-production" : "enabled"
}
# Vercel Authentication on preview deployments (L1)
vercel_authentication = {
deployment_type = "all_deployments"
}
}
# --- L2: Password-protect preview deployments ---
resource "vercel_project" "preview_password_protection" {
count = var.profile_level >= 2 && var.preview_password != "" ? 1 : 0
name = data.vercel_project.current.name
password_protection = {
deployment_type = "preview"
password = var.preview_password
}
}
# --- L3: Trusted IPs restrict access to known networks (Enterprise) ---
resource "vercel_project" "trusted_ips" {
count = var.profile_level >= 3 && length(var.trusted_ip_addresses) > 0 ? 1 : 0
name = data.vercel_project.current.name
trusted_ips = {
addresses = var.trusted_ip_addresses
deployment_type = "all_deployments"
protection_mode = "trusted_ip_required"
}
}
# --- L3: Disable automation bypass ---
resource "vercel_project" "automation_bypass" {
count = var.profile_level >= 3 ? 1 : 0
name = data.vercel_project.current.name
protection_bypass_for_automation = false
}
# --- Data source to read current project configuration ---
data "vercel_project" "current" {
name = null
id = var.project_id
team_id = var.vercel_team_id
}
Validation & Testing
- Unauthenticated access to preview URL returns login prompt
- Password-protected deployment requires correct password
- Access from non-trusted IP is blocked (Enterprise)
- Automation bypass secret is 32+ characters if enabled
Expected result: All non-production deployments require authentication
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.1 | Logical and physical access controls |
| NIST 800-53 | CM-3, AC-3 | Configuration change control, access enforcement |
| ISO 27001 | A.14.2.5 | Secure system engineering principles |
| PCI DSS | 6.4.1 | Separate development/test from production |
2.2 Harden Git Integration
Profile Level: L1 (Baseline)
NIST 800-53: CM-7, SA-10
Description
Secure the Git integration pipeline to prevent unauthorized deployments from forks, unverified commits, and compromised repositories.
Rationale
Why This Matters:
- Fork-based deployments can inject malicious code into your deployment pipeline
- Unverified commits may contain unauthorized changes
- Unrestricted deployment triggers enable supply chain attacks
Attack Prevented: Supply chain injection via forks, unauthorized code deployment, commit impersonation
ClickOps Implementation
Step 1: Enable Fork Protection
- Navigate to: Project Settings → Git
- Ensure Git Fork Protection is enabled (blocks deployments from forked repos without approval)
Step 2: Restrict Deployment Creation (L2)
- Set Create Deployments to Only Production – prevents preview deployments from PRs
- Or set to Disabled for fully manual deployment control
Step 3: Require Verified Commits (L2)
- Enable Require Verified Commits in Git provider options
- Configure commit signing in your Git provider (GPG or SSH keys)
Step 4: Review Connected Repositories
- Navigate to: Team Settings → Integrations
- Audit all connected Git repositories
- Remove access to repositories no longer in use
- Limit repository access to specific repos rather than full organization access
Time to Complete: ~10 minutes
Code Pack: Terraform
# --- L1: Git fork protection prevents unauthorized fork deployments ---
# Note: git_fork_protection is configured in hth-vercel-2.01 as part of
# the vercel_project resource.
# --- L2: Require verified (signed) commits for deployments ---
resource "vercel_project" "verified_commits" {
count = var.profile_level >= 2 && var.require_verified_commits ? 1 : 0
name = data.vercel_project.current.name
git_provider_options = {
require_verified_commits = true
}
}
Validation & Testing
- Fork deployment is blocked without explicit approval
- Unsigned commits fail deployment (when verified commits enabled)
- Only authorized repositories are connected
- Deployment creation restricted to production-only (L2)
Expected result: Deployment pipeline only accepts authorized, verified code
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC8.1 | Change management controls |
| NIST 800-53 | CM-7, SA-10 | Least functionality, developer security testing |
| ISO 27001 | A.14.2.2 | System change control procedures |
| PCI DSS | 6.3.2 | Review custom code prior to release |
2.3 Configure Rolling Releases
Profile Level: L2 (Hardened)
NIST 800-53: CM-3(2)
Description
Enable progressive deployment rollouts to limit blast radius of production changes.
Rationale
Why This Matters:
- Full instant deployments expose 100% of traffic to potential issues
- Rolling releases enable canary-style testing with real production traffic
- Manual approval gates add human verification before full rollout
Attack Prevented: Blast radius of compromised deployments, rapid exploitation of deployed vulnerabilities
ClickOps Implementation
Step 1: Configure Rolling Release
- Navigate to: Project Settings → Rolling Releases
- Choose Manual Approval for production deployments
- Configure stages (e.g., 10% → 50% → 100%)
- Set duration for automatic advancement if using automatic mode
Time to Complete: ~10 minutes
Code Pack: API Script
# --- Check current project deployment settings ---
echo "=== Project Deployment Configuration ==="
curl -s -H "Authorization: Bearer ${VERCEL_TOKEN}" \
"https://api.vercel.com/v9/projects/${VERCEL_PROJECT_ID}?teamId=${VERCEL_TEAM_ID}" | \
jq '{
name: .name,
framework: .framework,
productionDeploymentWorkflow: .productionDeploymentWorkflow,
skewProtection: .skewProtection
}'
# --- List recent deployments with rollout status ---
echo ""
echo "=== Recent Deployments ==="
curl -s -H "Authorization: Bearer ${VERCEL_TOKEN}" \
"https://api.vercel.com/v6/deployments?projectId=${VERCEL_PROJECT_ID}&teamId=${VERCEL_TEAM_ID}&limit=5" | \
jq '.deployments[] | {uid, state, createdAt, meta: .meta.githubCommitMessage}'
# --- Verify skew protection is enabled ---
echo ""
echo "=== Skew Protection Status ==="
curl -s -H "Authorization: Bearer ${VERCEL_TOKEN}" \
"https://api.vercel.com/v9/projects/${VERCEL_PROJECT_ID}?teamId=${VERCEL_TEAM_ID}" | \
jq '.skewProtection // "not configured"'
Validation & Testing
- New deployment starts at first stage percentage
- Manual approval required before advancing (if configured)
- Rollback available at any stage
Expected result: Production deployments roll out progressively with approval gates
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC8.1 | Change management process |
| NIST 800-53 | CM-3(2) | Testing, validation, and documentation of changes |
| ISO 27001 | A.14.2.9 | System acceptance testing |
| PCI DSS | 6.4.5 | Change control procedures |
3. Web Application Firewall
3.1 Enable WAF with Managed Rulesets
Profile Level: L2 (Hardened)
NIST 800-53: SC-7, SI-3
Description
Enable the Vercel Web Application Firewall with OWASP managed rulesets, bot protection, and AI bot filtering.
Rationale
Why This Matters:
- Vercel WAF cannot be bypassed once enabled – all traffic passes through it
- Managed rulesets protect against OWASP Top 10 without custom rule writing
- Vercel paid $1M+ to 116 security researchers for WAF bypass testing
- Rules propagate globally in under 300ms with instant rollback capability
Attack Prevented: SQL injection, XSS, command injection, path traversal, remote file inclusion, bot abuse, AI scraping
Real-World Incidents:
- CVE-2025-29927: Next.js middleware authorization bypass via
x-middleware-subrequestheader – Vercel WAF updated proactively - React2Shell (CVE-2025-55182): Critical RCE in React Server Components – Vercel WAF rules deployed before disclosure
Prerequisites
- Vercel Enterprise plan for managed rulesets
- Pro plan for custom rules (up to 40)
ClickOps Implementation
Step 1: Enable Firewall
- Navigate to: Project → Firewall
- Toggle firewall to Enabled
Step 2: Enable OWASP Managed Rulesets (Enterprise)
- Navigate to: Firewall → Managed Rulesets
- Enable OWASP Core Ruleset in Log mode first
- Monitor for 48-72 hours for false positives
- Switch to Deny mode after tuning
Step 3: Enable Bot Protection (Enterprise)
- Enable Bot Protection Managed Ruleset in Challenge mode
- Enable AI Bots Managed Ruleset in Deny mode (unless AI crawling desired)
Step 4: Configure Custom Rules (Pro+)
- Create rules for application-specific protection
- Always test in Log mode before promoting to Deny/Challenge
Time to Complete: ~30 minutes
Code Pack: Terraform
# --- L2: Enable Web Application Firewall with OWASP managed rulesets ---
resource "vercel_firewall_config" "waf" {
project_id = var.project_id
team_id = var.vercel_team_id
enabled = var.firewall_enabled
managed_rulesets = {
owasp = {
active = true
action = var.waf_owasp_action
}
}
# Note: Bot Protection and AI Bot rulesets are managed via
# Vercel dashboard or API (not yet in Terraform provider)
}
Validation & Testing
- WAF is enabled and processing traffic (check Firewall tab)
- OWASP rules detecting common attack patterns in logs
- Bot protection challenging automated requests
- AI bots blocked (if configured)
Expected result: WAF actively filtering malicious traffic with managed rulesets
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.6 | Security measures against threats outside boundaries |
| NIST 800-53 | SC-7, SI-3 | Boundary protection, malicious code protection |
| ISO 27001 | A.13.1.1 | Network controls |
| PCI DSS | 6.6 | Web application firewall |
3.2 Configure IP Blocking and Rate Limiting
Profile Level: L1 (Baseline)
NIST 800-53: SC-5, SI-4
Description
Implement IP-based access control and rate limiting to protect against brute force attacks, abuse, and targeted threats.
Rationale
Why This Matters:
- IP blocking available on all plans (Hobby: 10, Pro: 100, Enterprise: custom)
- Rate limiting prevents brute force, credential stuffing, and API abuse
- Persistent actions automatically block repeat offenders for configurable durations
Attack Prevented: Brute force attacks, credential stuffing, API abuse, scraping, DDoS amplification
ClickOps Implementation
Step 1: Block Known Bad IPs
- Navigate to: Project → Firewall → IP Blocking
- Add known malicious IP addresses or ranges
- Use per-host blocking for domain-specific rules
Step 2: Configure Rate Limiting Rules (Pro+)
- Navigate to: Firewall → Configure → Rules
- Create rate limiting rules for sensitive endpoints:
- Authentication endpoints: 10 requests/minute per IP
- API endpoints: appropriate limits per use case
- Registration: 5 requests/minute per IP
- Set follow-up action to Deny with persistent duration (e.g., 5 minutes)
- Use Log action first to validate thresholds
Step 3: Enable Persistent Actions
- Configure persistent actions on deny/challenge rules
- Set duration based on attack type (1 min for rate limits, longer for abuse patterns)
Time to Complete: ~15 minutes
Code Pack: Terraform
# --- L1: Configure firewall with IP blocking rules ---
resource "vercel_firewall_config" "ip_blocking" {
project_id = var.project_id
team_id = var.vercel_team_id
enabled = true
# IP blocking rules
dynamic "rules" {
for_each = var.blocked_ip_addresses
content {
name = rules.value.note != "" ? rules.value.note : "Block ${rules.value.value}"
action = "deny"
active = true
condition_group = [{
conditions = [{
type = "ip_address"
op = "eq"
value = rules.value.value
}]
}]
}
}
# L2: Rate limiting rules for sensitive endpoints
dynamic "rules" {
for_each = var.profile_level >= 2 ? var.rate_limit_rules : []
content {
name = rules.value.name
action = "rate_limit"
active = true
rate_limit = {
limit = rules.value.limit
window = rules.value.window
action = rules.value.follow_up_action
}
condition_group = [{
conditions = [{
type = "path"
op = "pre"
value = rules.value.path
}]
}]
}
}
}
Validation & Testing
- Blocked IPs return 403/challenge response
- Rate-limited endpoints enforce configured thresholds
- Persistent actions block repeat offenders
- Rules show in Firewall activity logs
Expected result: Malicious and abusive traffic blocked at the edge
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.6 | Security measures against external threats |
| NIST 800-53 | SC-5, SI-4 | Denial of service protection, system monitoring |
| ISO 27001 | A.13.1.2 | Security of network services |
| PCI DSS | 11.4 | Intrusion-detection/prevention techniques |
4. Network Security
4.1 Enable Secure Compute
Profile Level: L3 (Maximum Security)
NIST 800-53: SC-7, SC-8
Description
Deploy Serverless Functions within dedicated private networks with static IPs, VPC peering, and network isolation using Vercel Secure Compute.
Rationale
Why This Matters:
- Dedicated IP pairs not shared with any other customer
- Enables IP allowlisting on backend databases and APIs
- Full network isolation in a private VPC
- Regional failover with active + passive networks
Attack Prevented: Shared IP abuse, unauthorized backend access, network-level lateral movement
Prerequisites
- Vercel Enterprise plan
- Secure Compute add-on ($6,500/year + $0.15/GB data transfer)
- Backend services supporting IP allowlisting
ClickOps Implementation
Step 1: Create Secure Compute Network
- Navigate to: Team Settings → Connectivity → Create Network
- Select AWS region closest to your backend
- Configure CIDR block (must not overlap with VPC peer ranges)
- Select availability zones
Step 2: Assign Projects
- Add projects to the network
- Configure per-environment (Production, Preview, etc.)
- Optionally include build container (adds ~5s provisioning delay)
Step 3: Configure VPC Peering (Optional)
- Create peering connection from Vercel dashboard
- Accept in AWS VPC dashboard
- Update route tables in both VPCs
- Configure security groups to allow Vercel IP ranges
Step 4: Update Backend Allowlists
- Add Vercel dedicated IPs to backend database firewall rules
- Add to API gateway IP allowlists
- Always layer authentication on top of IP filtering
Time to Complete: ~60 minutes
Code Pack: Terraform
# --- L3: Create Secure Compute network with static IPs ---
resource "vercel_network" "secure_compute" {
count = var.profile_level >= 3 && var.secure_compute_enabled ? 1 : 0
team_id = var.vercel_team_id
name = var.secure_compute_name
region = var.secure_compute_region
}
# --- L3: Link project to Secure Compute network ---
resource "vercel_network_project_link" "secure_link" {
count = var.profile_level >= 3 && var.secure_compute_enabled ? 1 : 0
team_id = var.vercel_team_id
network_id = vercel_network.secure_compute[0].id
project_id = var.project_id
}
Validation & Testing
- Functions connect to backend via private network
- Backend rejects connections from non-Vercel IPs
- Region failover switches to passive network on outage
- VPC peering routes traffic correctly (if configured)
Expected result: Serverless Functions operate in isolated private network with static egress IPs
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.1 | Logical access controls |
| NIST 800-53 | SC-7, SC-8 | Boundary protection, transmission confidentiality |
| ISO 27001 | A.13.1.3 | Segregation in networks |
| PCI DSS | 1.3 | Network access to the cardholder data environment is restricted |
4.2 Configure DDoS Protection and Attack Challenge Mode
Profile Level: L1 (Baseline)
NIST 800-53: SC-5, CP-10
Description
Leverage Vercel’s automatic DDoS mitigation and configure Attack Challenge Mode for active attack response.
Rationale
Why This Matters:
- Automatic L3/L4/L7 DDoS mitigation on all plans at no cost
- Blocked DDoS traffic is NOT billed
- Attack Challenge Mode provides additional layer during active targeted attacks
- System Bypass Rules prevent legitimate traffic from being blocked
Attack Prevented: Volumetric DDoS, SYN floods, application-layer floods, amplification attacks
ClickOps Implementation
Step 1: Verify DDoS Protection (Automatic)
- DDoS protection is always enabled – no configuration required
- Verify in: Project → Firewall – should show traffic monitoring
Step 2: Configure Attack Challenge Mode (During Attacks)
- Navigate to: Project → Firewall → Bot Management → Attack Challenge Mode
- Enable during active attacks – challenges ALL visitors with JS security check
- Known bots (Googlebot, webhooks) auto-bypass
- Cron jobs from your account auto-bypass
- Disable when attack subsides
Step 3: Configure Spend Management (Pro+)
- Navigate to: Team Settings → Billing → Spend Management
- Set usage thresholds with automatic actions
- Configure webhook notifications for usage spikes
- Enable auto-pause for non-critical projects
Step 4: Configure System Bypass Rules (L2 – Pro+)
- Create rules to ensure essential traffic (proxies, shared networks) is never blocked
- Use for business-critical external services
Time to Complete: ~10 minutes
Code Pack: Terraform
# --- L1: Enable Attack Challenge Mode (activate during active attacks) ---
resource "vercel_attack_challenge_mode" "protection" {
project_id = var.project_id
team_id = var.vercel_team_id
enabled = var.attack_challenge_mode_enabled
}
Validation & Testing
- DDoS mitigation active (always on – verify via Firewall dashboard)
- Attack Challenge Mode can be enabled/disabled
- Spend management alerts configured
- Blocked traffic not appearing in billing
Expected result: Multi-layered DDoS protection with cost controls
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | A1.2 | Environmental protections |
| NIST 800-53 | SC-5, CP-10 | Denial of service protection, system recovery |
| ISO 27001 | A.17.2.1 | Availability of information processing facilities |
| PCI DSS | 11.4 | Intrusion detection/prevention |
5. Security Headers
5.1 Configure Security Response Headers
Profile Level: L1 (Baseline)
NIST 800-53: SI-10, SC-28
Description
Configure security headers (CSP, X-Frame-Options, Referrer-Policy, etc.) to protect against client-side attacks. Vercel does NOT set these automatically beyond HSTS – you must configure them.
Rationale
Why This Matters:
- Vercel auto-configures HSTS but NO other security headers
- Missing CSP enables XSS attacks; missing X-Frame-Options enables clickjacking
- Security headers are the primary defense against client-side attacks
- Headers must be set by the customer per Vercel’s shared responsibility model
Attack Prevented: Cross-site scripting (XSS), clickjacking, MIME-type sniffing, referrer leakage, unauthorized API embedding
Real-World Incidents:
- Vercel XSS in Clone URL (2024): Reflected XSS found in Vercel’s own clone functionality – reinforces need for CSP even on trusted platforms
ClickOps Implementation
Step 1: Configure via vercel.json
- Add a
headersconfiguration block to yourvercel.json - Apply to all routes using
source: "/(.*)"pattern
Step 2: Required Security Headers
Content-Security-Policy: Define allowed content sources (most impactful header)X-Frame-Options: Set toDENYorSAMEORIGINX-Content-Type-Options: Set tonosniffReferrer-Policy: Set tostrict-origin-when-cross-originPermissions-Policy: Restrict browser features (camera, microphone, geolocation, etc.)X-XSS-Protection: Set to1; mode=block(legacy but still useful)
Step 3: Validate
- Test with SecurityHeaders.com
- Review CSP reports if using
report-uriorreport-todirective
Time to Complete: ~20 minutes
Code Pack: CLI Script
# --- Deploy vercel.json with security headers ---
# Add this configuration to your project's vercel.json:
cat > /tmp/hth-vercel-headers.json << 'HEADERS_EOF'
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Content-Security-Policy",
"value": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
},
{
"key": "X-Frame-Options",
"value": "DENY"
},
{
"key": "X-Content-Type-Options",
"value": "nosniff"
},
{
"key": "Referrer-Policy",
"value": "strict-origin-when-cross-origin"
},
{
"key": "Permissions-Policy",
"value": "camera=(), microphone=(), geolocation=(), interest-cohort=()"
},
{
"key": "Strict-Transport-Security",
"value": "max-age=63072000; includeSubDomains; preload"
},
{
"key": "X-XSS-Protection",
"value": "1; mode=block"
}
]
}
]
}
HEADERS_EOF
echo "Security headers config written to /tmp/hth-vercel-headers.json"
echo "Merge this into your project's vercel.json, then deploy:"
echo " vercel deploy --prod"
# --- Validate deployed headers ---
echo ""
echo "=== Validating Security Headers ==="
DOMAIN="${1:-}"
if [ -n "${DOMAIN}" ]; then
echo "Checking headers for: ${DOMAIN}"
curl -sI "https://${DOMAIN}" | grep -iE \
"content-security-policy|x-frame-options|x-content-type|referrer-policy|permissions-policy|strict-transport|x-xss"
else
echo "Usage: $0 <your-domain.com>"
fi
Validation & Testing
- All six security headers present in response
- SecurityHeaders.com score of A or A+
- No CSP violations in browser console for legitimate resources
- X-Frame-Options prevents iframe embedding
Expected result: All security headers configured and validated
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.6 | Security measures against threats |
| NIST 800-53 | SI-10, SC-28 | Information input validation, protection of information at rest |
| ISO 27001 | A.14.1.2 | Securing application services on public networks |
| PCI DSS | 6.5.7 | Cross-site scripting (XSS) |
6. Secrets Management
6.1 Environment Variable Security
Profile Level: L1 (Baseline)
NIST 800-53: SC-28, SC-12
Description
Implement secure environment variable management with proper scoping, sensitivity flags, and access controls.
Rationale
Why This Matters:
- All environment variables are encrypted at rest (AES-256) by Vercel
- Variables scoped to production are only accessible to Owner/Member/Project Admin roles
NEXT_PUBLIC_prefixed variables are exposed to client-side code – never use for secrets- Preview branches can access production secrets if not properly scoped
- Total limit: 64 KB per deployment; Edge Functions: 5 KB per variable
Attack Prevented: Secret exposure in client bundles, credential leakage via preview deployments, unauthorized production secret access
Attack Scenario: Developer creates a NEXT_PUBLIC_API_SECRET variable, exposing it in the client-side JavaScript bundle. Attacker views page source to extract the API key.
ClickOps Implementation
Step 1: Audit Environment Variables
- Navigate to: Project Settings → Environment Variables
- Review all variables for proper environment scoping
- Verify no secrets use
NEXT_PUBLIC_prefix - Ensure sensitive variables are scoped to production only (not preview)
Step 2: Enable Sensitive Variable Policy (L2)
- Navigate to: Team Settings → General → Sensitive Environment Variable Policy
- Enable to enforce sensitive flag on all new variables
- Sensitive values cannot be read back from dashboard after creation
Step 3: Scope Variables Properly
- Production secrets: Scope to Production only
- Preview/staging secrets: Use separate, lower-privilege credentials for Preview
- Use branch-specific preview variables when different branches need different configs
- Use shared (team-level) variables for consistent cross-project configuration
Step 4: Implement OIDC Federation (L2)
- Replace static cloud credentials with OIDC tokens (see Section 1.4)
- OIDC provides 60-minute TTL tokens – no static secrets needed
Time to Complete: ~15 minutes
Code Pack: Terraform
# --- L1: Configure environment variables with sensitivity flags ---
resource "vercel_project_environment_variable" "secrets" {
for_each = var.environment_variables
project_id = var.project_id
team_id = var.vercel_team_id
key = each.key
value = each.value.value
target = each.value.target
sensitive = each.value.sensitive
}
# --- L2: Enforce sensitive environment variable policy at team level ---
resource "vercel_team_config" "sensitive_env_policy" {
count = var.profile_level >= 2 ? 1 : 0
id = var.vercel_team_id
sensitive_environment_variable_policy = "on"
}
# --- L2: Hide IP addresses in observability (privacy hardening) ---
resource "vercel_team_config" "hide_ips" {
count = var.profile_level >= 2 ? 1 : 0
id = var.vercel_team_id
hide_ip_addresses = true
hide_ip_addresses_in_log_drains = true
}
Validation & Testing
- No
NEXT_PUBLIC_variables contain secret values - Production secrets not accessible in preview environment
- Sensitive variable policy enforced at team level (L2)
- OIDC federation active for cloud provider access (L2)
Expected result: Secrets properly scoped, flagged sensitive, and not exposed client-side
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.1 | Logical access controls |
| NIST 800-53 | SC-28, SC-12 | Protection of information at rest, cryptographic key management |
| ISO 27001 | A.10.1.2 | Key management |
| PCI DSS | 3.4 | Render PAN unreadable anywhere it is stored |
6.2 Deployment Retention Policy
Profile Level: L2 (Hardened)
NIST 800-53: SI-12
Description
Configure deployment retention policies to automatically remove old deployments that may contain outdated secrets or vulnerable code.
Rationale
Why This Matters:
- Old deployments remain accessible with their original environment variables
- Retaining deployments indefinitely increases attack surface
- Compliance frameworks require data retention policies
Attack Prevented: Exploitation of outdated deployments with known vulnerabilities or leaked secrets
ClickOps Implementation
Step 1: Configure Retention
- Navigate to: Project Settings → Deployment Retention
- Set production retention: 1 year (or per compliance requirement)
- Set preview retention: 1 month
- Set errored/canceled retention: 1 week
Time to Complete: ~5 minutes
Code Pack: Terraform
# --- L2: Configure deployment retention to limit exposure of old deployments ---
resource "vercel_project" "deployment_retention" {
count = var.profile_level >= 2 ? 1 : 0
name = data.vercel_project.current.name
deployment_expiration = {
deploymentsToKeep = var.deployments_to_keep
expirationDays = var.deployment_expiration_days
expirationDaysCanceled = var.deployment_expiration_days_canceled
expirationDaysErrored = var.deployment_expiration_days_errored
expirationDaysProduction = var.deployment_expiration_days_production
}
}
Validation & Testing
- Retention policies set per environment type
- Old deployments automatically cleaned up
Expected result: Deployment history managed with appropriate retention limits
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.5 | Disposal of confidential information |
| NIST 800-53 | SI-12 | Information management and retention |
| ISO 27001 | A.8.3.2 | Disposal of media |
| PCI DSS | 3.1 | Data retention and disposal policies |
7. Domain & Certificate Security
7.1 Prevent Subdomain Takeover
Profile Level: L1 (Baseline)
NIST 800-53: CM-8, SC-20
Description
Audit DNS records to prevent subdomain takeover vulnerabilities when CNAME records point to Vercel without active deployments.
Rationale
Why This Matters:
- Dangling DNS records pointing to Vercel can be claimed by attackers
- Subdomain takeover enables phishing, cookie theft, and CSP bypass
- Security researchers actively scan for Vercel subdomain takeover opportunities
Attack Prevented: Subdomain takeover, phishing via legitimate domain, cookie scope exploitation
Real-World Incidents:
- Multiple Vercel subdomain takeover reports on HackerOne and Medium demonstrating exploitation of dangling CNAME records
ClickOps Implementation
Step 1: Audit DNS Records
- Navigate to: Team Settings → Domains
- Review all configured domains
- Identify any domains not actively assigned to projects
Step 2: Clean Up Dangling Records
- Remove DNS CNAME records for decommissioned Vercel projects
- Remove Vercel domain assignments when projects are deleted
- Verify all domains resolve to active deployments
Step 3: Monitor Domain Health
- Periodically scan for dangling DNS records using DNS auditing tools
- Set up alerts for domain configuration changes via audit logs
Time to Complete: ~15 minutes
Code Pack: CLI Script
# --- List all domains configured in the Vercel team ---
echo "=== Vercel Domain Inventory ==="
vercel domains ls
# --- Check for dangling CNAME records pointing to Vercel ---
echo ""
echo "=== Checking for Dangling DNS Records ==="
DOMAINS=$(vercel domains ls 2>/dev/null | awk 'NR>2 {print $1}' | grep -v '^$')
for domain in ${DOMAINS}; do
echo "Checking: ${domain}"
# Check if CNAME points to Vercel
cname=$(dig +short CNAME "${domain}" 2>/dev/null || true)
if echo "${cname}" | grep -qi "vercel\|now\.sh"; then
# Verify the domain resolves to an active deployment
http_code=$(curl -s -o /dev/null -w "%{http_code}" "https://${domain}" 2>/dev/null || echo "000")
if [ "${http_code}" = "000" ] || [ "${http_code}" = "404" ]; then
echo " WARNING: ${domain} has CNAME to Vercel but returns ${http_code} -- possible takeover risk!"
else
echo " OK: ${domain} -> ${cname} (HTTP ${http_code})"
fi
fi
done
# --- Remove a domain no longer in use ---
# Uncomment and customize:
# vercel domains rm "unused-subdomain.example.com"
Validation & Testing
- All DNS records pointing to Vercel have active deployments
- No orphaned domain entries in Vercel dashboard
- Domain configuration changes logged in audit log
Expected result: No dangling DNS records vulnerable to subdomain takeover
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.1 | Logical access controls |
| NIST 800-53 | CM-8, SC-20 | Component inventory, secure name resolution |
| ISO 27001 | A.13.1.1 | Network controls |
| PCI DSS | 2.4 | Maintain inventory of system components |
7.2 Harden TLS and Certificate Configuration
Profile Level: L1 (Baseline)
NIST 800-53: SC-8, SC-13
Description
Verify TLS configuration and optionally deploy custom certificates for domains requiring specific certificate authorities.
Rationale
Why This Matters:
- Vercel automatically provides TLS 1.2/1.3 with strong ciphers and forward secrecy
- HSTS is automatic for all domains but custom domains lack
includeSubDomainsandpreload - Post-quantum key exchange (X25519MLKEM768) available for supporting browsers
- Custom certificates needed for CAA/CT policy compliance in some organizations
Attack Prevented: Man-in-the-middle attacks, protocol downgrade attacks, certificate impersonation
ClickOps Implementation
Step 1: Verify TLS Configuration
- Confirm HTTPS enforced (automatic – HTTP 308 redirects to HTTPS)
- Verify TLS 1.2+ in use via SSL Labs test
- Confirm forward secrecy enabled on all ciphers
Step 2: Enhance HSTS for Custom Domains (L2)
- Add custom header:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload - Submit custom domain to HSTS Preload list at hstspreload.org
Step 3: Deploy Custom Certificates (L3)
- Use
vercel certs issue [domain]for custom certificate management - Upload organization-specific certificates if required by policy
Time to Complete: ~10 minutes
Code Pack: CLI Script
DOMAIN="${1:-}"
if [ -z "${DOMAIN}" ]; then
echo "Usage: $0 <your-domain.com>"
exit 1
fi
# --- Verify TLS configuration ---
echo "=== TLS Verification for ${DOMAIN} ==="
# Check TLS version and cipher
echo "--- TLS Protocol and Cipher ---"
echo | openssl s_client -connect "${DOMAIN}:443" -servername "${DOMAIN}" 2>/dev/null | \
grep -E "Protocol|Cipher|Server certificate"
# Verify HSTS header
echo ""
echo "--- HSTS Header ---"
curl -sI "https://${DOMAIN}" | grep -i "strict-transport-security" || \
echo "WARNING: No HSTS header found!"
# Verify HTTP to HTTPS redirect
echo ""
echo "--- HTTP Redirect Check ---"
redirect=$(curl -sI -o /dev/null -w "%{http_code}" "http://${DOMAIN}" 2>/dev/null || echo "000")
if [ "${redirect}" = "308" ] || [ "${redirect}" = "301" ]; then
echo "OK: HTTP redirects to HTTPS (${redirect})"
else
echo "WARNING: HTTP returned ${redirect} -- expected 308 redirect"
fi
# --- Issue custom certificate (L3) ---
# Uncomment for custom certificate management:
# vercel certs issue "${DOMAIN}"
# --- List existing certificates ---
echo ""
echo "=== Certificate Inventory ==="
vercel certs ls
Validation & Testing
- SSL Labs grade A+ with HSTS preloading
- No TLS 1.0/1.1 negotiation possible
- All ciphers support forward secrecy
- HSTS preload header present on custom domains (L2)
Expected result: Strong TLS configuration with HSTS across all domains
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.7 | Encryption of data in transit |
| NIST 800-53 | SC-8, SC-13 | Transmission confidentiality, cryptographic protection |
| ISO 27001 | A.10.1.1 | Policy on use of cryptographic controls |
| PCI DSS | 4.1 | Strong cryptography for transmission of cardholder data |
8. Monitoring & Detection
8.1 Configure Log Drains for SIEM
Profile Level: L1 (Baseline)
NIST 800-53: AU-2, AU-6
Description
Forward Vercel runtime, build, and firewall logs to your SIEM via log drains for security monitoring and incident response.
Rationale
Why This Matters:
- Vercel only retains runtime logs short-term – log drains required for long-term retention
- Firewall logs capture blocked/challenged requests for threat intelligence
- Log drain payloads can be cryptographically verified via shared secret
- SIEM integration enables correlation with other security data sources
Attack Prevented: Undetected attacks, delayed incident response, evidence loss, compliance gaps in log retention
Prerequisites
- Vercel Pro or Enterprise plan
- SIEM endpoint accepting HTTPS POST with JSON payloads
ClickOps Implementation
Step 1: Create Log Drain
- Navigate to: Team Settings → Log Drains
- Create new drain with your SIEM endpoint URL
- Select delivery format: JSON or NDJSON
- Select environments: Production and Preview
- Select sources: static, edge, external, build, lambda, firewall
- Set a strong shared secret for payload verification
Step 2: Include Firewall Logs (L2)
- Add firewall source to log drain
- Critical for detecting blocked attacks and WAF activity
- Consider separate drain for firewall logs if volume is high
Step 3: Configure Sampling (Optional)
- Set sampling rate for high-volume applications
- Use 1.0 (100%) for security-critical projects
- Lower rates acceptable for development/preview
Time to Complete: ~15 minutes
Code Pack: Terraform
# --- L1: Configure log drain to forward deployment and runtime logs ---
resource "vercel_log_drain" "security_logging" {
count = var.log_drain_endpoint != "" ? 1 : 0
name = "hth-security-log-drain"
team_id = var.vercel_team_id
delivery_format = "json"
endpoint = var.log_drain_endpoint
environments = var.log_drain_environments
sources = var.log_drain_sources
secret = var.log_drain_secret != "" ? var.log_drain_secret : null
}
# --- L2: Separate firewall log drain for WAF activity ---
resource "vercel_log_drain" "firewall_logging" {
count = var.profile_level >= 2 && var.log_drain_endpoint != "" ? 1 : 0
name = "hth-firewall-log-drain"
team_id = var.vercel_team_id
delivery_format = "json"
endpoint = var.log_drain_endpoint
environments = ["production", "preview"]
sources = ["firewall"]
secret = var.log_drain_secret != "" ? var.log_drain_secret : null
}
Validation & Testing
- Log drain receiving events in SIEM
- Payload signature verification working
- Firewall logs appearing for blocked requests
- All configured environments and sources flowing
Expected result: All Vercel logs forwarded to SIEM with cryptographic verification
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC7.2, CC7.3 | System monitoring, anomaly detection |
| NIST 800-53 | AU-2, AU-6 | Audit events, audit review and analysis |
| ISO 27001 | A.12.4.1 | Event logging |
| PCI DSS | 10.2 | Implement automated audit trails |
8.2 Enable Audit Logging with SIEM Streaming
Profile Level: L2 (Hardened)
NIST 800-53: AU-2, AU-3, AU-12
Description
Enable enterprise audit logging with real-time SIEM streaming to track all administrative actions, configuration changes, and security events.
Rationale
Why This Matters:
- Audit logs capture 90 days of immutable administrative activity
- Tracks: member changes, environment variable CRUD, deployment protection changes, domain changes, integration installs, and more
- SIEM streaming enables real-time alerting on security-relevant events
- CSV export available for compliance reporting
Attack Prevented: Undetected administrative compromise, unauthorized configuration changes, insider threat
Prerequisites
- Vercel Enterprise plan
ClickOps Implementation
Step 1: Access Audit Log
- Navigate to: Team Settings → Security → Audit Log
- Review available event types and current activity
Step 2: Configure SIEM Streaming
- Navigate to: Team Settings → Security & Privacy → Audit Log → Configure
- Select SIEM destination: AWS S3, Splunk, Datadog, Google Cloud Storage, or Generic HTTP
- Configure authentication (API key, header-based, or AWS credentials)
- Select format: JSON or NDJSON
- Allowlist Vercel SIEM IPs if endpoint is firewalled
Step 3: Build Detection Rules
- Create alerts for critical events:
team.member.role.updated,project.env_variable.created,password_protection.disabled,saml.updated - Monitor for unusual patterns: bulk member additions, env var decryption events, integration installs
Time to Complete: ~30 minutes
Code Pack: API Script
# --- Retrieve recent audit log events (Enterprise) ---
echo "=== Recent Audit Log Events ==="
curl -s -H "Authorization: Bearer ${VERCEL_TOKEN}" \
"https://api.vercel.com/v1/events?teamId=${VERCEL_TEAM_ID}&limit=20&types=team.member.role.updated,project.env_variable.created,saml.updated" | \
jq '.events[]? | {
id,
type,
createdAt,
actor: .actor.slug,
entityId: .entityId
}'
# --- List security-critical event types to monitor ---
echo ""
echo "=== Critical Events for SIEM Alerting ==="
echo "Configure SIEM detection rules for these event types:"
echo " - team.member.role.updated (privilege escalation)"
echo " - team.member.invited (new access grants)"
echo " - team.member.removed (access revocation)"
echo " - project.env_variable.created (secret addition)"
echo " - project.env_variable.updated (secret modification)"
echo " - deployment-protection.updated (protection changes)"
echo " - password_protection.disabled (protection removal)"
echo " - saml.updated (SSO config changes)"
echo " - integration.installed (new integrations)"
echo " - domain.added (domain changes)"
# --- Export audit log CSV (for compliance reporting) ---
echo ""
echo "=== Export Audit Log (last 30 days) ==="
# Navigate to Team Settings > Security > Audit Log > Export CSV
echo "Manual export available at: https://vercel.com/team/${VERCEL_TEAM_ID}/settings/security"
# --- Verify log drain is receiving audit events ---
echo ""
echo "=== Log Drain Status ==="
curl -s -H "Authorization: Bearer ${VERCEL_TOKEN}" \
"https://api.vercel.com/v1/log-drains?teamId=${VERCEL_TEAM_ID}" | \
jq '.[] | {id, name, url: .endpoint, status, sources, environments}'
Validation & Testing
- Audit log shows recent administrative events
- SIEM receiving streamed audit events in real-time
- Detection rules firing on test events
- CSV export produces valid compliance report
Expected result: All administrative actions logged, streamed, and alerted on
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC7.2 | Monitor system components for anomalies |
| NIST 800-53 | AU-2, AU-3, AU-12 | Audit events, content, generation |
| ISO 27001 | A.12.4.1, A.12.4.3 | Event logging, administrator and operator logs |
| PCI DSS | 10.1, 10.5 | Audit trails, secure audit trails |
8.3 Cron Job Security
Profile Level: L1 (Baseline)
NIST 800-53: AC-3, SI-10
Description
Secure cron job endpoints with the CRON_SECRET mechanism to prevent unauthorized invocation.
Rationale
Why This Matters:
- Cron endpoints are publicly accessible URLs without protection
- Without CRON_SECRET verification, anyone can trigger cron jobs
- Compromised cron endpoints enable unauthorized data processing or exfiltration
Attack Prevented: Unauthorized cron invocation, data exfiltration via scheduled jobs, resource abuse
ClickOps Implementation
Step 1: Generate Strong CRON_SECRET
- Generate:
openssl rand -hex 32(minimum 16 characters) - Add as production environment variable:
CRON_SECRET
Step 2: Verify in Application Code
- Check
Authorization: Bearer <CRON_SECRET>header in every cron handler - Return 401 for missing or mismatched secrets
- Vercel automatically sends the bearer token when invoking cron endpoints
Time to Complete: ~10 minutes
Code Pack: CLI Script
# --- Generate a strong CRON_SECRET ---
echo "=== Generate CRON_SECRET ==="
CRON_SECRET=$(openssl rand -hex 32)
echo "Generated CRON_SECRET: ${CRON_SECRET}"
# --- Set CRON_SECRET as production environment variable ---
echo ""
echo "=== Setting CRON_SECRET Environment Variable ==="
vercel env add CRON_SECRET production <<< "${CRON_SECRET}"
# --- Verify cron endpoint rejects unauthenticated requests ---
echo ""
echo "=== Testing Cron Endpoint Security ==="
DOMAIN="${1:-}"
CRON_PATH="${2:-/api/cron}"
if [ -n "${DOMAIN}" ]; then
# Test without auth (should return 401)
echo "Testing without auth header..."
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
"https://${DOMAIN}${CRON_PATH}" 2>/dev/null || echo "000")
if [ "${http_code}" = "401" ]; then
echo " OK: Returns 401 without auth"
else
echo " WARNING: Returns ${http_code} -- expected 401 for unauthenticated request!"
fi
# Test with correct auth (should return 200)
echo "Testing with bearer token..."
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer ${CRON_SECRET}" \
"https://${DOMAIN}${CRON_PATH}" 2>/dev/null || echo "000")
echo " Auth response: HTTP ${http_code}"
else
echo "Usage: $0 <your-domain.com> [/api/cron-path]"
fi
Validation & Testing
- CRON_SECRET set as production environment variable
- Direct HTTP request without bearer token returns 401
- Vercel-triggered cron execution succeeds with correct token
Expected result: Cron endpoints only accessible via authenticated Vercel invocation
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.1 | Logical access controls |
| NIST 800-53 | AC-3, SI-10 | Access enforcement, information input validation |
| ISO 27001 | A.9.4.1 | Information access restriction |
| PCI DSS | 8.1 | Unique identification for system components |
Appendix A: Edition Compatibility
| Control | Section | Hobby | Pro | Enterprise |
|---|---|---|---|---|
| SAML SSO | 1.1 | ❌ | Add-on | ✅ |
| Directory Sync (SCIM) | 1.2 | ❌ | ❌ | ✅ |
| RBAC (full roles) | 1.3 | Basic | Extended | Full |
| Access Groups | 1.3 | ❌ | Limited | ✅ |
| Security Role | 1.3 | ❌ | ❌ | ✅ |
| OIDC Federation | 1.4 | ✅ | ✅ | ✅ |
| Deployment Protection (Standard) | 2.1 | ✅ | ✅ | ✅ |
| Password Protection | 2.1 | ❌ | Add-on ($150/mo) | ✅ |
| Trusted IPs | 2.1 | ❌ | ❌ | ✅ |
| Git Fork Protection | 2.2 | ✅ | ✅ | ✅ |
| Rolling Releases | 2.3 | ❌ | ✅ | ✅ |
| WAF Custom Rules | 3.1 | 3 rules | 40 rules | 1,000 rules |
| WAF Managed Rulesets | 3.1 | ❌ | ❌ | ✅ |
| IP Blocking (project) | 3.2 | 10 IPs | 100 IPs | Custom |
| IP Blocking (account) | 3.2 | ❌ | ❌ | ✅ |
| Rate Limiting | 3.2 | ❌ | ✅ | ✅ |
| Secure Compute | 4.1 | ❌ | ❌ | ✅ ($6.5K/yr) |
| VPC Peering | 4.1 | ❌ | ❌ | ✅ |
| DDoS Mitigation | 4.2 | ✅ | ✅ | ✅ + dedicated |
| Attack Challenge Mode | 4.2 | ✅ | ✅ | ✅ |
| Spend Management | 4.2 | ❌ | ✅ | ✅ |
| Custom Security Headers | 5.1 | ✅ | ✅ | ✅ |
| Sensitive Env Var Policy | 6.1 | ❌ | ✅ | ✅ |
| Deployment Retention | 6.2 | ✅ | ✅ | ✅ |
| Log Drains | 8.1 | ❌ | ✅ | ✅ |
| Audit Logs | 8.2 | ❌ | ❌ | ✅ (90 days) |
| SIEM Streaming | 8.2 | ❌ | ❌ | ✅ |
Appendix B: References
Official Vercel Documentation:
- Vercel Trust Center
- Vercel Documentation
- Security Overview
- Shared Responsibility Model
- Production Checklist
- Deployment Protection
- Vercel Firewall / WAF
- DDoS Mitigation
- Secure Compute
- Encryption
- RBAC Access Roles
- SAML SSO
- Directory Sync
- OIDC Federation
- Audit Logs
- Log Drains
- Environment Variables
- Security & Compliance
- Security Bulletins
CLI & API Documentation:
Compliance Frameworks:
- SOC 2 Type II (Security, Confidentiality, Availability)
- ISO 27001:2022
- PCI DSS v4.0 (SAQ-D AOC for Service Providers, SAQ-A AOC for Merchants)
- HIPAA BAA (Enterprise)
- EU-U.S. Data Privacy Framework
- TISAX Assessment Level 2
Security Incidents:
- 2025 – Next.js Middleware Authorization Bypass (CVE-2025-29927): Vulnerability allowed bypassing authorization in Next.js Middleware via the
x-middleware-subrequestheader. Vercel WAF was updated proactively to protect hosted projects. - 2025 – React2Shell (CVE-2025-55182, CVSS 10.0): Critical unsafe deserialization in React Server Components enabling Remote Code Execution. Vercel deployed WAF rules before public disclosure. China-nexus threat groups observed exploiting in the wild.
- 2025 – React Server Components DoS (CVE-2025-55184, CVSS 7.5): Infinite loop via crafted Server Function request.
- 2025 – React Server Components Source Disclosure (CVE-2025-55183, CVSS 5.3): Source code leakage under specific conditions.
- 2024 – Next.js SSRF (CVE-2024-34351): Server-Side Request Forgery in Server Actions via Host header manipulation. Vercel routing mitigates by default.
Community Security Research:
- Vercel XSS in Clone URL (Medium) – Reflected XSS in clone functionality
- Vercel Subdomain Takeover (Medium) – Dangling CNAME exploitation
- dSSRF: Deterministic SSRF Protection – Community SSRF protection library
- Next.js Security Checklist (Arcjet) – Framework-level hardening guide
- Nosecone Security Headers Library – Open source security headers for Next.js
Changelog
| Date | Version | Maturity | Changes | Author |
|---|---|---|---|---|
| 2025-12-14 | 0.1.0 | draft | Initial Vercel hardening guide | Claude Code (Opus 4.5) |
| 2026-02-24 | 1.0.0 | draft | [SECURITY] Complete guide revamp: expanded from 4 to 8 sections covering WAF, network security, security headers, domain security; added 20 controls with ClickOps and code pack references; integrated Vercel Shared Responsibility Model, production checklist, Terraform provider v4.6, CLI docs, and API docs; added comprehensive compliance mappings; updated edition compatibility matrix; incorporated security researcher findings and CVE references | Claude Code (Opus 4.6) |