Skip to content

Commit

Permalink
Get default Azure credentials from runtime env
Browse files Browse the repository at this point in the history
Signed-off-by: Somtochi Onyekwere <[email protected]>
  • Loading branch information
somtochiama committed Apr 3, 2023
1 parent 05f5b0e commit 5c22372
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 81 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
2 changes: 1 addition & 1 deletion internal/controllers/kustomization_wait_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ parameters:
g.Eventually(func() bool {
_ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK)
return isReconcileFailure(resultK)
}, 35*time.Second, time.Second).Should(BeTrue())
}, timeout, time.Second).Should(BeTrue())
logStatus(t, resultK)

for _, c := range []string{kustomizev1.HealthyCondition, meta.ReadyCondition} {
Expand Down
22 changes: 0 additions & 22 deletions internal/sops/azkv/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ package azkv

import (
"fmt"
"os"

"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 Expand Up @@ -60,9 +58,6 @@ type AZConfig struct {
// `clientCertificate` (and optionally `clientCertificatePassword`) fields
// are found.
// - azidentity.ClientSecretCredential when AZConfig fields are found.
// - azidentity.WorkloadIdentityCredential for a User ID, when a `clientId`
// field and environment variables have been set by the workload identity
// mutating webhook
// - azidentity.ManagedIdentityCredential for a User ID, when a `clientId`
// field but no `tenantId` is found.
//
Expand Down Expand Up @@ -109,23 +104,6 @@ func TokenFromAADConfig(c AADConfig) (_ *Token, err error) {
}
return NewToken(token), nil
case c.ClientID != "":
if file, ok := os.LookupEnv("AZURE_FEDERATED_TOKEN_FILE"); ok {
if authorityHost, ok := os.LookupEnv("AZURE_AUTHORITY_HOST"); ok {
if tenantID, ok := os.LookupEnv("AZURE_TENANT_ID"); ok {
c.AuthorityHost = authorityHost
if token, err = azidentity.NewWorkloadIdentityCredential(tenantID, c.ClientID, file, &azidentity.WorkloadIdentityCredentialOptions{
ClientOptions: azcore.ClientOptions{
Cloud: c.GetCloudConfig(),
},
}); err != nil {
return
}

return NewToken(token), nil
}
}
}

if token, err = azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{
ID: azidentity.ClientID(c.ClientID),
}); err != nil {
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 5c22372

Please sign in to comment.