Google Cloud Storage Signed URLs without Service Account Keys

no more passing keys, background auth everywhere

SEAN K.H. LIAO

Google Cloud Storage Signed URLs without Service Account Keys

no more passing keys, background auth everywhere

signed urls

So say you need to generate short lived urls granting access to a resource in GCP Cloud Storage, because one of your third party SaaS solutions doesn't integrate with GCP and their S3 integration is limited to actual AWS S3 and not just API compatible S3.

The Google documented way is to create a service account key and use that to sign URLs, but we don't want that: managing service account keys is a PITA.

permissions

Because you never create a key, you don't have anything to sign the url with. So you need to grant roles/iam.serviceAccountTokenCreator to allow the service to create tokens to sign things with.

If it's the service's identity is what has access to bucket contents, then you'll be granting it the role on itself.

In terraform format with workload identity:

 1resource "google_service_account" "your-sa" {
 2  account_id = "your-sa"
 3}
 4resource "google_service_account_iam_policy" "your-sa" {
 5  service_account_id = google_service_account.your-sa.name
 6  policy_data        = data.google_iam_policy.your-sa.policy_data
 7}
 8data "google_iam_policy" "your-sa" {
 9  binding {
10    role = "roles/iam.workloadIdentityUser"
11    members = [
12      "serviceAccount:your-project.svc.id.goog[your-namespace/your-k8s-sa]",
13    ]
14  }
15  binding {
16    role = "roles/iam.serviceAccountTokenCreator"
17    members = [
18      "serviceAccount:your-sa@your-project.iam.gserviceaccount.com",
19    ]
20  }
21}

internet version

Obviously someone else also wants to do this and dumped it in a gist.

https://gist.github.com/pdecat/80f21e36583420abbfdeae0494a53501

my version

I thought it could be shorter, mainly taking advantage of the fact that all google api libraries know how to find/use default credentials.

 1package main
 2
 3import (
 4        "context"
 5        "fmt"
 6        "time"
 7
 8        "cloud.google.com/go/compute/metadata"
 9        credentials "cloud.google.com/go/iam/credentials/apiv1"
10        "cloud.google.com/go/storage"
11        credentialspb "google.golang.org/genproto/googleapis/iam/credentials/v1"
12)
13
14func main() {
15        ctx := context.Background()
16
17        saEmail, _ := metadata.Email("default")
18        iamClient, _ := credentials.NewIamCredentialsClient(ctx)
19        defer iamClient.Close()
20
21        u, _ := storage.SignedURL("bucket-name", "path/to/object", &storage.SignedURLOptions{
22                GoogleAccessID: saEmail,
23                SignBytes: func(b []byte) ([]byte, error) {
24                        res, _ := iamClient.SignBlob(ctx, &credentialspb.SignBlobRequest{
25                                Payload: b,
26                                Name: saEmail,
27                        })
28                        return res.SignedBlob, nil
29                },
30                Method: "GET",
31                Expires: time.Now().Add(5*time.Minute),
32        })
33
34        fmt.Println(u)
35}