CloudFront CDN in Practice (3) — Private Content, Edge Logic, Security, Monitoring

CloudFront CDN in Practice (3) — Private Content, Edge Logic, Security, Monitoring


Introduction

Part 1 covered concepts, and Part 2 put a Spring Boot + Kotlin origin behind CloudFront. With the basics working, Part 3 covers four things you need in real operations.

Topics: ① private content (Signed URLs/cookies), ② edge logic (Functions vs Lambda@Edge), ③ security (custom domain, OAC, WAF), ④ monitoring & cost.


TL;DR

  • Protect private content with signatures. A signed URL (single file) or signed cookies (multiple files) let only authorized users access content for a limited time.
  • Run lightweight logic at the edge. Simple header tweaks/redirects use ultra-light CloudFront Functions; network calls or complex logic use Lambda@Edge.
  • The dividing line is weight. Functions run sub-1ms, viewer stage only, very cheap; Lambda@Edge is heavier with 4 triggers and can make network calls.
  • Security is three layers. A TLS certificate for a custom domain (HTTPS), S3 locked behind Origin Access Control (OAC), and a web firewall (WAF) plus geo-restriction to filter traffic.
  • Operate by hit ratio. Watch cache hit ratio, error rate, and bytes transferred; reduce cost with price class, compression, and hit ratio.

1. Private Content — Signed URLs and Signed Cookies

A default CloudFront distribution is accessible by anyone who knows the URL. To let only authorized users access paid videos or member-only files, you need signing.

MethodBest forTrait
Signed URLA single file (a specific video/download link)The URL embeds the signature and expiry. URL gets long
Signed CookieMultiple files / a whole path (a member-only section)Grants access via a cookie, URL unchanged. Good for many assets

How it works: your backend (a trusted server) signs a policy (expiry, allowed paths) with a private key, and CloudFront verifies it with the registered public key. Content is served only if the signature is valid and unexpired.

flowchart LR
    app["Backend<br/>(signs with private key)"] -->|"issue signed URL/cookie"| client["Client"]
    client -->|"request + signature"| cf["CloudFront<br/>(verifies with public key)"]
    cf -->|"forward if valid"| origin["Origin"]

Register the public key and key group in Terraform, and attach to the behavior.

resource "aws_cloudfront_public_key" "app" {
  name        = "app-public-key"
  encoded_key = file("public_key.pem")   # RSA public key (PEM)
}

resource "aws_cloudfront_key_group" "app" {
  name  = "app-key-group"
  items = [aws_cloudfront_public_key.app.id]
}

# Add to the protected behavior:
#   trusted_key_groups = [aws_cloudfront_key_group.app.id]

A behavior with trusted_key_groups returns 403 without a valid signature. The backend generates signatures with the private key (e.g., the AWS SDK’s CloudFront signer).


2. Edge Logic — CloudFront Functions vs Lambda@Edge

You can intercept requests/responses at the edge to run logic. There are two tools, split by “weight.”

CriterionCloudFront FunctionsLambda@Edge
LanguageJavaScript (lightweight runtime)Node.js / Python
Runs atAll edge locationsRegional edge caches (+ viewer)
Triggersviewer-request, viewer-responseviewer/origin × request/response (4)
RuntimeSub-1msUp to seconds
Network callsNoYes (external API/DB)
CostVery cheapMore expensive
Use forHeader tweaks, redirects, URL rewrites, token format checksOrigin branching, external auth, image transforms, heavy logic

How to choose: For “tweak headers / simple redirect / normalize URL,” almost always use CloudFront Functions (cheap, fast). Reach for Lambda@Edge only when you need external calls, complex branching, or to rewrite origin responses.

Example — check a security header at viewer-request (CloudFront Functions)

// auth.js — 401 if no Authorization header
function handler(event) {
  var request = event.request;
  if (!request.headers['authorization']) {
    return {
      statusCode: 401,
      statusDescription: 'Unauthorized',
    };
  }
  return request; // pass
}
resource "aws_cloudfront_function" "auth" {
  name    = "viewer-auth"
  runtime = "cloudfront-js-2.0"
  publish = true
  code    = file("auth.js")
}

# Attach to a behavior:
#   function_association {
#     event_type   = "viewer-request"
#     function_arn = aws_cloudfront_function.auth.arn
#   }

3. Security — Custom Domain, OAC, WAF

3.1 Custom domain + TLS certificate (ACM)

To use cdn.example.com instead of d123.cloudfront.net, you need an ACM certificate. A certificate for CloudFront must live in us-east-1 (it’s a global service, managed in N. Virginia).

# CloudFront certificates are us-east-1 only
resource "aws_acm_certificate" "cdn" {
  provider          = aws.us_east_1
  domain_name       = "cdn.example.com"
  validation_method = "DNS"
}

# Add to the distribution:
#   aliases = ["cdn.example.com"]
#   viewer_certificate {
#     acm_certificate_arn      = aws_acm_certificate.cdn.arn
#     ssl_support_method       = "sni-only"
#     minimum_protocol_version = "TLSv1.2_2021"
#   }

Then CNAME/alias cdn.example.com to the distribution domain in Route53 (or your DNS provider).

3.2 S3 origin — lock direct access with OAC

If you keep static assets in S3 (the option from Part 2, §1), don’t make the bucket public. Use OAC (Origin Access Control) so “only CloudFront accesses S3” and keep the bucket private. (The older OAI is no longer recommended.)

resource "aws_cloudfront_origin_access_control" "s3" {
  name                              = "s3-oac"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

# Attach origin_access_control_id to the S3 origin, and in the bucket policy
# allow the cloudfront.amazonaws.com service principal only with the
# AWS:SourceArn (distribution ARN) condition
flowchart LR
    user["User"] --> cf["CloudFront (OAC signs)"]
    cf -->|sigv4-signed requests only| s3["S3 (private bucket)"]
    direct["Direct access attempt"] -.blocked (403).-> s3

3.3 WAF & geo-restriction

A web firewall (WAF) filters SQL injection, malicious bots, and request floods (rate limiting) at the edge. A CloudFront WAF is created with scope = "CLOUDFRONT" in us-east-1.

resource "aws_wafv2_web_acl" "cdn" {
  provider = aws.us_east_1
  name     = "cdn-waf"
  scope    = "CLOUDFRONT"
  # ... managed rule groups (AWSManagedRulesCommonRuleSet, etc.) + a rate-based rule ...
}

# Add to the distribution: web_acl_id = aws_wafv2_web_acl.cdn.arn

To allow/block specific countries, use the distribution’s geo_restriction.

restrictions {
  geo_restriction {
    restriction_type = "whitelist"
    locations        = ["KR", "US", "JP"]
  }
}

4. Monitoring & Cost

4.1 What to watch

CloudFront exposes key metrics via CloudWatch.

MetricMeaningWatching it tells you
Cache Hit Ratehit / total ratioLow = more origin load and cost → check cache key/TTL
4xx / 5xx error rateError response ratio5xx spike = origin failure; 4xx = auth/path issues
BytesDownloadedBytes transferredCost/traffic trend
OriginLatencyOrigin response latencyHigh = origin bottleneck

Note: Detailed metrics like hit rate require enabling additional metrics on the distribution to appear in CloudWatch. Default metrics alone won’t show hit rate accurately.

Two log types: standard logs dumped to S3 (post-hoc analysis) and real-time logs streamed by the second (to Kinesis, etc.). Logs are decisive for finding paths that miss the cache often.

4.2 Cutting cost

CloudFront cost is mainly data transfer + request count + (invalidations / Lambda@Edge).

  • Raise hit ratio: minimize the cache key (drop cookies), cache static long. Hit ratio is cost.
  • Enable compression: compress = true cuts transfer.
  • Price Class: if you don’t need every edge worldwide, limit to PriceClass_100 (NA/EU), etc., to lower cost.
  • Origin Shield: an extra layer in front of the origin reduces origin hits, cutting load and transfer (for high traffic).

Recap

So far we’ve covered CloudFront from concepts to operations.

PartTopicCore
Part 1ConceptsEdge cache, cache key, Cache-Control/TTL, invalidation vs versioning
Part 2Hands-onSpring Boot+Kotlin origin + behavior split + Terraform + X-Cache verification
Part 3OperationsPrivate content, edge logic, security (domain/OAC/WAF), monitoring

The essence of CDN design is one sentence: split “what to cache, for whom, and for how long” by path, and stamp that intent consistently into the origin headers and CloudFront behaviors. Add signing, edge logic, security, and monitoring on top, and you have a production CDN.

The final Part 4 covers media: resize user-uploaded images on demand and cache them (Lambda@Edge), and transcode video with MediaConvert to deliver HLS/DASH via CloudFront — media serving beyond static and dynamic.


Appendix

A. Decision cheat sheet

SituationChoice
One private fileSigned URL
A whole private pathSigned Cookie
Header tweak / redirectCloudFront Functions
External call / complex logicLambda@Edge
Custom-domain HTTPSACM cert (us-east-1) + sni-only
Private static in S3OAC + private bucket policy
Defend bots/injection/floodsWAF (scope=CLOUDFRONT)
Reduce costHigher hit ratio + compression + Price Class + Origin Shield

B. Glossary

TermDescription
Signed URL/CookiePrivate-content method granting time-limited access to authorized users via signing
CloudFront FunctionsUltra-light JS running at every edge (headers, redirects)
Lambda@EdgeNode/Python running at regional edges (network, complex logic)
ACMAWS Certificate Manager. Manages TLS certs (CloudFront uses us-east-1)
OACOrigin Access Control. Lets only CloudFront access S3
WAFWeb application firewall. Filters malicious traffic at the edge
Origin ShieldAn extra cache layer in front of the origin; cuts origin hits/load
Price ClassThe range of edge regions used; narrowing it lowers cost

C. References

Shop on Amazon

As an Amazon Associate, I earn from qualifying purchases.