🔐 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

  1. Navigate to Cloud CDN page
  2. Click on your Origin
  3. Click Edit button
  4. Go to Cache performance section
  5. Under Restricted content, select "Restrict access using signed URLs and signed cookies"
  6. Click Add signing key
  7. Choose key creation method:
    • Automatically generate - Google creates the key
    • Let me enter - You provide your own key
  8. Copy and securely store the generated key
  9. Configure Cache entry maximum age
  10. 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:

🔍 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