diff --git a/README.md b/README.md index c84e0bc..f3876bb 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,27 @@ This involves creating a Google Service Account Key and using it as `--secret-fi Note that Google Service Account keys are valid for decades (no clear expiry date) - so store it securely or rotate them as often as possible or both. -#### Option 2: Using Workload Identity +#### Option 2: Using File-sourced workforce identity federation short lived credentials + +Keep in mind that [Workforce Identity Federation Users cannot generate signed URLs](https://cloud.google.com/iam/docs/federated-identity-supported-services#:~:text=workforce%20identity%20federation%20users%20cannot%20generate%20signed%20URLs.). This means, if you are using Workforce Identity Federation, you will not be able to run `velero backup logs`, `velero backup download`, `velero backup describe` and `velero restore describe`. + +This involves creating an external credential file and using it as `--secret-file` during [installation](#Install-and-start-Velero). + +1. Create a Workforce Identity Federation external credential file. + + ```bash + gcloud iam workforce-pools create-cred-config \ + locations/global/workforcePools/WORKFORCE_POOL_ID/providers/PROVIDER_ID \ + --subject-token-type=urn:ietf:params:oauth:token-type:id_token \ + --credential-source-file=PATH_TO_OIDC_ID_TOKEN \ + --workforce-pool-user-project=WORKFORCE_POOL_USER_PROJECT \ + --output-file=config.json + ``` + +#### Option 3: Using GKE Workload Identity + +Keep in mind that [Workforce Identity Federation Users cannot generate signed URLs](https://cloud.google.com/iam/docs/federated-identity-supported-services#:~:text=workforce%20identity%20federation%20users%20cannot%20generate%20signed%20URLs.). This means, if you are using Workforce Identity Federation, you will not be able to run `velero backup logs`, `velero backup download`, `velero backup describe` and `velero restore describe`. + This requires a GKE cluster with workload identity enabled. 1. Create Velero Namespace diff --git a/changelogs/unreleased/142-kaovilai b/changelogs/unreleased/142-kaovilai new file mode 100644 index 0000000..3482c39 --- /dev/null +++ b/changelogs/unreleased/142-kaovilai @@ -0,0 +1 @@ +Disable blob signing initialization for non service account file based credentials diff --git a/velero-plugin-for-gcp/object_store.go b/velero-plugin-for-gcp/object_store.go index f22a8c8..a99909f 100644 --- a/velero-plugin-for-gcp/object_store.go +++ b/velero-plugin-for-gcp/object_store.go @@ -19,6 +19,7 @@ package main import ( "context" "encoding/base64" + "encoding/json" "io" "io/ioutil" "time" @@ -70,12 +71,30 @@ type ObjectStore struct { privateKey []byte bucketWriter bucketWriter iamSvc *iamcredentials.Service + fileCredType credAccountKeys } func newObjectStore(logger logrus.FieldLogger) *ObjectStore { return &ObjectStore{log: logger} } +type credAccountKeys string + +// From https://github.com/golang/oauth2/blob/d3ed0bb246c8d3c75b63937d9a5eecff9c74d7fe/google/google.go#L95 +const ( + serviceAccountKey credAccountKeys = "service_account" + externalAccountKey credAccountKeys = "external_account" +) + +func getSecretAccountTypeKey(secretByte []byte) (credAccountKeys, error) { + var f map[string]interface{} + if err := json.Unmarshal(secretByte, &f); err != nil { + return "", err + } + // following will panic if cannot cast to credAccountKeys + return credAccountKeys(f["type"].(string)), nil +} + func (o *ObjectStore) Init(config map[string]string) error { if err := veleroplugin.ValidateObjectStoreConfigKeys(config, kmsKeyNameConfigKey, serviceAccountConfig, credentialsFileConfigKey); err != nil { return err @@ -116,8 +135,14 @@ func (o *ObjectStore) Init(config map[string]string) error { } if creds.JSON != nil { - // Using Credentials File - err = o.initFromKeyFile(creds) + o.fileCredType, err = getSecretAccountTypeKey(creds.JSON) + if err != nil { + return errors.WithStack(err) + } + if o.fileCredType == serviceAccountKey { + // Using Credentials File + err = o.initFromKeyFile(creds) + } } else { // Using compute engine credentials. Use this if workload identity is enabled. err = o.initFromComputeEngine(config) @@ -140,6 +165,9 @@ func (o *ObjectStore) Init(config map[string]string) error { return nil } +// This function is used to populate the googleAccessID and privateKey fields when using a service account credentials file. +// it will error if credential file is not for a service account. +// Do not run this function if using non SA credentials such as external_account. func (o *ObjectStore) initFromKeyFile(creds *google.Credentials) error { jwtConfig, err := google.JWTConfigFromJSON(creds.JSON) if err != nil { @@ -273,6 +301,9 @@ func (o *ObjectStore) SignBytes(bytes []byte) ([]byte, error) { } func (o *ObjectStore) CreateSignedURL(bucket, key string, ttl time.Duration) (string, error) { + if o.fileCredType != serviceAccountKey { + return "", errors.New("cannot sign blob using non SA file credentials") + } options := storage.SignedURLOptions{ GoogleAccessID: o.googleAccessID, Method: "GET", diff --git a/velero-plugin-for-gcp/object_store_test.go b/velero-plugin-for-gcp/object_store_test.go index 7b66a2a..6777e3a 100644 --- a/velero-plugin-for-gcp/object_store_test.go +++ b/velero-plugin-for-gcp/object_store_test.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -24,7 +24,6 @@ import ( "cloud.google.com/go/storage" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - velerotest "github.com/vmware-tanzu/velero/pkg/test" ) @@ -152,3 +151,68 @@ func TestObjectExists(t *testing.T) { }) } } + +func Test_getSecretAccountKey(t *testing.T) { + type args struct { + secretByte []byte + } + tests := []struct { + name string + args args + want credAccountKeys + wantErr bool + }{ + { + name: "get secret service account account key", + args: args{ + // test data from https://cloud.google.com/iam/docs/keys-create-delete + secretByte: []byte(`{ + "type": "service_account", + "project_id": "PROJECT_ID", + "private_key_id": "KEY_ID", + "private_key": "-----BEGIN PRIVATE KEY-----\nPRIVATE_KEY\n-----END PRIVATE KEY-----\n", + "client_email": "SERVICE_ACCOUNT_EMAIL", + "client_id": "CLIENT_ID", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://accounts.google.com/o/oauth2/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/SERVICE_ACCOUNT_EMAIL" +} +`), + }, + want: serviceAccountKey, + wantErr: false, + }, + { + name: "get secret external account key", + args: args{ + // test data from https://cloud.google.com/iam/docs/workforce-obtaining-short-lived-credentials + secretByte: []byte(`{ + "type": "external_account", + "audience": "//iam.googleapis.com/locations/global/workforcePools/WORKFORCE_POOL_ID/providers/PROVIDER_ID", + "subject_token_type": "urn:ietf:params:oauth:token-type:id_token", + "token_url": "https://sts.googleapis.com/v1/token", + "workforce_pool_user_project": "WORKFORCE_POOL_USER_PROJECT", + "credential_source": { + "file": "PATH_TO_OIDC_CREDENTIALS_FILE" + } +} +`), + }, + want: externalAccountKey, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getSecretAccountTypeKey(tt.args.secretByte) + if (err != nil) != tt.wantErr { + t.Errorf("getSecretAccountKey() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("getSecretAccountKey() = %v, want %v", got, tt.want) + } + }) + } +}