CloudFront Beyond Caching: Signed URLs, Geo-Restriction, Origin Shield, and Real-Time Logs

3 min readCloud Infrastructure

CloudFront's caching and CDN basics are well-documented. The less-obvious features — signed URLs for private content, geographic access controls, Origin Shield for reducing origin load, and real-time log streaming — are where production configurations diverge from tutorials.

awscloudfrontsecurity

Restricting content access with signed URLs and signed cookies

By default, CloudFront serves cached content to anyone who requests it. For private content — paid video, user-specific files, time-limited downloads — you restrict access using signed URLs or signed cookies.

Signed URL: a single URL with an embedded signature, expiration, and optional IP restriction. One URL = one resource. Appropriate for email links, download buttons, single-file access.

Signed cookie: a set of cookies (CloudFront-Policy, CloudFront-Signature, CloudFront-Key-Pair-Id) that authorize access to multiple resources matching a path pattern. Appropriate for streaming video (many segment files), authenticated web applications where users should access an entire path prefix.

Trusted key groups and key management for signed content

ConceptAWS CloudFront

Signed URLs and cookies are created using RSA private keys. CloudFront validates signatures using the public key you configure in a key group. The private key lives outside AWS — typically in Secrets Manager. Never use root account CloudFront key pairs (deprecated) — use key groups instead.

Prerequisites

  • RSA signatures
  • CloudFront distributions and behaviors
  • Secrets Manager

Key Points

  • Generate a 2048-bit RSA key pair. Upload the public key to CloudFront as a public key resource.
  • Create a key group containing one or more public keys. Attach the key group to the distribution behavior.
  • Sign URLs/cookies with the private key using CloudFront's signing protocol (not standard JWT).
  • Key rotation: add a new key to the key group, update signers to use the new key, then remove the old key.
import boto3
from botocore.signers import CloudFrontSigner
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from datetime import datetime, timedelta, timezone

def get_private_key():
    ssm = boto3.client('secretsmanager')
    response = ssm.get_secret_value(SecretId='cloudfront/private-key')
    return response['SecretString'].encode('utf-8')

def rsa_signer(message):
    private_key = serialization.load_pem_private_key(get_private_key(), password=None)
    return private_key.sign(message, padding.PKCS1v15(), hashes.SHA1())

def generate_signed_url(url: str, key_id: str, expiry_hours: int = 1) -> str:
    signer = CloudFrontSigner(key_id, rsa_signer)

    expire_date = datetime.now(timezone.utc) + timedelta(hours=expiry_hours)

    signed_url = signer.generate_presigned_url(
        url,
        date_less_than=expire_date
    )
    return signed_url

To restrict the behavior to signed requests only, set the distribution behavior's trusted key groups and ensure no other behavior allows unsigned access to the same path.

Geo-restriction: blocking or allowing by country

CloudFront can allow or block requests from specific countries based on the viewer's IP address:

resource "aws_cloudfront_distribution" "main" {
  # ... other config ...

  restrictions {
    geo_restriction {
      restriction_type = "whitelist"  # or "blacklist"
      locations        = ["US", "CA", "GB", "DE"]  # ISO 3166-1 alpha-2 codes
    }
  }
}

Blocked requests receive HTTP 403. This is a distribution-level setting — it applies to all behaviors in the distribution. For path-specific geo-restriction (allow all content except videos to certain countries), use CloudFront Functions:

// CloudFront Function: geo-restrict /premium/* path
function handler(event) {
    var request = event.request;
    var viewerCountry = event.context.viewerCountry;

    // Block access to premium content from restricted regions
    if (request.uri.startsWith('/premium/') &&
        ['CN', 'RU', 'KP'].includes(viewerCountry)) {
        return {
            statusCode: 403,
            statusDescription: 'Forbidden',
            body: JSON.stringify({ error: 'Content not available in your region' })
        };
    }
    return request;
}

Geographic IP data has a small error rate (~0.1% for country-level). For compliance-grade geo-restriction (GDPR data residency, content licensing), supplement CloudFront geo-restriction with application-level checks.

Origin Shield: reducing origin load with an additional caching layer

Origin Shield adds a centralized caching layer between CloudFront's edge locations and your origin. Normally, each edge location can independently query your origin on cache misses. With Origin Shield, all cache misses from all edge locations first check the Origin Shield node — the origin only gets hit if Origin Shield also misses.

resource "aws_cloudfront_distribution" "main" {
  origin {
    domain_name = "api.example.com"
    origin_id   = "api"

    origin_shield {
      enabled              = true
      origin_shield_region = "us-east-1"  # choose region closest to your origin
    }

    custom_origin_config {
      http_port              = 80
      https_port             = 443
      origin_protocol_policy = "https-only"
      origin_ssl_protocols   = ["TLSv1.2"]
    }
  }
  # ...
}

Origin Shield reduces origin request volume significantly for globally distributed traffic. For an origin that gets 10,000 requests/minute across 20 edge locations, Origin Shield can collapse 200+ simultaneous origin queries (for the same cache miss) into 1.

Cost: Origin Shield adds a per-request charge (~$0.01/10,000 requests). The origin cost reduction usually exceeds this.

📝Real-time logs: streaming access logs to Kinesis

CloudFront standard logs have up to 24-hour latency and are delivered to S3. Real-time logs stream access log records to Kinesis Data Streams with under 1-second latency.

resource "aws_kinesis_stream" "cloudfront_logs" {
  name             = "cloudfront-logs"
  shard_count      = 2
  retention_period = 24
}

resource "aws_cloudfront_realtime_log_config" "main" {
  name          = "main-realtime-logs"
  sampling_rate = 100  # 1-100: percentage of requests to log

  endpoint {
    stream_type = "Kinesis"
    kinesis_stream_config {
      role_arn   = aws_iam_role.cloudfront_kinesis.arn
      stream_arn = aws_kinesis_stream.cloudfront_logs.arn
    }
  }

  fields = [
    "timestamp", "c-ip", "sc-status", "cs-method",
    "cs-uri-stem", "time-taken", "x-edge-location",
    "x-edge-result-type", "cs-referer", "x-forwarded-for"
  ]
}

Attach the real-time log config to a specific cache behavior:

default_cache_behavior {
  realtime_log_config_arn = aws_cloudfront_realtime_log_config.main.arn
  # ... other settings
}

Common downstream consumers: Kinesis Data Firehose to S3 for storage, Kinesis to Lambda for real-time alerting on error rates or unusual patterns, Kinesis to OpenSearch for live dashboards.

Sampling rate matters: 100% captures everything but generates high Kinesis costs. 1% is sufficient for trend analysis. For security monitoring (WAF correlation, DDoS detection), use 100% to catch all anomalies.

Field-level encryption: protecting sensitive data beyond HTTPS

HTTPS encrypts in transit, but the content is decrypted at CloudFront before forwarding to the origin. Field-level encryption keeps specific POST body fields encrypted all the way to your application, using a public key:

Client POSTs: { "card_number": "4111..." }

CloudFront encrypts "card_number" field with your public key
Origin receives: { "card_number": "ENCRYPTED_BLOB" }

Only the service with the private key can decrypt — not CloudFront, not other services

Field-level encryption is niche — most applications don't need it because TLS provides sufficient protection for most compliance requirements. Use it when you need to guarantee that specific sensitive fields (card numbers, SSNs) are not accessible to the broader infrastructure, only to the specific microservice that needs them.

A video streaming service uses signed URLs to protect content. Users report that after purchasing a video, they can share the signed URL with friends who are then able to watch without purchasing. What signed URL configuration would prevent this?

medium

Signed URLs currently use date-less-than expiration set to 72 hours after purchase. No other restrictions are configured on the signed URL.

  • AReduce the expiration time from 72 hours to 1 hour
    Incorrect.Shorter expiration limits the sharing window but doesn't prevent sharing — the shared URL still works within the expiration window. It reduces risk but doesn't solve the problem.
  • BAdd an IP restriction to the signed URL using the custom policy — the signed URL will only work from the purchaser's IP address
    Correct!CloudFront signed URLs with custom policy support an IpAddress condition. The URL is only valid from the specified CIDR. When shared with someone on a different IP, CloudFront rejects the request. Combine with a reasonable expiration (24h) for the best tradeoff. Limitation: mobile users on cellular networks change IPs frequently — a strict /32 restriction can break legitimate access. Use /24 to allow minor IP variation, or use signed cookies (which the browser sends automatically) with per-session tokens instead.
  • CEncrypt the video at rest in S3 with a customer-managed key
    Incorrect.S3 encryption protects data at rest. CloudFront fetches and decrypts from S3 transparently before serving. Encryption at rest doesn't affect who can access content via CloudFront.
  • DUse signed cookies instead of signed URLs — cookies cannot be shared
    Incorrect.Cookies can be shared (by copying cookie values). Signed cookies solve a different problem — authorizing access to multiple resources without embedding the signature in each URL.

Hint:The URL is being shared with different users. What attribute of the viewer can you bind the URL to?