CtrlK
BlogDocsLog inGet started
Tessl Logo

nitinjain999/platform-skills

Production-grade platform engineering handbook — Kubernetes, Terraform, Flux CD, GitHub Actions, AWS, and more.

67

Quality

84%

Does it follow best practices?

Impact

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files

aws-cloudfront.mdreferences/

AWS CloudFront Reference

Contents

  • Distribution anatomy
  • Origin Access Control (OAC)
  • Cache and origin request policies
  • Security headers
  • WAF integration
  • Lambda@Edge
  • CloudFront Functions vs Lambda@Edge
  • Logging
  • Multi-account patterns
  • Production checklist

Distribution anatomy

A CloudFront distribution defines:

  • Origins — up to 25 per distribution (S3, ALB, API Gateway, custom HTTP, MediaStore, Lambda URL)
  • Cache behaviors — path pattern → origin mapping, evaluated top to bottom; default (*) always last
  • Price class — controls which edge locations serve traffic
Price ClassEdge LocationsUse Case
PriceClass_100US, EU, IsraelCheapest — most orgs start here
PriceClass_200Above + Asia, Africa, Middle EastMid-tier
PriceClass_AllAll edge locations globallyLowest latency worldwide
  • Alternate CNAMEs — custom domains; require a matching ACM certificate in us-east-1
  • IPv6 — enable unless your origin explicitly rejects IPv6 connections
  • Continuous deployment — staging distribution receives a configurable traffic weight before promoting config to primary

Origin Access Control (OAC)

OAC is the modern replacement for Origin Access Identity (OAI). Always use OAC for new distributions.

How it works

CloudFront signs requests to the origin using AWS Signature Version 4 (SigV4). The origin's resource policy allows only requests from the specific CloudFront distribution.

Supported origins

Origin TypeTerraform resource attribute
S3origin_access_control_origin_type = "s3"
AWS Elemental MediaStore"mediastore"
Lambda Function URL"lambda"
API Gateway"apigateway"
AWS Elemental MediaPackage v2"mediapackagev2"

Signing behavior

Use always for all new distributions. no-override is only for origins that set their own auth header.

resource "aws_cloudfront_origin_access_control" "this" {
  name                              = "${var.name}-oac"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

S3 bucket policy

data "aws_iam_policy_document" "cloudfront_oac" {
  statement {
    sid    = "AllowCloudFrontServicePrincipal"
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["cloudfront.amazonaws.com"]
    }

    actions   = ["s3:GetObject"]
    resources = ["${aws_s3_bucket.origin.arn}/*"]

    condition {
      test     = "StringEquals"
      variable = "AWS:SourceArn"
      # Scope to the specific distribution — not all CloudFront
      values = [aws_cloudfront_distribution.this.arn]
    }
  }
}

OAI vs OAC

FeatureOAI (Legacy)OAC (Recommended)
S3 support
Non-S3 origins
Signing protocolCustomSigV4
Principal typeCanonicalUserService
Per-distribution scope✅ via AWS:SourceArn
StatusDo not use for new distributions

Cache and origin request policies

The split

  • Cache policy — what goes into the cache key (headers, cookies, query strings) and TTL settings
  • Origin request policy — what CloudFront forwards to the origin (can include values not in the cache key)

Keep the cache key narrow. Every unique combination of cache-key values creates a separate cache entry.

AWS managed cache policies

Policy nameCache keyUse for
CachingOptimizedURI onlyStatic assets (S3)
CachingDisabledEverythingAPI pass-through
CachingOptimizedForUncompressedObjectsURI only, no compressionBinary/already compressed
CORS-S3OriginURI + Origin headerS3 CORS
UseOriginCacheControlHeadersURI + Cache-Control from originDynamic content

TTL hierarchy (highest precedence first)

  1. Cache-Control: max-age / Expires from origin response
  2. default_ttl in the cache policy
  3. max_ttl cap (CloudFront never exceeds this regardless of origin headers)

Custom cache policy

resource "aws_cloudfront_cache_policy" "api" {
  name        = "${var.name}-api"
  default_ttl = 0
  max_ttl     = 0
  min_ttl     = 0

  parameters_in_cache_key_and_forwarded_to_origin {
    cookies_config  { cookie_behavior = "none" }
    headers_config  { header_behavior = "none" }
    query_strings_config { query_string_behavior = "none" }
    enable_accept_encoding_gzip   = false
    enable_accept_encoding_brotli = false
  }
}

Security headers

Use a response headers policy to attach security headers without Lambda@Edge.

resource "aws_cloudfront_response_headers_policy" "security" {
  name = "${var.name}-security-headers"

  security_headers_config {
    strict_transport_security {
      access_control_max_age_sec = 31536000
      include_subdomains         = true
      preload                    = true
      override                   = true
    }

    content_type_options {
      override = true  # X-Content-Type-Options: nosniff
    }

    frame_options {
      frame_option = "DENY"
      override     = true
    }

    referrer_policy {
      referrer_policy = "strict-origin-when-cross-origin"
      override        = true
    }

    xss_protection {
      mode_block = true
      protection = true
      override   = true
    }
  }

  custom_headers_config {
    items {
      header   = "Permissions-Policy"
      value    = "camera=(), microphone=(), geolocation=()"
      override = true
    }
  }
}

Custom origin header (ALB secret)

Prevent direct ALB access by requiring a secret header that only CloudFront adds:

origin {
  domain_name = aws_lb.this.dns_name
  origin_id   = "alb"

  custom_header {
    name  = "X-CloudFront-Secret"
    value = var.cloudfront_origin_secret  # stored in Secrets Manager, rotated
  }
}

On the ALB WAF: block requests where X-CloudFront-Secret is absent or wrong.


WAF integration

Critical constraint

WAF web ACLs for CloudFront must use CLOUDFRONT scope and be created in us-east-1 — even if your origin is in another region. Use a provider alias.

# In your CloudFront distribution
resource "aws_cloudfront_distribution" "this" {
  web_acl_id = var.waf_web_acl_arn  # pass in from WAF module (us-east-1)
  ...
}

The WAF module must be instantiated with a us-east-1 provider. See references/aws-waf.md for the full WAF reference.


Lambda@Edge

Lambda@Edge lets you run Node.js or Python code at CloudFront edge locations. All functions must be authored in us-east-1 and are replicated globally by CloudFront.

Four event types

EventWhereCacheMax MemoryMax TimeoutBody AccessUse Cases
viewer-requestEdge, before cache lookupAlways runs128 MB5sAuth, geo redirect, A/B routing
viewer-responseEdge, after cacheAlways runs128 MB5sAdd/modify response headers
origin-requestEdge, cache miss onlyCache miss512 MB30sURL rewrite, dynamic origin selection
origin-responseEdge, cache miss onlyCache miss512 MB30sError handling, fallback origin

Cost note: viewer-request and viewer-response run on every request. origin-request and origin-response run only on cache misses. Prefer origin events for expensive logic.

Common footguns

Must use numbered version ARN — not $LATEST

resource "aws_lambda_function" "edge" {
  provider  = aws.us_east_1
  publish   = true  # creates a numbered version
  ...
}

# Use qualified_arn (includes version number), NOT arn
lambda_function_association {
  event_type   = "viewer-request"
  lambda_arn   = aws_lambda_function.edge.qualified_arn  # ✅ e.g. arn:...:function:name:3
  include_body = false
}

IAM trust policy must include both principals

data "aws_iam_policy_document" "edge_trust" {
  statement {
    principals {
      type = "Service"
      identifiers = [
        "lambda.amazonaws.com",
        "edgelambda.amazonaws.com",  # required — without this, replication fails silently
      ]
    }
    actions = ["sts:AssumeRole"]
  }
}

No VPC, no environment variables, no layers with platform-specific binaries

Lambda@Edge functions cannot be attached to a VPC and cannot use environment variables. Use CloudFront KeyValueStore for configuration data that needs to change without redeployment.

Viewer-request auth example (Node.js)

// index.js — runs at viewer-request, validates JWT in Authorization header
exports.handler = async (event) => {
  const request = event.Records[0].cf.request;
  const headers = request.headers;

  const auth = headers['authorization'] ? headers['authorization'][0].value : null;

  if (!auth || !auth.startsWith('Bearer ')) {
    return {
      status: '401',
      statusDescription: 'Unauthorized',
      headers: {
        'www-authenticate': [{ key: 'WWW-Authenticate', value: 'Bearer' }],
      },
    };
  }

  // validate token here (no network calls — validate signature locally)
  return request;
};

Terraform — Lambda@Edge function

# Must use us-east-1 provider alias
resource "aws_lambda_function" "edge" {
  provider         = aws.us_east_1
  filename         = data.archive_file.edge.output_path
  source_code_hash = data.archive_file.edge.output_base64sha256
  function_name    = "${var.name}-edge-viewer-request"
  role             = aws_iam_role.edge.arn
  handler          = "index.handler"
  runtime          = "nodejs22.x"
  publish          = true  # numbered version required

  depends_on = [aws_cloudwatch_log_group.edge]
}

data "archive_file" "edge" {
  type        = "zip"
  source_file = "${path.module}/functions/index.js"
  output_path = "${path.module}/functions/edge.zip"
}

resource "aws_iam_role" "edge" {
  provider           = aws.us_east_1
  name               = "${var.name}-edge-role"
  assume_role_policy = data.aws_iam_policy_document.edge_trust.json
}

resource "aws_iam_role_policy_attachment" "edge_basic" {
  provider   = aws.us_east_1
  role       = aws_iam_role.edge.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

# CloudWatch log group in us-east-1 — edge logs replicate here
resource "aws_cloudwatch_log_group" "edge" {
  provider          = aws.us_east_1
  name              = "/aws/lambda/us-east-1.${var.name}-edge-viewer-request"
  retention_in_days = 30
}

CloudFront Functions vs Lambda@Edge

CapabilityCloudFront FunctionsLambda@Edge
Supported eventsviewer-request, viewer-responseAll 4 event types
RuntimeJavaScript 2.0 (ES5.1 + partial ES6–12)Node.js 22.x, Python 3.x
Max execution time1ms5s (viewer) / 30s (origin)
Max memory2 MB128–512 MB
Network calls✅ (allowed but discouraged — adds latency and reliability risk at edge)
Body accessOrigin events only
Environment variables❌ (use KeyValueStore)
KV configuration store✅ CloudFront KeyValueStore
Cost~6× cheaper than Lambda@EdgePer invocation + GB-second
DeploymentSecondsMinutes (replication)
Best forURL rewrites, header manipulation, simple redirects, A/B by cookieAuth, complex routing, dynamic origin selection, error handling

CloudFront Function — URL rewrite example

// Rewrites /product/123 → /product?id=123
function handler(event) {
  var request = event.request;
  var uri = request.uri;

  var match = uri.match(/^\/product\/(\d+)$/);
  if (match) {
    request.uri = '/product';
    request.querystring = { id: { value: match[1] } };
  }

  return request;
}
resource "aws_cloudfront_function" "url_rewrite" {
  name    = "${var.name}-url-rewrite"
  runtime = "cloudfront-js-2.0"
  publish = true
  code    = file("${path.module}/functions/url-rewrite.js")
}

# Associate in cache behavior
function_association {
  event_type   = "viewer-request"
  function_arn = aws_cloudfront_function.url_rewrite.arn
}

Logging

Standard logs (v2)

Detailed per-request logs. Destinations: S3, CloudWatch Logs, Kinesis Data Firehose.

resource "aws_cloudfront_distribution" "this" {
  logging_config {
    bucket          = aws_s3_bucket.logs.bucket_domain_name
    prefix          = "cloudfront/${var.name}/"
    include_cookies = false  # enable for debugging, disable for cost
  }
}

Real-time logs

Sub-second delivery to Kinesis Data Streams. Use for live dashboards and anomaly detection.

resource "aws_cloudfront_realtime_log_config" "this" {
  name          = "${var.name}-realtime"
  sampling_rate = 5  # 1–100 percent of requests

  endpoint {
    stream_type = "Kinesis"
    kinesis_stream_config {
      role_arn   = aws_iam_role.realtime_log.arn
      stream_arn = aws_kinesis_stream.logs.arn
    }
  }

  fields = [
    "timestamp", "c-ip", "sc-status", "cs-method",
    "cs-uri-stem", "time-taken", "x-edge-location",
    "cs-protocol", "cs-bytes", "x-edge-result-type",
    "x-host-header", "cs-user-agent",
  ]
}

Athena partitioning

Partition the S3 log prefix by date for cost-efficient queries:

s3://logs-bucket/cloudfront/my-distribution/year=2026/month=05/day=22/

Use a Lambda or Glue crawler to create partitions, or use Athena partition projection:

CREATE EXTERNAL TABLE cloudfront_logs (
  date DATE, time STRING, x_edge_location STRING,
  sc_bytes BIGINT, cs_method STRING, cs_host STRING,
  cs_uri_stem STRING, sc_status INT, cs_referer STRING,
  cs_user_agent STRING, cs_uri_query STRING
)
PARTITIONED BY (year STRING, month STRING, day STRING)
ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t'
LOCATION 's3://logs-bucket/cloudfront/my-distribution/'
TBLPROPERTIES (
  'projection.enabled' = 'true',
  'projection.year.type' = 'integer', 'projection.year.range' = '2024,2030',
  'projection.month.type' = 'integer', 'projection.month.range' = '1,12', 'projection.month.digits' = '2',
  'projection.day.type' = 'integer', 'projection.day.range' = '1,31', 'projection.day.digits' = '2',
  'storage.location.template' = 's3://logs-bucket/cloudfront/my-distribution/year=${year}/month=${month}/day=${day}/'
);

Cross-account log bucket

Central logging account S3 bucket resource policy:

{
  "Effect": "Allow",
  "Principal": { "Service": "delivery.logs.amazonaws.com" },
  "Action": "s3:PutObject",
  "Resource": "arn:aws:s3:::central-logs-bucket/cloudfront/*",
  "Condition": {
    "StringEquals": { "aws:SourceOrgID": "o-xxxxxxxxxxxx" }
  }
}

Multi-account patterns

Pattern A — Shared CloudFront account (recommended for platform teams)

Platform team owns distributions in a dedicated CDN account. Origin S3 buckets live in spoke accounts.

CDN account (platform)          Spoke account (app team)
──────────────────────          ─────────────────────────
aws_cloudfront_distribution  →  aws_s3_bucket (origin)
aws_cloudfront_origin_access_control
                                aws_s3_bucket_policy:
                                  Principal: cloudfront.amazonaws.com
                                  Condition: AWS:SourceArn = distribution ARN
                                  (cross-account — account ID in ARN differs)

The bucket policy AWS:SourceArn condition works cross-account — the distribution ARN includes the CDN account ID, so spoke-account buckets are locked to that specific distribution.

Pattern B — Per-account distributions (app team autonomy)

Each application account owns its own distribution. Security team enforces WAF via Firewall Manager from the security account.

Security account                App account
────────────────                ──────────────────────────
aws_fms_policy          →       aws_wafv2_web_acl (auto-created by FMS)
  scope: OU=production           aws_cloudfront_distribution
  auto_remediation: true           web_acl_id = (FMS-managed)

Organizations SCP — enforce WAF attachment

{
  "Effect": "Deny",
  "Action": "cloudfront:CreateDistribution",
  "Resource": "*",
  "Condition": {
    "Null": { "cloudfront:WebAclId": "true" }
  }
}

Blocks creating distributions without a WAF web ACL. Apply to production OU.

Centralized logging bucket

resource "aws_s3_bucket_policy" "central_logs" {
  bucket = aws_s3_bucket.central_logs.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "AllowCloudFrontLogsFromOrg"
        Effect = "Allow"
        Principal = { Service = "delivery.logs.amazonaws.com" }
        Action   = "s3:PutObject"
        Resource = "${aws_s3_bucket.central_logs.arn}/cloudfront/*"
        Condition = {
          StringEquals = { "aws:SourceOrgID" = var.org_id }
        }
      },
      {
        Sid    = "AllowCloudFrontACLCheck"
        Effect = "Allow"
        Principal = { Service = "delivery.logs.amazonaws.com" }
        Action   = "s3:GetBucketAcl"
        Resource = aws_s3_bucket.central_logs.arn
      }
    ]
  })
}

Production checklist

Before any distribution goes live:

  • Viewer protocol policy: redirect-to-https or https-only — never allow-all
  • Minimum TLS version: TLSv1.2_2021 — not TLSv1 or TLSv1.1
  • OAC configured — not OAI, not public bucket
  • WAF web ACL attached (CLOUDFRONT scope, us-east-1)
  • Response headers policy with HSTS, X-Frame-Options, X-Content-Type-Options
  • Standard logging enabled (S3 or CloudWatch)
  • Custom domain with ACM certificate (not CloudFront default *.cloudfront.net for production)
  • Geo restriction reviewed — allowlist or blocklist as required by compliance
  • No wildcard CORS — scope AllowedOrigins to specific domains
  • default_cache_behavior has explicit TTLs — not relying on Cache-Control from untrusted origins
  • Lambda@Edge functions using numbered version ARN (not $LATEST)
  • Price class reviewed — PriceClass_100 for EU/US-only services

BEFORE_AFTER.md

CHANGELOG.md

CODE_OF_CONDUCT.md

COMMANDS.md

CONTRIBUTING.md

EDITOR_INTEGRATIONS.md

GETTING_STARTED.md

HOW_IT_WORKS.md

install.sh

INSTALLATION.md

LAUNCH.md

PROMPTS.md

QUICKSTART.md

README.md

renovate.json

SECURITY.md

SKILL.md

tessl.json

tile.json