Skip to content

Commit

Permalink
Update azidentity v1.3.0-beta.3 to support Workload Identity
Browse files Browse the repository at this point in the history
Signed-off-by: Somtochi Onyekwere <[email protected]>
Co-authored-by: Hidde Beydals <[email protected]>
  • Loading branch information
somtochiama and hiddeco committed Apr 3, 2023
1 parent 9edf618 commit 20d5c3d
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 65 deletions.
57 changes: 49 additions & 8 deletions docs/spec/v1/kustomization.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <AZURE_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
---
Expand All @@ -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`
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
1 change: 0 additions & 1 deletion internal/sops/azkv/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
100 changes: 98 additions & 2 deletions internal/sops/azkv/keysource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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"))
}
36 changes: 18 additions & 18 deletions internal/sops/keyservice/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand All @@ -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
Expand Down
30 changes: 0 additions & 30 deletions internal/sops/keyservice/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down

0 comments on commit 20d5c3d

Please sign in to comment.