diff --git a/docs/spec/v1/kustomization.md b/docs/spec/v1/kustomization.md index d8ccb109..3a0ba602 100644 --- a/docs/spec/v1/kustomization.md +++ b/docs/spec/v1/kustomization.md @@ -1271,12 +1271,53 @@ env: #### Azure Key Vault +##### Workload Identity + +If you have Workload Identity set up on your AKS cluster, you can establish +a federated identity between the kustomize-controller ServiceAccount and an +identity that has "Decrypt" role on the Azure Key Vault. Once, this is done +you can label and annotate the kustomize-controller ServiceAccount and Pod +with the patch shown below: + +```yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - gotk-components.yaml + - gotk-sync.yaml +patches: + - patch: |- + apiVersion: v1 + kind: ServiceAccount + metadata: + name: kustomize-controller + namespace: flux-system + annotations: + azure.workload.identity/client-id: + labels: + azure.workload.identity/use: "true" + - patch: |- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: kustomize-controller + namespace: flux-system + labels: + azure.workload.identity/use: "true" + spec: + template: + metadata: + labels: + azure.workload.identity/use: "true" +``` + +##### AAD Pod Identity + 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`. +selector of the binding. ```yaml --- @@ -1290,18 +1331,18 @@ spec: 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. +##### Kubelet Identity + +If the kubelet managed identity has `Decrypt` permissions on Azure Key Vault, +no additional configuration is required for the kustomize-controller to decrypt +data. + #### GCP KMS While making use of Google Cloud Platform, the [`GOOGLE_APPLICATION_CREDENTIALS` diff --git a/go.mod b/go.mod index 1cd366f7..69451acf 100644 --- a/go.mod +++ b/go.mod @@ -12,8 +12,8 @@ require ( cloud.google.com/go/kms v1.10.0 filippo.io/age v1.1.1 github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0 - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.2 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.5.0-beta.1 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0-beta.4 github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.9.0 github.com/aws/aws-sdk-go v1.44.231 github.com/aws/aws-sdk-go-v2 v1.17.7 diff --git a/go.sum b/go.sum index 4b266cdc..6addc24b 100644 --- a/go.sum +++ b/go.sum @@ -47,10 +47,10 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 h1:EKPd1 github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1/go.mod h1:VzwV+t+dZ9j/H867F1M2ziD+yLHtB46oM35FxxMJ4d0= github.com/Azure/azure-sdk-for-go v63.3.0+incompatible h1:INepVujzUrmArRZjDLHbtER+FkvCoEwyRCXGqOlmDII= github.com/Azure/azure-sdk-for-go v63.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0 h1:rTnT/Jrcm+figWlYz4Ixzt0SJVR2cMC8lvZcimipiEY= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.2 h1:uqM+VoHjVH6zdlkLF2b6O0ZANcHoj3rO0PoQ3jglUJA= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.2/go.mod h1:twTKAa1E6hLmSDjLhaCkbTMQKc7p/rNLU40rLxGEOCI= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.5.0-beta.1 h1:yLM4ZIC+NRvzwFGpXjUbf5FhPBVxJgmYXkjePgNAx64= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.5.0-beta.1/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0-beta.4 h1:jpSh2461XzXBEw1MJwvVRJwZS0CAgqS0h6jBdoIFtLk= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0-beta.4/go.mod h1:oWa/ZXP08smIi12UyWVbVikBxoZHZCyxijZamTK1i8Q= github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0 h1:leh5DwKv6Ihwi+h60uHtn6UWAxBbZ0q8DwQVMzf61zw= github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.9.0 h1:TOFrNxfjslms5nLLIMjW7N0+zSALX4KiGsptmpb16AA= diff --git a/internal/sops/azkv/config.go b/internal/sops/azkv/config.go index 4685cdda..4d3a97bf 100644 --- a/internal/sops/azkv/config.go +++ b/internal/sops/azkv/config.go @@ -8,7 +8,6 @@ package azkv import ( "fmt" - "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" diff --git a/internal/sops/azkv/keysource.go b/internal/sops/azkv/keysource.go index ce91c5ba..b0b0f496 100644 --- a/internal/sops/azkv/keysource.go +++ b/internal/sops/azkv/keysource.go @@ -11,13 +11,17 @@ import ( "context" "encoding/base64" "encoding/binary" + "errors" "fmt" "io/ioutil" + "os" + "strings" "time" "unicode/utf16" "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys" "github.com/dimchansky/utfbom" ) @@ -74,7 +78,11 @@ func (t Token) ApplyToMasterKey(key *MasterKey) { // Encrypt takes a SOPS data key, encrypts it with Azure Key Vault, and stores // the result in the EncryptedKey field. func (key *MasterKey) Encrypt(dataKey []byte) error { - c, err := azkeys.NewClient(key.VaultURL, key.token, nil) + creds, err := key.getTokenCredential() + if err != nil { + return fmt.Errorf("failed to get Azure token credential to encrypt: %w", err) + } + c, err := azkeys.NewClient(key.VaultURL, creds, nil) if err != nil { return fmt.Errorf("failed to construct Azure Key Vault crypto client to encrypt data: %w", err) } @@ -115,7 +123,11 @@ 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, err := azkeys.NewClient(key.VaultURL, key.token, nil) + creds, err := key.getTokenCredential() + if err != nil { + return nil, fmt.Errorf("failed to get Azure token credential to decrypt: %w", err) + } + c, err := azkeys.NewClient(key.VaultURL, creds, nil) if err != nil { return nil, fmt.Errorf("failed to construct Azure Key Vault crypto client to decrypt data: %w", err) } @@ -177,3 +189,87 @@ func decode(b []byte) ([]byte, error) { } return ioutil.ReadAll(reader) } + +// getTokenCredential returns the tokenCredential of the MasterKey, or +// azidentity.NewDefaultAzureCredential. +func (key *MasterKey) getTokenCredential() (azcore.TokenCredential, error) { + if key.token == nil { + return getDefaultAzureCredential() + } + return key.token, nil +} + +// getDefaultAzureCredentials is a modification of +// azidentity.NewDefaultAzureCredential, specifically adapted to not shell out +// to the Azure CLI. +// +// It attemps to return an azcore.TokenCredential based on the following order: +// +// - azidentity.NewEnvironmentCredential if environment variables AZURE_CLIENT_ID, +// AZURE_CLIENT_ID is set with either one of the following: (AZURE_CLIENT_SECRET) +// or (AZURE_CLIENT_CERTIFICATE_PATH and AZURE_CLIENT_CERTIFICATE_PATH) or +// (AZURE_USERNAME, AZURE_PASSWORD) +// - azidentity.WorkloadIdentity if environment variable configuration +// (AZURE_AUTHORITY_HOST, AZURE_CLIENT_ID, AZURE_FEDERATED_TOKEN_FILE, AZURE_TENANT_ID) +// is set by the Azure workload identity webhook. +// - azidentity.ManagedIdentity if only AZURE_CLIENT_ID env variable is set. +func getDefaultAzureCredential() (azcore.TokenCredential, error) { + var ( + azureClientID = "AZURE_CLIENT_ID" + azureFederatedTokenFile = "AZURE_FEDERATED_TOKEN_FILE" + azureAuthorityHost = "AZURE_AUTHORITY_HOST" + azureTenantID = "AZURE_TENANT_ID" + ) + + var errorMessages []string + var creds []azcore.TokenCredential + options := &azidentity.DefaultAzureCredentialOptions{} + + envCred, err := azidentity.NewEnvironmentCredential(&azidentity.EnvironmentCredentialOptions{ + ClientOptions: options.ClientOptions, DisableInstanceDiscovery: options.DisableInstanceDiscovery}, + ) + if err == nil { + creds = append(creds, envCred) + } else { + errorMessages = append(errorMessages, "EnvironmentCredential: "+err.Error()) + } + + // workload identity requires values for AZURE_AUTHORITY_HOST, AZURE_CLIENT_ID, AZURE_FEDERATED_TOKEN_FILE, AZURE_TENANT_ID + haveWorkloadConfig := false + clientID, haveClientID := os.LookupEnv(azureClientID) + if haveClientID { + if file, ok := os.LookupEnv(azureFederatedTokenFile); ok { + if _, ok := os.LookupEnv(azureAuthorityHost); ok { + if tenantID, ok := os.LookupEnv(azureTenantID); ok { + haveWorkloadConfig = true + workloadCred, err := azidentity.NewWorkloadIdentityCredential(tenantID, clientID, file, &azidentity.WorkloadIdentityCredentialOptions{ + ClientOptions: options.ClientOptions, + DisableInstanceDiscovery: options.DisableInstanceDiscovery, + }) + if err == nil { + return workloadCred, nil + } else { + errorMessages = append(errorMessages, "Workload Identity"+": "+err.Error()) + } + } + } + } + } + if !haveWorkloadConfig { + err := errors.New("missing environment variables for workload identity. Check webhook and pod configuration") + errorMessages = append(errorMessages, fmt.Sprintf("Workload Identity: %s", err)) + } + + o := &azidentity.ManagedIdentityCredentialOptions{ClientOptions: options.ClientOptions} + if haveClientID { + o.ID = azidentity.ClientID(clientID) + } + miCred, err := azidentity.NewManagedIdentityCredential(o) + if err == nil { + return miCred, nil + } else { + errorMessages = append(errorMessages, "ManagedIdentity"+": "+err.Error()) + } + + return nil, errors.New(strings.Join(errorMessages, "\n")) +} diff --git a/internal/sops/keyservice/server.go b/internal/sops/keyservice/server.go index 5c3190d3..da7b14eb 100644 --- a/internal/sops/keyservice/server.go +++ b/internal/sops/keyservice/server.go @@ -119,15 +119,13 @@ func (ks Server) Encrypt(ctx context.Context, req *keyservice.EncryptRequest) (* Ciphertext: cipherText, }, nil case *keyservice.Key_AzureKeyvaultKey: - if ks.azureToken != nil { - ciphertext, err := ks.encryptWithAzureKeyVault(k.AzureKeyvaultKey, req.Plaintext) - if err != nil { - return nil, err - } - return &keyservice.EncryptResponse{ - Ciphertext: ciphertext, - }, nil + ciphertext, err := ks.encryptWithAzureKeyVault(k.AzureKeyvaultKey, req.Plaintext) + if err != nil { + return nil, err } + return &keyservice.EncryptResponse{ + Ciphertext: ciphertext, + }, nil case *keyservice.Key_GcpKmsKey: ciphertext, err := ks.encryptWithGCPKMS(k.GcpKmsKey, req.Plaintext) if err != nil { @@ -183,15 +181,13 @@ func (ks Server) Decrypt(ctx context.Context, req *keyservice.DecryptRequest) (* Plaintext: plaintext, }, nil case *keyservice.Key_AzureKeyvaultKey: - if ks.azureToken != nil { - plaintext, err := ks.decryptWithAzureKeyVault(k.AzureKeyvaultKey, req.Ciphertext) - if err != nil { - return nil, err - } - return &keyservice.DecryptResponse{ - Plaintext: plaintext, - }, nil + plaintext, err := ks.decryptWithAzureKeyVault(k.AzureKeyvaultKey, req.Ciphertext) + if err != nil { + return nil, err } + return &keyservice.DecryptResponse{ + Plaintext: plaintext, + }, nil case *keyservice.Key_GcpKmsKey: plaintext, err := ks.decryptWithGCPKMS(k.GcpKmsKey, req.Ciphertext) if err != nil { @@ -321,7 +317,9 @@ func (ks *Server) encryptWithAzureKeyVault(key *keyservice.AzureKeyVaultKey, pla Name: key.Name, Version: key.Version, } - ks.azureToken.ApplyToMasterKey(&azureKey) + if ks.azureToken != nil { + ks.azureToken.ApplyToMasterKey(&azureKey) + } if err := azureKey.Encrypt(plaintext); err != nil { return nil, err } @@ -334,7 +332,9 @@ func (ks *Server) decryptWithAzureKeyVault(key *keyservice.AzureKeyVaultKey, cip Name: key.Name, Version: key.Version, } - ks.azureToken.ApplyToMasterKey(&azureKey) + if ks.azureToken != nil { + ks.azureToken.ApplyToMasterKey(&azureKey) + } azureKey.EncryptedKey = string(ciphertext) plaintext, err := azureKey.Decrypt() return plaintext, err diff --git a/internal/sops/keyservice/server_test.go b/internal/sops/keyservice/server_test.go index fb4da311..0848b8e3 100644 --- a/internal/sops/keyservice/server_test.go +++ b/internal/sops/keyservice/server_test.go @@ -181,36 +181,6 @@ func TestServer_EncryptDecrypt_azkv(t *testing.T) { } -func TestServer_EncryptDecrypt_azkv_Fallback(t *testing.T) { - g := NewWithT(t) - - fallback := NewMockKeyServer() - s := NewServer(WithDefaultServer{Server: fallback}) - - key := KeyFromMasterKey(azkv.MasterKeyFromURL("", "", "")) - encReq := &keyservice.EncryptRequest{ - Key: &key, - Plaintext: []byte("some data key"), - } - _, err := s.Encrypt(context.TODO(), encReq) - g.Expect(err).To(HaveOccurred()) - g.Expect(fallback.encryptReqs).To(HaveLen(1)) - g.Expect(fallback.encryptReqs).To(ContainElement(encReq)) - g.Expect(fallback.decryptReqs).To(HaveLen(0)) - - fallback = NewMockKeyServer() - s = NewServer(WithDefaultServer{Server: fallback}) - - decReq := &keyservice.DecryptRequest{ - Key: &key, - Ciphertext: []byte("some ciphertext"), - } - _, err = s.Decrypt(context.TODO(), decReq) - g.Expect(fallback.decryptReqs).To(HaveLen(1)) - g.Expect(fallback.decryptReqs).To(ContainElement(decReq)) - g.Expect(fallback.encryptReqs).To(HaveLen(0)) -} - func TestServer_EncryptDecrypt_gcpkms(t *testing.T) { g := NewWithT(t)