diff --git a/README.rst b/README.rst index e930bf229..ce3b47a46 100644 --- a/README.rst +++ b/README.rst @@ -266,8 +266,12 @@ It is also possible to use ``updatekeys``, when adding or removing age recipient Encrypting using GCP KMS ~~~~~~~~~~~~~~~~~~~~~~~~ -GCP KMS uses `Application Default Credentials -`_. +GCP KMS has support for authorization with the use of `Application Default Credentials +`_ and using an OAuth 2.0 token. +Application default credentials precedes the use of access token. + +Using Application Default Credentials you can authorize by doing this: + If you already logged in using .. code:: sh @@ -280,6 +284,18 @@ you can enable application default credentials using the sdk: $ gcloud auth application-default login +Using OAauth tokens you can authorize by doing this: + +.. code:: sh + + $ export GOOGLE_OAUTH_ACCESS_TOKEN= + +Or if you are logged in you can authorize by generating an access token: + +.. code:: sh + + $ export GOOGLE_OAUTH_ACCESS_TOKEN="$(gcloud auth print-access-token)" + Encrypting/decrypting with GCP KMS requires a KMS ResourceID. You can use the cloud console the get the ResourceID or you can create one using the gcloud sdk: diff --git a/gcpkms/keysource.go b/gcpkms/keysource.go index 7c79e24aa..408a22de9 100644 --- a/gcpkms/keysource.go +++ b/gcpkms/keysource.go @@ -24,6 +24,9 @@ const ( // a path to a credentials file, or directly as the variable's value in JSON // format. SopsGoogleCredentialsEnv = "GOOGLE_CREDENTIALS" + // SopsGoogleCredentialsOAuthTokenEnv is the environment variable used for the + // GCP OAuth 2.0 Token. + SopsGoogleCredentialsOAuthTokenEnv = "GOOGLE_OAUTH_ACCESS_TOKEN" // KeyTypeIdentifier is the string used to identify a GCP KMS MasterKey. KeyTypeIdentifier = "gcp_kms" ) @@ -245,12 +248,19 @@ func (key *MasterKey) newKMSClient() (*kms.KeyManagementClient, error) { default: credentials, err := getGoogleCredentials() if err != nil { - return nil, err + return nil, fmt.Errorf("credentials: failed to obtain credentials from %q: %w", SopsGoogleCredentialsEnv, err) } if credentials != nil { opts = append(opts, option.WithCredentialsJSON(credentials)) + break + } + + if atCredentials := getGoogleOAuthTokenFromEnv(); atCredentials != nil { + opts = append(opts, option.WithTokenSource(atCredentials)) + break } } + if key.grpcConn != nil { opts = append(opts, option.WithGRPCConn(key.grpcConn)) } @@ -266,8 +276,8 @@ func (key *MasterKey) newKMSClient() (*kms.KeyManagementClient, error) { // getGoogleCredentials returns the SopsGoogleCredentialsEnv variable, as // either the file contents of the path of a credentials file, or as value in -// JSON format. It returns an error if the file cannot be read, and may return -// a nil byte slice if no value is set. +// JSON format. +// It returns an error and a nil byte slice if the file cannot be read. func getGoogleCredentials() ([]byte, error) { if defaultCredentials, ok := os.LookupEnv(SopsGoogleCredentialsEnv); ok && len(defaultCredentials) > 0 { if _, err := os.Stat(defaultCredentials); err == nil { @@ -277,3 +287,16 @@ func getGoogleCredentials() ([]byte, error) { } return nil, nil } + +// getGoogleOAuthTokenFromEnv returns the SopsGoogleCredentialsOauthTokenEnv variable, +// as the OAauth 2.0 token. +// It returns an error and a nil byte slice if the envrionment variable is not set. +func getGoogleOAuthTokenFromEnv() oauth2.TokenSource { + if token, ok := os.LookupEnv(SopsGoogleCredentialsOAuthTokenEnv); ok && len(token) > 0 { + tokenSource := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: token}, + ) + return tokenSource + } + return nil +} diff --git a/gcpkms/keysource_test.go b/gcpkms/keysource_test.go index b9e3b243c..c2442eeca 100644 --- a/gcpkms/keysource_test.go +++ b/gcpkms/keysource_test.go @@ -61,8 +61,9 @@ func TestMasterKey_Encrypt(t *testing.T) { }) key := MasterKey{ - grpcConn: newGRPCServer("0"), - ResourceID: testResourceID, + grpcConn: newGRPCServer("0"), + ResourceID: testResourceID, + credentialJSON: []byte("arbitrary credentials"), } err := key.Encrypt([]byte("encrypt")) assert.NoError(t, err) @@ -88,9 +89,10 @@ func TestMasterKey_Decrypt(t *testing.T) { Plaintext: []byte(decryptedData), }) key := MasterKey{ - grpcConn: newGRPCServer("0"), - ResourceID: testResourceID, - EncryptedKey: "encryptedKey", + grpcConn: newGRPCServer("0"), + ResourceID: testResourceID, + EncryptedKey: "encryptedKey", + credentialJSON: []byte("arbitrary credentials"), } data, err := key.Decrypt() assert.NoError(t, err) @@ -124,7 +126,7 @@ func TestMasterKey_ToMap(t *testing.T) { }, key.ToMap()) } -func TestMasterKey_createCloudKMSService(t *testing.T) { +func TestMasterKey_createCloudKMSService_withCredentialsFile(t *testing.T) { tests := []struct { key MasterKey errString string @@ -144,6 +146,12 @@ func TestMasterKey_createCloudKMSService(t *testing.T) { "type": "authorized_user"}`), }, }, + { + key: MasterKey{ + ResourceID: testResourceID, + }, + errString: `credentials: failed to obtain credentials from "SOPS_GOOGLE_CREDENTIALS"`, + }, } for _, tt := range tests { @@ -157,6 +165,29 @@ func TestMasterKey_createCloudKMSService(t *testing.T) { } } +func TestMasterKey_createCloudKMSService_withOauthToken(t *testing.T) { + t.Setenv(SopsGoogleCredentialsOAuthTokenEnv, "token") + + masterKey := MasterKey{ + ResourceID: testResourceID, + } + + _, err := masterKey.newKMSClient() + + assert.NoError(t, err) +} + +func TestMasterKey_createCloudKMSService_withoutCredentials(t *testing.T) { + masterKey := MasterKey{ + ResourceID: testResourceID, + } + + _, err := masterKey.newKMSClient() + + assert.Error(t, err) + assert.ErrorContains(t, err, "credentials: could not find default credentials") +} + func newGRPCServer(port string) *grpc.ClientConn { serv := grpc.NewServer() kmspb.RegisterKeyManagementServiceServer(serv, &mockKeyManagement)