Google Cloud CDN
Google Cloud CDN

How to Set Up Google Cloud CDN with Signed Keys

Google Cloud CDN (Content Delivery Network) is a globally distributed caching layer built into Google Cloud’s infrastructure, designed to deliver content to users with minimal latency and high reliability. By leveraging Google’s vast network of edge locations, it stores (caches) content closer to your users, reducing the load on your origin servers and improving response times.

Google Cloud CDN combines speed, scalability, and security—making it a go-to choice for enterprises, startups, and developers looking to deliver content faster to a global audience while reducing infrastructure costs.

High-level architecture

Store objects in a private GCS bucket → expose via a Global HTTPS Load Balancer + Cloud CDN (backend bucket) → protect access with Cloud CDN signed URLs → store the CDN signing key in Secret Manager/VAULT/K8s Secrets → GKE workload (via Workload Identity) retrieves that key and generates signed URLs for customers

Google Cloud CDN
Google Cloud CDN

Prerequisite

  1. First set the Project Name
% gcloud config set project <PROJECT_NAME>
Updated property [core/project].

2. Enable the below required services in GCP

 % gcloud services enable compute.googleapis.com \
                       storage.googleapis.com \
                       container.googleapis.com \
                       iam.googleapis.com \
                       secretmanager.googleapis.com

Operation "operations/acf.p2-795088716185-60ca6342-1cfb-425d-9cb6-b807e530aeab" finished successfully.

1) Create a private Cloud Storage bucket and upload files

1.Create a bucket to store your file

% gsutil mb -p my-kubernetes-project-447204 -l us-central1 gs://cdn-gcp-demo 
Creating gs://cdn-gcp-demo/...
 % touch test.txt
% cat >> test.txt 
This is a test cdn-gcp-demo file
^C
% gsutil cp test.txt gs://cdn-gcp-demo/test.txt   
Copying file://test.txt [Content-Type=text/plain]...
/ [1 files][   33.0 B/   33.0 B]                                                
Operation completed over 1 objects/33.0 B.        

2. Set bucket level access permission to sure bucket access is UNIFORM (recommended) and not public

% gsutil uniformbucketlevelaccess set on gs://cdn-gcp-demo 
Enabling Uniform bucket-level access for gs://cdn-gcp-demo...

2. Create a backend-bucket (Cloud CDN origin) and an external HTTP(S) load balancer

% gcloud compute backend-buckets create cdn-gcp-backend-bucket \
  --gcs-bucket-name=cdn-gcp-demo \
  --enable-cdn \
  --description="backend bucket for CDN"
Created [https://www.googleapis.com/compute/v1/projects/my-kubernetes-project-447204/global/backendBuckets/cdn-gcp-backend-bucket].
NAME                    GCS_BUCKET_NAME  ENABLE_CDN
cdn-gcp-backend-bucket  cdn-gcp-demo     True

3) Create a Cloud CDN signing key and add it to the backend-bucket

Cloud CDN supports signing keys (up to 3 per backend). The key must be a 16-byte random value encoded in base64url. You can get the Automatically generated one or generate your own 16-byte random value encoded in base64 url.

4) Restrict direct bucket access and let Cloud CDN fetch objects

Remove public access (we set uniform access earlier).

Add the Cloud CDN cache-fill service account to the bucket with roles/storage.objectViewer

PROJECT_NUM=$(gcloud projects describe PROJECT_ID --format='value(projectNumber)')

gcloud storage buckets add-iam-policy-binding gs://my-cdn-bucket \
  --member="serviceAccount:service-${PROJECT_NUM}@cloud-cdn-fill.iam.gserviceaccount.com" \
  --role="roles/storage.objectViewer"

Example:
 % gcloud storage buckets add-iam-policy-binding gs://cdn-gcp-demo \                 
  --member="serviceAccount:service-79508812345@cloud-cdn-fill.iam.gserviceaccount.com" \
  --role="roles/storage.objectViewer"
bindings:
- members:
  - projectEditor:my-kubernetes-project-447204
  - projectOwner:my-kubernetes-project-447204
  role: roles/storage.legacyBucketOwner
- members:
  - projectViewer:my-kubernetes-project-447204
  role: roles/storage.legacyBucketReader
- members:
  - serviceAccount:service-79508812345@cloud-cdn-fill.iam.gserviceaccount.com
  role: roles/storage.objectViewer
etag: CAM=
kind: storage#policy
resourceId: projects/_/buckets/cdn-gcp-demo
version: 1

This lets Cloud CDN fetch objects from your private bucket while preventing other direct (public) reads.

5) Create GKE cluster (enable Workload Identity)

If already have the GKE cluster then skip this step.

Use Workload Identity so pods authenticate as a Google SA (no JSON keys).

% gcloud container clusters create my-gke-cluster \
  --region=us-central1 \
  --num-nodes=1 \
  --workload-pool=my-kubernetes-project-447204.svc.id.goog
WARNING: Accessing a Kubernetes Engine cluster requires the kubernetes commandline
client [kubectl]. To install, run
  $ gcloud components install kubectl

Creating cluster my-gke-cluster in us-central1... Cluster is being health-checked (Kubernetes Control Plane is healthy)...done.                  
Created [https://container.googleapis.com/v1/projects/my-kubernetes-project-447204/zones/us-central1/clusters/my-gke-cluster].
To inspect the contents of your cluster, go to: https://console.cloud.google.com/kubernetes/workload_/gcloud/us-central1/my-gke-cluster?project=my-kubernetes-project-447204

kubeconfig entry generated for my-gke-cluster.
NAME            LOCATION     MASTER_VERSION      MASTER_IP     MACHINE_TYPE  NODE_VERSION        NUM_NODES  STATUS   STACK_TYPE
my-gke-cluster  us-central1  1.33.2-gke.1240000  34.170.224.3  e2-medium     1.33.2-gke.1240000  3          RUNNING  IPV4

6) Create a Google Service Account (GSA) and a Kubernetes ServiceAccount (KSA) and bind them

Create a GSA (GSA_SIGNER) that has permission to read the secret (and optionally read objects if your app needs to list objects). Then bind a KSA to that GSA.

% gcloud iam service-accounts create gsa-signer \
  --display-name="GSA for CDN signing"
Created service account [gsa-signer].

GSA_EMAIL="gsa-signer@${PROJECT_ID}.iam.gserviceaccount.com"
% gcloud projects add-iam-policy-binding ${PROJECT_ID} \
  --member="serviceAccount:${GSA_EMAIL}" \
  --role="roles/storage.objectViewer"
Updated IAM policy for project [my-kubernetes-project-447204].
bindings:
- members:
  - serviceAccount:service-795088716185@compute-system.iam.gserviceaccount.com
  role: roles/compute.serviceAgent
- members:
  - serviceAccount:service-795088716185@gcp-sa-gkenode.iam.gserviceaccount.com
  role: roles/container.defaultNodeServiceAgent
- members:
  - serviceAccount:service-795088716185@container-engine-robot.iam.gserviceaccount.com
  role: roles/container.serviceAgent
- members:
  - serviceAccount:service-795088716185@containerregistry.iam.gserviceaccount.com
  role: roles/containerregistry.ServiceAgent
- members:
  - serviceAccount:795088716185-compute@developer.gserviceaccount.com
  - serviceAccount:795088716185@cloudservices.gserviceaccount.com
  role: roles/editor
- members:
  - serviceAccount:service-795088716185@gcp-sa-networkconnectivity.iam.gserviceaccount.com
  role: roles/networkconnectivity.serviceAgent
- members:
  - user:huzefa.hamdard53@gmail.com
  role: roles/owner
- members:
  - serviceAccount:service-795088716185@gcp-sa-pubsub.iam.gserviceaccount.com
  role: roles/pubsub.serviceAgent
- members:
  - serviceAccount:gsa-signer@my-kubernetes-project-447204.iam.gserviceaccount.com
  role: roles/storage.objectViewer
etag: BwY8A--vkqI=
version: 1
# Create a Kubernetes SA in a namespace for your app
% kubectl create namespace cdn-ns                 
namespace/cdn-ns created

% kubectl create serviceaccount ksa-signer -n cdn-ns
serviceaccount/ksa-signer created
# Allow the KSA to impersonate/use the GSA (Workload Identity binding)

% gcloud iam service-accounts add-iam-policy-binding ${GSA_EMAIL} \
  --role roles/iam.workloadIdentityUser \
  --member "serviceAccount:${PROJECT_ID}.svc.id.goog[my-app/ksa-signer]"
Updated IAM policy for serviceAccount [gsa-signer@my-kubernetes-project-447204.iam.gserviceaccount.com].
bindings:
- members:
  - serviceAccount:my-kubernetes-project-447204.svc.id.goog[my-app/ksa-signer]
  role: roles/iam.workloadIdentityUser
etag: BwY8A_-n9ko=
version: 1
# Annotate the Kubernetes SA so GKE knows which GSA it should use
% kubectl annotate serviceaccount ksa-signer -n cdn-ns \
  iam.gke.io/gcp-service-account=${GSA_EMAIL}
serviceaccount/ksa-signer annotated

7)Python script to generate the CDN signed URL

Script will perform following operation

  • Use Workload Identity (application default credentials) — your pod will run as the KSA and be authenticated as the GSA.
  • Access Secret Manager to fetch the base64url key.
  • Use the Cloud CDN signing algorithm (HMAC-SHA1 over the URL + Expires + KeyName) and append Expires, KeyName, Signature query parameters.
  • Return that signed URL to the customer.

NOTE: To test I have put the generated signed-key (Step3) directly but for production usage you can store in the google secret manager and fetch from it. Or create a deployment and refer the secret from kubernetes secrets.

import base64
import hmac
import hashlib
from datetime import datetime, timezone, timedelta

# === CONFIG ===
# Replace with your actual base64url key from `cdn-key-file`
BASE64URL_KEY = "ur3r32r3297942342-abc=="  # example only
KEY_NAME = "gcp-cdn-sign-key"  # must match the backend-bucket key name
EXPIRATION_SECONDS = 60 * 30  # URL valid for 30 minutes
CDN_BASE_URL = "http://34.102.152.162"  # your CDN/LB domain

def sign_cdn_url(filename: str) -> str:
    """
    filename: name of the file in the bucket (root level, e.g., 'test.txt')
    Returns: Signed Cloud CDN URL
    """
    raw_key = base64.urlsafe_b64decode(BASE64URL_KEY)
    expires = int((datetime.now(timezone.utc) + timedelta(seconds=EXPIRATION_SECONDS)).timestamp())
    unsigned_url = f"{CDN_BASE_URL}/{filename}?Expires={expires}&KeyName={KEY_NAME}"
    signature = hmac.new(raw_key, unsigned_url.encode("utf-8"), hashlib.sha1).digest()
    sig_b64url = base64.urlsafe_b64encode(signature).decode("utf-8")
    return f"{unsigned_url}&Signature={sig_b64url}"

# === TEST ===
if __name__ == "__main__":
    signed_url = sign_cdn_url("test.txt")
    print("Signed URL:", signed_url)
% python3 cloud-cdn-signed-url.py 
Signed URL: https://34.102.152.162/test.txt?Expires=1754840058&KeyName=gcp-cdn-sign-key&Signature=nILQUBWeXwfSx3ZF1SvCB2Hry30=

8. Test the CDN url

Curl the above generated signed url and see the output.

Below are the important parameters to check in the output

  • age: >0 means the object is being served from cache (in seconds since cached).
  • x-cache: HIT means the response came from Cloud CDN’s edge cache; MISS means it was fetched from the origin (your GCS bucket) and cached for next time.
  • via: often shows 1.1 google indicating Google’s CDN layer.
% curl -I "http://34.102.152.162/test.txt?Expires=1754842540&KeyName=gcp-cdn-sign-key&Signature=-rHHqW8i8pN_rupwigGH4JjYRvc="
HTTP/1.1 200 OK
x-guploader-uploadid: ABgVH8_E7cPIf7xgnBQmXYSRA0kMYI1zUcbYqyrheznpERa06GCQ50NKHKClnoTeZKmLGJN5
x-goog-generation: 1754838869485428
x-goog-metageneration: 1
x-goog-stored-content-encoding: identity
x-goog-stored-content-length: 33
x-goog-hash: crc32c=NnbUqQ==
x-goog-hash: md5=gSa9nBf298OvhydB+IzBLA==
x-goog-storage-class: STANDARD
accept-ranges: bytes
Content-Length: 33
server: UploadServer
via: 1.1 google
Date: Sun, 10 Aug 2025 15:14:36 GMT
Last-Modified: Sun, 10 Aug 2025 15:14:29 GMT
ETag: "8126bd9c17f6f7c3af872741f88cc12c"
Content-Type: text/plain
Age: 1890
Cache-Control: private,max-age=0
% curl "http://34.102.152.162/test.txt?Expires=1754842540&KeyName=gcp-cdn-sign-key&Signature=-rHHqW8i8pN_rupwigGH4JjYRvc=" 
This is a test cdn-gcp-demo file

9. Check in GCP console

You can also check in the GCP console

Comments

No comments yet. Why don’t you start the discussion?

    Leave a Reply