Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Workload Identity for Azure Vault #813

Merged
merged 1 commit into from
Apr 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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