Secure Signed URLs on Google Cloud Storage

Ad-hoc Secure Data Upload to Cloud Storage

Thanks to Paul Williams and Thinh Ha for their ideas and advice on Go

Overview

A key problem for sharing data is to ingest securely to cloud storage systems with enterprise standard controls. Whilst there are many methods for automated ingestion, user-driven ad-hoc upload of data is common practice but with a less well-established pattern, especially where the users sit outside the security boundaries of the cloud platform, such as outside a VPCSC boundary.

One solution for adhoc uploads is to generate a signed url - a i.e. URL which contains authentication information in the string and can be used to access an object on Cloud Storage. A signed URL can be used for both uploads and downloads of objects. The signed-url authenticates using a service account with permissions restricted to the relevant cloud storage bucket.

Generation of the signed url can be done several ways, either via gsutil or through a client library. Here, the process is to authenticate as the users’ GCP identity, then impersonate the service account before generating the particular URL using the service account’s authentication.

Most of the GCP client (Java, Python, NodeJS) libraries are implemented such that authentication for the service account comes via a key. The use of service account keys is often blocked at the organisational level and is generally bad practice. However, the Go client library allows for service account impersonation without a private key, meaning that signed urls can be generated securely.

I will demonstrate how to setup a service account, configure a go script with the appropriate and generate a signed URL for use in upload.

Run-through

You will need a GCP organisation (myorg) with service account keys disabled and a GCP project within it (myproject). Your GCP identity will need the following permissions

  • Service Account Admin (roles/iam.serviceAccountAdmin) - to create the service account,
  • Service Account User (roles/iam.serviceAccountUser) - to use the service account
  • Service Account Token Creator (roles/iam.serviceAccountTokenCreator)- to generate a token for the service account. Both your account and the service account will need permission to access resources within the VPC-SC perimeter.

Create a bucket for upload

Choose a bucket name and use the gsutil application:

gsutil mb gs://BUCKET_NAME

Configure the service account

Create the service account (SA_NAME)

gcloud iam service-accounts create SA_NAME \
    --description="DESCRIPTION" \
    --display-name="DISPLAY_NAME"

And allow the service account to access Google Cloud Storage:

gcloud projects add-iam-policy-binding PROJECT_ID \
    --member="serviceAccount:SA_NAME@PROJECT_ID.iam.gserviceaccount.com" \
    --role="ROLE_NAME"

Where ROLE_NAME = “roles/storage.objectAdmin” (give more fine-grained permissions for your project).

Configure the binary

Download the go script to generate the signed URL

$ git clone git@github.com:alexshires/google-cloud-demos.git

And navigate to the code

$ cd gen-signed-url

The key parts are setting up the impersonation at:

ts, err := impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{
    	TargetPrincipal: "signurl-service-account@ashires-joonix-test-1.iam.gserviceaccount.com",
    	Scopes:          []string{"https://www.googleapis.com/auth/cloud-platform"},
    	// Optionally supply delegates.
    	// Delegates: []string{"bar@project-id.iam.gserviceaccount.com"},
    })

and then generating the url with the impersonated service account

opts := &storage.SignedURLOptions{
        Scheme: storage.SigningSchemeV4,
        Method: "PUT",
        Headers: []string{
            "Content-Type:application/octet-stream",
        },
        Expires: time.Now().Add(12 * time.Hour),
        GoogleAccessID: "signurl-service-account@ashires-joonix-test-1.iam.gserviceaccount.com",
    }

In a suitable terminal, edit line 25 and 56 of the main.go file for your service account’s full identity: (SA_NAME@PROJECT_ID.iam.gserviceaccount.com).

Other configuration for the program is on line 55, for the duration of validity for this URL and line 53 for the content type of object expected to be uploaded.

To run, compile the go program:

$ go mod init example.com/ashires/gensignedurl
$ go build example.com/ashires/gensignedurl

Execute the go program as follows, with the bucket name from above and the object name

$ ./gensignedurl BUCKET_NAME OBJECT_NAME

This will return a URL of the form, which you can share for parties interested in uploading to the specific bucket.

Future changes

There are a few ways this can be extended / wrapped up for a user-friendly enterprise experience:

  • Generate the URL through a cloud function
  • Generate the URL based on the object name through a form
  • Generate the URL and upload in a client-side script for automatic upload for a large set of objects from a third party.