🔐 GCP Cloud CDN Signed URLs Configuration Guide
📖 What are Signed URLs?
Signed URLs provide time-limited access to Cloud CDN content without requiring users to have Google accounts. They contain cryptographic signatures that validate the request's authenticity and authorization.
Think of it like this: Imagine you're giving someone a temporary keycard to access a secure building. The keycard works only for a specific time period and only for certain areas. That's exactly what signed URLs do for web content!
🛡️ Key Security Features
Time-limited access: URLs expire automatically
Cryptographic validation: HMAC-SHA1 signatures prevent tampering
Granular control: Per-file or prefix-based access
CDN integration: Works seamlessly with Cloud CDN caching
🔄 Signed URL Request Flow
sequenceDiagram
participant User as User/Client
participant App as Your Application
participant CDN as Cloud CDN
participant Origin as Origin Server
User->>App: Request access to content
App->>App: Validate user permissions
App->>App: Generate signed URL with expiration
App->>User: Return signed URL
User->>CDN: Request content with signed URL
CDN->>CDN: Validate signature & expiration
alt Valid signature & not expired
CDN->>Origin: Forward request (if cache miss)
Origin->>CDN: Return content
CDN->>User: Serve content (cache for future)
else Invalid/expired signature
CDN->>User: Return 403 Forbidden
end
🔧 Prerequisites and Setup
Enable Cloud CDN
Signed URLs only work with Cloud CDN enabled backends.
gcloud compute backend-services update BACKEND_SERVICE_NAME \
--enable-cdn \
--global
gcloud compute backend-buckets update BACKEND_BUCKET_NAME \
--enable-cdn
Create Signing Keys
Each backend can have up to 3 signing keys for rotation purposes.
For Backend Services
gcloud compute backend-services add-signed-url-key BACKEND_SERVICE_NAME \
--key-name="signing-key-1" \
--key-file="path/to/private-key.txt" \
--global
For Backend Buckets
gcloud compute backend-buckets add-signed-url-key BACKEND_BUCKET_NAME \
--key-name="signing-key-1" \
--key-file="path/to/private-key.txt"
Generate Random Key
openssl rand -base64 32 > private-key.txt
🔑 Key Management Best Practices:
- Store keys securely using Cloud Key Management Service
- Use descriptive key names (e.g., "prod-key-2025-01")
- Rotate keys regularly (monthly/quarterly)
- Never expose keys in source code or logs
🏗️ Configuration Methods
1. 🖥️ Google Cloud Console
- Navigate to Cloud CDN page
- Click on your Origin
- Click Edit button
- Go to Cache performance section
- Under Restricted content, select "Restrict access using signed URLs and signed cookies"
- Click Add signing key
- Choose key creation method:
- Automatically generate - Google creates the key
- Let me enter - You provide your own key
- Copy and securely store the generated key
- Configure Cache entry maximum age
- Click Done
2. ⚡ gcloud CLI Configuration
List Existing Keys
gcloud compute backend-services describe BACKEND_SERVICE_NAME \
--global
gcloud compute backend-buckets describe BACKEND_BUCKET_NAME
Configure Cache Settings
gcloud compute backend-services update BACKEND_SERVICE_NAME \
--signed-url-cache-max-age=3600 \
--global
Delete Old Keys (for rotation)
gcloud compute backend-services delete-signed-url-key BACKEND_SERVICE_NAME \
--key-name="old-signing-key" \
--global
🔐 Generating Signed URLs
Using gcloud CLI
Sign Specific URL
gcloud compute sign-url \
--key-name="signing-key-1" \
--key-file="path/to/private-key.txt" \
--expires-in=3600 \
"https://cdn.example.com/path/to/file.jpg"
Sign URL with Custom Expiration
gcloud compute sign-url \
--key-name="signing-key-1" \
--key-file="path/to/private-key.txt" \
--expires="2025-12-31T23:59:59Z" \
"https://cdn.example.com/secure/document.pdf"
Sign URL Prefix (Multiple Files)
gcloud compute sign-url \
--key-name="signing-key-1" \
--key-file="path/to/private-key.txt" \
--expires-in=7200 \
--url-prefix="https://cdn.example.com/user/123/" \
"https://cdn.example.com/user/123/"
Programmatic URL Signing
Node.js Implementation
const crypto = require('crypto');
function signUrl(url, keyName, key, expiration) {
// Create URL to sign
const urlToSign = `${url}?Expires=${expiration}&KeyName=${keyName}`;
// Create HMAC-SHA1 signature
const signature = crypto
.createHmac('sha1', Buffer.from(key, 'base64'))
.update(urlToSign)
.digest('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
// Return signed URL
return `${urlToSign}&Signature=${signature}`;
}
// Usage example
const keyName = 'signing-key-1';
const key = 'your-base64-encoded-key';
const expiration = Math.floor(Date.now() / 1000) + 3600; // 1 hour
const baseUrl = 'https://cdn.example.com/secure/file.pdf';
const signedUrl = signUrl(baseUrl, keyName, key, expiration);
console.log('Signed URL:', signedUrl);
Python Implementation
import base64
import hashlib
import hmac
import time
from urllib.parse import quote
def sign_url(url, key_name, key, expiration_time):
"""Generate a signed URL for Cloud CDN"""
# Create the URL to sign
url_to_sign = f"{url}?Expires={expiration_time}&KeyName={key_name}"
# Decode the base64-encoded key
key_bytes = base64.b64decode(key)
# Create HMAC-SHA1 signature
signature = hmac.new(
key_bytes,
url_to_sign.encode('utf-8'),
hashlib.sha1
).digest()
# Encode signature as URL-safe base64
signature_b64 = base64.urlsafe_b64encode(signature).decode('utf-8')
signature_b64 = signature_b64.rstrip('=') # Remove padding
# Return the signed URL
return f"{url_to_sign}&Signature={signature_b64}"
# Usage example
key_name = "signing-key-1"
key = "your-base64-encoded-key"
expiration = int(time.time()) + 3600 # 1 hour from now
base_url = "https://cdn.example.com/secure/file.pdf"
signed_url = sign_url(base_url, key_name, key, expiration)
print(f"Signed URL: {signed_url}")
🎯 Signed URL Structure
Base URL:
https://cdn.example.com/secure/document.pdf
Signed URL Components:
https://cdn.example.com/secure/document.pdf?
Expires=1735689599&
KeyName=signing-key-1&
Signature=abcd1234-efgh5678_ijkl9012
URL Parameters Explained:
- Expires: Unix timestamp when URL expires
- KeyName: Name of the signing key (case-sensitive)
- Signature: URL-safe base64 encoded HMAC-SHA1 signature
🔍 URL Prefix Support
Configuring URL Prefixes
Allow access to multiple files under a common path with a single signed URL.
gcloud compute sign-url \
--key-name="signing-key-1" \
--key-file="private-key.txt" \
--expires-in=3600 \
--url-prefix="https://cdn.example.com/user/123/documents/" \
"https://cdn.example.com/user/123/documents/"
✅ This signed URL grants access to all files under:
- https://cdn.example.com/user/123/documents/file1.pdf
- https://cdn.example.com/user/123/documents/file2.jpg
- https://cdn.example.com/user/123/documents/subfolder/file3.doc
⚙️ Advanced Configuration
Origin Server Validation
Always validate signatures at the origin server for defense in depth.
Apache Configuration
# Add to .htaccess or Apache config
RewriteEngine On
# Require signature parameters for protected content
RewriteCond %{QUERY_STRING} !Expires=
RewriteCond %{REQUEST_URI} ^/secure/
RewriteRule ^(.*)$ - [F,L]
RewriteCond %{QUERY_STRING} !KeyName=
RewriteCond %{REQUEST_URI} ^/secure/
RewriteRule ^(.*)$ - [F,L]
RewriteCond %{QUERY_STRING} !Signature=
RewriteCond %{REQUEST_URI} ^/secure/
RewriteRule ^(.*)$ - [F,L]
Nginx Configuration
location /secure/ {
# Validate signed URL parameters
if ($arg_expires = "") {
return 403;
}
if ($arg_keyname = "") {
return 403;
}
if ($arg_signature = "") {
return 403;
}
# Add custom validation logic here
try_files $uri $uri/ =404;
}
Cache Behavior Configuration
Set Cache Maximum Age
gcloud compute backend-services update BACKEND_SERVICE_NAME \
--signed-url-cache-max-age=7200 \
--global
Cache Behavior for Signed URLs:
- Cloud CDN caches responses for signed requests regardless of Cache-Control headers
- Responses to signed and unsigned requests don't share cache entries
- Default cache max age is 1 hour, configurable up to your needs
- Content marked as uncacheable can be overridden for signed requests
🔄 Key Rotation Strategy
graph TD
A[Current Key: key-1] --> B[Add New Key: key-2]
B --> C[Update Application to Use key-2]
C --> D[Monitor for 24-48 hours]
D --> E[Delete Old Key: key-1]
E --> F[Current Key: key-2]
F --> G[Repeat Process Monthly]
style A fill:#4285f4,color:#fff
style F fill:#34a853,color:#fff
style G fill:#fbbc04,color:#000
Implementing Key Rotation
Add New Key
openssl rand -base64 32 > new-signing-key.txt
gcloud compute backend-services add-signed-url-key BACKEND_SERVICE_NAME \
--key-name="signing-key-2" \
--key-file="new-signing-key.txt" \
--global
List All Keys
gcloud compute backend-services describe BACKEND_SERVICE_NAME \
--global \
--format="value(cdnPolicy.signedUrlKeyNames[])"
Remove Old Key
gcloud compute backend-services delete-signed-url-key BACKEND_SERVICE_NAME \
--key-name="signing-key-1" \
--global
🛡️ Security Best Practices
🔒 Essential Security Measures
✅ DO:
- Sign only HTTPS URLs to prevent signature interception
- Distribute signed URLs over secure transport protocols
- Set short expiration times (minutes/hours, not days)
- Validate signatures at origin servers
- Store keys in Cloud Key Management Service
- Rotate keys regularly (monthly/quarterly)
- Monitor for unusual access patterns
❌ DON'T:
- Expose signing keys in source code or logs
- Use overly long expiration times
- Share the same key across multiple environments
- Forget to validate at the origin server
- Use HTTP for signed URLs
🔍 Monitoring and Troubleshooting
Validation Checks
Cloud CDN validates these parameters for signed URLs:
🔍 Validation Criteria:
- HTTP Method: Must be GET, HEAD, OPTIONS, or TRACE
- Expiration: Must be set to a future time
- Signature: Must match computed signature using named key
- Key Existence: KeyName must exist in backend configuration
Test Signed URL
curl -I "https://cdn.example.com/secure/file.pdf?Expires=1735689599&KeyName=signing-key-1&Signature=abc123..."
Check Response Headers
# Look for these in response headers:
# HTTP/1.1 200 OK - Valid signed URL
# HTTP/1.1 403 Forbidden - Invalid/expired signature
# Age: 0 - Fresh content from origin
# Via: 1.1 google - Served through Cloud CDN
Common Issues and Solutions
❓ 403 Forbidden Errors
- Check expiration time: Ensure it's in the future
- Verify key name: Must match exactly (case-sensitive)
- Validate signature: Recalculate using correct algorithm
- Check HTTP method: Only GET, HEAD, OPTIONS, TRACE allowed
❓ Signature Mismatch
- URL encoding: Ensure proper encoding of special characters
- Key format: Verify base64 encoding is correct
- Algorithm: Must use HMAC-SHA1
- Parameter order: Include Expires and KeyName before signing
📊 Comparison: Signed URLs vs Alternatives
| Method |
Use Case |
Pros |
Cons |
Best For |
| Signed URLs |
Time-limited file access |
No login required, CDN cacheable |
URLs can be shared, temporary |
Download links, media sharing |
| Signed Cookies |
Multiple file access |
Prefix-based access, harder to share |
Requires cookie support |
Video streaming, bulk downloads |
| IAM Authentication |
Service-to-service |
Strong security, permanent access |
Requires Google accounts |
API access, internal services |
| Public Access |
Open content |
Simple, no restrictions |
No access control |
Public websites, marketing materials |
📚 Quick Reference Commands
# Create signing key
openssl rand -base64 32 > signing-key.txt
# Add key to backend service
gcloud compute backend-services add-signed-url-key BACKEND_NAME \
--key-name="my-key" \
--key-file="signing-key.txt" \
--global
# Generate signed URL
gcloud compute sign-url \
--key-name="my-key" \
--key-file="signing-key.txt" \
--expires-in=3600 \
"https://cdn.example.com/secure/file.pdf"
# List keys
gcloud compute backend-services describe BACKEND_NAME \
--global
# Update cache settings
gcloud compute backend-services update BACKEND_NAME \
--signed-url-cache-max-age=7200 \
--global
# Delete key
gcloud compute backend-services delete-signed-url-key BACKEND_NAME \
--key-name="old-key" \
--global
# Test signed URL
curl -I "https://cdn.example.com/secure/file.pdf?Expires=...&KeyName=...&Signature=..."
🎯 Key Takeaways
- Each backend supports up to 3 signing keys for rotation
- Always use HTTPS URLs for security
- Validate signatures at origin servers for defense in depth
- URL prefixes enable efficient bulk access control
- Regular key rotation is essential for security
- Signed and unsigned requests have separate cache entries