diff --git a/README.rst b/README.rst index fb0f52959..04ac0a4b7 100644 --- a/README.rst +++ b/README.rst @@ -242,8 +242,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 oauth2 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 @@ -256,6 +260,19 @@ you can enable application default credentials using the sdk: $ gcloud auth application-default login +Using oauth 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: @@ -278,6 +295,7 @@ And decrypt it using:: $ sops decrypt test.enc.yaml + Encrypting using Azure Key Vault ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/gcpkms/keysource.go b/gcpkms/keysource.go index 8ff51357d..d7bf3db4f 100644 --- a/gcpkms/keysource.go +++ b/gcpkms/keysource.go @@ -12,6 +12,7 @@ import ( kms "cloud.google.com/go/kms/apiv1" "cloud.google.com/go/kms/apiv1/kmspb" "github.com/sirupsen/logrus" + "golang.org/x/oauth2" "google.golang.org/api/option" "google.golang.org/grpc" @@ -23,6 +24,9 @@ const ( // a path to a credentials file, or directly as the variable's value in JSON // format. SopsGoogleCredentialsEnv = "GOOGLE_CREDENTIALS" + // SopsGoogleCredentialsOAuthToken is the environment variable used for the + // GCP Oauth2 Token. + SopsGoogleCredentialsOAuthToken = "GOOGLE_OAUTH_ACCESS_TOKEN" // KeyTypeIdentifier is the string used to identify a GCP KMS MasterKey. KeyTypeIdentifier = "gcp_kms" ) @@ -203,8 +207,8 @@ func (key *MasterKey) TypeToIdentifier() string { return KeyTypeIdentifier } -// newKMSClient returns a GCP KMS client configured with the credentialJSON -// and/or grpcConn, falling back to environmental defaults. +// newKMSClient returns a GCP KMS client configured with the credentialJSON, +// tokenSource and/or grpcConn, falling back to environmental defaults. // It returns an error if the ResourceID is invalid, or if the setup of the // client fails. func (key *MasterKey) newKMSClient() (*kms.KeyManagementClient, error) { @@ -219,22 +223,30 @@ func (key *MasterKey) newKMSClient() (*kms.KeyManagementClient, error) { case key.credentialJSON != nil: opts = append(opts, option.WithCredentialsJSON(key.credentialJSON)) default: - credentials, err := getGoogleCredentials() - if err != nil { - return nil, err - } + credentials, err_credentials_file := getGoogleCredentials() if credentials != nil { opts = append(opts, option.WithCredentialsJSON(credentials)) + break + } + + at_credentials, err_credentials_token := getGoogleOAuthToken() + if at_credentials != nil { + opts = append(opts, option.WithTokenSource(at_credentials)) + } + + if err_credentials_file != nil && err_credentials_token != nil { + return nil, fmt.Errorf("credentials: failed to get credentials for gcp kms, add default credentials or oauth access token") } } + if key.grpcConn != nil { opts = append(opts, option.WithGRPCConn(key.grpcConn)) } ctx := context.Background() - client, err := kms.NewKeyManagementClient(ctx, opts...) - if err != nil { - return nil, err + client, err_credentials := kms.NewKeyManagementClient(ctx, opts...) + if err_credentials != nil { + return nil, err_credentials } return client, nil @@ -242,14 +254,31 @@ 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 environment variable is not set, +// or 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 { return os.ReadFile(defaultCredentials) } + return []byte(defaultCredentials), nil } - return nil, nil + return nil, fmt.Errorf("could not find Google credential file") +} + +// getGoogleOAuthToken returns the SopsGoogleCredentialsOauthToken variable, +// as the oauth token. +// It returns an error and a nil byte slice if the envrionment variable is not set. +func getGoogleOAuthToken() (oauth2.TokenSource, error) { + if token, isSet := os.LookupEnv(SopsGoogleCredentialsOAuthToken); isSet { + tokenSource := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: token}, + ) + + return tokenSource, nil + } + + return nil, fmt.Errorf("could not find Google OAuth token") } diff --git a/gcpkms/keysource_test.go b/gcpkms/keysource_test.go index 153bfb260..79015ff02 100644 --- a/gcpkms/keysource_test.go +++ b/gcpkms/keysource_test.go @@ -53,8 +53,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) @@ -80,9 +81,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) @@ -116,7 +118,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 @@ -136,6 +138,12 @@ func TestMasterKey_createCloudKMSService(t *testing.T) { "type": "authorized_user"}`), }, }, + { + key: MasterKey{ + ResourceID: testResourceID, + }, + errString: "credentials: failed to get credentials", + }, } for _, tt := range tests { @@ -149,6 +157,29 @@ func TestMasterKey_createCloudKMSService(t *testing.T) { } } +func TestMasterKey_createCloudKMSService_withOauthToken(t *testing.T) { + t.Setenv(SopsGoogleCredentialsOAuthToken, "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: failed to get credentials") +} + func newGRPCServer(port string) *grpc.ClientConn { serv := grpc.NewServer() kmspb.RegisterKeyManagementServiceServer(serv, &mockKeyManagement) diff --git a/go.mod b/go.mod index e09e8e459..b670ae1a8 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,7 @@ require ( github.com/stretchr/testify v1.9.0 github.com/urfave/cli v1.22.15 golang.org/x/net v0.29.0 + golang.org/x/oauth2 v0.23.0 golang.org/x/sys v0.25.0 golang.org/x/term v0.24.0 google.golang.org/api v0.197.0 @@ -126,7 +127,6 @@ require ( go.opentelemetry.io/otel/metric v1.29.0 // indirect go.opentelemetry.io/otel/trace v1.29.0 // indirect golang.org/x/crypto v0.27.0 // indirect - golang.org/x/oauth2 v0.23.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/text v0.18.0 // indirect golang.org/x/time v0.6.0 // indirect