Google Cloud Storage SignedURL with Cloud Run, Cloud Functions and GCE VMs


Code snippet to create a GCS Signed URL in Cloud Run, Cloud Functions and GCE VMs

  • Why am i writing this repo?
    because it isn’t clear that in those environment that with some languages you can “just use” the default credentials (node, java, go) if and only if you enabled service account impersonation.

  • Whats wrong with the Documented samples for signedURL?
    They use service account keys.. don’t do that!

  • Why Impersonation?
    Well, Cloud Run, Cloud Functions and GCE environments do not have anyway to sign anything (and no, do NOT embed a service account key file anywhere!). Since those environments can’t sign by themselves, they need to use an API to sign on behalf of itself. That API is is listed above

  • Why are they different in different languages?
    java, node and go (now) automatically detects that its running in a Cloud Run|GCE and “knows” it can’t sign by itself and instead attempts to use impersonation automatically. In python, all the examples i saw around incorrectly uses the wrong Credential type to sign.

Impersonated signing uses (iamcredentials.serviceAccounts.signBlob.

The following examples uses the ambient service account to sign (eg, the service account cloud run uses is what signs the URL). It would be a couple more steps to make cloud run sign on behalf of another service account (and significantly more steps for java and node that made some assumptions on your behalf already)

Finally, if you must use a key, try to embed it into hardware, if possible.

Once you enable a service account to impersonate itself and allow signBlob, the access_token is essentially a long-term credentials:
an user that gets hold of a 1hour long access_token can turn around and reuse it every ~50mins to get a new token valid for another hour. For more information, see Concentric IAMCredentials Permissions: The secret life of signBlob.

you’ll find the source here


export PROJECT_ID=`gcloud config get-value core/project`
export PROJECT_NUMBER=`gcloud projects describe $PROJECT_ID --format="value(projectNumber)"`
export SA_EMAIL=$

gsutil mb gs://$BUCKET_NAME

echo foo > file.txt
gsutil cp file.txt gs://$BUCKET_NAME

# allow cloud run's default service account access
gsutil acl ch -u $SA_EMAIL:R gs://$BUCKET_NAME/file.txt

gcloud iam service-accounts  add-iam-policy-binding   --role=roles/iam.serviceAccountTokenCreator  \
 --member=serviceAccount:$SA_EMAIL $SA_EMAIL

gcloud config set run/region us-central1

Then build and push the language your’e interested in below

docker build -t$PROJECT_ID/crsigner .
docker push$PROJECT_ID/crsigner

gcloud run deploy signertest --image$PROJECT_ID/crsigner --platform=managed --set-env-vars="BUCKET_NAME=$BUCKET_NAME,SA_EMAIL=$SA_EMAIL"

export CR_URL=`gcloud run services describe  signertest --format="value(status.url)"`

curl -s $CR_URL

curl -s `curl -s $CR_URL`

If you are running in interactive mode in a GCE VM and have gsutil handy, first enable impersonation on the service account and then on the vm, use the -i switch as shown here:

$ gsutil -i $ signurl \
   -d 10m -u  gs://your-bucket/foo.txt

google-auth python offers two signer interfaces you can use:

You might be wondering why an IDToken credentials has a signer? Well, thats a side effect of an incorrect initial implementation of the compute engine ID Token (see issue #344). The interface users should use is impersonated_credential

import os

from flask import Flask

import google.auth
from google.auth import impersonated_credentials
from google.auth import compute_engine

from datetime import datetime, timedelta
from import storage

app = Flask(__name__)

def hello_world():
    bucket_name = os.environ.get("BUCKET_NAME")
    sa_email =  os.environ.get("SA_EMAIL")

    credentials, project = google.auth.default()   

    storage_client = storage.Client()
    data_bucket = storage_client.bucket(bucket_name)
    blob = data_bucket.get_blob("file.txt")
    expires_at_ms = + timedelta(minutes=30)
    signing_credentials = impersonated_credentials.Credentials(
        target_scopes = '',

    # using the default credentials in the blob.generate_signed_url() will not work
    # signed_url = blob.generate_signed_url(expires_at_ms, credentials=credentials)
    signed_url = blob.generate_signed_url(expires_at_ms, credentials=signing_credentials)

    return signed_url, 200, {'Content-Type': 'text/plain'}

if __name__ == "__main__":, host="", port=int(os.environ.get("PORT", 8080)))

In golang, we’re using the IAMCredentials api to sign the bytes.

After PR 4604 was merged, this is done automatically if you are using

	storageClient, _ := storage.NewClient(ctx)
	s, _ := storageClient.Bucket(bucketName).SignedURL(objectName, &storage.SignedURLOptions{
		Method:  http.MethodGet,
		Expires: expires,

but if you use

See issues/1495#issuecomment-915514120

The full source

  • main.go
	ctx := context.Background()
	rootTokenSource, _ := google.DefaultTokenSource(ctx, "")
	delegates := []string{}
	s, _ := storage.SignedURL(bucketName, objectName, &storage.SignedURLOptions{
		Scheme:         storage.SigningSchemeV4,
		GoogleAccessID: serviceAccountName,
		SignBytes: func(b []byte) ([]byte, error) {
			client := oauth2.NewClient(context.TODO(), rootTokenSource)
			service, err := iamcredentials.New(client)
			if err != nil {
				return nil, fmt.Errorf("storage: Error creating IAMCredentials: %v", err)
			signRequest := &iamcredentials.SignBlobRequest{
				Payload:   base64.StdEncoding.EncodeToString(b),
				Delegates: delegates,
			name := fmt.Sprintf("projects/-/serviceAccounts/%s", serviceAccountName)
			at, err := service.Projects.ServiceAccounts.SignBlob(name, signRequest).Do()
			if err != nil {
				return nil, fmt.Errorf("storage: Error calling iamcredentials.SignBlob: %v", err)
			sDec, err := base64.StdEncoding.DecodeString(at.SignedBlob)
			if err != nil {
				return nil, fmt.Errorf("storage: Error decoding iamcredentials.SignBlob response: %v", err)
			return sDec, nil
		Method:  http.MethodGet,
		Expires: expires,

for java, first build

mvn clean install

then build the docker image, push to gcr then deploy to cloud run

package com.test;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

public class TestApp {

  class HelloworldController {
    @RequestMapping(value="/", produces = MediaType.TEXT_PLAIN_VALUE)
    String hello() {

		String bucketName = System.getenv("BUCKET_NAME");
		String saEmail = System.getenv("SA_EMAIL");
		String objectName = "file.txt";

		String signedURLString = "";
		GoogleCredentials sourceCredentials;
		try {
			sourceCredentials =  GoogleCredentials.getApplicationDefault();

			// you don't need to explicitly use the impersonatedcredentials class in java; the standard defaults detects the env and uses it automatically
			//ImpersonatedCredentials targetCredentials = ImpersonatedCredentials.create(sourceCredentials, saEmail, null,Arrays.asList(""), 2);
			//Storage storage_service = StorageOptions.newBuilder().setCredentials(targetCredentials).build().getService();    

			Storage storage_service = StorageOptions.newBuilder().setCredentials(sourceCredentials).build().getService();      
			BlobId blobId = BlobId.of(bucketName, objectName);
			BlobInfo blobInfo = BlobInfo.newBuilder(blobId).build();
			URL signedUrl = storage_service.signUrl(blobInfo, 600,  TimeUnit.SECONDS, 	SignUrlOption.httpMethod(HttpMethod.GET));
			signedURLString = signedUrl.toExternalForm();
		} catch (IOException ioex) {
			return "Error " + ioex.getMessage();

      return signedURLString;

  public static void main(String[] args){, args);

Nodejs its pretty easy since by default, the library automatically tries to use IAMCredentials API in these environments;

see bucket.getSignedUrl()

In Google Cloud Platform environments, such as Cloud Functions and App Engine, 
you usually don't provide a keyFilename or credentials during instantiation. In those environments, we call the signBlob API

However, my preference would’ve been to make it explicit applied which would also allow you to set a different account to sign with. For example, like in the sample below using the IAM google-auth-library-nodejs#impersonated-credentials-client

const { GoogleAuth, Impersonated } = require('google-auth-library');
const {Storage} = require('@google-cloud/storage');
const {IAMCredentialsClient} = require('@google-cloud/iam-credentials');

    let targetClient = new Impersonated({
        sourceClient: client,
        targetPrincipal: saEmail,
        lifetime: 10,
        delegates: [],
        targetScopes: ['']

    const storage = new Storage({
        auth: {
            getClient: () => targetClient,

    const options = {
        version: 'v4',
        action: 'read',
        expires: + 10 * 60 * 1000, // 10 minutes

    const [url] = await storage

Unfortunately, google-cloud node js auth storage library does not support signing using impersonation with signer

(contributions welcome)

Use Workload Identity

export PROJECT_ID=`gcloud config get-value core/project`
export PROJECT_NUMBER=`gcloud projects describe $PROJECT_ID --format='value(projectNumber)'`

gcloud iam service-accounts create ocsp-svc
gcloud  container clusters create cluster-1 --workload-pool=$

kubectl create namespace ns1
kubectl create serviceaccount  --namespace ns1  ksa-1

gcloud iam service-accounts add-iam-policy-binding \
    --role roles/iam.serviceAccountTokenCreator \
    --member "serviceAccount:$[ns1/ksa-1]" \

gcloud iam service-accounts add-iam-policy-binding \
    --role roles/iam.serviceAccountTokenCreator \
    --member "serviceAccount:ocsp-svc@$" \

gsutil mb  gs://$PROJECT_ID-cab1/
echo -n foo >/tmp/foo.txt
gsutil cp /tmp/foo.txt gs://$PROJECT_ID-cab1/

gsutil iam ch  serviceAccount:ocsp-svc@$      gs://$PROJECT_ID-cab1/

kubectl annotate serviceaccount --namespace ns1 ksa-1$

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
  name: workload-identity-test
  namespace: ns1
  - image: google/cloud-sdk:slim
    name: workload-identity-test
    command: ["sleep","infinity"]
  serviceAccountName: ksa-1
  nodeSelector: "true"

kubectl get po -n ns1
  NAME                     READY   STATUS    RESTARTS   AGE
  workload-identity-test   1/1     Running   0          3m28s

kubectl exec -it workload-identity-test \
    --namespace ns1 \
    -- gsutil -i ocsp-svc@$ signurl -r us -d 10m -u  gs://$PROJECT_ID-cab1/foo.txt

This site supports webmentions. Send me a mention via this form.

comments powered by Disqus