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
11 changes: 11 additions & 0 deletions azkv/keysource.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,17 @@ 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
}

// Encrypt takes a SOPS data key, encrypts it with Azure Key Vault, and stores
// the result in the EncryptedKey field.
//
Expand Down
9 changes: 8 additions & 1 deletion cmd/sops/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1837,6 +1837,10 @@ func main() {
Usage: "comma separated list of decryption key types",
EnvVar: "SOPS_DECRYPTION_ORDER",
},
cli.BoolFlag{
Name: "azure-kv-skip-uri-validation",
Usage: "skip Azure Key Vault URI validation",
},
}, keyserviceFlags...)

app.Action = func(c *cli.Context) error {
Expand Down Expand Up @@ -2262,7 +2266,10 @@ func toExitError(err error) error {

func keyservices(c *cli.Context) (svcs []keyservice.KeyServiceClient) {
if c.Bool("enable-local-keyservice") {
svcs = append(svcs, keyservice.NewLocalClient())
// propagate azure-kv-skip-uri-validation flag to local keyservice server instance
skipAzureKvUriValidation := c.Bool("azure-kv-skip-uri-validation") || c.GlobalBool("azure-kv-skip-uri-validation")
local := keyservice.NewCustomLocalClient(keyservice.Server{Prompt: false, SkipAzureKvUriValidation: skipAzureKvUriValidation})
svcs = append(svcs, local)
}
uris := c.StringSlice("keyservice")
for _, uri := range uris {
Expand Down
2 changes: 2 additions & 0 deletions cmd/sops/subcommand/keyservice/keyservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ func Run(opts Opts) error {
grpcServer := grpc.NewServer()
keyservice.RegisterKeyServiceServer(grpcServer, keyservice.Server{
Prompt: opts.Prompt,
// remote keyservice currently does not receive CLI flags; default to false.
SkipAzureKvUriValidation: false,
})
log.Infof("Listening on %s://%s", opts.Network, opts.Address)

Expand Down
33 changes: 33 additions & 0 deletions keyservice/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,20 @@ 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
Prompt bool
// SkipAzureKvUriValidation indicates whether Azure Key Vault URI/challenge resource validation should be skipped
SkipAzureKvUriValidation bool
}

func (ks *Server) encryptWithPgp(key *PgpKey, plaintext []byte) ([]byte, error) {
Expand Down Expand Up @@ -55,6 +65,18 @@ func (ks *Server) encryptWithAzureKeyVault(key *AzureKeyVaultKey, plaintext []by
Name: key.Name,
Version: key.Version,
}

// only disable challenge resource (URI) verification if flag was provided.
if ks.SkipAzureKvUriValidation {
azkv.ApplyDisableChallengeResourceVerification(&azkvKey)
}
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 @@ -116,6 +138,17 @@ func (ks *Server) decryptWithAzureKeyVault(key *AzureKeyVaultKey, ciphertext []b
Name: key.Name,
Version: key.Version,
}

// only disable challenge resource (URI) verification if flag was provided.
if ks.SkipAzureKvUriValidation {
azkv.ApplyDisableChallengeResourceVerification(&azkvKey)
}
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
85 changes: 85 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,87 @@ func TestKmsKeyToMasterKey(t *testing.T) {
})
}
}

// Azure KV tests for skip URI validation flag affecting client options.
func TestAzureKeyVaultClientOptionsAppliedOnEncryptDecrypt(t *testing.T) {
// ensure we don't perform network calls
testHookSkipAzureNetwork = true

t.Run("encrypt applies option when flag true", func(t *testing.T) {
captured := []*azkv.MasterKey{}
testHookCaptureAzureKey = func(mk *azkv.MasterKey) { captured = append(captured, mk) }
server := &Server{SkipAzureKvUriValidation: true}
key := &AzureKeyVaultKey{VaultUrl: "https://vault.example", Name: "keyname", Version: "v1"}
_, err := server.encryptWithAzureKeyVault(key, []byte("secret"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(captured) != 1 {
t.Fatalf("expected 1 captured key, got %d", len(captured))
}
co := captured[0].ClientOptions()
if co == nil {
t.Fatalf("expected clientOptions to be set when flag true")
}
if !co.DisableChallengeResourceVerification {
t.Fatalf("expected DisableChallengeResourceVerification=true")
}
})

t.Run("encrypt leaves option nil when flag false", func(t *testing.T) {
captured := []*azkv.MasterKey{}
testHookCaptureAzureKey = func(mk *azkv.MasterKey) { captured = append(captured, mk) }
server := &Server{SkipAzureKvUriValidation: false}
key := &AzureKeyVaultKey{VaultUrl: "https://vault.example", Name: "keyname", Version: "v1"}
_, err := server.encryptWithAzureKeyVault(key, []byte("secret"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(captured) != 1 {
t.Fatalf("expected 1 captured key, got %d", len(captured))
}
co := captured[0].ClientOptions()
if co != nil {
t.Fatalf("expected clientOptions to be nil when flag false, got %#v", co)
}
})

t.Run("decrypt applies option when flag true", func(t *testing.T) {
captured := []*azkv.MasterKey{}
testHookCaptureAzureKey = func(mk *azkv.MasterKey) { captured = append(captured, mk) }
server := &Server{SkipAzureKvUriValidation: true}
key := &AzureKeyVaultKey{VaultUrl: "https://vault.example", Name: "keyname", Version: "v1"}
_, err := server.decryptWithAzureKeyVault(key, []byte("c2VjcmV0"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(captured) != 1 {
t.Fatalf("expected 1 captured key, got %d", len(captured))
}
co := captured[0].ClientOptions()
if co == nil {
t.Fatalf("expected clientOptions to be set when flag true (decrypt)")
}
if !co.DisableChallengeResourceVerification {
t.Fatalf("expected DisableChallengeResourceVerification=true (decrypt)")
}
})

t.Run("decrypt leaves option nil when flag false", func(t *testing.T) {
captured := []*azkv.MasterKey{}
testHookCaptureAzureKey = func(mk *azkv.MasterKey) { captured = append(captured, mk) }
server := &Server{SkipAzureKvUriValidation: false}
key := &AzureKeyVaultKey{VaultUrl: "https://vault.example", Name: "keyname", Version: "v1"}
_, err := server.decryptWithAzureKeyVault(key, []byte("c2VjcmV0"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(captured) != 1 {
t.Fatalf("expected 1 captured key, got %d", len(captured))
}
co := captured[0].ClientOptions()
if co != nil {
t.Fatalf("expected clientOptions to be nil when flag false (decrypt), got %#v", co)
}
})
}
Loading