Using signed URLs

This page provides an overview of signed URLs and instructions for using them with Cloud CDN.

Signed URLs give time-limited resource access to anyone in possession of the URL, regardless of whether they have a Google account.

Overview

A signed URL is a URL that provides limited permission and time to make a request. Signed URLs contain authentication information in their query strings, allowing users without credentials to perform specific actions on a resource. When you generate a signed URL, you specify a user or service account that must have sufficient permission to make the request associated with the URL.

After you generate a signed URL, anyone who possesses it can use the signed URL to perform specified actions, such as reading an object, within a specified period of time.

Signed URLs also support an optional URLPrefix parameter, allowing you to provide access to multiple URLs based on a common prefix.

If you prefer to use signed cookies instead of signed URLs, see Using signed cookies.

Before you begin

Before you use signed URLs:

  • Ensure that Cloud CDN is enabled. For instructions, see Google Cloud CDN documentation. You can configure signed cookies on a backend before enabling Cloud CDN, but there will be no effect until Cloud CDN is enabled.

  • If necessary, update to the latest version of Cloud SDK:

    gcloud components update
    
  • For an overview, see Signed URLs and signed cookies overview.

Configuring signed request keys

Creating keys for your signed URLs or signed cookies requires several steps, which are described in the following sections.

Security considerations

You must configure your origin web servers to validate the signatures on every signed request they serve and accept or reject unsigned requests.

  • Cloud CDN doesn't block requests without a Signature query parameter. It will reject requests with invalid (or otherwise malformed) request parameters.
  • When your application detects an invalid signature, make sure that your application responds with an HTTP 403 (Unauthorized) response code. HTTP 403 response codes aren't cacheable.
  • If your application sends a cacheable response code to an invalid request, valid future requests might be incorrectly rejected.

For Cloud Storage backends, you should ensure you have removed public access, which will allow Cloud Storage to reject requests missing a valid signature.

Creating signed request keys

You enable support for Cloud CDN signed URLs and signed cookies by creating one or more keys on a Cloud CDN-enabled backend service, backend bucket, or both.

For each backend service or backend bucket, you can create and delete keys as your security needs dictate. Each backend can have up to three keys configured at a time. We suggest periodically rotating your keys by deleting the oldest, adding a new key, and using the new key when signing URLs or cookies.

You can use the same key name in multiple backend services and backend buckets, because each set of keys is independent of the others. Key names can be up to 63 characters. Use the characters A-Z, a-z, 0-9, _ (underscore), and - (hyphen) to name your keys.

When you create keys, be sure to keep them secure because anyone who has one of your keys can create signed URLs or signed cookies that Cloud CDN accepts until the key is deleted from Cloud CDN. The keys are stored on the computer where you generate the signed URLs or signed cookies. Cloud CDN also stores the keys to verify request signatures.

To keep the keys secret, the key values aren't included in responses to any API requests. If you lose a key, you must create a new one.

Console

To create keys using the Cloud Console:

  1. Go to the Cloud CDN page in the Google Cloud Console.
    Go to the Cloud CDN page
  2. Click Add Origin.
  3. Select an HTTP(S) load balancer as the origin.
  4. Select backend services or backend buckets. For each one:
    1. Click Configure and then click Add signing key.
    2. Under Name, give the new signing key a name.
    3. Under Key creation method, elect Automatically generate or Let me enter.
    4. If you're entering your own key, type the key into the text field.
    5. Click Done.
    6. Under Cache entry maximum age, provide a value, and select a Unit of time from the drop-down list. You can choose among second, minute, hour, and day. The maximum amount of time is three (3) days.
  5. Click Save.
  6. Click Add.

gcloud

The gcloud command line tool reads keys from a local file that you specify. The key file must be created by generating strongly random 128 bits, encoding them with base64, then replacing the character + with - and replacing the character / with _. For more information, see RFC 4648. It is vital that the key is strongly random. On a UNIX-like system, you can generate a strongly random key and store it in the key file with the following command:

head -c 16 /dev/urandom | base64 | tr +/ -_ > [KEY_FILE_NAME]

To add the key to a backend service:

gcloud compute backend-services \
   add-signed-url-key [BACKEND_NAME] \
   --key-name [KEY_NAME] \
   --key-file [KEY_FILE_NAME]

To add the key to a backend bucket:

gcloud compute backend-buckets \
  add-signed-url-key [BACKEND_NAME] \
  --key-name [KEY_NAME] \
  --key-file [KEY_FILE_NAME]

Configuring Cloud Storage permissions

If you use Cloud Storage and you have restricted who can read the objects, you must give Cloud CDN permission to read the objects by adding the Cloud CDN service account to Cloud Storage's ACLs.

You don't need to create the service account. The service account is created automatically the first time you add a key to a backend bucket in a project.

Use the following command, where [PROJECT_NUM] is your project number and [BUCKET] is your storage bucket.

Run the command after you add at least one key to a backend bucket in your project. Otherwise, the command fails with an error because the Cloud CDN cache fill service account is not created until you add one or more keys for the project.

gsutil iam ch \
  serviceAccount:service-PROJECT_NUM@cloud-cdn-fill.iam.gserviceaccount.com:objectViewer \
  gs://[BUCKET]

The Cloud CDN service account, service-PROJECT_NUM@cloud-cdn-fill.iam.gserviceaccount.com, doesn't appear in the list of service accounts in your project. This is because the Cloud CDN service account is owned by Cloud CDN, not your project.

For more information on project numbers, see Locate the project ID and project number in the Cloud Platform Console Help documentation.

Optionally customizing the maximum cache time

Cloud CDN caches responses for signed requests regardless of the backend's Cache-Control header. The maximum time they can be cached without revalidation is set by the signed-url-cache-max-age flag, which defaults to 1 hour and can be modified as shown here.

To set the maximum cache time for a backend service or backend bucket:

gcloud compute backend-services update [BACKEND_NAME]
  --signed-url-cache-max-age [max-age]
gcloud compute backend-buckets update [BACKEND_NAME]
  --signed-url-cache-max-age [max-age]

Listing signed request key names

To list the keys on a backend service or backend bucket, use these commands.

gcloud compute backend-services describe [BACKEND_NAME]
gcloud compute backend-buckets describe [BACKEND_NAME]

Deleting signed request keys

When URLs signed by a particular key should no longer be honored, delete that key from the backend service or backend bucket.

gcloud compute backend-services \
   delete-signed-url-key [BACKEND_NAME] --key-name [KEY_NAME]
gcloud compute backend-buckets \
   delete-signed-url-key [BACKEND_NAME] --key-name [KEY_NAME]

Signing URLs

The last step is to sign URLs and distribute them. You can sign URLs with the gcloud compute sign-url command or with code you write yourself, following the examples below. If you need a large number of signed URLs, custom code provides better performance.

Creating signed URLs

Use these instructions to create signed URLs.

This step assumes that you have already created the keys.

Console

You cannot create signed URLs using the Console. You can use the gcloud command-line tool or write custom code, following the examples below.

gcloud

The gcloud command line tool includes a command for signing URLs. The command implements the algorithm described in the section on writing your own code.

gcloud compute sign-url \
  --key-name [KEY_NAME] \
  --key-file [KEY_FILE_NAME] \
  --expires-in [TIME_UNTIL_EXPIRATION] \
  [--validate] \
  "[URL]"

This command reads and decodes the base64url encoded key value from [KEY_FILE_NAME], then outputs a signed URL that you can use for GET or HEAD requests for the given URL.

For example:

gcloud compute sign-url \
  --key-name my-test-key \
  --expires-in 30m \
  --key-file sign-url-key-file \
  "https://example.com/media/video.mp4"

The URL must be a valid URL that has a path component. For example, http://example.com is invalid, but https://example.com/ and https://example.com/whatever are both valid URLs.

If the optional --validate flag is given, this command sends a HEAD request with the resulting URL, and prints out the HTTP response code. If the signed URL is correct, the response code is the same as the result code sent by your backend. If the response code isn't the same, recheck [KEY_NAME] and the contents of the specified file, and make sure that the value of [TIME_UNTIL_EXPIRATION] is at least several seconds. If --validate is not given, neither the inputs nor the generated URL are verified.

If the --validate flag is not given, the generated signed URL is not verified.

Programmatically creating signed URLs

The code samples below demonstrate how to programmatically create signed URLs.

Go

import (
	"crypto/hmac"
	"crypto/sha1"
	"encoding/base64"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"strings"
	"time"
)

// SignURL creates a signed URL for an endpoint on Cloud CDN.
//
// - url must start with "https://" and should not have the "Expires", "KeyName", or "Signature"
// query parameters.
// - key should be in raw form (not base64url-encoded) which is 16-bytes long.
// - keyName must match a key added to the backend service or bucket.
func signURL(url, keyName string, key []byte, expiration time.Time) string {
	sep := "?"
	if strings.Contains(url, "?") {
		sep = "&"
	}
	url += sep
	url += fmt.Sprintf("Expires=%d", expiration.Unix())
	url += fmt.Sprintf("&KeyName=%s", keyName)

	mac := hmac.New(sha1.New, key)
	mac.Write([]byte(url))
	sig := base64.URLEncoding.EncodeToString(mac.Sum(nil))
	url += fmt.Sprintf("&Signature=%s", sig)
	return url
}

// SignURLWithPrefix creates a signed URL prefix for an endpoint on Cloud CDN.
// Prefixes allow access to any URL with the same prefix, and can be useful for
// granting access broader content without signing multiple URLs.
//
// - urlPrefix must start with "https://" and should not include query parameters.
// - key should be in raw form (not base64url-encoded) which is 16-bytes long.
// - keyName must match a key added to the backend service or bucket.
func signURLWithPrefix(urlPrefix, keyName string, key []byte, expiration time.Time) (string, error) {
	if strings.Contains(urlPrefix, "?") {
		return "", fmt.Errorf("urlPrefix must not include query params: %s", urlPrefix)
	}

	encodedURLPrefix := base64.URLEncoding.EncodeToString([]byte(urlPrefix))
	input := fmt.Sprintf("URLPrefix=%s&Expires=%d&KeyName=%s",
		encodedURLPrefix, expiration.Unix(), keyName)

	mac := hmac.New(sha1.New, key)
	mac.Write([]byte(input))
	sig := base64.URLEncoding.EncodeToString(mac.Sum(nil))

	signedValue := fmt.Sprintf("%s&Signature=%s", input, sig)

	return signedValue, nil
}

// readKeyFile reads the base64url-encoded key file and decodes it.
func readKeyFile(path string) ([]byte, error) {
	b, err := ioutil.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("failed to read key file: %+v", err)
	}
	d := make([]byte, base64.URLEncoding.DecodedLen(len(b)))
	n, err := base64.URLEncoding.Decode(d, b)
	if err != nil {
		return nil, fmt.Errorf("failed to base64url decode: %+v", err)
	}
	return d[:n], nil
}

func generateSignedURLs(w io.Writer) error {
	// The path to a file containing the base64-encoded signing key
	keyPath := os.Getenv("KEY_PATH")

	// Note: consider using the GCP Secret Manager for managing access to your
	// signing key(s).
	key, err := readKeyFile(keyPath)
	if err != nil {
		return err
	}

	// Generate a signed URL for a specific URL
	url := signURL("https://example.com/media/1234.m3e8", "my-key", key, time.Now().Add(time.Hour*24))
	fmt.Fprintln(w, url)

	// Generate a signed URL for
	urlPrefix, err := signURLWithPrefix("https://www.google.com/", "my-key", key, time.Unix(1549751401, 0))
	if err != nil {
		return err
	}

	fmt.Fprintln(w, urlPrefix)

	return nil
}

Ruby

def signed_url url:, key_name:, key:, expiration:
  # url        = "URL of the endpoint served by Cloud CDN"
  # key_name   = "Name of the signing key added to the Google Cloud Storage bucket or service"
  # key        = "Signing key as urlsafe base64 encoded string"
  # expiration = Ruby Time object with expiration time

  require "base64"
  require "openssl"
  require "time"

  # Decode the URL safe base64 encode key
  decoded_key = Base64.urlsafe_decode64 key

  # Get UTC time in seconds
  expiration_utc = expiration.utc.to_i

  # Determine which separator makes sense given a URL
  separator = "?"
  separator = "&" if url.include? "?"

  # Concatenate url with expected query parameters Expires and KeyName
  url = "#{url}#{separator}Expires=#{expiration_utc}&KeyName=#{key_name}"

  # Sign the url using the key and url safe base64 encode the signature
  signature         = OpenSSL::HMAC.digest "SHA1", decoded_key, url
  encoded_signature = Base64.urlsafe_encode64 signature

  # Concatenate the URL and encoded signature
  signed_url = "#{url}&Signature=#{encoded_signature}"
end

.NET

        /// <summary>
        /// Creates signed URL for Google Cloud SDN
        /// More details about order of operations is here: 
        /// <see cref="https://cloud.google.com/cdn/docs/using-signed-urls#programmatically_creating_signed_urls"/>
        /// </summary>
        /// <param name="url">The Url to sign. This URL can't include Expires and KeyName query parameters in it</param>
        /// <param name="keyName">The name of the key used to sign the URL</param>
        /// <param name="encodedKey">The key used to sign the Url</param>
        /// <param name="expirationTime">Expiration time of the signature</param>
        /// <returns>Signed Url that is valid until {expirationTime}</returns>
        public static string CreateSignedUrl(string url, string keyName, string encodedKey, DateTime expirationTime)
        {
            var builder = new UriBuilder(url);

            long unixTimestampExpiration = ToUnixTime(expirationTime);

            char queryParam = string.IsNullOrEmpty(builder.Query) ? '?' : '&';
            builder.Query += $"{queryParam}Expires={unixTimestampExpiration}&KeyName={keyName}".ToString();

            // Key is passed as base64url encoded
            byte[] decodedKey = Base64UrlDecode(encodedKey);

            // Computes HMAC SHA-1 hash of the URL using the key
            byte[] hash = ComputeHash(decodedKey, builder.Uri.AbsoluteUri);
            string encodedHash = Base64UrlEncode(hash);

            builder.Query += $"&Signature={encodedHash}";
            return builder.Uri.AbsoluteUri;
        }

        private static long ToUnixTime(DateTime date)
        {
            var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
            return Convert.ToInt64((date - epoch).TotalSeconds);
        }

        private static byte[] Base64UrlDecode(string arg)
        {
            string s = arg;
            s = s.Replace('-', '+'); // 62nd char of encoding
            s = s.Replace('_', '/'); // 63rd char of encoding

            return Convert.FromBase64String(s); // Standard base64 decoder
        }

        private static string Base64UrlEncode(byte[] inputBytes)
        {
            var output = Convert.ToBase64String(inputBytes);

            output = output.Replace('+', '-')      // 62nd char of encoding
                           .Replace('/', '_');     // 63rd char of encoding

            return output;
        }

        private static byte[] ComputeHash(byte[] secretKey, string signatureString)
        {
            var enc = Encoding.ASCII;
            using (HMACSHA1 hmac = new HMACSHA1(secretKey))
            {
                hmac.Initialize();

                byte[] buffer = enc.GetBytes(signatureString);

                return hmac.ComputeHash(buffer);
            }
        }

Java

/**
 * Creates a signed URL for a Cloud CDN endpoint with the given key
 * URL must start with http:// or https://, and must contain a forward
 * slash (/) after the hostname.
 * @param url the Cloud CDN endpoint to sign
 * @param key url signing key uploaded to the backend service/bucket, as a 16-byte array
 * @param keyName the name of the signing key added to the back end bucket or service
 * @param expirationTime the date that the signed URL expires
 * @return a properly formatted signed URL
 * @throws InvalidKeyException when there is an error generating the signature for the input key
 * @throws NoSuchAlgorithmException when HmacSHA1 algorithm is not available in the environment
 */
public static String signUrl(String url,
                             byte[] key,
                             String keyName,
                             Date expirationTime)
        throws InvalidKeyException, NoSuchAlgorithmException {

  final long unixTime = expirationTime.getTime() / 1000;

  String urlToSign = url
                      + (url.contains("?") ? "&" : "?")
                      + "Expires=" + unixTime
                      + "&KeyName=" + keyName;

  String encoded = SignedUrls.getSignature(key, urlToSign);
  return urlToSign + "&Signature=" + encoded;
}

public static String getSignature(byte[] privateKey, String input)
    throws InvalidKeyException, NoSuchAlgorithmException {

  final String algorithm = "HmacSHA1";
  final int offset = 0;
  Key key = new SecretKeySpec(privateKey, offset, privateKey.length, algorithm);
  Mac mac = Mac.getInstance(algorithm);
  mac.init(key);
  return  Base64.getUrlEncoder().encodeToString(mac.doFinal(input.getBytes()));
}

Python

def sign_url(url, key_name, base64_key, expiration_time):
    """Gets the Signed URL string for the specified URL and configuration.

    Args:
        url: URL to sign as a string.
        key_name: name of the signing key as a string.
        base64_key: signing key as a base64 encoded string.
        expiration_time: expiration time as a UTC datetime object.

    Returns:
        Returns the Signed URL appended with the query parameters based on the
        specified configuration.
    """
    stripped_url = url.strip()
    parsed_url = urllib.parse.urlsplit(stripped_url)
    query_params = urllib.parse.parse_qs(
        parsed_url.query, keep_blank_values=True)
    epoch = datetime.datetime.utcfromtimestamp(0)
    expiration_timestamp = int((expiration_time - epoch).total_seconds())
    decoded_key = base64.urlsafe_b64decode(base64_key)

    url_pattern = u'{url}{separator}Expires={expires}&KeyName={key_name}'

    url_to_sign = url_pattern.format(
            url=stripped_url,
            separator='&' if query_params else '?',
            expires=expiration_timestamp,
            key_name=key_name)

    digest = hmac.new(
        decoded_key, url_to_sign.encode('utf-8'), hashlib.sha1).digest()
    signature = base64.urlsafe_b64encode(digest).decode('utf-8')

    signed_url = u'{url}&Signature={signature}'.format(
            url=url_to_sign, signature=signature)

    print(signed_url)


def sign_url_prefix(url, url_prefix, key_name, base64_key, expiration_time):
    """Gets the Signed URL string for the specified URL prefix and configuration.

    Args:
        url: URL of request.
        url_prefix: URL prefix to sign as a string.
        key_name: name of the signing key as a string.
        base64_key: signing key as a base64 encoded string.
        expiration_time: expiration time as a UTC datetime object.

    Returns:
        Returns the Signed URL appended with the query parameters based on the
        specified URL prefix and configuration.
    """
    stripped_url = url.strip()
    parsed_url = urllib.parse.urlsplit(stripped_url)
    query_params = urllib.parse.parse_qs(
        parsed_url.query, keep_blank_values=True)
    encoded_url_prefix = base64.urlsafe_b64encode(
            url_prefix.strip().encode('utf-8')).decode('utf-8')
    epoch = datetime.datetime.utcfromtimestamp(0)
    expiration_timestamp = int((expiration_time - epoch).total_seconds())
    decoded_key = base64.urlsafe_b64decode(base64_key)

    policy_pattern = u'URLPrefix={encoded_url_prefix}&Expires={expires}&KeyName={key_name}'
    policy = policy_pattern.format(
            encoded_url_prefix=encoded_url_prefix,
            expires=expiration_timestamp,
            key_name=key_name)

    digest = hmac.new(
            decoded_key, policy.encode('utf-8'), hashlib.sha1).digest()
    signature = base64.urlsafe_b64encode(digest).decode('utf-8')

    signed_url = u'{url}{separator}{policy}&Signature={signature}'.format(
            url=stripped_url,
            separator='&' if query_params else '?',
            policy=policy,
            signature=signature)

    print(signed_url)

PHP

/**
 * Decodes base64url (RFC4648 Section 5) string
 *
 * @param string $input base64url encoded string
 *
 * @return string
 */
function base64url_decode($input)
{
    $input .= str_repeat('=', (4 - strlen($input) % 4) % 4);
    return base64_decode(strtr($input, '-_', '+/'));
}

/**
* Encodes a string with base64url (RFC4648 Section 5)
* Keeps the '=' padding by default.
*
* @param string $input   String to be encoded
* @param bool   $padding Keep the '=' padding
*
* @return string
*/
function base64url_encode($input, $padding = true)
{
    $output = strtr(base64_encode($input), '+/', '-_');
    return ($padding) ? $output : str_replace('=', '',  $output);
}

/**
 * Creates signed URL for Google Cloud CDN
 * Details about order of operations: https://cloud.google.com/cdn/docs/using-signed-urls#creating_signed_urls
 *
 * Example function invocation (In production store the key safely with other secrets):
 *
 *     <?php
 *     $base64UrlKey = 'wpLL7f4VB9RNe_WI0BBGmA=='; // head -c 16 /dev/urandom | base64 | tr +/ -_
 *     $signedUrl = sign_url('https://example.com/foo', 'my-key', $base64UrlKey, time() + 1800);
 *     echo $signedUrl;
 *     ?>
 *
 * @param string $url             URL of the endpoint served by Cloud CDN
 * @param string $keyName         Name of the signing key added to the Google Cloud Storage bucket or service
 * @param string $base64UrlKey    Signing key as base64url (RFC4648 Section 5) encoded string
 * @param int    $expirationTime  Expiration time as a UNIX timestamp (GMT, e.g. time())
 *
 * @return string
 */
function sign_url($url, $keyName, $base64UrlKey, $expirationTime)
{
    // Decode the key
    $decodedKey = base64url_decode($base64UrlKey, true);

    // Determine which separator makes sense given a URL
    $separator = (strpos($url, '?') === false) ? '?' : '&';

    // Concatenate url with expected query parameters Expires and KeyName
    $url = "{$url}{$separator}Expires={$expirationTime}&KeyName={$keyName}";

    // Sign the url using the key and encode the signature using base64url
    $signature = hash_hmac('sha1', $url, $decodedKey, true);
    $encodedSignature = base64url_encode($signature);

    // Concatenate the URL and encoded signature
    return "{$url}&Signature={$encodedSignature}";
}

Algorithm for signing URLs

When you write your own code to generate signed URLs, your goal is to create URLs with the following format:

https://example.com/foo?Expires=[EXPIRATION]&KeyName=[KEY_NAME]&Signature=[SIGNATURE]

All URL parameters are case sensitive and must be in the order above.

  1. Ensure that the URL for signing does not have a Signature query parameter.

  2. Determine when the URL expires and append an Expires query parameter with the desired expiration time in UTC time (the number of seconds since 1970-01-01 00:00:00 UTC). To maximize security, set the value to the shortest time period possible for your use case. The longer a signed URL is valid, the bigger the risk that the user you give it to shares it with others, accidentally or otherwise.

  3. Set the key name. The URL must be signed with a key of the backend service or backend bucket that will serve the URL. It is best to use the most recently added key for key rotation. Add the key to the URL by appending &KeyName=[KEY_NAME], where [KEY_NAME] is the name of the chosen key created in Creating keys.

  4. Sign the URL. Create the signed URL by following these steps. Make sure that the query parameters are in the order shown immediately before Step 1, and make sure that nothing in the signed URL changes case.

    a. Hash the entire URL (including http:// or https:// at the beginning and the `&KeyName... at the end) with HMAC-SHA1, using the secret key that corresponds to the key name chosen above. Use the raw 16-byte secret key, not the base64url encoded key. Decode it if needed.

    b. Base64url encode the result.

    c. Append &Signature= to the URL, followed by the encoded signature.

Using URL prefixes for signed URLs

Instead of signing the full request URL with the Expires and KeyName query parameters, you can instead sign the URLPrefix, Expires, and KeyName query parameters only. This allows a given combination of URLPrefix, Expires, KeyName, and Signature query parameters to be reused verbatim across multiple URLs that match the URLPrefix, avoiding the need to create a new signature for each distinct URL.

In the following example, the highlighted text shows the parameters that you sign. The Signature is appended as the final query parameter, as usual.

https://media.example.com/videos/id/master.m3u8?userID=abc123&starting_profile=1&URLPrefix=aHR0cHM6Ly9tZWRpYS5leGFtcGxlLmNvbS92aWRlb3Mv&Expires=1566268009&KeyName=mySigningKey&Signature=8NBSdQGzvDftrOIa3WHpp646Iis=

URLPrefix denotes a URL-safe base64 encoded URL prefix encompassing all paths that the cookie should be valid for.

A URLPrefix encodes a scheme (either http:// or https://), FQDN, and an optional path. Ending the path with a / is optional but recommended. The prefix shouldn't include query parameters or fragments such as ? or #.

For example, https://media.example.com/videos matches requests to both:

  • https://media.example.com/videos?video_id=138183&user_id=138138
  • https://media.example.com/videos/137138595?quality=low

The prefix's path is used as a text substring, not strictly a directory path. For example, the prefix https://example.com/data grants access to both:

  • /data/file1
  • /database

We recommend ending all prefixes with / to avoid this mistake, unless you intentionally choose to end the prefix with a partial filename, such as https://media.example.com/videos/123 to grant access to:

  • /videos/123_chunk1
  • /videos/123_chunk2
  • /videos/123_chunkN

If the requested URL doesn't match the URLPrefix, Cloud CDN rejects the request and returns an HTTP 403 error to the client.

Removing public access to the Cloud Storage bucket

For signed URLs to properly protect content, it is important that the origin server not grant public access to that content. When using a Cloud Storage bucket, a common approach is to make objects public temporarily for testing purposes. After enabling signed URLs, it's important to remove the allUsers (and allAuthenticatedUsers, if applicable) READ permissions (in other words, the Storage Object Viewer IAM role) on the bucket.

After you disable public access on the bucket, individual users can still access Cloud Storage without signed URLs if they have access permission, such as OWNER permission.

To remove public allUsers READ access on a Cloud Storage bucket, reverse the action described in Making groups of objects publicly readable.

Distributing and using signed URLs

The URL returned from the gcloud command line tool or produced by your custom code can be distributed according to your needs. We recommend signing only HTTPS URLs, because HTTPS provides a secure transport that prevents the Signature component of the signed URL from being intercepted. Similarly, make sure that you distribute the signed URLs over secure transport protocols such as TLS/HTTPS.