From ac5dd0a826cb7546482c8ddba7958217bab9f055 Mon Sep 17 00:00:00 2001 From: David Quagebeur Date: Fri, 24 Sep 2021 11:39:21 +0200 Subject: [PATCH] feat: add support for azure keyvault --- .gitignore | 2 + controllers/kustomization_decryptor.go | 5 + go.mod | 6 +- go.sum | 16 +++ internal/sops/keyservice/server.go | 171 +++++++++++++++++++++++++ main.go | 2 +- 6 files changed, 200 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 5a3a724f..7d459ca4 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ bin/ config/release/ config/crd/bases/gitrepositories.yaml config/crd/bases/buckets.yaml + +kustomize-controller diff --git a/controllers/kustomization_decryptor.go b/controllers/kustomization_decryptor.go index ea14dba1..2a41e91e 100644 --- a/controllers/kustomization_decryptor.go +++ b/controllers/kustomization_decryptor.go @@ -160,6 +160,11 @@ func (kd *KustomizeDecryptor) ImportKeys(ctx context.Context) error { if err := kd.gpgImport(keyPath); err != nil { return err } + case ".json": + keyPath := filepath.Join(kd.homeDir, name) + if err := os.WriteFile(keyPath, file, os.ModePerm); err != nil { + return fmt.Errorf("unable to write key to storage: %w", err) + } case ".agekey": ageIdentities = append(ageIdentities, string(file)) } diff --git a/go.mod b/go.mod index 9a349bf6..cce93ff6 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,11 @@ replace github.com/fluxcd/kustomize-controller/api => ./api require ( filippo.io/age v1.0.0 + github.com/Azure/azure-sdk-for-go v57.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest v0.11.22 // indirect + github.com/Azure/go-autorest/autorest/azure/auth v0.5.9 // indirect github.com/cyphar/filepath-securejoin v0.2.2 + github.com/dimchansky/utfbom v1.1.1 // indirect github.com/drone/envsubst v1.0.3-0.20200804185402-58bc65f69603 github.com/fluxcd/kustomize-controller/api v0.18.0 github.com/fluxcd/pkg/apis/kustomize v0.2.0 @@ -21,7 +25,7 @@ require ( github.com/onsi/gomega v1.15.0 github.com/spf13/pflag v1.0.5 go.mozilla.org/sops/v3 v3.7.1 - golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 golang.org/x/net v0.0.0-20210520170846-37e1c6afe023 google.golang.org/grpc v1.42.0 k8s.io/api v0.22.2 diff --git a/go.sum b/go.sum index 1fc10b90..01ed7026 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,8 @@ filippo.io/edwards25519 v1.0.0-alpha.2/go.mod h1:X+pm78QAUPtFLi1z9PYIlS/bdDnvbCO filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= github.com/Azure/azure-sdk-for-go v31.2.0+incompatible h1:kZFnTLmdQYNGfakatSivKHUfUnDZhqNdchHD4oIhp5k= github.com/Azure/azure-sdk-for-go v31.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v57.2.0+incompatible h1:zoJapafogLazoyp0x9aQENzNNqxvU6pnGtb2P8/i+HI= +github.com/Azure/azure-sdk-for-go v57.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= @@ -43,15 +45,25 @@ github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+B github.com/Azure/go-autorest/autorest v0.11.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= github.com/Azure/go-autorest/autorest v0.11.18 h1:90Y4srNYrwOtAgVo3ndrQkTYn6kf1Eg/AjTFJ8Is2aM= github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= +github.com/Azure/go-autorest/autorest v0.11.19 h1:7/IqD2fEYVha1EPeaiytVKhzmPV223pfkRIQUGOK2IE= +github.com/Azure/go-autorest/autorest v0.11.19/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= +github.com/Azure/go-autorest/autorest v0.11.22 h1:bXiQwDjrRmBQOE67bwlvUKAC1EU1yZTPQ38c+bstZws= +github.com/Azure/go-autorest/autorest v0.11.22/go.mod h1:BAWYUWGPEtKPzjVkp0Q6an0MJcJDsoh5Z1BFAEFs4Xs= github.com/Azure/go-autorest/autorest/adal v0.1.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E= github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= github.com/Azure/go-autorest/autorest/adal v0.9.13 h1:Mp5hbtOePIzM8pJVRa3YLrWWmZtoxRXqUEzCfJt3+/Q= github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= +github.com/Azure/go-autorest/autorest/adal v0.9.14 h1:G8hexQdV5D4khOXrWG2YuLCFKhWYmWD8bHYaXN5ophk= +github.com/Azure/go-autorest/autorest/adal v0.9.14/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= github.com/Azure/go-autorest/autorest/azure/auth v0.1.0 h1:YgO/vSnJEc76NLw2ecIXvXa8bDWiqf1pOJzARAoZsYU= github.com/Azure/go-autorest/autorest/azure/auth v0.1.0/go.mod h1:Gf7/i2FUpyb/sGBLIFxTBzrNzBo7aPXXE3ZVeDRwdpM= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.9 h1:Y2CgdzitFDsdMwYMzf9LIZWrrTFysqbRc7b94XVVJ78= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.9/go.mod h1:hg3/1yw0Bq87O3KvvnJoAh34/0zbP7SFizX/qN5JvjU= github.com/Azure/go-autorest/autorest/azure/cli v0.1.0 h1:YTtBrcb6mhA+PoSW8WxFDoIIyjp13XqJeX80ssQtri4= github.com/Azure/go-autorest/autorest/azure/cli v0.1.0/go.mod h1:Dk8CUAt/b/PzkfeRsWzVG9Yj3ps8mS8ECztu43rdU8U= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.2 h1:dMOmEJfkLKW/7JsokJqkyoYSgmR08hi9KrhjZb+JALY= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.2/go.mod h1:7qkJkT+j6b+hIpzMOwPChJhTqS8VbsqqgULzMNRugoM= github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= @@ -181,6 +193,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4= github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= +github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= +github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= @@ -810,6 +824,8 @@ golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/internal/sops/keyservice/server.go b/internal/sops/keyservice/server.go index 0b1d9b54..592eb26c 100644 --- a/internal/sops/keyservice/server.go +++ b/internal/sops/keyservice/server.go @@ -5,7 +5,16 @@ package keyservice import ( + "bytes" + "encoding/base64" + "encoding/binary" + "encoding/json" + "errors" "fmt" + "io/ioutil" + "os" + "path/filepath" + "unicode/utf16" "go.mozilla.org/sops/v3/keyservice" "golang.org/x/net/context" @@ -14,6 +23,12 @@ import ( "github.com/fluxcd/kustomize-controller/internal/sops/age" "github.com/fluxcd/kustomize-controller/internal/sops/pgp" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/keyvault/keyvault" + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/azure/auth" + "github.com/dimchansky/utfbom" ) // Server is a key service server that uses SOPS MasterKeys to fulfill @@ -86,6 +101,138 @@ func (ks *Server) decryptWithAge(key *keyservice.AgeKey, ciphertext []byte) ([]b return plaintext, err } +func (ks *Server) newKeyvaultAuthorizerFromFile(fileLocation string) (autorest.Authorizer, error) { + s := auth.FileSettings{} + s.Values = map[string]string{} + + contents, err := ioutil.ReadFile(fileLocation) + if err != nil { + return nil, err + } + + // Auth file might be encoded + var decoded []byte + reader, enc := utfbom.Skip(bytes.NewReader(contents)) + switch enc { + case utfbom.UTF16LittleEndian: + u16 := make([]uint16, (len(contents)/2)-1) + err := binary.Read(reader, binary.LittleEndian, &u16) + if err != nil { + return nil, err + } + decoded = []byte(string(utf16.Decode(u16))) + case utfbom.UTF16BigEndian: + u16 := make([]uint16, (len(contents)/2)-1) + err := binary.Read(reader, binary.BigEndian, &u16) + if err != nil { + return nil, err + } + decoded = []byte(string(utf16.Decode(u16))) + default: + decoded, err = ioutil.ReadAll(reader) + if err != nil { + return nil, err + } + } + + // unmarshal file + authFile := map[string]interface{}{} + err = json.Unmarshal(decoded, &authFile) + if err != nil { + return nil, err + } + + if val, ok := authFile["clientId"]; ok { + s.Values["AZURE_CLIENT_ID"] = val.(string) + } + if val, ok := authFile["clientSecret"]; ok { + s.Values["AZURE_CLIENT_SECRET"] = val.(string) + } + if val, ok := authFile["clientCertificate"]; ok { + s.Values["AZURE_CERTIFICATE_PATH"] = val.(string) + } + if val, ok := authFile["clientCertificatePassword"]; ok { + s.Values["AZURE_CERTIFICATE_PASSWORD"] = val.(string) + } + if val, ok := authFile["subscriptionId"]; ok { + s.Values["AZURE_SUBSCRIPTION_ID"] = val.(string) + } + if val, ok := authFile["tenantId"]; ok { + s.Values["AZURE_TENANT_ID"] = val.(string) + } + if val, ok := authFile["activeDirectoryEndpointUrl"]; ok { + s.Values["ActiveDirectoryEndpoint"] = val.(string) + } + if val, ok := authFile["resourceManagerEndpointUrl"]; ok { + s.Values["ResourceManagerEndpoint"] = val.(string) + } + if val, ok := authFile["activeDirectoryGraphResourceId"]; ok { + s.Values["GraphResourceID"] = val.(string) + } + if val, ok := authFile["sqlManagementEndpointUrl"]; ok { + s.Values["SQLManagementEndpoint"] = val.(string) + } + if val, ok := authFile["galleryEndpointUrl"]; ok { + s.Values["GalleryEndpoint"] = val.(string) + } + if val, ok := authFile["managementEndpointUrl"]; ok { + s.Values["ManagementEndpoint"] = val.(string) + } + + resource := azure.PublicCloud.ResourceIdentifiers.KeyVault + if a, err := s.ClientCredentialsAuthorizerWithResource(resource); err == nil { + return a, err + } + if a, err := s.ClientCertificateAuthorizerWithResource(resource); err == nil { + return a, err + } + return nil, errors.New("auth file missing client and certificate credentials") +} + +func (ks *Server) encryptWithAzureKeyvault(key *keyservice.AzureKeyVaultKey, plaintext []byte) ([]byte, error) { + var err error + + kv := keyvault.New() + if kv.Authorizer, err = ks.newKeyvaultAuthorizerFromFile(filepath.Join(ks.HomeDir, "azure_kv.json")); err != nil { + return nil, err + } + + plainstring := string(plaintext) + result, err := kv.Encrypt(context.Background(), key.VaultUrl, key.Name, "", keyvault.KeyOperationsParameters{Algorithm: keyvault.RSAOAEP256, Value: &plainstring}) + if err != nil { + return nil, fmt.Errorf("Failed to encrypt data: %w", err) + } + + ciphertext, err := base64.RawURLEncoding.DecodeString(*result.Result) + if err != nil { + return nil, fmt.Errorf("Failed to encrypt data: %w", err) + } + + return ciphertext, nil +} + +func (ks *Server) decryptWithAzureKeyvault(key *keyservice.AzureKeyVaultKey, ciphertext []byte) ([]byte, error) { + var err error + + kv := keyvault.New() + if kv.Authorizer, err = ks.newKeyvaultAuthorizerFromFile(filepath.Join(ks.HomeDir, "azure_kv.json")); err != nil { + return nil, err + } + + cipherstring := string(ciphertext) + result, err := kv.Decrypt(context.Background(), key.VaultUrl, key.Name, key.Version, keyvault.KeyOperationsParameters{Algorithm: keyvault.RSAOAEP256, Value: &cipherstring}) + if err != nil { + return nil, fmt.Errorf("Failed to decrypt data: %w", err) + } + + plaintext, err := base64.RawURLEncoding.DecodeString(*result.Result) + if err != nil { + return nil, fmt.Errorf("Failed to decrypt data: %w", err) + } + + return plaintext, nil +} + // Encrypt takes an encrypt request and encrypts the provided plaintext with the provided key, // returning the encrypted result. func (ks Server) Encrypt(ctx context.Context, @@ -109,6 +256,18 @@ func (ks Server) Encrypt(ctx context.Context, response = &keyservice.EncryptResponse{ Ciphertext: ciphertext, } + case *keyservice.Key_AzureKeyvaultKey: + if _, err := os.Stat(filepath.Join(ks.HomeDir, "azure_kv.json")); os.IsNotExist(err) { + return ks.Encrypt(ctx, req) + } else { + ciphertext, err := ks.encryptWithAzureKeyvault(k.AzureKeyvaultKey, req.Plaintext) + if err != nil { + return nil, err + } + response = &keyservice.EncryptResponse{ + Ciphertext: ciphertext, + } + } default: return ks.Encrypt(ctx, req) } @@ -169,6 +328,18 @@ func (ks Server) Decrypt(ctx context.Context, response = &keyservice.DecryptResponse{ Plaintext: plaintext, } + case *keyservice.Key_AzureKeyvaultKey: + if _, err := os.Stat(filepath.Join(ks.HomeDir, "azure_kv.json")); os.IsNotExist(err) { + return ks.DefaultServer.Decrypt(ctx, req) + } else { + plaintext, err := ks.decryptWithAzureKeyvault(k.AzureKeyvaultKey, req.Ciphertext) + if err != nil { + return nil, err + } + response = &keyservice.DecryptResponse{ + Plaintext: plaintext, + } + } default: return ks.DefaultServer.Decrypt(ctx, req) } diff --git a/main.go b/main.go index e0537004..1a95c7e9 100644 --- a/main.go +++ b/main.go @@ -24,7 +24,7 @@ import ( flag "github.com/spf13/pflag" "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" - _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" + _ "k8s.io/client-go/plugin/pkg/client/auth" "sigs.k8s.io/cli-utils/pkg/kstatus/polling" ctrl "sigs.k8s.io/controller-runtime" crtlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics"