A perfectly healthy deployment that scores 58/100

We recently scanned a production single-page app hosted the way AWS's own tutorials suggest: static assets in S3, fronted by a CloudFront distribution with an ACM certificate. By AWS standards this deployment was done right. TLS was modern (no 1.0/1.1), the certificate chain was clean and strongly keyed, HTTP redirected to HTTPS, DNS had SPF, DMARC and CAA records, and no risky ports were exposed.

It still scored 58/100, with seven findings including one rated high. Every single one traced back to the same root cause: things AWS does not set by default.

What the defaults actually expose

Here is the full finding list from that scan. If you have a stock CloudFront + S3 setup, this is almost certainly your finding list too:

  • Missing HSTS (high). CloudFront happily redirects HTTP to HTTPS, but without Strict-Transport-Security the browser makes that first plain-HTTP request every time, leaving users open to SSL stripping on hostile networks.
  • Missing Content-Security-Policy (medium). No CSP means any script injection runs with no containment. For a single-page app, where the entire UI is JavaScript, this is the header that matters most.
  • No clickjacking protection (medium). Neither X-Frame-Options nor CSP frame-ancestors is set, so any site on the internet can load yours in an invisible iframe and overlay it.
  • Missing X-Content-Type-Options: nosniff (low). Browsers may second-guess content types and execute responses that were never meant to be scripts.
  • Server version disclosed (low). S3 answers every request with Server: AmazonS3, telling anyone probing you exactly what is behind the CDN.
  • Missing Referrer-Policy (info). Full URLs leak to every third party you link out to.
  • Technology stack fingerprinted (info). CloudFront adds Via: 1.1 <hash>.cloudfront.net (CloudFront) and x-amz-cf-id to responses, so the whole S3 + CloudFront stack is identifiable from a single request.

Why AWS ships it this way

None of this is negligence on AWS's part. S3 and CloudFront serve millions of wildly different workloads, and every one of these headers can break somebody: HSTS bricks subdomains that still speak HTTP, CSP breaks pages that inline scripts, frame-ancestors breaks legitimate embedding. So the defaults are maximally permissive, and hardening is opt-in.

The trap is that the opt-in step is invisible. The deployment works perfectly without it. Nothing in the console warns you, the tutorial ends at "your site is live", and the gap only shows up when someone looks at your infrastructure from the outside, which is precisely what attackers and external scanners do.

The fix: one response headers policy

CloudFront has a built-in feature for exactly this: response headers policies. A policy attaches to the distribution's cache behavior and stamps headers onto every response at the edge, with no application changes and no Lambda invocation cost.

AWS even ships a managed policy called Managed-SecurityHeadersPolicy that sets HSTS, X-Content-Type-Options: nosniff, X-Frame-Options, Referrer-Policy and X-XSS-Protection in one click: CloudFront console → your distribution → Behaviors → Edit → Response headers policy. It exists, it is free, and it is simply not attached by default.

For anything beyond a demo, define your own policy so you control the values and can add a CSP. In Terraform:

resource "aws_cloudfront_response_headers_policy" "security" {
  name = "security-headers"

  security_headers_config {
    strict_transport_security {
      access_control_max_age_sec = 63072000
      include_subdomains         = true
      preload                    = true
      override                   = true
    }
    content_security_policy {
      content_security_policy = "default-src 'self'; frame-ancestors 'none'"
      override                = true
    }
    content_type_options { override = true }
    frame_options {
      frame_option = "DENY"
      override     = true
    }
    referrer_policy {
      referrer_policy = "strict-origin-when-cross-origin"
      override        = true
    }
  }
}

Attach it via response_headers_policy_id on the distribution's cache behavior, create an invalidation, and five of the seven findings above disappear in one deploy. Tailor the CSP to your app's real asset origins before enforcing it; start in report-only mode if you are unsure (our security headers guide covers the staging process).

The fingerprint you mostly can't remove

The remaining two findings are about disclosure. The Server: AmazonS3 header can be rewritten at the edge with a CloudFront Function on the viewer response. The Via and x-amz-cf-id headers cannot: CloudFront adds them after your code runs. Treat CloudFront's own fingerprint as an accepted, low-severity fact of the platform, and make sure the components it reveals stay patched, which AWS handles for the managed pieces.

The defaults problem is bigger than headers

This pattern (secure-enough to ship, permissive by default, hardening opt-in and invisible) repeats across cloud providers. S3 buckets were public-by-default for a decade. Default security groups allow all egress. New CloudFront distributions accept TLS 1.0 unless you pick a newer security policy. In each case the provider's incentive is that your deployment works on the first try; making it hostile-network-ready is left to you.

The practical consequence: you cannot infer your external posture from your architecture diagram. A textbook-correct AWS deployment and a hardened one look identical from the inside. The only way to know which one you are running is to look from the outside, the way an attacker would, and to keep looking, because the next distribution someone spins up will ship with the same defaults.

Frequently asked questions

Does CloudFront add security headers by default?

No. A new CloudFront distribution sends no HSTS, no Content-Security-Policy, no X-Frame-Options, no X-Content-Type-Options and no Referrer-Policy. AWS provides a managed response headers policy (Managed-SecurityHeadersPolicy) that sets most of these, but you must attach it to the distribution's cache behavior yourself.

How do I add security headers to a static site on S3 and CloudFront?

Use a CloudFront response headers policy: either attach AWS's managed SecurityHeadersPolicy in the console (Behaviors → Response headers policy) or define a custom policy with your own HSTS, CSP, frame and referrer settings. Headers are stamped at the edge on every response; S3 static website hosting cannot set them itself.

Can I hide the Server: AmazonS3 header?

Yes, a CloudFront Function on the viewer response can rewrite the Server header to a generic value. CloudFront's own Via and x-amz-cf-id headers cannot be removed; they are added after your edge code runs, so a determined observer can always identify CloudFront itself.

Is a default CloudFront + S3 setup insecure?

The transport layer is solid by default: modern TLS, trusted certificates and HTTPS redirects. What is missing is browser-side hardening (HSTS, CSP, clickjacking and MIME protections), which shifts real risk onto your users, especially for single-page apps where the whole UI is JavaScript. It is one policy attachment away from fixed.

Sources