From 343f937e59555eb533801f6625c7ee59d112429e Mon Sep 17 00:00:00 2001 From: David Quagebeur Date: Fri, 19 Nov 2021 17:03:10 +0100 Subject: [PATCH 1/7] sops/azkv: support for credentials via SecretRef Signed-off-by: David Quagebeur --- controllers/kustomization_decryptor.go | 7 ++ go.mod | 10 +- go.sum | 19 ++-- internal/sops/azkv/keysource.go | 150 +++++++++++++++++++++++++ internal/sops/keyservice/server.go | 68 +++++++++++ main.go | 1 + 6 files changed, 240 insertions(+), 15 deletions(-) create mode 100644 internal/sops/azkv/keysource.go diff --git a/controllers/kustomization_decryptor.go b/controllers/kustomization_decryptor.go index a403bdfc..6bf2455a 100644 --- a/controllers/kustomization_decryptor.go +++ b/controllers/kustomization_decryptor.go @@ -155,6 +155,13 @@ func (kd *KustomizeDecryptor) ImportKeys(ctx context.Context) error { var ageIdentities []string var vaultToken string for name, value := range secret.Data { + if name == intkeyservice.SOPSCredentialsFileAzureKeyvault { + 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) + } + continue + } switch filepath.Ext(name) { case ".asc": keyPath, err := securejoin.SecureJoin(tmpDir, name) diff --git a/go.mod b/go.mod index 9468406b..42660a83 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,12 @@ 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 + github.com/Azure/go-autorest/autorest v0.11.22 + github.com/Azure/go-autorest/autorest/azure/auth v0.5.9 github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3 github.com/cyphar/filepath-securejoin v0.2.2 + github.com/dimchansky/utfbom v1.1.1 github.com/drone/envsubst v1.0.3-0.20200804185402-58bc65f69603 github.com/fluxcd/kustomize-controller/api v0.21.1 github.com/fluxcd/pkg/apis/acl v0.0.3 @@ -39,12 +43,9 @@ require ( require ( cloud.google.com/go v0.99.0 // indirect - github.com/Azure/azure-sdk-for-go v31.2.0+incompatible // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect - github.com/Azure/go-autorest/autorest v0.11.21 // indirect github.com/Azure/go-autorest/autorest/adal v0.9.14 // indirect - github.com/Azure/go-autorest/autorest/azure/auth v0.5.8 // indirect github.com/Azure/go-autorest/autorest/azure/cli v0.4.2 // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect @@ -67,7 +68,6 @@ require ( github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect github.com/containerd/continuity v0.2.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dimchansky/utfbom v1.1.1 // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect @@ -159,7 +159,7 @@ require ( go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.19.1 // indirect - golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect diff --git a/go.sum b/go.sum index 913bed35..da269a04 100644 --- a/go.sum +++ b/go.sum @@ -56,8 +56,9 @@ filippo.io/age v1.0.0 h1:V6q14n0mqYU3qKFkZ6oOaF9oXneOviS3ubXsSVBRSzc= filippo.io/age v1.0.0/go.mod h1:PaX+Si/Sd5G8LgfCwldsSba3H1DDQZhIhFGkhbHaBq8= filippo.io/edwards25519 v1.0.0-alpha.2/go.mod h1:X+pm78QAUPtFLi1z9PYIlS/bdDnvbCOGKtZ+ACWEf7o= 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= @@ -66,20 +67,19 @@ github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.1.0/go.mod h1:AKyIcETwSUFxIcs/Wnq/C+kwCtlEYGUVd7FPNb2slmg= github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= -github.com/Azure/go-autorest/autorest v0.11.17/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= -github.com/Azure/go-autorest/autorest v0.11.21 h1:w77zY/9RnUAWcIQyDC0Fc89mCvwftR8F+zsR/OH6enk= -github.com/Azure/go-autorest/autorest v0.11.21/go.mod h1:Do/yuMSW/13ayUkcVREpsMHGG+MvV81uzSCFgYPj4tM= +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.11/go.mod h1:nBKAnTomx8gDtl+3ZCJv2v0KACFHWTB2drffI1B68Pk= 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/go.mod h1:Gf7/i2FUpyb/sGBLIFxTBzrNzBo7aPXXE3ZVeDRwdpM= -github.com/Azure/go-autorest/autorest/azure/auth v0.5.8 h1:TzPg6B6fTZ0G1zBf3T54aI7p3cAT6u//TOXGPmFMOXg= -github.com/Azure/go-autorest/autorest/azure/auth v0.5.8/go.mod h1:kxyKZTSfKh8OVFWPAgOgQ/frrJgeYQJPyR5fLFmXko4= +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/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= @@ -97,7 +97,6 @@ github.com/Azure/go-autorest/autorest/validation v0.2.0/go.mod h1:3EEqHnBxQGHXRY github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac= github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= -github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvdeRAgDr0izn4z5Ij88= @@ -925,10 +924,10 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -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/azkv/keysource.go b/internal/sops/azkv/keysource.go new file mode 100644 index 00000000..326721ed --- /dev/null +++ b/internal/sops/azkv/keysource.go @@ -0,0 +1,150 @@ +package azkv + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/binary" + "encoding/json" + "fmt" + "io/ioutil" + "unicode/utf16" + + "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" +) + +type Key struct { + VaultUrl string + Name string + Version string + + keyvaultAuthorizer autorest.Authorizer +} + +func (key *Key) LoadCredentialsFromFile(fileLocation string) error { + s := auth.FileSettings{} + s.Values = map[string]string{} + + contents, err := ioutil.ReadFile(fileLocation) + if err != nil { + return 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 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 err + } + decoded = []byte(string(utf16.Decode(u16))) + default: + decoded, err = ioutil.ReadAll(reader) + if err != nil { + return err + } + } + + // unmarshal file + authFile := map[string]interface{}{} + err = json.Unmarshal(decoded, &authFile) + if err != nil { + return 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) + } + + key.keyvaultAuthorizer, err = s.ClientCredentialsAuthorizerWithResource(azure.PublicCloud.ResourceIdentifiers.KeyVault) + if err != nil { + return fmt.Errorf("failed to load azure credentials file: %w", err) + } + + return nil +} + +func (key *Key) Encrypt(plaintext []byte) ([]byte, error) { + plainstring := string(plaintext) + + kv := keyvault.New() + kv.Authorizer = key.keyvaultAuthorizer + + 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 (key *Key) Decrypt(ciphertext []byte) ([]byte, error) { + cipherstring := string(ciphertext) + + kv := keyvault.New() + kv.Authorizer = key.keyvaultAuthorizer + + 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 +} diff --git a/internal/sops/keyservice/server.go b/internal/sops/keyservice/server.go index 3007c7cd..6a4b5590 100644 --- a/internal/sops/keyservice/server.go +++ b/internal/sops/keyservice/server.go @@ -6,6 +6,8 @@ package keyservice import ( "fmt" + "os" + "path/filepath" "go.mozilla.org/sops/v3/keyservice" "golang.org/x/net/context" @@ -13,10 +15,16 @@ import ( "google.golang.org/grpc/status" "github.com/fluxcd/kustomize-controller/internal/sops/age" + "github.com/fluxcd/kustomize-controller/internal/sops/azkv" "github.com/fluxcd/kustomize-controller/internal/sops/hcvault" "github.com/fluxcd/kustomize-controller/internal/sops/pgp" ) +const ( + // SOPSCredentialsFileAzureKeyvault is the expected filename for a JSON file containing an Azure Key Vault authentication file. + SOPSCredentialsFileAzureKeyvault = "azure_kv.json" +) + // Server is a key service server that uses SOPS MasterKeys to fulfill // requests. It intercepts encryption and decryption requests made for // PGP and Age keys, so that they can be run in a contained environment @@ -103,6 +111,41 @@ func (ks *Server) decryptWithVault(key *keyservice.VaultKey, ciphertext []byte) return []byte(plaintext), err } +func (ks *Server) encryptWithAzureKeyvault(key *keyservice.AzureKeyVaultKey, plaintext []byte) ([]byte, error) { + azureKey := azkv.Key{ + VaultUrl: key.VaultUrl, + Name: key.Name, + Version: key.Version, + } + err := azureKey.LoadCredentialsFromFile(filepath.Join(ks.HomeDir, SOPSCredentialsFileAzureKeyvault)) + if err != nil { + return nil, fmt.Errorf("azure credentials file missing: %w", err) + } + ciphertext, err := azureKey.Encrypt(plaintext) + if err != nil { + return nil, err + } + return ciphertext, nil +} + +func (ks *Server) decryptWithAzureKeyvault(key *keyservice.AzureKeyVaultKey, ciphertext []byte) ([]byte, error) { + azureKey := azkv.Key{ + VaultUrl: key.VaultUrl, + Name: key.Name, + Version: key.Version, + } + err := azureKey.LoadCredentialsFromFile(filepath.Join(ks.HomeDir, SOPSCredentialsFileAzureKeyvault)) + if err != nil { + return nil, fmt.Errorf("azure credentials file missing: %w", err) + } + + plaintext, err := azureKey.Decrypt(ciphertext) + if err != nil { + return nil, 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, @@ -126,6 +169,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, SOPSCredentialsFileAzureKeyvault)); 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.DefaultServer.Encrypt(ctx, req) } @@ -200,6 +255,19 @@ func (ks Server) Decrypt(ctx context.Context, response = &keyservice.DecryptResponse{ Plaintext: plaintext, } + case *keyservice.Key_AzureKeyvaultKey: + if _, err := os.Stat(filepath.Join(ks.HomeDir, SOPSCredentialsFileAzureKeyvault)); 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 0eafd6de..c1a11ac1 100644 --- a/main.go +++ b/main.go @@ -24,6 +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/azure" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" "sigs.k8s.io/cli-utils/pkg/kstatus/polling" ctrl "sigs.k8s.io/controller-runtime" From a72e6876073f4f4c57e636075069f583155bdf32 Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Tue, 7 Dec 2021 16:59:59 +0100 Subject: [PATCH 2/7] sops/azkv: refactor initial Azure Keyvault impl - Ensure key source follows upstream SOPS contracts as closely as possible (e.g. `MasterKey` interface). - Prevent unnecesary FS operations by allowing token creation and and authorizer configuration to be factored from file bytes. - Ensure a limited number of configuration option is taken into account, excluding e.g. file path references. - Ensure server maintains backwards compatibility with previously supported "global" Azure configuration, _without_ relying on file assumptions and/or inspections (but rather, server configurations). Signed-off-by: Hidde Beydals --- controllers/kustomization_decryptor.go | 24 ++- go.mod | 35 ++-- go.sum | 47 ++--- internal/sops/azkv/keysource.go | 231 +++++++++++++++---------- internal/sops/keyservice/server.go | 87 +++++----- 5 files changed, 226 insertions(+), 198 deletions(-) diff --git a/controllers/kustomization_decryptor.go b/controllers/kustomization_decryptor.go index 6bf2455a..2f9c625e 100644 --- a/controllers/kustomization_decryptor.go +++ b/controllers/kustomization_decryptor.go @@ -42,6 +42,7 @@ import ( "sigs.k8s.io/yaml" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + "github.com/fluxcd/kustomize-controller/internal/sops/azkv" intkeyservice "github.com/fluxcd/kustomize-controller/internal/sops/keyservice" ) @@ -50,14 +51,18 @@ const ( DecryptionProviderSOPS = "sops" // DecryptionVaultTokenFileName is the name of the file containing the Vault token DecryptionVaultTokenFileName = "sops.vault-token" + // DecryptionAzureAuthFile is the Azure authentication file + DecryptionAzureAuthFile = "azure_kv.json" ) type KustomizeDecryptor struct { client.Client - kustomization kustomizev1.Kustomization - homeDir string - ageIdentities []string - vaultToken string + + kustomization kustomizev1.Kustomization + homeDir string + ageIdentities []string + vaultToken string + azureAADConfig *azkv.AADSettings } func NewDecryptor(kubeClient client.Client, @@ -155,11 +160,12 @@ func (kd *KustomizeDecryptor) ImportKeys(ctx context.Context) error { var ageIdentities []string var vaultToken string for name, value := range secret.Data { - if name == intkeyservice.SOPSCredentialsFileAzureKeyvault { - 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) + if name == DecryptionAzureAuthFile { + azureConf := azkv.AADSettings{} + if err = azkv.LoadAADSettingsFromBytes(value, &azureConf); err != nil { + return err } + kd.azureAADConfig = &azureConf continue } switch filepath.Ext(name) { @@ -279,7 +285,7 @@ func (kd KustomizeDecryptor) DataWithFormat(data []byte, inputFormat, outputForm metadataKey, err := tree.Metadata.GetDataKeyWithKeyServices( []keyservice.KeyServiceClient{ - intkeyservice.NewLocalClient(intkeyservice.NewServer(false, kd.homeDir, kd.vaultToken, kd.ageIdentities)), + intkeyservice.NewLocalClient(intkeyservice.NewServer(false, kd.homeDir, kd.vaultToken, kd.ageIdentities, kd.azureAADConfig)), }, ) if err != nil { diff --git a/go.mod b/go.mod index 42660a83..e1313842 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,8 @@ require ( filippo.io/age v1.0.0 github.com/Azure/azure-sdk-for-go v57.2.0+incompatible github.com/Azure/go-autorest/autorest v0.11.22 - github.com/Azure/go-autorest/autorest/azure/auth v0.5.9 + github.com/Azure/go-autorest/autorest/adal v0.9.14 + github.com/Azure/go-autorest/autorest/azure/auth v0.5.9 // indirect github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3 github.com/cyphar/filepath-securejoin v0.2.2 github.com/dimchansky/utfbom v1.1.1 @@ -45,11 +46,10 @@ require ( cloud.google.com/go v0.99.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect - github.com/Azure/go-autorest/autorest/adal v0.9.14 // indirect github.com/Azure/go-autorest/autorest/azure/cli v0.4.2 // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect - github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect - github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect + github.com/Azure/go-autorest/autorest/to v0.3.0 // indirect + github.com/Azure/go-autorest/autorest/validation v0.2.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect @@ -59,29 +59,28 @@ require ( github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/armon/go-metrics v0.3.10 // indirect github.com/armon/go-radix v1.0.0 // indirect - github.com/aws/aws-sdk-go v1.37.19 // indirect + github.com/aws/aws-sdk-go v1.37.18 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver v3.5.1+incompatible // indirect github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/cenkalti/backoff/v3 v3.0.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect - github.com/containerd/continuity v0.2.1 // indirect + github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect github.com/fatih/color v1.13.0 // indirect - github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect + github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect - github.com/go-errors/errors v1.4.1 // indirect + github.com/go-errors/errors v1.0.1 // indirect github.com/go-logr/logr v1.2.2 // indirect github.com/go-logr/zapr v1.2.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect - github.com/go-test/deep v1.0.8 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect @@ -107,10 +106,10 @@ require ( github.com/hashicorp/go-secure-stdlib/strutil v0.1.1 // indirect github.com/hashicorp/go-sockaddr v1.0.2 // indirect github.com/hashicorp/go-uuid v1.0.2 // indirect - github.com/hashicorp/go-version v1.3.0 // indirect + github.com/hashicorp/go-version v1.2.0 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect - github.com/hashicorp/hcl v1.0.1-vault-3 // indirect - github.com/hashicorp/vault/sdk v0.3.1-0.20211209192327-a0822e64eae0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hashicorp/vault/sdk v0.3.0 // indirect github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c // indirect github.com/imdario/mergo v0.3.12 // indirect @@ -126,7 +125,7 @@ require ( github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/go-testing-interface v1.14.0 // indirect + github.com/mitchellh/go-testing-interface v1.0.0 // indirect github.com/mitchellh/go-wordwrap v1.0.0 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect github.com/mitchellh/reflectwalk v1.0.0 // indirect @@ -137,7 +136,7 @@ require ( github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/oklog/run v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/opencontainers/image-spec v1.0.1 // indirect github.com/opencontainers/runc v0.1.1 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pierrec/lz4 v2.5.2+incompatible // indirect @@ -172,7 +171,7 @@ require ( google.golang.org/protobuf v1.27.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.66.2 // indirect - gopkg.in/square/go-jose.v2 v2.6.0 // indirect + gopkg.in/square/go-jose.v2 v2.5.1 // indirect gopkg.in/urfave/cli.v1 v1.20.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect @@ -186,10 +185,10 @@ require ( sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect ) -// pin kustomize to v4.5.2 +// pin kustomize to v4.4.1 replace ( - sigs.k8s.io/kustomize/api => sigs.k8s.io/kustomize/api v0.11.2 - sigs.k8s.io/kustomize/kyaml => sigs.k8s.io/kustomize/kyaml v0.13.3 + sigs.k8s.io/kustomize/api => sigs.k8s.io/kustomize/api v0.10.1 + sigs.k8s.io/kustomize/kyaml => sigs.k8s.io/kustomize/kyaml v0.13.0 ) // Fix CVE-2021-30465 diff --git a/go.sum b/go.sum index da269a04..012ccefc 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,3 @@ -bazil.org/fuse v0.0.0-20200407214033-5883e5a4b512/go.mod h1:FbcW6z/2VytnFDhZfumh8Ss8zxHE6qpMP5sHTRe0EaM= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -90,12 +89,10 @@ github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxB github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/to v0.3.0 h1:zebkZaadz7+wIQYgC7GXaz3Wb28yKYfVkkBKwc38VF8= github.com/Azure/go-autorest/autorest/to v0.3.0/go.mod h1:MgwOyqaIuKdG4TL/2ywSsIWKAfJfgHDo8ObuUk3t5sA= -github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= -github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= +github.com/Azure/go-autorest/autorest/validation v0.2.0 h1:15vMO4y76dehZSq7pAaOLQxC6dZYsSrj2GQpflyM/L4= github.com/Azure/go-autorest/autorest/validation v0.2.0/go.mod h1:3EEqHnBxQGHXRYq3HT1WyXAvT7LLY3tl70hw6tQIbjI= -github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac= -github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= @@ -141,9 +138,8 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/aws/aws-sdk-go v1.37.18 h1:SRdWLg+DqMFWX8HB3UvXyAoZpw9IDIUYnSTwgzOYbqg= github.com/aws/aws-sdk-go v1.37.18/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= -github.com/aws/aws-sdk-go v1.37.19 h1:/xKHoSsYfH9qe16pJAHIjqTVpMM2DRSsEt8Ok1bzYiw= -github.com/aws/aws-sdk-go v1.37.19/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -195,9 +191,8 @@ github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= +github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc h1:TP+534wVlf61smEIq1nwLLAjQVEK2EADoW3CX9AuT+8= github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.2.1 h1:/EeEo2EtN3umhbbgCveyjifoMYg0pS+nMMEemaYw634= -github.com/containerd/continuity v0.2.1/go.mod h1:wCYX+dRqZdImhGucXOqTQn05AhX6EUDaGEMUzTFFpLg= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -283,9 +278,8 @@ github.com/fluxcd/pkg/untar v0.1.0/go.mod h1:aGswNyzB1mlz/T/kpOS58mITBMxMKc9tlJB github.com/fluxcd/source-controller/api v0.21.2 h1:J0S5NN4V8FPLrkSMXIUoUvj1X/RuTpVJSjIRF414wmc= github.com/fluxcd/source-controller/api v0.21.2/go.mod h1:Ab2qDmAUz6ZCp8UaHYLYzxyFrC1FQqEqjxiROb/Rdiw= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c= github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/form3tech-oss/jwt-go v3.2.5+incompatible h1:/l4kBbb4/vGSsdtB5nUe8L7B9mImVMaBPw9L/0TBHU8= -github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk= @@ -299,9 +293,8 @@ github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSy github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= -github.com/go-errors/errors v1.4.1 h1:IvVlgbzSsaUNudsw5dcXSzF3EWyXTi5XrAdngnuhRyg= -github.com/go-errors/errors v1.4.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -332,9 +325,8 @@ github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= -github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -517,17 +509,15 @@ github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw= -github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/hcl v1.0.1-vault-3 h1:V95v5KSTu6DB5huDSKiq4uAfILEuNigK/+qPET6H/Mg= -github.com/hashicorp/hcl v1.0.1-vault-3/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= @@ -542,9 +532,8 @@ github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoI github.com/hashicorp/vault/api v1.3.0 h1:uDy39PLSvy6gtKyjOCRPizy2QdFiIYSWBR2pxCEzYL8= github.com/hashicorp/vault/api v1.3.0/go.mod h1:EabNQLI0VWbWoGlA+oBLC8PXmR9D60aUVgQGvangFWQ= github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M= +github.com/hashicorp/vault/sdk v0.3.0 h1:kR3dpxNkhh/wr6ycaJYqp6AFT/i2xaftbfnwZduTKEY= github.com/hashicorp/vault/sdk v0.3.0/go.mod h1:aZ3fNuL5VNydQk8GcLJ2TV8YCRVvyaakYkhZRoVuhj0= -github.com/hashicorp/vault/sdk v0.3.1-0.20211209192327-a0822e64eae0 h1:bFbj+/yH2Qs3GBbyR48Yde0nYA19FEn1A0jS5a0Jjbo= -github.com/hashicorp/vault/sdk v0.3.1-0.20211209192327-a0822e64eae0/go.mod h1:aZ3fNuL5VNydQk8GcLJ2TV8YCRVvyaakYkhZRoVuhj0= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= @@ -640,9 +629,8 @@ github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-testing-interface v1.14.0 h1:/x0XQ6h+3U3nAyk1yx+bHPURrKa9sVVvYbuqZ7pIAtI= -github.com/mitchellh/go-testing-interface v1.14.0/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= @@ -839,7 +827,6 @@ github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= -github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= @@ -1085,7 +1072,6 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1423,9 +1409,8 @@ gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= -gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/urfave/cli.v1 v1.20.0 h1:NdAVW6RYxDif9DhDHaAortIu956m2c0v+09AZBPTbE0= @@ -1510,12 +1495,12 @@ sigs.k8s.io/controller-runtime v0.11.1/go.mod h1:KKwLiTooNGu+JmLZGn9Sl3Gjmfj66eM sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 h1:kDi4JBNAsJWfz1aEXhO8Jg87JJaPNLh5tIzYHgStQ9Y= sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY= -sigs.k8s.io/kustomize/api v0.11.2 h1:6YvCJHFDwsLwAX7zNHBxMZi3k7dGIXI8G9l0saYQI0E= -sigs.k8s.io/kustomize/api v0.11.2/go.mod h1:GZuhith5YcqxIDe0GnRJNx5xxPTjlwaLTt/e+ChUtJA= +sigs.k8s.io/kustomize/api v0.10.1 h1:KgU7hfYoscuqag84kxtzKdEC3mKMb99DPI3a0eaV1d0= +sigs.k8s.io/kustomize/api v0.10.1/go.mod h1:2FigT1QN6xKdcnGS2Ppp1uIWrtWN28Ms8A3OZUZhwr8= sigs.k8s.io/kustomize/cmd/config v0.10.2/go.mod h1:K2aW7nXJ0AaT+VA/eO0/dzFLxmpFcTzudmAgDwPY1HQ= sigs.k8s.io/kustomize/kustomize/v4 v4.4.1/go.mod h1:qOKJMMz2mBP+vcS7vK+mNz4HBLjaQSWRY22EF6Tb7Io= -sigs.k8s.io/kustomize/kyaml v0.13.3 h1:tNNQIC+8cc+aXFTVg+RtQAOsjwUdYBZRAgYOVI3RBc4= -sigs.k8s.io/kustomize/kyaml v0.13.3/go.mod h1:/ya3Gk4diiQzlE4mBh7wykyLRFZNvqlbh+JnwQ9Vhrc= +sigs.k8s.io/kustomize/kyaml v0.13.0 h1:9c+ETyNfSrVhxvphs+K2dzT3dh5oVPPEqPOE/cUpScY= +sigs.k8s.io/kustomize/kyaml v0.13.0/go.mod h1:FTJxEZ86ScK184NpGSAQcfEqee0nul8oLCK30D47m4E= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= sigs.k8s.io/structured-merge-diff/v4 v4.2.0/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= diff --git a/internal/sops/azkv/keysource.go b/internal/sops/azkv/keysource.go index 326721ed..e150eb04 100644 --- a/internal/sops/azkv/keysource.go +++ b/internal/sops/azkv/keysource.go @@ -1,3 +1,7 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + package azkv import ( @@ -12,131 +16,119 @@ import ( "github.com/Azure/azure-sdk-for-go/profiles/latest/keyvault/keyvault" "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/adal" "github.com/Azure/go-autorest/autorest/azure" - "github.com/Azure/go-autorest/autorest/azure/auth" "github.com/dimchansky/utfbom" ) -type Key struct { - VaultUrl string +// MasterKey is an Azure Key Vault key used to encrypt and decrypt SOPS' data key. +// The underlying authentication token can be configured using AADSettings. +type MasterKey struct { + VaultURL string Name string Version string - keyvaultAuthorizer autorest.Authorizer -} + EncryptedKey string -func (key *Key) LoadCredentialsFromFile(fileLocation string) error { - s := auth.FileSettings{} - s.Values = map[string]string{} + token *adal.ServicePrincipalToken +} - contents, err := ioutil.ReadFile(fileLocation) +// LoadAADSettingsFromBytes attempts to load the given bytes into the given AADSettings. +// By first decoding it if UTF-16, and then unmarshalling it into the given struct. +// It returns an error for any failure. +func LoadAADSettingsFromBytes(b []byte, s *AADSettings) error { + b, err := decode(b) if err != nil { - return err + return fmt.Errorf("failed to decode Azure authentication file bytes: %w", 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 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 err - } - decoded = []byte(string(utf16.Decode(u16))) - default: - decoded, err = ioutil.ReadAll(reader) - if err != nil { - return err - } + if err = json.Unmarshal(b, s); err != nil { + err = fmt.Errorf("failed to unmarshal Azure authentication file: %w", err) } + return err +} - // unmarshal file - authFile := map[string]interface{}{} - err = json.Unmarshal(decoded, &authFile) - if err != nil { - return err - } +// AADSettings contains the selection of fields from an Azure authentication file +// required for Active Directory authentication. +// +// It is based on the unpublished contract in +// https://github.com/Azure/go-autorest/blob/c7f947c0610de1bc279f76e6d453353f95cd1bfa/autorest/azure/auth/auth.go#L331-L342, +// which seems to be due to an assumption of configuration through environment +// variables over file based configuration. +type AADSettings struct { + ClientID string `json:"clientId"` + ClientSecret string `json:"clientSecret"` + TenantID string `json:"tenantId"` + ActiveDirectoryEndpoint string `json:"activeDirectoryEndpointUrl"` +} - 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) +// SetToken configures the token on the given MasterKey using the AADSettings. +func (s *AADSettings) SetToken(key *MasterKey) error { + if s == nil { + return nil } - 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) + config, err := adal.NewOAuthConfig(s.GetAADEndpoint(), s.TenantID) + if err != nil { + return err } - if val, ok := authFile["managementEndpointUrl"]; ok { - s.Values["ManagementEndpoint"] = val.(string) + if key.token, err = adal.NewServicePrincipalToken(*config, s.ClientID, s.ClientSecret, + azure.PublicCloud.ResourceIdentifiers.KeyVault); err != nil { + return err } + return nil +} - key.keyvaultAuthorizer, err = s.ClientCredentialsAuthorizerWithResource(azure.PublicCloud.ResourceIdentifiers.KeyVault) - if err != nil { - return fmt.Errorf("failed to load azure credentials file: %w", err) +// GetAADEndpoint returns the ActiveDirectoryEndpoint, or the Azure Public Cloud +// default. +func (s *AADSettings) GetAADEndpoint() string { + if s.ActiveDirectoryEndpoint != "" { + return s.ActiveDirectoryEndpoint } + return azure.PublicCloud.ActiveDirectoryEndpoint +} - return nil +// EncryptedDataKey returns the encrypted data key this master key holds. +func (key *MasterKey) EncryptedDataKey() []byte { + return []byte(key.EncryptedKey) } -func (key *Key) Encrypt(plaintext []byte) ([]byte, error) { - plainstring := string(plaintext) +// SetEncryptedDataKey sets the encrypted data key for this master key. +func (key *MasterKey) SetEncryptedDataKey(enc []byte) { + key.EncryptedKey = string(enc) +} - kv := keyvault.New() - kv.Authorizer = key.keyvaultAuthorizer +// Encrypt takes a SOPS data key, encrypts it with Key Vault and stores the result in the EncryptedKey field. +func (key *MasterKey) Encrypt(plaintext []byte) error { + c := newKeyvaultClient(key.authorizer()) - result, err := kv.Encrypt(context.Background(), key.VaultUrl, key.Name, "", keyvault.KeyOperationsParameters{Algorithm: keyvault.RSAOAEP256, Value: &plainstring}) + data := base64.RawURLEncoding.EncodeToString(plaintext) + p := keyvault.KeyOperationsParameters{Value: &data, Algorithm: keyvault.RSAOAEP256} + res, err := c.Encrypt(context.Background(), key.VaultURL, key.Name, key.Version, p) if err != nil { - return nil, fmt.Errorf("failed to encrypt data: %w", err) + return fmt.Errorf("failed to encrypt data: %w", err) } - ciphertext, err := base64.RawURLEncoding.DecodeString(*result.Result) + key.EncryptedKey = *res.Result if err != nil { - return nil, fmt.Errorf("failed to encrypt data: %w", err) + return fmt.Errorf("failed to encrypt data: %w", err) } - - return ciphertext, nil + return nil } -func (key *Key) Decrypt(ciphertext []byte) ([]byte, error) { - cipherstring := string(ciphertext) +// EncryptIfNeeded encrypts the provided SOPS' data key and encrypts it if it hasn't been encrypted yet. +func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error { + if key.EncryptedKey == "" { + return key.Encrypt(dataKey) + } + return nil +} - kv := keyvault.New() - kv.Authorizer = key.keyvaultAuthorizer +// Decrypt decrypts the EncryptedKey field with Azure Key Vault and returns the result. +func (key *MasterKey) Decrypt() ([]byte, error) { + c := newKeyvaultClient(key.authorizer()) - result, err := kv.Decrypt(context.Background(), key.VaultUrl, key.Name, key.Version, keyvault.KeyOperationsParameters{Algorithm: keyvault.RSAOAEP256, Value: &cipherstring}) + result, err := c.Decrypt(context.Background(), key.VaultURL, key.Name, key.Version, keyvault.KeyOperationsParameters{ + Algorithm: keyvault.RSAOAEP256, Value: &key.EncryptedKey, + }) if err != nil { return nil, fmt.Errorf("failed to decrypt data: %w", err) } @@ -145,6 +137,59 @@ func (key *Key) Decrypt(ciphertext []byte) ([]byte, error) { if err != nil { return nil, fmt.Errorf("failed to decrypt data: %w", err) } - return plaintext, nil } + +// NeedsRotation returns whether the data key needs to be rotated or not. +func (key *MasterKey) NeedsRotation() bool { + return key.token.Token().IsExpired() +} + +// ToString converts the key to a string representation. +func (key *MasterKey) ToString() string { + return fmt.Sprintf("%s/keys/%s/%s", key.VaultURL, key.Name, key.Version) +} + +// ToMap converts the MasterKey to a map for serialization purposes. +func (key MasterKey) ToMap() map[string]interface{} { + out := make(map[string]interface{}) + out["vaultUrl"] = key.VaultURL + out["key"] = key.Name + out["version"] = key.Version + out["enc"] = key.EncryptedKey + return out +} + +func (key *MasterKey) authorizer() autorest.Authorizer { + if key.token == nil { + return &autorest.NullAuthorizer{} + } + return autorest.NewBearerAuthorizer(key.token) +} + +func newKeyvaultClient(authorizer autorest.Authorizer) keyvault.BaseClient { + c := keyvault.New() + c.Authorizer = authorizer + return c +} + +func decode(b []byte) ([]byte, error) { + reader, enc := utfbom.Skip(bytes.NewReader(b)) + switch enc { + case utfbom.UTF16LittleEndian: + u16 := make([]uint16, (len(b)/2)-1) + err := binary.Read(reader, binary.LittleEndian, &u16) + if err != nil { + return nil, err + } + return []byte(string(utf16.Decode(u16))), nil + case utfbom.UTF16BigEndian: + u16 := make([]uint16, (len(b)/2)-1) + err := binary.Read(reader, binary.BigEndian, &u16) + if err != nil { + return nil, err + } + return []byte(string(utf16.Decode(u16))), nil + } + return ioutil.ReadAll(reader) +} diff --git a/internal/sops/keyservice/server.go b/internal/sops/keyservice/server.go index 6a4b5590..7b42feb6 100644 --- a/internal/sops/keyservice/server.go +++ b/internal/sops/keyservice/server.go @@ -6,8 +6,6 @@ package keyservice import ( "fmt" - "os" - "path/filepath" "go.mozilla.org/sops/v3/keyservice" "golang.org/x/net/context" @@ -20,11 +18,6 @@ import ( "github.com/fluxcd/kustomize-controller/internal/sops/pgp" ) -const ( - // SOPSCredentialsFileAzureKeyvault is the expected filename for a JSON file containing an Azure Key Vault authentication file. - SOPSCredentialsFileAzureKeyvault = "azure_kv.json" -) - // Server is a key service server that uses SOPS MasterKeys to fulfill // requests. It intercepts encryption and decryption requests made for // PGP and Age keys, so that they can be run in a contained environment @@ -45,17 +38,22 @@ type Server struct { // VaultToken configures the Vault token used by the server. VaultToken string + // AzureAADConfig configures the Azure Active Directory settings used + // by the server. + AzureAADConfig *azkv.AADSettings + // DefaultServer is the server used for any other request than a PGP // or age encryption/decryption. DefaultServer keyservice.KeyServiceServer } -func NewServer(prompt bool, homeDir, vaultToken string, agePrivateKeys []string) keyservice.KeyServiceServer { +func NewServer(prompt bool, homeDir, vaultToken string, agePrivateKeys []string, azureCfg *azkv.AADSettings) keyservice.KeyServiceServer { server := &Server{ Prompt: prompt, HomeDir: homeDir, AgePrivateKeys: agePrivateKeys, VaultToken: vaultToken, + AzureAADConfig: azureCfg, DefaultServer: &keyservice.Server{ Prompt: prompt, }, @@ -112,38 +110,32 @@ func (ks *Server) decryptWithVault(key *keyservice.VaultKey, ciphertext []byte) } func (ks *Server) encryptWithAzureKeyvault(key *keyservice.AzureKeyVaultKey, plaintext []byte) ([]byte, error) { - azureKey := azkv.Key{ - VaultUrl: key.VaultUrl, + azureKey := azkv.MasterKey{ + VaultURL: key.VaultUrl, Name: key.Name, Version: key.Version, } - err := azureKey.LoadCredentialsFromFile(filepath.Join(ks.HomeDir, SOPSCredentialsFileAzureKeyvault)) - if err != nil { - return nil, fmt.Errorf("azure credentials file missing: %w", err) + if err := ks.AzureAADConfig.SetToken(&azureKey); err != nil { + return nil, fmt.Errorf("failed to set token for Azure encryption request: %w", err) } - ciphertext, err := azureKey.Encrypt(plaintext) - if err != nil { + if err := azureKey.Encrypt(plaintext); err != nil { return nil, err } - return ciphertext, nil + return []byte(azureKey.EncryptedKey), nil } func (ks *Server) decryptWithAzureKeyvault(key *keyservice.AzureKeyVaultKey, ciphertext []byte) ([]byte, error) { - azureKey := azkv.Key{ - VaultUrl: key.VaultUrl, + azureKey := azkv.MasterKey{ + VaultURL: key.VaultUrl, Name: key.Name, Version: key.Version, } - err := azureKey.LoadCredentialsFromFile(filepath.Join(ks.HomeDir, SOPSCredentialsFileAzureKeyvault)) - if err != nil { - return nil, fmt.Errorf("azure credentials file missing: %w", err) + if err := ks.AzureAADConfig.SetToken(&azureKey); err != nil { + return nil, fmt.Errorf("failed to set token for Azure decryption request: %w", err) } - - plaintext, err := azureKey.Decrypt(ciphertext) - if err != nil { - return nil, err - } - return plaintext, nil + azureKey.EncryptedKey = string(ciphertext) + plaintext, err := azureKey.Decrypt() + return plaintext, err } // Encrypt takes an encrypt request and encrypts the provided plaintext with the provided key, @@ -170,16 +162,17 @@ func (ks Server) Encrypt(ctx context.Context, Ciphertext: ciphertext, } case *keyservice.Key_AzureKeyvaultKey: - if _, err := os.Stat(filepath.Join(ks.HomeDir, SOPSCredentialsFileAzureKeyvault)); os.IsNotExist(err) { + // Fallback to default server if no custom settings are configured + // to ensure backwards compatibility with global configurations + if ks.AzureAADConfig == nil { 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, - } + } + ciphertext, err := ks.encryptWithAzureKeyvault(k.AzureKeyvaultKey, req.Plaintext) + if err != nil { + return nil, err + } + response = &keyservice.EncryptResponse{ + Ciphertext: ciphertext, } default: return ks.DefaultServer.Encrypt(ctx, req) @@ -256,17 +249,17 @@ func (ks Server) Decrypt(ctx context.Context, Plaintext: plaintext, } case *keyservice.Key_AzureKeyvaultKey: - if _, err := os.Stat(filepath.Join(ks.HomeDir, SOPSCredentialsFileAzureKeyvault)); 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, - } + // Fallback to default server if no custom settings are configured + // to ensure backwards compatibility with global configurations + if ks.AzureAADConfig == nil { + return ks.Decrypt(ctx, req) + } + 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) From f67efe0dc6a7b767deafa093d079ae9725cf4789 Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Wed, 12 Jan 2022 10:43:43 +0100 Subject: [PATCH 3/7] sops/azkv: configure retry and throttle on client Signed-off-by: Hidde Beydals --- internal/sops/azkv/keysource.go | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/internal/sops/azkv/keysource.go b/internal/sops/azkv/keysource.go index e150eb04..a15a24fc 100644 --- a/internal/sops/azkv/keysource.go +++ b/internal/sops/azkv/keysource.go @@ -12,6 +12,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "time" "unicode/utf16" "github.com/Azure/azure-sdk-for-go/profiles/latest/keyvault/keyvault" @@ -98,7 +99,7 @@ func (key *MasterKey) SetEncryptedDataKey(enc []byte) { // Encrypt takes a SOPS data key, encrypts it with Key Vault and stores the result in the EncryptedKey field. func (key *MasterKey) Encrypt(plaintext []byte) error { - c := newKeyvaultClient(key.authorizer()) + c := newThrottledKeyvaultClient(key.authorizer()) data := base64.RawURLEncoding.EncodeToString(plaintext) p := keyvault.KeyOperationsParameters{Value: &data, Algorithm: keyvault.RSAOAEP256} @@ -124,7 +125,7 @@ func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error { // Decrypt decrypts the EncryptedKey field with Azure Key Vault and returns the result. func (key *MasterKey) Decrypt() ([]byte, error) { - c := newKeyvaultClient(key.authorizer()) + c := newThrottledKeyvaultClient(key.authorizer()) result, err := c.Decrypt(context.Background(), key.VaultURL, key.Name, key.Version, keyvault.KeyOperationsParameters{ Algorithm: keyvault.RSAOAEP256, Value: &key.EncryptedKey, @@ -167,9 +168,21 @@ func (key *MasterKey) authorizer() autorest.Authorizer { return autorest.NewBearerAuthorizer(key.token) } -func newKeyvaultClient(authorizer autorest.Authorizer) keyvault.BaseClient { +// newThrottledKeyvaultClient returns a client configured to retry requests. +// +// Ref: https://docs.microsoft.com/en-us/azure/key-vault/general/overview-throttling +func newThrottledKeyvaultClient(authorizer autorest.Authorizer) keyvault.BaseClient { + const ( + // Number of times the client will attempt to make an HTTP request + retryAttempts = 6 + // Duration between HTTP request retries + retryDuration = 5 * time.Second + ) + c := keyvault.New() c.Authorizer = authorizer + c.RetryAttempts = retryAttempts + c.RetryDuration = retryDuration return c } From 369193ee591b1c01fb74dc896a8ae53bb935293d Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Thu, 3 Mar 2022 23:37:47 +0100 Subject: [PATCH 4/7] sops/azkv: update to SDK including azidentity This updates to the `github.com/Azure/azure-sdk-for-go` SDK, which is the (apparent) successor of the previous SDK, and allows for easier configuration of credentials through the `azidentity` package. Signed-off-by: Hidde Beydals --- controllers/kustomization_decryptor.go | 6 +- go.mod | 17 ++- go.sum | 43 ++++++- internal/sops/azkv/keysource.go | 150 ++++++++++++------------- internal/sops/keyservice/server.go | 6 +- 5 files changed, 130 insertions(+), 92 deletions(-) diff --git a/controllers/kustomization_decryptor.go b/controllers/kustomization_decryptor.go index 2f9c625e..b80babfb 100644 --- a/controllers/kustomization_decryptor.go +++ b/controllers/kustomization_decryptor.go @@ -62,7 +62,7 @@ type KustomizeDecryptor struct { homeDir string ageIdentities []string vaultToken string - azureAADConfig *azkv.AADSettings + azureAADConfig *azkv.AADConfig } func NewDecryptor(kubeClient client.Client, @@ -161,8 +161,8 @@ func (kd *KustomizeDecryptor) ImportKeys(ctx context.Context) error { var vaultToken string for name, value := range secret.Data { if name == DecryptionAzureAuthFile { - azureConf := azkv.AADSettings{} - if err = azkv.LoadAADSettingsFromBytes(value, &azureConf); err != nil { + azureConf := azkv.AADConfig{} + if err = azkv.LoadAADConfigFromBytes(value, &azureConf); err != nil { return err } kd.azureAADConfig = &azureConf diff --git a/go.mod b/go.mod index e1313842..6a373bc0 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,9 @@ 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 - github.com/Azure/go-autorest/autorest v0.11.22 - github.com/Azure/go-autorest/autorest/adal v0.9.14 - github.com/Azure/go-autorest/autorest/azure/auth v0.5.9 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.13.1 + github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.3.0 github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3 github.com/cyphar/filepath-securejoin v0.2.2 github.com/dimchansky/utfbom v1.1.1 @@ -44,14 +43,21 @@ require ( require ( cloud.google.com/go v0.99.0 // indirect + github.com/Azure/azure-sdk-for-go v31.2.0+incompatible // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.2.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest v0.11.19 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.14 // indirect + github.com/Azure/go-autorest/autorest/azure/auth v0.1.0 // indirect github.com/Azure/go-autorest/autorest/azure/cli v0.4.2 // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/autorest/to v0.3.0 // indirect github.com/Azure/go-autorest/autorest/validation v0.2.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 // indirect github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect github.com/Microsoft/go-winio v0.4.14 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect @@ -82,6 +88,7 @@ require ( github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt v3.2.1+incompatible // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect @@ -117,6 +124,7 @@ require ( github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/lib/pq v1.2.0 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.6 // indirect @@ -140,6 +148,7 @@ require ( github.com/opencontainers/runc v0.1.1 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pierrec/lz4 v2.5.2+incompatible // indirect + github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.12.1 // indirect diff --git a/go.sum b/go.sum index 012ccefc..0791c23d 100644 --- a/go.sum +++ b/go.sum @@ -55,9 +55,23 @@ filippo.io/age v1.0.0 h1:V6q14n0mqYU3qKFkZ6oOaF9oXneOviS3ubXsSVBRSzc= filippo.io/age v1.0.0/go.mod h1:PaX+Si/Sd5G8LgfCwldsSba3H1DDQZhIhFGkhbHaBq8= filippo.io/edwards25519 v1.0.0-alpha.2/go.mod h1:X+pm78QAUPtFLi1z9PYIlS/bdDnvbCOGKtZ+ACWEf7o= 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/azure-sdk-for-go/sdk/azcore v0.20.0/go.mod h1:ZPW/Z0kLCTdDZaDbYTetxc9Cxl/2lNqxYHYNOF2bti0= +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.0/go.mod h1:fBF9PQNqB8scdgpZ3ufzaLntG0AG7C1WjPMsiFOmfHM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1 h1:qoVeMsc9/fh/yhxVaA0obYjVH/oI/ihrOoMwsLS9KSA= +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1/go.mod h1:fBF9PQNqB8scdgpZ3ufzaLntG0AG7C1WjPMsiFOmfHM= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.13.0/go.mod h1:TmXReXZ9yPp5D5TBRMTAtyz+UyOl15Py4hL5E5p6igQ= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.13.1 h1:RxemzI2cHD0A8WyMqHu/UnDjfpGES/cmjtPbQoktWqs= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.13.1/go.mod h1:+nVKciyKD2J9TyVcEQ82Bo9b+3F92PiQfHrIE/zqLqM= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.1/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1 h1:sLZ/Y+P/5RRtsXWylBjB5lkgixYfm0MQPiwrSX//JSo= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.3.0 h1:FVbZiAs9cncAB9eIUwDrfBA2PfJIMeTcdMxy6W2nRMo= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.3.0/go.mod h1:Klp8aJcaCELXQHa/Cg0rFFIFlE0EjOUAzt9x1cB00TY= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.2.1 h1:lirjIOHv5RrmDbZXw9lUz/fY68uU05qR4uIef58WMvQ= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.2.1/go.mod h1:j1J9XXIo/eXD7YSrr73sYZTEY/AQ0+/Q6Aa96z1e2j8= 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= @@ -67,18 +81,16 @@ github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSW github.com/Azure/go-autorest/autorest v0.1.0/go.mod h1:AKyIcETwSUFxIcs/Wnq/C+kwCtlEYGUVd7FPNb2slmg= github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= 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/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/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= @@ -100,6 +112,8 @@ github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvd github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 h1:WVsrXCnHlDDX8ls+tootqRE87/hL9S/g4ewig9RsD/c= +github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= @@ -219,6 +233,8 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8 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/dnaeon/go-vcr v1.1.0 h1:ReYa/UBrRyQdant9B4fNHGoCNKw6qh6P0fsdGmZpR7c= +github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= 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= @@ -334,6 +350,10 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= +github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= +github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -587,6 +607,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= @@ -655,8 +677,10 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= @@ -711,6 +735,8 @@ github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI= github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 h1:Qj1ukM4GlMWXNdMBuXcXfz/Kw9s1qm0CLY32QxuSImI= +github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -909,6 +935,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -989,6 +1016,7 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -1003,9 +1031,11 @@ golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211215060638-4ddde0e984e9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM= @@ -1128,6 +1158,7 @@ golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/sops/azkv/keysource.go b/internal/sops/azkv/keysource.go index a15a24fc..a2901ecc 100644 --- a/internal/sops/azkv/keysource.go +++ b/internal/sops/azkv/keysource.go @@ -7,7 +7,6 @@ package azkv import ( "bytes" "context" - "encoding/base64" "encoding/binary" "encoding/json" "fmt" @@ -15,29 +14,29 @@ import ( "time" "unicode/utf16" - "github.com/Azure/azure-sdk-for-go/profiles/latest/keyvault/keyvault" - "github.com/Azure/go-autorest/autorest" - "github.com/Azure/go-autorest/autorest/adal" - "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys/crypto" "github.com/dimchansky/utfbom" ) // MasterKey is an Azure Key Vault key used to encrypt and decrypt SOPS' data key. -// The underlying authentication token can be configured using AADSettings. +// The underlying authentication token can be configured using AADConfig. type MasterKey struct { VaultURL string Name string Version string EncryptedKey string + CreationDate time.Time - token *adal.ServicePrincipalToken + token azcore.TokenCredential } -// LoadAADSettingsFromBytes attempts to load the given bytes into the given AADSettings. +// LoadAADConfigFromBytes attempts to load the given bytes into the given AADConfig. // By first decoding it if UTF-16, and then unmarshalling it into the given struct. // It returns an error for any failure. -func LoadAADSettingsFromBytes(b []byte, s *AADSettings) error { +func LoadAADConfigFromBytes(b []byte, s *AADConfig) error { b, err := decode(b) if err != nil { return fmt.Errorf("failed to decode Azure authentication file bytes: %w", err) @@ -48,31 +47,63 @@ func LoadAADSettingsFromBytes(b []byte, s *AADSettings) error { return err } -// AADSettings contains the selection of fields from an Azure authentication file +// AADConfig contains the selection of fields from an Azure authentication file // required for Active Directory authentication. -// -// It is based on the unpublished contract in -// https://github.com/Azure/go-autorest/blob/c7f947c0610de1bc279f76e6d453353f95cd1bfa/autorest/azure/auth/auth.go#L331-L342, -// which seems to be due to an assumption of configuration through environment -// variables over file based configuration. -type AADSettings struct { - ClientID string `json:"clientId"` - ClientSecret string `json:"clientSecret"` - TenantID string `json:"tenantId"` - ActiveDirectoryEndpoint string `json:"activeDirectoryEndpointUrl"` -} - -// SetToken configures the token on the given MasterKey using the AADSettings. -func (s *AADSettings) SetToken(key *MasterKey) error { +type AADConfig struct { + TenantID string `json:"tenantId,omitempty"` + ClientID string `json:"clientId,omitempty"` + ClientSecret string `json:"clientSecret,omitempty"` + ClientCertificate string `json:"clientCertificate,omitempty"` + ClientCertificatePassword string `json:"clientCertificatePassword,omitempty"` + ResourceID string `json:"resourceId,omitempty"` + ActiveDirectoryEndpoint string `json:"activeDirectoryEndpointUrl,omitempty"` +} + +// SetToken attempts to configure the token on the MasterKey using the +// AADConfig values. +func (s *AADConfig) SetToken(key *MasterKey) error { if s == nil { return nil } - config, err := adal.NewOAuthConfig(s.GetAADEndpoint(), s.TenantID) - if err != nil { - return err + + var err error + if s.TenantID != "" && s.ClientID != "" { + if s.ClientSecret != "" { + if key.token, err = azidentity.NewClientSecretCredential(s.TenantID, s.ClientID, s.ClientSecret, &azidentity.ClientSecretCredentialOptions{ + AuthorityHost: s.GetAADEndpoint(), + }); err != nil { + return err + } + return nil + } + if s.ClientCertificate != "" { + certs, pk, err := azidentity.ParseCertificates([]byte(s.ClientCertificate), []byte(s.ClientCertificatePassword)) + if key.token, err = azidentity.NewClientCertificateCredential(s.TenantID, s.ClientID, certs, pk, &azidentity.ClientCertificateCredentialOptions{ + AuthorityHost: s.GetAADEndpoint(), + }); err != nil { + return err + } + return nil + } + } + if s.ClientID != "" { + if key.token, err = azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{ + ID: azidentity.ClientID(s.ClientID), + }); err != nil { + return err + } + return nil + } + if s.ResourceID != "" { + if key.token, err = azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{ + ID: azidentity.ResourceID(s.ResourceID), + }); err != nil { + return err + } + return nil } - if key.token, err = adal.NewServicePrincipalToken(*config, s.ClientID, s.ClientSecret, - azure.PublicCloud.ResourceIdentifiers.KeyVault); err != nil { + + if key.token, err = azidentity.NewEnvironmentCredential(nil); err != nil { return err } return nil @@ -80,11 +111,11 @@ func (s *AADSettings) SetToken(key *MasterKey) error { // GetAADEndpoint returns the ActiveDirectoryEndpoint, or the Azure Public Cloud // default. -func (s *AADSettings) GetAADEndpoint() string { +func (s *AADConfig) GetAADEndpoint() azidentity.AuthorityHost { if s.ActiveDirectoryEndpoint != "" { - return s.ActiveDirectoryEndpoint + return azidentity.AuthorityHost(s.ActiveDirectoryEndpoint) } - return azure.PublicCloud.ActiveDirectoryEndpoint + return azidentity.AzurePublicCloud } // EncryptedDataKey returns the encrypted data key this master key holds. @@ -98,20 +129,16 @@ func (key *MasterKey) SetEncryptedDataKey(enc []byte) { } // Encrypt takes a SOPS data key, encrypts it with Key Vault and stores the result in the EncryptedKey field. -func (key *MasterKey) Encrypt(plaintext []byte) error { - c := newThrottledKeyvaultClient(key.authorizer()) - - data := base64.RawURLEncoding.EncodeToString(plaintext) - p := keyvault.KeyOperationsParameters{Value: &data, Algorithm: keyvault.RSAOAEP256} - res, err := c.Encrypt(context.Background(), key.VaultURL, key.Name, key.Version, p) +func (key *MasterKey) Encrypt(dataKey []byte) error { + c, err := crypto.NewClient(key.ToString(), key.token, nil) if err != nil { - return fmt.Errorf("failed to encrypt data: %w", err) + return fmt.Errorf("failed to construct client to encrypt data: %w", err) } - - key.EncryptedKey = *res.Result + resp, err := c.Encrypt(context.Background(), crypto.AlgorithmRSAOAEP256, dataKey, nil) if err != nil { return fmt.Errorf("failed to encrypt data: %w", err) } + key.EncryptedKey = string(resp.Result) return nil } @@ -125,25 +152,20 @@ func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error { // Decrypt decrypts the EncryptedKey field with Azure Key Vault and returns the result. func (key *MasterKey) Decrypt() ([]byte, error) { - c := newThrottledKeyvaultClient(key.authorizer()) - - result, err := c.Decrypt(context.Background(), key.VaultURL, key.Name, key.Version, keyvault.KeyOperationsParameters{ - Algorithm: keyvault.RSAOAEP256, Value: &key.EncryptedKey, - }) + c, err := crypto.NewClient(key.ToString(), key.token, nil) if err != nil { - return nil, fmt.Errorf("failed to decrypt data: %w", err) + return nil, fmt.Errorf("failed to construct client to decrypt data: %w", err) } - - plaintext, err := base64.RawURLEncoding.DecodeString(*result.Result) + resp, err := c.Decrypt(context.Background(), crypto.AlgorithmRSAOAEP256, []byte(key.EncryptedKey), nil) if err != nil { return nil, fmt.Errorf("failed to decrypt data: %w", err) } - return plaintext, nil + return resp.Result, nil } // NeedsRotation returns whether the data key needs to be rotated or not. func (key *MasterKey) NeedsRotation() bool { - return key.token.Token().IsExpired() + return time.Since(key.CreationDate) > (time.Hour * 24 * 30 * 6) } // ToString converts the key to a string representation. @@ -157,35 +179,11 @@ func (key MasterKey) ToMap() map[string]interface{} { out["vaultUrl"] = key.VaultURL out["key"] = key.Name out["version"] = key.Version + out["created_at"] = key.CreationDate.UTC().Format(time.RFC3339) out["enc"] = key.EncryptedKey return out } -func (key *MasterKey) authorizer() autorest.Authorizer { - if key.token == nil { - return &autorest.NullAuthorizer{} - } - return autorest.NewBearerAuthorizer(key.token) -} - -// newThrottledKeyvaultClient returns a client configured to retry requests. -// -// Ref: https://docs.microsoft.com/en-us/azure/key-vault/general/overview-throttling -func newThrottledKeyvaultClient(authorizer autorest.Authorizer) keyvault.BaseClient { - const ( - // Number of times the client will attempt to make an HTTP request - retryAttempts = 6 - // Duration between HTTP request retries - retryDuration = 5 * time.Second - ) - - c := keyvault.New() - c.Authorizer = authorizer - c.RetryAttempts = retryAttempts - c.RetryDuration = retryDuration - return c -} - func decode(b []byte) ([]byte, error) { reader, enc := utfbom.Skip(bytes.NewReader(b)) switch enc { diff --git a/internal/sops/keyservice/server.go b/internal/sops/keyservice/server.go index 7b42feb6..bb6df590 100644 --- a/internal/sops/keyservice/server.go +++ b/internal/sops/keyservice/server.go @@ -40,14 +40,14 @@ type Server struct { // AzureAADConfig configures the Azure Active Directory settings used // by the server. - AzureAADConfig *azkv.AADSettings + AzureAADConfig *azkv.AADConfig // DefaultServer is the server used for any other request than a PGP // or age encryption/decryption. DefaultServer keyservice.KeyServiceServer } -func NewServer(prompt bool, homeDir, vaultToken string, agePrivateKeys []string, azureCfg *azkv.AADSettings) keyservice.KeyServiceServer { +func NewServer(prompt bool, homeDir, vaultToken string, agePrivateKeys []string, azureCfg *azkv.AADConfig) keyservice.KeyServiceServer { server := &Server{ Prompt: prompt, HomeDir: homeDir, @@ -106,7 +106,7 @@ func (ks *Server) decryptWithVault(key *keyservice.VaultKey, ciphertext []byte) } vaultKey.EncryptedKey = string(ciphertext) plaintext, err := vaultKey.Decrypt() - return []byte(plaintext), err + return plaintext, err } func (ks *Server) encryptWithAzureKeyvault(key *keyservice.AzureKeyVaultKey, plaintext []byte) ([]byte, error) { From 02343b047b0d1125892f2cd3916071dc89404d98 Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Fri, 4 Mar 2022 01:06:09 +0100 Subject: [PATCH 5/7] sops/azkv: support fields from `az` generated SP This supports the fields as documented in the AKS documentation: https://docs.microsoft.com/en-us/azure/aks/kubernetes-service-principal?tabs=azure-cli#manually-create-a-service-principal Signed-off-by: Hidde Beydals --- internal/sops/azkv/keysource.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/internal/sops/azkv/keysource.go b/internal/sops/azkv/keysource.go index a2901ecc..ed214746 100644 --- a/internal/sops/azkv/keysource.go +++ b/internal/sops/azkv/keysource.go @@ -50,6 +50,7 @@ func LoadAADConfigFromBytes(b []byte, s *AADConfig) error { // AADConfig contains the selection of fields from an Azure authentication file // required for Active Directory authentication. type AADConfig struct { + AZConfig TenantID string `json:"tenantId,omitempty"` ClientID string `json:"clientId,omitempty"` ClientSecret string `json:"clientSecret,omitempty"` @@ -59,6 +60,13 @@ type AADConfig struct { ActiveDirectoryEndpoint string `json:"activeDirectoryEndpointUrl,omitempty"` } +// Ref: https://docs.microsoft.com/en-us/azure/aks/kubernetes-service-principal?tabs=azure-cli#manually-create-a-service-principal +type AZConfig struct { + AppID string `json:"appId,omitempty"` + Tenant string `json:"tenant,omitempty"` + Password string `json:"password,omitempty"` +} + // SetToken attempts to configure the token on the MasterKey using the // AADConfig values. func (s *AADConfig) SetToken(key *MasterKey) error { @@ -86,6 +94,14 @@ func (s *AADConfig) SetToken(key *MasterKey) error { return nil } } + if s.Tenant != "" && s.AppID != "" && s.Password != "" { + if key.token, err = azidentity.NewClientSecretCredential(s.Tenant, s.AppID, s.Password, &azidentity.ClientSecretCredentialOptions{ + AuthorityHost: s.GetAADEndpoint(), + }); err != nil { + return err + } + return nil + } if s.ClientID != "" { if key.token, err = azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{ ID: azidentity.ClientID(s.ClientID), From 6876a1e95a89667636e2d0914d95398950cb0a52 Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Wed, 9 Mar 2022 14:38:06 +0100 Subject: [PATCH 6/7] sops/azkv: support YAML Azure authentication files Signed-off-by: Hidde Beydals --- internal/sops/azkv/keysource.go | 96 ++++++----- internal/sops/azkv/keysource_test.go | 227 +++++++++++++++++++++++++++ 2 files changed, 273 insertions(+), 50 deletions(-) create mode 100644 internal/sops/azkv/keysource_test.go diff --git a/internal/sops/azkv/keysource.go b/internal/sops/azkv/keysource.go index ed214746..edddb946 100644 --- a/internal/sops/azkv/keysource.go +++ b/internal/sops/azkv/keysource.go @@ -8,7 +8,6 @@ import ( "bytes" "context" "encoding/binary" - "encoding/json" "fmt" "io/ioutil" "time" @@ -18,6 +17,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys/crypto" "github.com/dimchansky/utfbom" + "sigs.k8s.io/yaml" ) // MasterKey is an Azure Key Vault key used to encrypt and decrypt SOPS' data key. @@ -41,7 +41,7 @@ func LoadAADConfigFromBytes(b []byte, s *AADConfig) error { if err != nil { return fmt.Errorf("failed to decode Azure authentication file bytes: %w", err) } - if err = json.Unmarshal(b, s); err != nil { + if err = yaml.Unmarshal(b, s); err != nil { err = fmt.Errorf("failed to unmarshal Azure authentication file: %w", err) } return err @@ -51,15 +51,16 @@ func LoadAADConfigFromBytes(b []byte, s *AADConfig) error { // required for Active Directory authentication. type AADConfig struct { AZConfig - TenantID string `json:"tenantId,omitempty"` - ClientID string `json:"clientId,omitempty"` - ClientSecret string `json:"clientSecret,omitempty"` - ClientCertificate string `json:"clientCertificate,omitempty"` - ClientCertificatePassword string `json:"clientCertificatePassword,omitempty"` - ResourceID string `json:"resourceId,omitempty"` - ActiveDirectoryEndpoint string `json:"activeDirectoryEndpointUrl,omitempty"` + TenantID string `json:"tenantId,omitempty"` + ClientID string `json:"clientId,omitempty"` + ClientSecret string `json:"clientSecret,omitempty"` + ClientCertificate string `json:"clientCertificate,omitempty"` + ClientCertificatePassword string `json:"clientCertificatePassword,omitempty"` + ClientCertificateSendChain bool `json:"clientCertificateSendChain,omitempty"` + AuthorityHost string `json:"authorityHost,omitempty"` } +// AZConfig contains the Service Principal fields as generated by `az`. // Ref: https://docs.microsoft.com/en-us/azure/aks/kubernetes-service-principal?tabs=azure-cli#manually-create-a-service-principal type AZConfig struct { AppID string `json:"appId,omitempty"` @@ -68,68 +69,63 @@ type AZConfig struct { } // SetToken attempts to configure the token on the MasterKey using the -// AADConfig values. +// AADConfig values. It detects credentials in the following order: +// +// - azidentity.ClientSecretCredential when `tenantId`, `clientId` and +// `clientSecret` fields are found. +// - azidentity.ClientCertificateCredential when `tenantId`, +// `clientCertificate` (and optionally `clientCertificatePassword`) fields +// are found. +// - azidentity.ClientSecretCredential when AZConfig fields are found. +// - azidentity.ManagedIdentityCredential for a User ID, when a `clientId` +// field but no `tenantId` is found. +// +// If no set of credentials is found or the azcore.TokenCredential can not be +// created, an error is returned. func (s *AADConfig) SetToken(key *MasterKey) error { - if s == nil { + if s == nil || key == nil { return nil } var err error if s.TenantID != "" && s.ClientID != "" { if s.ClientSecret != "" { - if key.token, err = azidentity.NewClientSecretCredential(s.TenantID, s.ClientID, s.ClientSecret, &azidentity.ClientSecretCredentialOptions{ - AuthorityHost: s.GetAADEndpoint(), - }); err != nil { - return err - } - return nil + key.token, err = azidentity.NewClientSecretCredential(s.TenantID, s.ClientID, s.ClientSecret, &azidentity.ClientSecretCredentialOptions{ + AuthorityHost: s.GetAuthorityHost(), + }) + return err } if s.ClientCertificate != "" { certs, pk, err := azidentity.ParseCertificates([]byte(s.ClientCertificate), []byte(s.ClientCertificatePassword)) - if key.token, err = azidentity.NewClientCertificateCredential(s.TenantID, s.ClientID, certs, pk, &azidentity.ClientCertificateCredentialOptions{ - AuthorityHost: s.GetAADEndpoint(), - }); err != nil { - return err - } - return nil + key.token, err = azidentity.NewClientCertificateCredential(s.TenantID, s.ClientID, certs, pk, &azidentity.ClientCertificateCredentialOptions{ + SendCertificateChain: s.ClientCertificateSendChain, + AuthorityHost: s.GetAuthorityHost(), + }) + return err } } if s.Tenant != "" && s.AppID != "" && s.Password != "" { - if key.token, err = azidentity.NewClientSecretCredential(s.Tenant, s.AppID, s.Password, &azidentity.ClientSecretCredentialOptions{ - AuthorityHost: s.GetAADEndpoint(), - }); err != nil { - return err - } - return nil + key.token, err = azidentity.NewClientSecretCredential(s.Tenant, s.AppID, s.Password, &azidentity.ClientSecretCredentialOptions{ + AuthorityHost: s.GetAuthorityHost(), + }) + return err } if s.ClientID != "" { - if key.token, err = azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{ + key.token, err = azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{ ID: azidentity.ClientID(s.ClientID), - }); err != nil { - return err - } - return nil - } - if s.ResourceID != "" { - if key.token, err = azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{ - ID: azidentity.ResourceID(s.ResourceID), - }); err != nil { - return err - } - return nil - } - - if key.token, err = azidentity.NewEnvironmentCredential(nil); err != nil { + }) return err } - return nil + + return fmt.Errorf("invalid data: requires a '%s' field, a combination of '%s', '%s' and '%s', or '%s', '%s' and '%s'", + "clientId", "tenantId", "clientId", "clientSecret", "tenantId", "clientId", "clientCertificate") } -// GetAADEndpoint returns the ActiveDirectoryEndpoint, or the Azure Public Cloud +// GetAuthorityHost returns the AuthorityHost, or the Azure Public Cloud // default. -func (s *AADConfig) GetAADEndpoint() azidentity.AuthorityHost { - if s.ActiveDirectoryEndpoint != "" { - return azidentity.AuthorityHost(s.ActiveDirectoryEndpoint) +func (s *AADConfig) GetAuthorityHost() azidentity.AuthorityHost { + if s.AuthorityHost != "" { + return azidentity.AuthorityHost(s.AuthorityHost) } return azidentity.AzurePublicCloud } diff --git a/internal/sops/azkv/keysource_test.go b/internal/sops/azkv/keysource_test.go new file mode 100644 index 00000000..1601f768 --- /dev/null +++ b/internal/sops/azkv/keysource_test.go @@ -0,0 +1,227 @@ +/* +Copyright 2022 The Flux authors + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package azkv + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "math/big" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + . "github.com/onsi/gomega" +) + +func TestLoadAADConfigFromBytes(t *testing.T) { + tests := []struct { + name string + b []byte + want AADConfig + wantErr bool + }{ + { + name: "Service Principal with Secret", + b: []byte(`tenantId: "some-tenant-id" +clientId: "some-client-id" +clientSecret: "some-client-secret"`), + want: AADConfig{ + TenantID: "some-tenant-id", + ClientID: "some-client-id", + ClientSecret: "some-client-secret", + }, + }, + { + name: "Service Principal with Certificate", + b: []byte(`tenantId: "some-tenant-id" +clientId: "some-client-id" +clientCertificate: "some-client-certificate"`), + want: AADConfig{ + TenantID: "some-tenant-id", + ClientID: "some-client-id", + ClientCertificate: "some-client-certificate", + }, + }, + { + name: "Managed Identity with Client ID", + b: []byte(`clientId: "some-client-id"`), + want: AADConfig{ + ClientID: "some-client-id", + }, + }, + { + name: "Service Principal with Secret from az CLI", + b: []byte(`{"appId": "some-app-id", "tenant": "some-tenant", "password": "some-password"}`), + want: AADConfig{ + AZConfig: AZConfig{ + AppID: "some-app-id", + Tenant: "some-tenant", + Password: "some-password", + }, + }, + }, + { + name: "Authority host", + b: []byte(`{"authorityHost": "https://example.com"}`), + want: AADConfig{ + AuthorityHost: "https://example.com", + }, + }, + { + name: "invalid", + b: []byte("some string"), + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + got := AADConfig{} + err := LoadAADConfigFromBytes(tt.b, &got) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + g.Expect(got).To(Equal(tt.want)) + return + } + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(got).To(Equal(tt.want)) + }) + } +} + +func TestAADConfig_SetToken(t *testing.T) { + tlsMock := validTls(t) + + tests := []struct { + name string + config AADConfig + want azcore.TokenCredential + wantErr bool + }{ + { + name: "Service Principal with Secret", + config: AADConfig{ + TenantID: "some-tenant-id", + ClientID: "some-client-id", + ClientSecret: "some-client-secret", + }, + want: &azidentity.ClientSecretCredential{}, + }, + { + name: "Service Principal with Certificate", + config: AADConfig{ + TenantID: "some-tenant-id", + ClientID: "some-client-id", + ClientCertificate: string(tlsMock), + }, + want: &azidentity.ClientCertificateCredential{}, + }, + { + name: "Service Principal with az CLI format", + config: AADConfig{ + AZConfig: AZConfig{ + AppID: "some-app-id", + Tenant: "some-tenant", + Password: "some-password", + }, + }, + want: &azidentity.ClientSecretCredential{}, + }, + { + name: "Managed Identity with Client ID", + config: AADConfig{ + ClientID: "some-client-id", + }, + want: &azidentity.ManagedIdentityCredential{}, + }, + { + name: "empty config", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + key := MasterKey{} + got := tt.config.SetToken(&key) + + if tt.wantErr { + g.Expect(got).To(HaveOccurred()) + g.Expect(key.token).To(BeNil()) + return + } + + g.Expect(got).ToNot(HaveOccurred()) + g.Expect(key.token).ToNot(BeNil()) + g.Expect(key.token).To(BeAssignableToTypeOf(tt.want)) + }) + } +} + +func TestAADConfig_SetToken_Nil(t *testing.T) { + g := NewWithT(t) + + var c *AADConfig + g.Expect(c.SetToken(&MasterKey{})).To(Succeed()) + g.Expect((&AADConfig{}).SetToken(nil)).To(Succeed()) +} + +func TestAADConfig_GetAuthorityHost(t *testing.T) { + g := NewWithT(t) + + g.Expect((&AADConfig{}).GetAuthorityHost()).To(Equal(azidentity.AzurePublicCloud)) + g.Expect((&AADConfig{AuthorityHost: "https://example.com"}).GetAuthorityHost()).To(Equal(azidentity.AuthorityHost("https://example.com"))) +} + +func validTls(t *testing.T) []byte { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal("Private key cannot be created.", err.Error()) + } + + out := bytes.NewBuffer(nil) + + var privateKey = &pem.Block{ + Type: "PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + } + if err = pem.Encode(out, privateKey); err != nil { + t.Fatal("Private key cannot be PEM encoded.", err.Error()) + } + + certTemplate := x509.Certificate{ + SerialNumber: big.NewInt(1337), + } + cert, err := x509.CreateCertificate(rand.Reader, &certTemplate, &certTemplate, &key.PublicKey, key) + if err != nil { + t.Fatal("Certificate cannot be created.", err.Error()) + } + var certificate = &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert, + } + if err = pem.Encode(out, certificate); err != nil { + t.Fatal("Certificate cannot be PEM encoded.", err.Error()) + } + + return out.Bytes() +} From 9f768615a999a0354bfe4f7226ea83640f3905fe Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Fri, 11 Mar 2022 20:08:38 +0100 Subject: [PATCH 7/7] Document SOPS Azure Key Vault in spec This includes a refactor of the other entries, to start moving guides to the website while containing minimal technical (instructions) in-spec. Signed-off-by: Hidde Beydals --- controllers/kustomization_decryptor.go | 2 +- docs/spec/v1beta2/kustomization.md | 361 ++++++++++++++++++------- 2 files changed, 271 insertions(+), 92 deletions(-) diff --git a/controllers/kustomization_decryptor.go b/controllers/kustomization_decryptor.go index b80babfb..29b82cf3 100644 --- a/controllers/kustomization_decryptor.go +++ b/controllers/kustomization_decryptor.go @@ -52,7 +52,7 @@ const ( // DecryptionVaultTokenFileName is the name of the file containing the Vault token DecryptionVaultTokenFileName = "sops.vault-token" // DecryptionAzureAuthFile is the Azure authentication file - DecryptionAzureAuthFile = "azure_kv.json" + DecryptionAzureAuthFile = "sops.azure-kv" ) type KustomizeDecryptor struct { diff --git a/docs/spec/v1beta2/kustomization.md b/docs/spec/v1beta2/kustomization.md index 3a5c022b..711cb266 100644 --- a/docs/spec/v1beta2/kustomization.md +++ b/docs/spec/v1beta2/kustomization.md @@ -967,147 +967,326 @@ the controller will impersonate the service account on the target cluster. In order to store secrets safely in a public or private Git repository, you can use [Mozilla SOPS](https://github.com/mozilla/sops) -and encrypt your Kubernetes Secrets data with [OpenPGP](https://www.openpgp.org) -and [age](https://age-encryption.org/v1/) keys. +and encrypt your Kubernetes Secrets data with [age](https://age-encryption.org/v1/) +and [OpenPGP](https://www.openpgp.org) keys, or using provider +implementations like Azure Key Vault, GCP KMS or Hashicorp Vault. -### OpenPGP +> **Note:** You should encrypt only the `data` section of the Kubernetes +> Secret, encrypting the `metadata`, `kind` or `apiVersion` fields is not +> supported. An easy way to do this is by appending +> `--encrypted-regex '^(data|stringData)$'` to your `sops --encrypt` command. -Generate a GPG key **without passphrase** using [gnupg](https://www.gnupg.org/), -then use `sops` to encrypt a Kubernetes secret: +### Decryption Secret reference -```sh -sops --pgp=FBC7B9E2A4F9289AC0C1D4843D16CEE4A27381B4 \ ---encrypt --encrypted-regex '^(data|stringData)$' --in-place my-secret.yaml -``` - -Commit and push the encrypted file to Git. - -> **Note** that you should encrypt only the `data` section, encrypting the Kubernetes -> secret metadata, kind or apiVersion is not supported by kustomize-controller. - -Create a secret in the `default` namespace with the OpenPGP private key, -the key name must end with `.asc` to be detected as an OpenPGP key: - -```sh -gpg --export-secret-keys --armor FBC7B9E2A4F9289AC0C1D4843D16CEE4A27381B4 | -kubectl -n default create secret generic sops-gpg \ ---from-file=sops.asc=/dev/stdin -``` - -Configure decryption by referring the private key secret: +To configure what keys must be used for decryption, a `.decryption.secretRef` +can be specified with a reference to a Secret in the same namespace as the +Kustomization. ```yaml +--- apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 kind: Kustomization metadata: - name: my-secrets + name: sops-encrypted namespace: default spec: interval: 5m path: "./" sourceRef: kind: GitRepository - name: my-secrets + name: repository-with-secrets decryption: provider: sops secretRef: - name: sops-pgp + name: sops-keys ``` -### Age +The Secret's `.data` section is expected to contain entries with decryption +keys (for age and OpenPGP), or credentials (for any of the supported provider +implementations). The controller identifies the type of the entry by the suffix +of the key (e.g. `.agekey`), or a fixed key (e.g. `sops.vault-token`). -Generate an age key with [age](https://age-encryption.org) using `age-keygen`, -then use `sops` to encrypt a Kubernetes secret: +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: sops-keys + namespace: default +data: + # Exemplary age private key + identity.agekey: + # Examplary Hashicorp Vault token + sops.vault-token: +``` -```console -$ age-keygen -o age.agekey -Public key: age1helqcqsh9464r8chnwc2fzj8uv7vr5ntnsft0tn45v2xtz0hpfwq98cmsg -$ sops --age=age1helqcqsh9464r8chnwc2fzj8uv7vr5ntnsft0tn45v2xtz0hpfwq98cmsg \ ---encrypt --encrypted-regex '^(data|stringData)$' --in-place my-secret.yaml +#### age Secret entry + +To specify an age private key in a Kubernetes Secret, suffix the key of the +`.data` entry with `.agekey`. + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: sops-keys + namespace: default +data: + # Exemplary age private key + identity.agekey: +``` + +#### OpenPGP Secret entry + +To specify an OpenPGP (passwordless) keyring in armor format in a Kubernetes +Secret, suffix the key of the `.data` entry with `.asc`. + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: sops-keys + namespace: default +data: + # Exemplary OpenPGP keyring + identity.asc: ``` -Commit and push the encrypted file to Git. +#### Azure Key Vault Secret entry -> **Note** that you should encrypt only the `data` section, encrypting the Kubernetes -> secret metadata, kind or apiVersion is not supported by kustomize-controller. +To specify credentials for Azure Key Vault in a Secret, append a `.data` entry +with a fixed `sops.azure-kv` key. The value can contain a variety of JSON or +YAML formats depending on the authentication method you want to utilize. -Create a secret in the `default` namespace with the age private key, -the key name must end with `.agekey` to be detected as an age key: +##### Service Principal with Secret -```sh -cat age.agekey | -kubectl -n default create secret generic sops-age \ ---from-file=age.agekey=/dev/stdin +To configure a Service Principal with Secret credentials to access the Azure +Key Vault, a JSON or YAML object with `tenantId`, `clientId` and `clientSecret` +fields must be configured as the `sops.azure-kv` value. It +optionally supports `authorityHost` to configure an authority host other than +the Azure Public Cloud endpoint. + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: sops-keys + namespace: default +stringData: + # Exemplary Azure Service Principal with Secret + sops.azure-kv: | + tenantId: some-tenant-id + clientId: some-client-id + clientSecret: some-client-secret ``` -Configure decryption by referring the private key secret: +##### Service Principal with Certificate + +To configure a Service Principal with Certificate credentials to access the +Azure Key Vault, a JSON or YAML object with `tenantId`, `clientId` and +`clientCertificate` fields must be configured as the `sops.azure-kv` value. +It optionally supports `clientCertificateSendChain` and `authorityHost` to +control the sending of the certificate chain, or to specify an authority host +other than the Azure Public Cloud endpoint. ```yaml -apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 -kind: Kustomization +--- +apiVersion: v1 +kind: Secret metadata: - name: my-secrets + name: sops-keys namespace: default -spec: - interval: 5m - path: "./" - sourceRef: - kind: GitRepository - name: my-secrets - decryption: - provider: sops - secretRef: - name: sops-age +stringData: + # Exemplary Azure Service Principal with Certificate + sops.azure-kv: | + tenantId: some-tenant-id + clientId: some-client-id + clientCertificate: ``` -### HashiCorp Vault +##### `az` generated Service Principal -Export the `VAULT_ADDR` and `VAULT_TOKEN` environment variables to your shell, -then use `sops` to encrypt a Kubernetes Secret (see [HashiCorp Vault](https://www.vaultproject.io/docs/secrets/transit) -for more details on enabling the transit backend and [sops](https://github.com/mozilla/sops#encrypting-using-hashicorp-vault)). +To configure a Service Principal [generated using +`az`](https://docs.microsoft.com/en-us/azure/aks/kubernetes-service-principal?tabs=azure-cli#manually-create-a-service-principal), +the output of the command can be directly used as a `sops.azure-kv` value. -Then use `sops` to encrypt a Kubernetes Secret: +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: sops-keys + namespace: default +stringData: + # Exemplary Azure Service Principal generated with `az` + sops.azure-kv: | + { + "appId": "559513bd-0c19-4c1a-87cd-851a26afd5fc", + "displayName": "myAKSClusterServicePrincipal", + "name": "http://myAKSClusterServicePrincipal", + "password": "e763725a-5eee-40e8-a466-dc88d980f415", + "tenant": "72f988bf-86f1-41af-91ab-2d7cd011db48" + } +``` -```console -$ export VAULT_ADDR=https://vault.example.com:8200 -$ export VAULT_TOKEN=my-token -$ sops --hc-vault-transit $VAULT_ADDR/v1/sops/keys/my-encryption-key --encrypt \ ---encrypted-regex '^(data|stringData)$' --in-place my-secret.yaml +##### Managed Identity with Client ID + +To configure a Managed Identity making use of a Client ID, a JSON or YAML +object with a `clientId` must be configured as the `sops.azure-kv` value. It +optionally supports `authorityHost` to configure an authority host other than +the Azure Public Cloud endpoint. + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: sops-keys + namespace: default +stringData: + # Exemplary Azure Managed Identity with Client ID + sops.azure-kv: | + clientId: some-client-id ``` -Commit and push the encrypted file to Git. +#### Hashicorp Vault Secret entry -> **Note** that you should encrypt only the `data` section, encrypting the Kubernetes -> secret metadata, kind or apiVersion is not supported by kustomize-controller. +To specify credentials for Hashicorp Vault in a Kubernetes Secret, append a +`.data` entry with a fixed `sops.vault-token` key and the token as value. -Create a secret in the `default` namespace with the vault token, -the key name must be `sops.vault-token` to be detected as a vault token: +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: sops-keys + namespace: default +data: + # Exemplary Hashicorp Vault Secret token + sops.vault-token: +``` + +### Controller global decryption + +Other than [authentication using a Secret reference](#decryption-secret-reference), +it is possible to specify global decryption settings on the +kustomize-controller Pod. When the controller fails to find credentials on the +Kustomization object itself, it will fall back to these defaults. + +#### AWS + +While making use of the [IAM OIDC provider](https://eksctl.io/usage/iamserviceaccounts/) +on your EKS cluster, you can create an IAM Role and Service Account with access +to AWS KMS (using at least `kms:Decrypt` and `kms:DescribeKey`). Once these are +created, you can annotate the kustomize-controller Service Account with the +Role ARN, granting the controller permissions to decrypt the Secrets. ```sh -echo $VAULT_TOKEN | -kubectl -n default create secret generic sops-hcvault \ ---from-file=sops.vault-token=/dev/stdin +kubectl -n flux-system annotate serviceaccount kustomize-controller \ + --field-manager=flux-client-side-apply \ + eks.amazonaws.com/role-arn='arn:aws:iam:::role/' ``` -Configure decryption by referring the private key secret: +In addition to this, the +[general SOPS documentation around KMS AWS applies](https://github.com/mozilla/sops#27kms-aws-profiles), +allowing you to specify e.g. a `SOPS_KMS_ARN` environment variable. + +#### Azure Key Vault + +While making use of [AAD Pod Identity](https://github.com/Azure/aad-pod-identity), +you can bind a Managed Identity to Flux's kustomize-controller. Once the +`AzureIdentity` and `AzureIdentityBinding` for this are created, you can patch +the controller's Deployment with the `aadpodidbinding` label set to the +selector of the binding, and the `AZURE_AUTH_METHOD` environment variable set +to `msi`. ```yaml -apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 -kind: Kustomization +--- +apiVersion: apps/v1 +kind: Deployment metadata: - name: my-secrets - namespace: default + name: kustomize-controller + namespace: flux-system spec: - interval: 5m - path: "./" - sourceRef: - kind: GitRepository - name: my-secrets - decryption: - provider: sops - secretRef: - name: sops-hcvault + template: + metadata: + labels: + aadpodidbinding: sops-akv-decryptor # match the AzureIdentityBinding selector + spec: + containers: + - name: manager + env: + - name: AZURE_AUTH_METHOD + value: msi +``` + +In addition to this, the [default SOPS Azure Key Vault flow is +followed](https://github.com/mozilla/sops#encrypting-using-azure-key-vault), +allowing you to specify a variety of other environment variables. + +#### GCP KMS + +While making use of Google Cloud Platform, the [`GOOGLE_APPLICATION_CREDENTIALS` +environment variable](https://cloud.google.com/docs/authentication/production) +is automatically taken into account. +[Granting permissions](https://cloud.google.com/kms/docs/reference/permissions-and-roles) +to the Service Account attached to this will therefore be sufficient to decrypt +data. When running outside GCP, it is possible to manually patch the +kustomize-controller Deployment with a valid set of (mounted) credentials. + +```yaml +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kustomize-controller + namespace: flux-system +spec: + template: + spec: + containers: + - name: manager + env: + - name: GOOGLE_APPLICATION_CREDENTIALS + value: /var/gcp/credentials.json + volumeMounts: + - name: gcp-credentials + mountPath: /var/gcp/ + readOnly: true + volumes: + - name: gcp-credentials + secret: + secretName: mysecret + items: + - key: credentials + path: credentials.json ``` + +#### Hashicorp Vault + +To configure a global default for Hashicorp Vault, patch the controller's +Deployment with a `VAULT_TOKEN` environment variable. + +```yaml +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kustomize-controller + namespace: flux-system +spec: + template: + spec: + containers: + - name: manager + env: + - name: VAULT_TOKEN + value: +``` + ### Kustomize secretGenerator SOPS encrypted data can be stored as a base64 encoded Secret,