Skip to content
Open
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
50 changes: 47 additions & 3 deletions azkv/keysource.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
"context"
"encoding/base64"
"fmt"
"os"
"regexp"
"strconv"
"strings"
"time"

Expand All @@ -25,6 +27,9 @@ import (
const (
// KeyTypeIdentifier is the string used to identify an Azure Key Vault MasterKey.
KeyTypeIdentifier = "azure_kv"
// SopsAzureKeyvaultSkipUriVerificationEnv can be set to disable Azure Key Vault
// challenge resource verification (for example: "true", "1", "t").
SopsAzureKeyvaultSkipUriVerificationEnv = "SOPS_AZURE_KEYVAULT_SKIP_URI_VERIFICATION"
)

var (
Expand Down Expand Up @@ -162,6 +167,23 @@ func (c ClientOptions) ApplyToMasterKey(key *MasterKey) {
key.clientOptions = c.o
}

// ApplyDisableChallengeResourceVerification configures the MasterKey to disable challenge resource verification.
// This helper allows callers to avoid importing azkeys directly.
func ApplyDisableChallengeResourceVerification(key *MasterKey) {
NewClientOptions(&azkeys.ClientOptions{DisableChallengeResourceVerification: true}).ApplyToMasterKey(key)
}

// ClientOptions returns the azkeys.ClientOptions configured on the MasterKey (may be nil).
func (key *MasterKey) ClientOptions() *azkeys.ClientOptions {
return key.clientOptions
}

// EffectiveClientOptions returns the azkeys.ClientOptions that will be used by
// the Azure client, combining explicit options with environment defaults.
func (key *MasterKey) EffectiveClientOptions() *azkeys.ClientOptions {
return key.effectiveClientOptions()
}

// Encrypt takes a SOPS data key, encrypts it with Azure Key Vault, and stores
// the result in the EncryptedKey field.
//
Expand All @@ -183,7 +205,7 @@ func (key *MasterKey) ensureKeyHasVersion(ctx context.Context) error {
return fmt.Errorf("failed to get Azure token credential to retrieve key version: %w", err)
}

c, err := azkeys.NewClient(key.VaultURL, token, key.clientOptions)
c, err := azkeys.NewClient(key.VaultURL, token, key.effectiveClientOptions())
if err != nil {
log.WithFields(logrus.Fields{"key": key.Name, "version": key.Version}).Info("Encryption failed")
return fmt.Errorf("failed to construct Azure Key Vault client to retrieve key version: %w", err)
Expand All @@ -209,7 +231,7 @@ func (key *MasterKey) EncryptContext(ctx context.Context, dataKey []byte) error
return fmt.Errorf("failed to get Azure token credential to encrypt data: %w", err)
}

c, err := azkeys.NewClient(key.VaultURL, token, key.clientOptions)
c, err := azkeys.NewClient(key.VaultURL, token, key.effectiveClientOptions())
if err != nil {
log.WithFields(logrus.Fields{"key": key.Name, "version": key.Version}).Info("Encryption failed")
return fmt.Errorf("failed to construct Azure Key Vault client to encrypt data: %w", err)
Expand Down Expand Up @@ -272,7 +294,7 @@ func (key *MasterKey) DecryptContext(ctx context.Context) ([]byte, error) {
return nil, fmt.Errorf("failed to base64 decode Azure Key Vault encrypted key: %w", err)
}

c, err := azkeys.NewClient(key.VaultURL, token, key.clientOptions)
c, err := azkeys.NewClient(key.VaultURL, token, key.effectiveClientOptions())
if err != nil {
log.WithFields(logrus.Fields{"key": key.Name, "version": key.Version}).Info("Decryption failed")
return nil, fmt.Errorf("failed to construct Azure Key Vault client to decrypt data: %w", err)
Expand Down Expand Up @@ -324,3 +346,25 @@ func (key *MasterKey) getTokenCredential() (azcore.TokenCredential, error) {
}
return key.tokenCredential, nil
}

func (key *MasterKey) effectiveClientOptions() *azkeys.ClientOptions {
if key.clientOptions != nil {
return key.clientOptions
}
if !shouldSkipChallengeResourceVerificationFromEnv() {
return nil
}
return &azkeys.ClientOptions{DisableChallengeResourceVerification: true}
}

func shouldSkipChallengeResourceVerificationFromEnv() bool {
raw := os.Getenv(SopsAzureKeyvaultSkipUriVerificationEnv)
if raw == "" {
return false
}
v, err := strconv.ParseBool(raw)
if err != nil {
return false
}
return v
}
43 changes: 43 additions & 0 deletions azkv/keysource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"time"

"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -231,3 +232,45 @@ func TestMasterKey_getTokenCredential(t *testing.T) {
assert.IsType(t, &azidentity.DefaultAzureCredential{}, got)
})
}

func TestShouldSkipChallengeResourceVerificationFromEnv(t *testing.T) {
t.Run("unset", func(t *testing.T) {
t.Setenv(SopsAzureKeyvaultSkipUriVerificationEnv, "")
assert.False(t, shouldSkipChallengeResourceVerificationFromEnv())
})

t.Run("true", func(t *testing.T) {
t.Setenv(SopsAzureKeyvaultSkipUriVerificationEnv, "true")
assert.True(t, shouldSkipChallengeResourceVerificationFromEnv())
})

t.Run("false", func(t *testing.T) {
t.Setenv(SopsAzureKeyvaultSkipUriVerificationEnv, "false")
assert.False(t, shouldSkipChallengeResourceVerificationFromEnv())
})

t.Run("invalid", func(t *testing.T) {
t.Setenv(SopsAzureKeyvaultSkipUriVerificationEnv, "not-a-bool")
assert.False(t, shouldSkipChallengeResourceVerificationFromEnv())
})
}

func TestMasterKey_effectiveClientOptions(t *testing.T) {
t.Run("uses explicit client options first", func(t *testing.T) {
t.Setenv(SopsAzureKeyvaultSkipUriVerificationEnv, "true")
mk := &MasterKey{clientOptions: &azkeys.ClientOptions{DisableChallengeResourceVerification: false}}
opts := mk.effectiveClientOptions()
if assert.NotNil(t, opts) {
assert.False(t, opts.DisableChallengeResourceVerification)
}
})

t.Run("uses env var when no explicit options", func(t *testing.T) {
t.Setenv(SopsAzureKeyvaultSkipUriVerificationEnv, "1")
mk := &MasterKey{}
opts := mk.effectiveClientOptions()
if assert.NotNil(t, opts) {
assert.True(t, opts.DisableChallengeResourceVerification)
}
})
}
2 changes: 2 additions & 0 deletions cmd/sops/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ func main() {
SOPS_AZURE_KEYVAULT_URL environment variable.
(Authentication is based on environment variables, see
https://docs.microsoft.com/en-us/go/azure/azure-sdk-go-authorization#use-environment-based-authentication.
Set SOPS_AZURE_KEYVAULT_SKIP_URI_VERIFICATION=true to disable
challenge-resource URI verification when required by your environment.
The user/sp needs the key/encrypt and key/decrypt permissions.)

To encrypt or decrypt using age, specify the recipient in the -a flag,
Expand Down
21 changes: 21 additions & 0 deletions keyservice/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ import (
"google.golang.org/grpc/status"
)

var (
// testHookCaptureAzureKey, when set by tests, receives the Azure KV MasterKey after client options are applied.
testHookCaptureAzureKey func(*azkv.MasterKey)

// testHookSkipAzureNetwork, when true, causes Azure encrypt/decrypt helpers to skip real network calls.
testHookSkipAzureNetwork bool
)

// Server is a key service server that uses SOPS MasterKeys to fulfill requests
type Server struct {
// Prompt indicates whether the server should prompt before decrypting or encrypting data
Expand Down Expand Up @@ -56,6 +64,13 @@ func (ks *Server) encryptWithAzureKeyVault(key *AzureKeyVaultKey, plaintext []by
Name: key.Name,
Version: key.Version,
}
if testHookCaptureAzureKey != nil {
testHookCaptureAzureKey(&azkvKey)
}
if testHookSkipAzureNetwork {
return []byte("dummy"), nil
}

err := azkvKey.Encrypt(plaintext)
if err != nil {
return nil, err
Expand Down Expand Up @@ -129,6 +144,12 @@ func (ks *Server) decryptWithAzureKeyVault(key *AzureKeyVaultKey, ciphertext []b
Name: key.Name,
Version: key.Version,
}
if testHookCaptureAzureKey != nil {
testHookCaptureAzureKey(&azkvKey)
}
if testHookSkipAzureNetwork {
return []byte("dummy"), nil
}
azkvKey.EncryptedKey = string(ciphertext)
plaintext, err := azkvKey.Decrypt()
return []byte(plaintext), err
Expand Down
70 changes: 70 additions & 0 deletions keyservice/server_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package keyservice

import (
"github.com/getsops/sops/v3/azkv"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
Expand Down Expand Up @@ -79,3 +80,72 @@ func TestKmsKeyToMasterKey(t *testing.T) {
})
}
}

// Azure KV tests for env-driven skip URI validation affecting client options.
func TestAzureKeyVaultClientOptionsAppliedOnEncryptDecrypt(t *testing.T) {
// ensure we don't perform network calls
testHookSkipAzureNetwork = true
t.Cleanup(func() {
testHookSkipAzureNetwork = false
testHookCaptureAzureKey = nil
})

for _, tt := range []struct {
name string
envValue string
expectOptionSet bool
}{
{name: "encrypt sets option when env true", envValue: "true", expectOptionSet: true},
{name: "encrypt leaves option nil when env false", envValue: "false", expectOptionSet: false},
{name: "encrypt leaves option nil when env invalid", envValue: "not-a-bool", expectOptionSet: false},
} {
t.Run(tt.name, func(t *testing.T) {
t.Setenv(azkv.SopsAzureKeyvaultSkipUriVerificationEnv, tt.envValue)

captured := []*azkv.MasterKey{}
testHookCaptureAzureKey = func(mk *azkv.MasterKey) { captured = append(captured, mk) }
server := &Server{}
key := &AzureKeyVaultKey{VaultUrl: "https://vault.example", Name: "keyname", Version: "v1"}
_, err := server.encryptWithAzureKeyVault(key, []byte("secret"))
require.NoError(t, err)
require.Len(t, captured, 1)

co := captured[0].EffectiveClientOptions()
if tt.expectOptionSet {
require.NotNil(t, co)
assert.True(t, co.DisableChallengeResourceVerification)
} else {
assert.Nil(t, co)
}
})
}

for _, tt := range []struct {
name string
envValue string
expectOptionSet bool
}{
{name: "decrypt sets option when env true", envValue: "1", expectOptionSet: true},
{name: "decrypt leaves option nil when env false", envValue: "0", expectOptionSet: false},
} {
t.Run(tt.name, func(t *testing.T) {
t.Setenv(azkv.SopsAzureKeyvaultSkipUriVerificationEnv, tt.envValue)

captured := []*azkv.MasterKey{}
testHookCaptureAzureKey = func(mk *azkv.MasterKey) { captured = append(captured, mk) }
server := &Server{}
key := &AzureKeyVaultKey{VaultUrl: "https://vault.example", Name: "keyname", Version: "v1"}
_, err := server.decryptWithAzureKeyVault(key, []byte("c2VjcmV0"))
require.NoError(t, err)
require.Len(t, captured, 1)

co := captured[0].EffectiveClientOptions()
if tt.expectOptionSet {
require.NotNil(t, co)
assert.True(t, co.DisableChallengeResourceVerification)
} else {
assert.Nil(t, co)
}
})
}
}