Skip to content

Instantly share code, notes, and snippets.

@pdecat
Created January 31, 2020 17:39
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save pdecat/80f21e36583420abbfdeae0494a53501 to your computer and use it in GitHub Desktop.
Save pdecat/80f21e36583420abbfdeae0494a53501 to your computer and use it in GitHub Desktop.
Creating a GCS Signed URL without a Service Account Key from a GCE instance or a GKE Pod using Workload Identity
package main
import (
"context"
"flag"
"log"
"net/url"
"time"
"cloud.google.com/go/compute/metadata"
credentials "cloud.google.com/go/iam/credentials/apiv1"
"cloud.google.com/go/storage"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
credentialspb "google.golang.org/genproto/googleapis/iam/credentials/v1"
)
func main() {
var bucket string
var object string
var method string
flag.StringVar(&bucket, "bucket", "", "The bucket")
flag.StringVar(&object, "object", "", "The object in the bucket")
flag.StringVar(&method, "method", "GET", "The HTTP method to use")
flag.Parse()
log.Printf("bucket: %v", bucket)
log.Printf("object: %v", object)
log.Printf("method: %v", method)
creds, err := google.FindDefaultCredentials(oauth2.NoContext)
if err != nil {
log.Printf("Error while getting default credentials: %v", err)
return
}
token, err := creds.TokenSource.Token()
if err != nil {
log.Printf("Error while getting token: %v", err)
return
}
accountID, ok := token.Extra("oauth2.google.serviceAccount").(string)
if !ok {
log.Printf("Error while getting account ID: %v", err)
return
}
log.Printf("accountID: %v", accountID)
client, err := google.DefaultClient(oauth2.NoContext)
if err != nil {
log.Printf("Error while getting default client: %v", err)
return
}
computeMetadataClient := metadata.NewClient(client)
email, err := computeMetadataClient.Email(accountID)
if err != nil {
log.Printf("Error while getting email: %v", err)
return
}
log.Printf("email: %v", email)
projectID, err := computeMetadataClient.ProjectID()
if err != nil {
log.Printf("Error while getting project ID: %v", err)
return
}
log.Printf("projectID: %v", projectID)
sc := storage.SignedURLOptions{
GoogleAccessID: email,
Method: method,
Expires: time.Now().Add(60 * time.Second),
ContentType: "",
}
sc.SignBytes = func(payload []byte) ([]byte, error) {
ctx := context.Background()
credsClient, err := credentials.NewIamCredentialsClient(ctx)
if err != nil {
return nil, err
}
req := &credentialspb.SignBlobRequest{
Name: email,
Payload: payload,
}
res, err := credsClient.SignBlob(ctx, req)
if err != nil {
return nil, err
}
return res.SignedBlob, err
}
rawURL, err := storage.SignedURL(bucket, object, &sc)
if err != nil {
log.Printf("Error while generating GCS pre-signed URL: %v", err)
return
}
URL, err := url.Parse(rawURL)
if err != nil {
log.Printf("Error while parsing generated URL: %v", err)
return
}
log.Printf("URL to use with %v method: %v", method, URL)
return
}
@pdecat
Copy link
Author

pdecat commented Jan 31, 2020

Note: for this to work, the IAM service account must be granted the roles/iam.serviceAccountTokenCreator on itself, in addition to IAM permissions on the target GCS bucket.

@challarao
Copy link

challarao commented Oct 7, 2020

Hi @pdecat, why is roles/iam.serviceAccountTokenCreator required? How do other functions in the library like list buckets etc work without this?

What do they use internally?

Thanks

@pdecat
Copy link
Author

pdecat commented Oct 7, 2020

@challarao
Copy link

Thanks for responding @pdecat. You mentioned "must be granted the roles/iam.serviceAccountTokenCreator on itself". I'm trying to figure out the "itself" part. How to do that while creating the service account?

This is what I tried:

  1. Create service account in IAM
  2. In the roles section add "roles/iam.serviceAccountTokenCreator"
  3. In condition section ???

I imagine it has something to do with condition section, what do I need to do there to restrict the permission on self? Do I need to add name restriction?

Thanks

@pdecat
Copy link
Author

pdecat commented Oct 7, 2020

No need for conditions, the IAM binding is not done on the project resource, but on the IAM service account resource itself, e.g.:

#  gcloud iam service-accounts get-iam-policy myiamserviceaccount@mygcpproject.iam.gserviceaccount.com
bindings:
- members:
  - serviceAccount:myiamserviceaccount@mygcpproject.iam.gserviceaccount.com
  role: roles/iam.serviceAccountTokenCreator
- members:
  - serviceAccount:mygcpproject.svc.id.goog[myk8snamespace/myk8sapp]
  role: roles/iam.workloadIdentityUser
etag: BwWf3CXxQdE=
version: 1

image

@challarao
Copy link

challarao commented Oct 7, 2020

I'm not getting the bindings similar to what you are getting. This is what I've tried:

  1. Create service account

image

  1. Add role

I added this only because I didn't see that option in step 3.

image

  1. Granted itself access to itself
    image

  2. Invoked the command you used:

gcloud iam service-accounts get-iam-policy  test13@myproject.iam.gserviceaccount.com                                                                                        
bindings:
- members:
  - serviceAccount:test13@myproject.iam.gserviceaccount.com
  role: roles/iam.serviceAccountUser
etag: BwWxEVwMnV4=
version: 1

In my output I don't see service account creator in the role. While you have it in your policy. What am I missing in the service account creation?

Thanks

@challarao
Copy link

I was able to create the binding using this command:

gcloud iam service-accounts add-iam-policy-binding test13@myproject.iam.gserviceaccount.com --member serviceAccount:test13@myproject.iam.gserviceaccount.com --
role roles/iam.serviceAccountTokenCreator

Now I'm getting the output similar to yours:

bindings:
- members:
  - serviceAccount:test13@myproject.iam.gserviceaccount.com
  role: roles/iam.serviceAccountTokenCreator
etag: BwWxEg_JjV0=
version: 1

Hoping this is the way to do it. Thanks.

@sarjumulmi
Copy link

@pdecat, is the email passed to SignBlobRequest.Name field in the format projects/-/serviceAccounts/{ACCOUNT_EMAIL_OR_UNIQUEID} as mentioned in the docs??

@pdecat
Copy link
Author

pdecat commented Apr 13, 2021

@pdecat, is the email passed to SignBlobRequest.Name field in the format projects/-/serviceAccounts/{ACCOUNT_EMAIL_OR_UNIQUEID} as mentioned in the docs??

It is the raw email address of the service account.

@sarjumulmi
Copy link

@pdecat, is the email passed to SignBlobRequest.Name field in the format projects/-/serviceAccounts/{ACCOUNT_EMAIL_OR_UNIQUEID} as mentioned in the docs??

It is the raw email address of the service account.

@pdecat, thanks for the reply. I was referring to the docs here which seems to suggest to use projects/-/serviceAccounts/{rawEmail} instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment