Combining Origin Access Control (OAC) and Signed URLs with Trusted Key Groups
Executive Summary
This guide provides a comprehensive implementation framework for securing Amazon S3 static assets through AWS CloudFront using Origin Access Control (OAC) and Signed URLs with Trusted Key Groups. This architecture establishes multiple security layers to protect sensitive content while maintaining optimal delivery performance.
Architecture Overview
This architecture establishes a two-layer security model where both end-user access and origin access are independently verified and protected.
User → CloudFront (Signed URLs)
- Users must present a cryptographically signed URL containing an expiration timestamp.
- CloudFront validates the signature using public keys from trusted Key Groups.
- Any invalid or expired requests are rejected at the edge, preventing unauthorized access.
CloudFront → S3 (Origin Access Control)
- CloudFront authenticates to S3 using AWS Signature Version 4.
- S3 bucket policies ensure that requests only come from authorized CloudFront distributions.
- Direct access to S3 is blocked, enforcing all data delivery through CloudFront.
Security Layers
- Viewer Authentication: Signed URL validation using RSA key pairs
- Edge Security: CloudFront distribution with restricted access policies
- Origin Authentication: OAC with SigV4 signing for S3 access
- Bucket Security: S3 bucket policies restricting access to specific CloudFront distributions
Implementation Specifications
Phase 1: S3 Bucket Configuration
1.1 Create Private S3 Bucket (Console)
- Navigate to Amazon S3 Console
- Click Create bucket
- Enter Bucket name:
demo-oac-secure-assets
- Select AWS Region
- Under Block Public Access, select Block all public access
- (Optional) Enable Versioning for audit trail
- Click Create bucket
1.2 Upload Sample Content (Console)
- Select your newly created bucket
- Click Upload → Add files
- Select your static assets (HTML, CSS, JS, images)
- Click Upload
- Verify objects show No public access in the permissions column
Phase 2: Origin Access Control Setup
2.1 Create OAC Configuration (Console)
- Navigate to CloudFront Console
- In left navigation, select Origin access
- Click Create control setting
- Configure:
-
Name:
secure-oac-configuration
- Description: Origin Access Control for secure S3 access
- Signing protocol: SigV4
- Signing behavior: Always
-
Name:
- Click Create
Phase 3: Cryptographic Key Management
3.1 Generate Key Pair Locally
# Generate 2048-bit RSA private key
openssl genrsa -out private_key.pem 2048
# Extract public key
openssl rsa -in private_key.pem -pubout -out public_key.pem
# Set secure permissions
chmod 600 private_key.pem
3.2 Create CloudFront Public Key (Console)
- In CloudFront Console, open Key management → Public keys
- Click Create public key
- Configure:
-
Name:
secure-content-key
-
Public key: paste contents of
public_key.pem
-
Name:
- Click Create public key
- Note the generated Key pair ID (format:
K123456789ABCDEF
)
3.3 Establish Trusted Key Group (Console)
- In CloudFront Console, open Key management → Key groups
- Click Create key group
- Configure:
-
Name:
trusted-key-group
- Description: Key group for signed URL validation
- Public keys: select your created public key
-
Name:
- Click Create key group
Phase 4: CloudFront Distribution Configuration
4.1 Create Distribution (Console)
- Navigate to CloudFront Console → Create distribution
-
Origin settings:
-
Origin domain: select your S3 bucket (
demo-oac-secure-assets.s3.amazonaws.com
) - Origin path: (Leave blank)
-
Name:
SecureS3Origin
- Origin access: Origin access control settings (recommended)
- Select control setting: your created OAC
- Bucket policy: Yes, update the bucket policy
-
Origin domain: select your S3 bucket (
4.2 Configure Default Cache Behavior
-
Cache policy:
CachingOptimized
- Viewer protocol policy: Redirect HTTP to HTTPS
- Restrict viewer access: Yes
- Trusted key groups: select your created key group
- Additional:
- AWS WAF: Enable if required
- HTTP/2: Enabled
- IPv6: Enabled
4.3 Distribution Settings
- Price class: choose based on geographic requirements
- Alternate domain name (CNAME): configure if using custom domain
- SSL certificate: default or custom ACM certificate
- Click Create distribution
- Wait for status to change from In Progress → Deployed
Phase 5: S3 Bucket Policy Implementation
5.1 Verify Automatic Policy Creation (Console)
- Go to S3 Console → select bucket
demo-oac-secure-assets
→ Permissions tab - Verify an auto-generated bucket policy that allows CloudFront access via OAC
- If not present, apply the manual policy below
5.2 Manual Bucket Policy (if required)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontOACAccess",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::demo-oac-secure-assets/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/E2ABCD1234EXAMPLE"
}
}
}
]
}
Replace placeholders:
-
123456789012
→ your AWS Account ID -
E2ABCD1234EXAMPLE
→ your CloudFront Distribution ID
Signed URL Generation Implementation
Python Implementation
import base64
import json
import time
from urllib.parse import urlencode
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding
class CloudFrontSigner:
def __init__(self, key_pair_id: str, private_key_path: str):
self.key_pair_id = key_pair_id
self.private_key_path = private_key_path
def _b64url_encode(self, data: bytes) -> str:
"""Base64 URL-safe encoding without padding"""
return base64.b64encode(data).decode().replace('+', '-').replace('=', '_').replace('/', '~')
def _sign_policy(self, policy: bytes) -> str:
"""Sign policy document using RSA private key"""
with open(self.private_key_path, 'rb') as key_file:
private_key = serialization.load_pem_private_key(
key_file.read(),
password=None
)
signature = private_key.sign(policy, padding.PKCS1v15(), hashes.SHA1())
return self._b64url_encode(signature)
def generate_signed_url(self, resource_url: str, expires_in: int = 3600) -> str:
"""Generate CloudFront signed URL with custom policy"""
expiration = int(time.time()) + expires_in
policy = {
"Statement": [
{
"Resource": resource_url,
"Condition": {
"DateLessThan": { "AWS:EpochTime": expiration }
}
}
]
}
policy_json = json.dumps(policy, separators=(',', ':')).encode()
policy_encoded = self._b64url_encode(policy_json)
signature = self._sign_policy(policy_json)
return f"{resource_url}?Policy={policy_encoded}&Signature={signature}&Key-Pair-Id={self.key_pair_id}"
# Implementation Example
signer = CloudFrontSigner(
key_pair_id="K123456789ABCDEF",
private_key_path="/secure/keys/private_key.pem"
)
signed_url = signer.generate_signed_url(
resource_url="https://d123456789.cloudfront.net/secure-document.pdf",
expires_in=7200 # 2 hours validity
)
print("Signed URL:", signed_url)
Phase 6: Testing
Test Scenario | Expected Result | Validation Steps |
---|---|---|
Direct S3 Access | HTTP 403 Access Denied | 1) Copy S3 object URL → 2) Open in browser → 3) Verify Access Denied |
Unsigned CloudFront Access | HTTP 403 Access Denied | 1) Copy CloudFront URL → 2) Access w/o params → 3) Verify Access Denied |
Valid Signed URL | HTTP 200 OK | 1) Generate signed URL → 2) Open in browser → 3) Verify content loads |
Expired Signed URL | HTTP 403 Access Denied | 1) Generate URL with past expiration → 2) Access → 3) Verify rejection |
Modified Signature | HTTP 403 Access Denied | 1) Alter signature parameter → 2) Access → 3) Verify rejection |
Top comments (0)