Skip to content
Closed
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
19 changes: 19 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/go
{
"name": "Go",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/go:1-1.24-bookworm",
// Features to add to the dev container. More info: https://containers.dev/features.
"features": {
"ghcr.io/devcontainers/features/powershell:1": {}
}
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "go version",
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}
49 changes: 49 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Decrypt with config file",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}/cmd/sops",
"args": [
"--verbose",
"--config",
"/Users/mich7902/Documents/repos/github-ArcGISOnlineDev/ago-azure-secrets/.sops.yaml",
"--azure-kv-skip-uri-validation",
"/Users/mich7902/Documents/repos/github-ArcGISOnlineDev/ago-azure-secrets/BicepSecrets/devdeploy/management/serviceprincipal.enc.json"
]
},
{
"name": "Decrypt without config file",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}/cmd/sops",
"args": [
"--verbose",
"--azure-kv-skip-uri-validation",
"--azure-kv",
"https://arcgisazuredev.az-dev.arcgis.com/keys/sops-secrets-key/348d49aba5474974b41bae497e867c4f",
"--decrypt",
"/Users/mich7902/Documents/repos/github-ArcGISOnlineDev/ago-azure-secrets/BicepSecrets/devdeploy/management/serviceprincipal.enc.json"
]
},
{
"name": "Encrypt without config file",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}/cmd/sops",
"args": [
"--verbose",
"--azure-kv-skip-uri-validation",
"--azure-kv",
"https://arcgisazuredev.az-dev.arcgis.com/keys/sops-secrets-key/348d49aba5474974b41bae497e867c4f",
"--encrypt",
"/Users/mich7902/Documents/repos/sops/test.json",
]
}
]
}
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
11 changes: 9 additions & 2 deletions cmd/sops/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ func main() {
return toExitError(err)
}
if info.IsDir() && !c.Bool("recursive") {
return fmt.Errorf("can't operate on a directory without --recursive flag.")
return fmt.Errorf("can't operate on a directory without --recursive flag")
}
order, err := decryptionOrder(c.String("decryption-order"))
if err != nil {
Expand Down 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)
}
})
}