Stack guide
CloudFront response headers policy — security headers without Lambda@Edge
Until 2021 the only way to add custom security headers to a CloudFront distribution was Lambda@Edge — slow to deploy, expensive to run, and overkill for static headers. The Response Headers Policy feature replaced that for the common case: a single managed config object you attach to your distribution that injects security, CORS, and custom headers on every response. This guide covers the AWS-managed `SecurityHeadersPolicy`, when to write a custom policy instead, and a Terraform example for the custom path.
1. Use the AWS-managed SecurityHeadersPolicy first
AWS ships a managed policy named `SecurityHeadersPolicy` (id `67f7725c-6f97-4210-82d7-5512b31e9d03`) that sets X-Frame-Options DENY, X-Content-Type-Options nosniff, Strict-Transport-Security with a 1-year max-age, and Referrer-Policy strict-origin-when-cross-origin. Attach it via the console (Behaviors → Edit → Response Headers Policy) or `aws cloudfront update-distribution`. This covers the basic Scorifya headers checks with zero custom config.
HSTS missing — nginx (server / location) (nginx)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # If TLS terminates at a CDN, set the header there instead.
HSTS missing — Express (helmet) (typescript)
import helmet from "helmet";
app.use(helmet.hsts({ maxAge: 31_536_000, includeSubDomains: true }));HSTS missing — Apache (vhost) (apache)
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options missing — nginx (nginx)
add_header X-Frame-Options "DENY" always; # Use SAMEORIGIN instead of DENY only if same-origin framing is required.
X-Frame-Options missing — Express (helmet) (typescript)
import helmet from "helmet";
app.use(helmet.frameguard({ action: "deny" }));X-Frame-Options missing — Apache (apache)
Header set X-Frame-Options "DENY"
X-Content-Type-Options missing — nginx (nginx)
add_header X-Content-Type-Options "nosniff" always;
X-Content-Type-Options missing — Express (helmet) (typescript)
import helmet from "helmet"; app.use(helmet.noSniff());
X-Content-Type-Options missing — Apache (apache)
Header set X-Content-Type-Options "nosniff"
Related Scorifya checks: missing_hsts, missing_xfo, missing_xcto, missing_referrer_policy
2. Custom Response Headers Policy via Terraform
When you need CSP, Permissions-Policy, or non-default values for the existing headers, create a custom policy. The Terraform `aws_cloudfront_response_headers_policy` resource takes nested blocks for `security_headers_config`, `cors_config`, and `custom_headers_config`. CSP goes in `custom_headers_config` because the typed `security_headers_config.content_security_policy` block exists but is awkward for long policies.
CSP missing — nginx (nginx)
add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; form-action 'self'" always;
CSP missing — Express (helmet) (typescript)
import helmet from "helmet";
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
baseUri: ["'self'"],
formAction: ["'self'"],
},
}),
);CSP missing — Apache (apache)
Header set Content-Security-Policy "default-src 'self'; base-uri 'self'; form-action 'self'"
Related Scorifya checks: missing_csp, missing_permissions_policy
3. When you still need Lambda@Edge
Use Lambda@Edge only when you need request-conditional headers — for example, generating a CSP nonce per request and adding it to both the CSP header and the HTML body. Static headers belong in a Response Headers Policy. Edge Functions in CloudFront can also do this and are cheaper for simple transforms; consider them before reaching for Lambda@Edge.
Weak CSP — nginx (tighten script-src) (nginx)
# Drop 'unsafe-inline' / wildcards in production; use nonces or hashes for any inline script. add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'" always;
Weak CSP — Express (helmet) (typescript)
import helmet from "helmet";
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
objectSrc: ["'none'"],
},
}),
);Weak CSP — Apache (apache)
# Replace permissive policy with explicit hosts and no unsafe-inline where possible. Header set Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'"
Related Scorifya checks: weak_csp
Background
What is HSTS? HTTP Strict Transport Security explained →
How HSTS works, why the bootstrap window matters, what max-age and includeSubDomains do, and when (or whether) to submit your domain to the browser preload list.
Read more
Verify with a fresh scan
After deploy, run the scanner on the affected hostname. Headers and TLS settings update on the very next request, so you should see the score move within seconds.