sops/azkv: drop fork of keysource implementation

As the forked code has been contributed upstream in a modified format.

We continue to inject our own default credentials in the key server if
none are provided by the Kustomization, to ensure we do not shell out
to `az`.

Signed-off-by: Hidde Beydals <hidde@hhh.computer>
This commit is contained in:
Hidde Beydals 2023-08-15 16:53:28 +02:00
parent 2e50f41755
commit 1854a9f9dd
No known key found for this signature in database
GPG Key ID: 979F380FC2341744
12 changed files with 214 additions and 555 deletions

2
go.mod
View File

@ -14,7 +14,6 @@ require (
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0
github.com/aws/aws-sdk-go-v2/credentials v1.13.35
github.com/cyphar/filepath-securejoin v0.2.3
github.com/dimchansky/utfbom v1.1.1
@ -66,6 +65,7 @@ require (
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 // indirect

View File

@ -34,6 +34,7 @@ import (
"github.com/getsops/sops/v3"
"github.com/getsops/sops/v3/aes"
"github.com/getsops/sops/v3/age"
"github.com/getsops/sops/v3/azkv"
"github.com/getsops/sops/v3/cmd/sops/common"
"github.com/getsops/sops/v3/cmd/sops/formats"
"github.com/getsops/sops/v3/keyservice"
@ -50,7 +51,7 @@ import (
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
intawskms "github.com/fluxcd/kustomize-controller/internal/sops/awskms"
"github.com/fluxcd/kustomize-controller/internal/sops/azkv"
intazkv "github.com/fluxcd/kustomize-controller/internal/sops/azkv"
intkeyservice "github.com/fluxcd/kustomize-controller/internal/sops/keyservice"
"github.com/fluxcd/kustomize-controller/internal/sops/pgp"
)
@ -141,7 +142,7 @@ type Decryptor struct {
awsCredsProvider *awskms.CredentialsProvider
// azureToken is the Azure credential token used to authenticate towards
// any Azure Key Vault.
azureToken *azkv.Token
azureToken *azkv.TokenCredential
// gcpCredsJSON is the JSON credential file of the service account used to
// authenticate towards any GCP KMS.
gcpCredsJSON []byte
@ -244,13 +245,15 @@ func (d *Decryptor) ImportKeys(ctx context.Context) error {
case filepath.Ext(DecryptionAzureAuthFile):
// Make sure we have the absolute name
if name == DecryptionAzureAuthFile {
conf := azkv.AADConfig{}
if err = azkv.LoadAADConfigFromBytes(value, &conf); err != nil {
conf := intazkv.AADConfig{}
if err = intazkv.LoadAADConfigFromBytes(value, &conf); err != nil {
return fmt.Errorf("failed to import '%s' data from %s decryption Secret '%s': %w", name, provider, secretName, err)
}
if d.azureToken, err = azkv.TokenFromAADConfig(conf); err != nil {
azureToken, err := intazkv.TokenCredentialFromAADConfig(conf)
if err != nil {
return fmt.Errorf("failed to import '%s' data from %s decryption Secret '%s': %w", name, provider, secretName, err)
}
d.azureToken = azkv.NewTokenCredential(azureToken)
}
case filepath.Ext(DecryptionGCPCredsFile):
if name == DecryptionGCPCredsFile {
@ -554,19 +557,19 @@ func (d *Decryptor) sopsEncryptWithFormat(metadata sops.Metadata, data []byte, i
}
// keyServiceServer returns the SOPS (local) key service clients used to serve
// decryption requests. loadKeyServiceServers() is only configured on the first
// decryption requests. loadKeyServiceServer() is only configured on the first
// call.
func (d *Decryptor) keyServiceServer() []keyservice.KeyServiceClient {
d.localServiceOnce.Do(func() {
d.loadKeyServiceServers()
d.loadKeyServiceServer()
})
return d.keyServices
}
// loadKeyServiceServers loads the SOPS (local) key service clients used to
// loadKeyServiceServer loads the SOPS (local) key service clients used to
// serve decryption requests for the current set of Decryptor
// credentials.
func (d *Decryptor) loadKeyServiceServers() {
func (d *Decryptor) loadKeyServiceServer() {
serverOpts := []intkeyservice.ServerOption{
intkeyservice.WithGnuPGHome(d.gnuPGHome),
intkeyservice.WithVaultToken(d.vaultToken),

View File

@ -1,16 +1,32 @@
// Copyright (C) 2022 The Flux authors
//
// 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/.
/*
Copyright 2023 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"
"encoding/binary"
"fmt"
"io"
"unicode/utf16"
"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"
"github.com/dimchansky/utfbom"
"sigs.k8s.io/yaml"
)
@ -49,7 +65,7 @@ type AZConfig struct {
Password string `json:"password,omitempty"`
}
// TokenFromAADConfig attempts to construct a Token using the AADConfig values.
// TokenCredentialFromAADConfig attempts to construct a Token using the AADConfig values.
// It detects credentials in the following order:
//
// - azidentity.ClientSecretCredential when `tenantId`, `clientId` and
@ -63,53 +79,40 @@ type AZConfig struct {
//
// If no set of credentials is found or the azcore.TokenCredential can not be
// created, an error is returned.
func TokenFromAADConfig(c AADConfig) (_ *Token, err error) {
var token azcore.TokenCredential
func TokenCredentialFromAADConfig(c AADConfig) (token azcore.TokenCredential, err error) {
if c.TenantID != "" && c.ClientID != "" {
if c.ClientSecret != "" {
if token, err = azidentity.NewClientSecretCredential(c.TenantID, c.ClientID, c.ClientSecret, &azidentity.ClientSecretCredentialOptions{
return azidentity.NewClientSecretCredential(c.TenantID, c.ClientID, c.ClientSecret, &azidentity.ClientSecretCredentialOptions{
ClientOptions: azcore.ClientOptions{
Cloud: c.GetCloudConfig(),
},
}); err != nil {
return
}
return NewToken(token), nil
})
}
if c.ClientCertificate != "" {
certs, pk, err := azidentity.ParseCertificates([]byte(c.ClientCertificate), []byte(c.ClientCertificatePassword))
if err != nil {
return nil, err
}
if token, err = azidentity.NewClientCertificateCredential(c.TenantID, c.ClientID, certs, pk, &azidentity.ClientCertificateCredentialOptions{
return azidentity.NewClientCertificateCredential(c.TenantID, c.ClientID, certs, pk, &azidentity.ClientCertificateCredentialOptions{
SendCertificateChain: c.ClientCertificateSendChain,
ClientOptions: azcore.ClientOptions{
Cloud: c.GetCloudConfig(),
},
}); err != nil {
return nil, err
}
return NewToken(token), nil
})
}
}
switch {
case c.Tenant != "" && c.AppID != "" && c.Password != "":
if token, err = azidentity.NewClientSecretCredential(c.Tenant, c.AppID, c.Password, &azidentity.ClientSecretCredentialOptions{
return azidentity.NewClientSecretCredential(c.Tenant, c.AppID, c.Password, &azidentity.ClientSecretCredentialOptions{
ClientOptions: azcore.ClientOptions{
Cloud: c.GetCloudConfig(),
},
}); err != nil {
return
}
return NewToken(token), nil
})
case c.ClientID != "":
if token, err = azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{
return azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{
ID: azidentity.ClientID(c.ClientID),
}); err != nil {
return
}
return NewToken(token), nil
})
default:
return nil, 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")
@ -127,3 +130,24 @@ func (s AADConfig) GetCloudConfig() cloud.Configuration {
}
return cloud.AzurePublic
}
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 io.ReadAll(reader)
}

View File

@ -1,8 +1,18 @@
// Copyright (C) 2022 The Flux authors
//
// 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/.
/*
Copyright 2023 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
@ -148,16 +158,16 @@ func TestTokenFromAADConfig(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
got, err := TokenFromAADConfig(tt.config)
got, err := TokenCredentialFromAADConfig(tt.config)
if tt.wantErr {
g.Expect(err).To(HaveOccurred())
g.Expect(got.token).To(BeNil())
g.Expect(got).To(BeNil())
return
}
g.Expect(err).ToNot(HaveOccurred())
g.Expect(got.token).ToNot(BeNil())
g.Expect(got.token).To(BeAssignableToTypeOf(tt.want))
g.Expect(got).ToNot(BeNil())
g.Expect(got).To(BeAssignableToTypeOf(tt.want))
})
}
}

View File

@ -0,0 +1,103 @@
/*
Copyright 2023 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 (
"errors"
"fmt"
"os"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
)
// DefaultTokenCredential is a modification of azidentity.NewDefaultAzureCredential,
// specifically adapted to not shell out to the Azure CLI.
//
// It attempts 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.WorkloadIdentityCredential 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.ManagedIdentityCredential if only AZURE_CLIENT_ID env variable is set.
func DefaultTokenCredential() (azcore.TokenCredential, error) {
var (
azureClientID = "AZURE_CLIENT_ID"
azureFederatedTokenFile = "AZURE_FEDERATED_TOKEN_FILE"
azureAuthorityHost = "AZURE_AUTHORITY_HOST"
azureTenantID = "AZURE_TENANT_ID"
)
var errorMessages []string
options := &azidentity.DefaultAzureCredentialOptions{}
envCred, err := azidentity.NewEnvironmentCredential(&azidentity.EnvironmentCredentialOptions{
ClientOptions: options.ClientOptions, DisableInstanceDiscovery: options.DisableInstanceDiscovery},
)
if err == nil {
return envCred, nil
} 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(&azidentity.WorkloadIdentityCredentialOptions{
ClientID: clientID,
TenantID: tenantID,
TokenFilePath: file,
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"))
}

View File

@ -1,277 +0,0 @@
// Copyright (C) 2022 The Flux authors
//
// 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 (
"bytes"
"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/security/keyvault/azkeys"
"github.com/dimchansky/utfbom"
)
var (
// azkvTTL is the duration after which a MasterKey requires rotation.
azkvTTL = time.Hour * 24 * 30 * 6
)
// MasterKey is an Azure Key Vault Key used to Encrypt and Decrypt SOPS'
// data key.
//
// The underlying authentication token can be configured using TokenFromAADConfig
// and Token.ApplyToMasterKey().
type MasterKey struct {
VaultURL string
Name string
Version string
EncryptedKey string
CreationDate time.Time
token azcore.TokenCredential
}
// MasterKeyFromURL creates a new MasterKey from a Vault URL, key name, and key
// version.
func MasterKeyFromURL(url, name, version string) *MasterKey {
key := &MasterKey{
VaultURL: url,
Name: name,
Version: version,
CreationDate: time.Now().UTC(),
}
return key
}
// Token is an azcore.TokenCredential used for authenticating towards Azure Key
// Vault.
type Token struct {
token azcore.TokenCredential
}
// NewToken creates a new Token with the provided azcore.TokenCredential.
func NewToken(token azcore.TokenCredential) *Token {
return &Token{token: token}
}
// ApplyToMasterKey configures the Token on the provided key.
func (t Token) ApplyToMasterKey(key *MasterKey) {
key.token = t.token
}
// 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 {
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)
}
resp, err := c.Encrypt(context.Background(), key.Name, key.Version, azkeys.KeyOperationParameters{
Algorithm: to.Ptr(azkeys.EncryptionAlgorithmRSAOAEP256),
Value: dataKey,
}, nil)
if err != nil {
return fmt.Errorf("failed to encrypt sops data key with Azure Key Vault key '%s': %w", key.ToString(), err)
}
// This is for compatibility between the SOPS upstream which uses
// a much older Azure SDK, and our implementation which is up-to-date
// with the latest.
encodedEncryptedKey := base64.RawURLEncoding.EncodeToString(resp.Result)
key.SetEncryptedDataKey([]byte(encodedEncryptedKey))
return nil
}
// EncryptedDataKey returns the encrypted data key this master key holds.
func (key *MasterKey) EncryptedDataKey() []byte {
return []byte(key.EncryptedKey)
}
// SetEncryptedDataKey sets the encrypted data key for this master key.
func (key *MasterKey) SetEncryptedDataKey(enc []byte) {
key.EncryptedKey = string(enc)
}
// EncryptIfNeeded encrypts the provided SOPS data key, if it has not been
// encrypted yet.
func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error {
if key.EncryptedKey == "" {
return key.Encrypt(dataKey)
}
return nil
}
// Decrypt decrypts the EncryptedKey field with Azure Key Vault and returns
// the result.
func (key *MasterKey) Decrypt() ([]byte, error) {
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)
}
// This is for compatibility between the SOPS upstream which uses
// a much older Azure SDK, and our implementation which is up-to-date
// with the latest.
rawEncryptedKey, err := base64.RawURLEncoding.DecodeString(key.EncryptedKey)
if err != nil {
return nil, fmt.Errorf("failed to base64 decode Azure Key Vault encrypted key: %w", err)
}
resp, err := c.Decrypt(context.Background(), key.Name, key.Version, azkeys.KeyOperationParameters{
Algorithm: to.Ptr(azkeys.EncryptionAlgorithmRSAOAEP256),
Value: rawEncryptedKey,
}, nil)
if err != nil {
return nil, fmt.Errorf("failed to decrypt sops data key with Azure Key Vault key '%s': %w", key.ToString(), err)
}
return resp.Result, nil
}
// NeedsRotation returns whether the data key needs to be rotated or not.
func (key *MasterKey) NeedsRotation() bool {
return time.Since(key.CreationDate) > (azkvTTL)
}
// 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["created_at"] = key.CreationDate.UTC().Format(time.RFC3339)
out["enc"] = key.EncryptedKey
return out
}
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)
}
// 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
options := &azidentity.DefaultAzureCredentialOptions{}
envCred, err := azidentity.NewEnvironmentCredential(&azidentity.EnvironmentCredentialOptions{
ClientOptions: options.ClientOptions, DisableInstanceDiscovery: options.DisableInstanceDiscovery},
)
if err == nil {
return envCred, nil
} 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(&azidentity.WorkloadIdentityCredentialOptions{
ClientID: clientID,
TenantID: tenantID,
TokenFilePath: file,
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"))
}

View File

@ -1,147 +0,0 @@
//go:build integration
// +build integration
// Copyright (C) 2022 The Flux authors
//
// 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 (
"context"
"encoding/base64"
"os"
"testing"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys"
"github.com/getsops/sops/v3/azkv"
. "github.com/onsi/gomega"
)
// The following values should be created based on the instructions in:
// https://github.com/mozilla/sops#encrypting-using-azure-key-vault
var (
testVaultURL = os.Getenv("TEST_AZURE_VAULT_URL")
testVaultKeyName = os.Getenv("TEST_AZURE_VAULT_KEY_NAME")
testVaultKeyVersion = os.Getenv("TEST_AZURE_VAULT_KEY_VERSION")
testAADConfig = AADConfig{
TenantID: os.Getenv("TEST_AZURE_TENANT_ID"),
ClientID: os.Getenv("TEST_AZURE_CLIENT_ID"),
ClientSecret: os.Getenv("TEST_AZURE_CLIENT_SECRET"),
}
)
func TestMasterKey_Encrypt(t *testing.T) {
g := NewWithT(t)
key := MasterKeyFromURL(testVaultURL, testVaultKeyName, testVaultKeyVersion)
token, err := TokenFromAADConfig(testAADConfig)
g.Expect(err).ToNot(HaveOccurred())
token.ApplyToMasterKey(key)
g.Expect(key.Encrypt([]byte("foo"))).To(Succeed())
g.Expect(key.EncryptedDataKey()).ToNot(BeEmpty())
}
func TestMasterKey_Decrypt(t *testing.T) {
g := NewWithT(t)
key := MasterKeyFromURL(testVaultURL, testVaultKeyName, testVaultKeyVersion)
token, err := TokenFromAADConfig(testAADConfig)
g.Expect(err).ToNot(HaveOccurred())
token.ApplyToMasterKey(key)
dataKey := []byte("this is super secret data")
c, err := azkeys.NewClient(key.VaultURL, key.token, nil)
g.Expect(err).ToNot(HaveOccurred())
resp, err := c.Encrypt(context.Background(), key.Name, key.Version, azkeys.KeyOperationParameters{
Algorithm: to.Ptr(azkeys.EncryptionAlgorithmRSAOAEP256),
Value: dataKey,
}, nil)
g.Expect(err).ToNot(HaveOccurred())
key.EncryptedKey = base64.RawURLEncoding.EncodeToString(resp.Result)
g.Expect(key.EncryptedKey).ToNot(BeEmpty())
g.Expect(key.EncryptedKey).ToNot(Equal(dataKey))
got, err := key.Decrypt()
g.Expect(err).ToNot(HaveOccurred())
g.Expect(got).To(Equal(dataKey))
}
func TestMasterKey_EncryptDecrypt_RoundTrip(t *testing.T) {
g := NewWithT(t)
key := MasterKeyFromURL(testVaultURL, testVaultKeyName, testVaultKeyVersion)
token, err := TokenFromAADConfig(testAADConfig)
g.Expect(err).ToNot(HaveOccurred())
token.ApplyToMasterKey(key)
dataKey := []byte("some-data-that-should-be-secret")
g.Expect(key.Encrypt(dataKey)).To(Succeed())
g.Expect(key.EncryptedDataKey()).ToNot(BeEmpty())
dec, err := key.Decrypt()
g.Expect(err).ToNot(HaveOccurred())
g.Expect(dec).To(Equal(dataKey))
}
func TestMasterKey_Encrypt_SOPS_Compat(t *testing.T) {
g := NewWithT(t)
encryptKey := MasterKeyFromURL(testVaultURL, testVaultKeyName, testVaultKeyVersion)
token, err := TokenFromAADConfig(testAADConfig)
g.Expect(err).ToNot(HaveOccurred())
token.ApplyToMasterKey(encryptKey)
dataKey := []byte("foo")
g.Expect(encryptKey.Encrypt(dataKey)).To(Succeed())
t.Setenv("AZURE_CLIENT_ID", testAADConfig.ClientID)
t.Setenv("AZURE_TENANT_ID", testAADConfig.TenantID)
t.Setenv("AZURE_CLIENT_SECRET", testAADConfig.ClientSecret)
decryptKey := &azkv.MasterKey{
VaultURL: testVaultURL,
Name: testVaultKeyName,
Version: testVaultKeyVersion,
EncryptedKey: encryptKey.EncryptedKey,
CreationDate: time.Now(),
}
dec, err := decryptKey.Decrypt()
g.Expect(err).ToNot(HaveOccurred())
g.Expect(dec).To(Equal(dataKey))
}
func TestMasterKey_Decrypt_SOPS_Compat(t *testing.T) {
g := NewWithT(t)
t.Setenv("AZURE_CLIENT_ID", testAADConfig.ClientID)
t.Setenv("AZURE_TENANT_ID", testAADConfig.TenantID)
t.Setenv("AZURE_CLIENT_SECRET", testAADConfig.ClientSecret)
dataKey := []byte("foo")
encryptKey := &azkv.MasterKey{
VaultURL: testVaultURL,
Name: testVaultKeyName,
Version: testVaultKeyVersion,
CreationDate: time.Now(),
}
g.Expect(encryptKey.Encrypt(dataKey)).To(Succeed())
decryptKey := MasterKeyFromURL(testVaultURL, testVaultKeyName, testVaultKeyVersion)
token, err := TokenFromAADConfig(testAADConfig)
g.Expect(err).ToNot(HaveOccurred())
token.ApplyToMasterKey(decryptKey)
decryptKey.EncryptedKey = encryptKey.EncryptedKey
dec, err := decryptKey.Decrypt()
g.Expect(err).ToNot(HaveOccurred())
g.Expect(dec).To(Equal(dataKey))
}

View File

@ -1,74 +0,0 @@
// Copyright (C) 2022 The Flux authors
//
// 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 (
"testing"
"time"
. "github.com/onsi/gomega"
)
func TestToken_ApplyToMasterKey(t *testing.T) {
g := NewWithT(t)
token, err := TokenFromAADConfig(
AADConfig{TenantID: "tenant", ClientID: "client", ClientSecret: "secret"},
)
g.Expect(err).ToNot(HaveOccurred())
key := &MasterKey{}
token.ApplyToMasterKey(key)
g.Expect(key.token).To(Equal(token.token))
}
func TestMasterKey_EncryptedDataKey(t *testing.T) {
g := NewWithT(t)
key := &MasterKey{EncryptedKey: "some key"}
g.Expect(key.EncryptedDataKey()).To(BeEquivalentTo(key.EncryptedKey))
}
func TestMasterKey_SetEncryptedDataKey(t *testing.T) {
g := NewWithT(t)
encryptedKey := []byte("encrypted")
key := &MasterKey{}
key.SetEncryptedDataKey(encryptedKey)
g.Expect(key.EncryptedKey).To(BeEquivalentTo(encryptedKey))
}
func TestMasterKey_NeedsRotation(t *testing.T) {
g := NewWithT(t)
key := MasterKeyFromURL("", "", "")
g.Expect(key.NeedsRotation()).To(BeFalse())
key.CreationDate = key.CreationDate.Add(-(azkvTTL + time.Second))
g.Expect(key.NeedsRotation()).To(BeTrue())
}
func TestMasterKey_ToString(t *testing.T) {
g := NewWithT(t)
key := MasterKeyFromURL("https://myvault.vault.azure.net", "key-name", "key-version")
g.Expect(key.ToString()).To(Equal("https://myvault.vault.azure.net/keys/key-name/key-version"))
}
func TestMasterKey_ToMap(t *testing.T) {
g := NewWithT(t)
key := MasterKeyFromURL("https://myvault.vault.azure.net", "key-name", "key-version")
key.EncryptedKey = "data"
g.Expect(key.ToMap()).To(Equal(map[string]interface{}{
"vaultUrl": key.VaultURL,
"key": key.Name,
"version": key.Version,
"created_at": key.CreationDate.UTC().Format(time.RFC3339),
"enc": key.EncryptedKey,
}))
}

View File

@ -9,10 +9,10 @@ package keyservice
import (
extage "filippo.io/age"
"github.com/getsops/sops/v3/age"
"github.com/getsops/sops/v3/azkv"
"github.com/getsops/sops/v3/keyservice"
awskms "github.com/getsops/sops/v3/kms"
"github.com/fluxcd/kustomize-controller/internal/sops/azkv"
"github.com/fluxcd/kustomize-controller/internal/sops/gcpkms"
"github.com/fluxcd/kustomize-controller/internal/sops/hcvault"
"github.com/fluxcd/kustomize-controller/internal/sops/pgp"
@ -69,7 +69,7 @@ func (o WithGCPCredsJSON) ApplyToServer(s *Server) {
// WithAzureToken configures the Azure credential token on the Server.
type WithAzureToken struct {
Token *azkv.Token
Token *azkv.TokenCredential
}
// ApplyToServer applies this configuration to the given Server.

View File

@ -10,11 +10,12 @@ import (
"fmt"
"github.com/getsops/sops/v3/age"
"github.com/getsops/sops/v3/azkv"
"github.com/getsops/sops/v3/keyservice"
awskms "github.com/getsops/sops/v3/kms"
"golang.org/x/net/context"
"github.com/fluxcd/kustomize-controller/internal/sops/azkv"
intazkv "github.com/fluxcd/kustomize-controller/internal/sops/azkv"
"github.com/fluxcd/kustomize-controller/internal/sops/gcpkms"
"github.com/fluxcd/kustomize-controller/internal/sops/hcvault"
"github.com/fluxcd/kustomize-controller/internal/sops/pgp"
@ -45,7 +46,7 @@ type Server struct {
// azureToken is the credential token used for Encrypt and Decrypt
// operations of Azure Key Vault requests.
// When nil, the request will be handled by defaultServer.
azureToken *azkv.Token
azureToken *azkv.TokenCredential
// awsCredsProvider is the Credentials object used for Encrypt and Decrypt
// operations of AWS KMS requests.
@ -300,7 +301,15 @@ func (ks *Server) encryptWithAzureKeyVault(key *keyservice.AzureKeyVaultKey, pla
Name: key.Name,
Version: key.Version,
}
if ks.azureToken != nil {
if ks.azureToken == nil {
// Ensure we use the default token credential if none is provided
// _without_ shelling out to `az`.
defaultToken, err := intazkv.DefaultTokenCredential()
if err != nil {
return nil, fmt.Errorf("failed to get Azure token credential to encrypt data: %w", err)
}
azkv.NewTokenCredential(defaultToken).ApplyToMasterKey(&azureKey)
} else {
ks.azureToken.ApplyToMasterKey(&azureKey)
}
if err := azureKey.Encrypt(plaintext); err != nil {
@ -315,7 +324,15 @@ func (ks *Server) decryptWithAzureKeyVault(key *keyservice.AzureKeyVaultKey, cip
Name: key.Name,
Version: key.Version,
}
if ks.azureToken != nil {
if ks.azureToken == nil {
// Ensure we use the default token credential if none is provided
// _without_ shelling out to `az`.
defaultToken, err := intazkv.DefaultTokenCredential()
if err != nil {
return nil, fmt.Errorf("failed to get Azure token credential to decrypt data: %w", err)
}
azkv.NewTokenCredential(defaultToken).ApplyToMasterKey(&azureKey)
} else {
ks.azureToken.ApplyToMasterKey(&azureKey)
}
azureKey.EncryptedKey = string(ciphertext)

View File

@ -14,12 +14,12 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/getsops/sops/v3/age"
"github.com/getsops/sops/v3/azkv"
"github.com/getsops/sops/v3/keyservice"
awskms "github.com/getsops/sops/v3/kms"
. "github.com/onsi/gomega"
"golang.org/x/net/context"
"github.com/fluxcd/kustomize-controller/internal/sops/azkv"
"github.com/fluxcd/kustomize-controller/internal/sops/gcpkms"
"github.com/fluxcd/kustomize-controller/internal/sops/hcvault"
"github.com/fluxcd/kustomize-controller/internal/sops/pgp"
@ -164,9 +164,9 @@ func TestServer_EncryptDecrypt_azkv(t *testing.T) {
identity, err := azidentity.NewDefaultAzureCredential(nil)
g.Expect(err).ToNot(HaveOccurred())
s := NewServer(WithAzureToken{Token: azkv.NewToken(identity)})
s := NewServer(WithAzureToken{Token: azkv.NewTokenCredential(identity)})
key := KeyFromMasterKey(azkv.MasterKeyFromURL("", "", ""))
key := KeyFromMasterKey(azkv.NewMasterKey("", "", ""))
_, err = s.Encrypt(context.TODO(), &keyservice.EncryptRequest{
Key: &key,
})

View File

@ -11,11 +11,11 @@ import (
"fmt"
"github.com/getsops/sops/v3/age"
"github.com/getsops/sops/v3/azkv"
"github.com/getsops/sops/v3/keys"
"github.com/getsops/sops/v3/keyservice"
awskms "github.com/getsops/sops/v3/kms"
"github.com/fluxcd/kustomize-controller/internal/sops/azkv"
"github.com/fluxcd/kustomize-controller/internal/sops/gcpkms"
"github.com/fluxcd/kustomize-controller/internal/sops/hcvault"
"github.com/fluxcd/kustomize-controller/internal/sops/pgp"