diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 0872efaf..3ac74981 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -54,7 +54,19 @@ jobs: version: 1.21.2 - name: Setup SOPS uses: fluxcd/pkg/actions/sops@main + - name: Enable integration tests + # Only run integration tests for main branch + if: github.ref == 'refs/heads/main' + run: | + echo 'GO_TEST_ARGS=-tags integration' >> $GITHUB_ENV - name: Run controller tests + env: + TEST_AZURE_CLIENT_ID: ${{ secrets.TEST_AZURE_CLIENT_ID }} + TEST_AZURE_TENANT_ID: ${{ secrets.TEST_AZURE_TENANT_ID }} + TEST_AZURE_CLIENT_SECRET: ${{ secrets.TEST_AZURE_CLIENT_SECRET }} + TEST_AZURE_VAULT_URL: ${{ secrets.TEST_AZURE_VAULT_URL }} + TEST_AZURE_VAULT_KEY_NAME: ${{ secrets.TEST_AZURE_VAULT_KEY_NAME }} + TEST_AZURE_VAULT_KEY_VERSION: ${{ secrets.TEST_AZURE_VAULT_KEY_VERSION }} run: make test - name: Check if working tree is dirty run: | diff --git a/Makefile b/Makefile index 7795ff78..e027fcb4 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,9 @@ else GOBIN=$(shell go env GOBIN) endif +# Allows for defining additional Go test args, e.g. '-tags integration'. +GO_TEST_ARGS ?= + # Allows for defining additional Docker buildx arguments, e.g. '--push'. BUILD_ARGS ?= --load # Architectures to build images for. @@ -31,7 +34,7 @@ install-envtest: setup-envtest # Run controller tests KUBEBUILDER_ASSETS?="$(shell $(ENVTEST) --arch=$(ENVTEST_ARCH) use -i $(ENVTEST_KUBERNETES_VERSION) --bin-dir=$(ENVTEST_ASSETS_DIR) -p path)" test: tidy generate fmt vet manifests api-docs download-crd-deps install-envtest - KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) go test ./controllers/... -v -coverprofile cover.out + KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) go test ./... $(GO_TEST_ARGS) -v -coverprofile cover.out # Build manager binary manager: generate fmt vet diff --git a/internal/sops/azkv/keysource.go b/internal/sops/azkv/keysource.go index 816b76ec..94f5789a 100644 --- a/internal/sops/azkv/keysource.go +++ b/internal/sops/azkv/keysource.go @@ -7,6 +7,7 @@ package azkv import ( "bytes" "context" + "encoding/base64" "encoding/binary" "fmt" "io/ioutil" @@ -150,7 +151,11 @@ func (key *MasterKey) Encrypt(dataKey []byte) error { if err != nil { return fmt.Errorf("failed to encrypt data: %w", err) } - key.EncryptedKey = string(resp.Result) + // 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 } @@ -168,7 +173,14 @@ func (key *MasterKey) Decrypt() ([]byte, error) { if err != nil { return nil, fmt.Errorf("failed to construct client to decrypt data: %w", err) } - resp, err := c.Decrypt(context.Background(), crypto.EncryptionAlgorithmRSAOAEP256, []byte(key.EncryptedKey), nil) + // 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 decode encrypted key: %w", err) + } + resp, err := c.Decrypt(context.Background(), crypto.EncryptionAlgorithmRSAOAEP256, rawEncryptedKey, nil) if err != nil { return nil, fmt.Errorf("failed to decrypt data: %w", err) } diff --git a/internal/sops/azkv/keysource_integration_test.go b/internal/sops/azkv/keysource_integration_test.go new file mode 100644 index 00000000..85b21baa --- /dev/null +++ b/internal/sops/azkv/keysource_integration_test.go @@ -0,0 +1,159 @@ +// +tag integration + +/* +Copyright 2022 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 ( + "os" + "testing" + "time" + + . "github.com/onsi/gomega" + "go.mozilla.org/sops/v3/azkv" +) + +// 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 := &MasterKey{ + VaultURL: testVaultURL, + Name: testVaultKeyName, + Version: testVaultKeyVersion, + CreationDate: time.Now(), + } + + g.Expect(testAADConfig.SetToken(key)).To(Succeed()) + + g.Expect(key.Encrypt([]byte("foo"))).To(Succeed()) + g.Expect(key.EncryptedDataKey()).ToNot(BeEmpty()) +} + +func TestMasterKey_Decrypt(t *testing.T) { + g := NewWithT(t) + + key := &MasterKey{ + VaultURL: testVaultURL, + Name: testVaultKeyName, + Version: testVaultKeyVersion, + // EncryptedKey equals "foo" in bytes + EncryptedKey: "AdvS9HGJG7thHiUAisVJ8XqZiKfTjjbMETl5pIUBcOiHhMS6nLJpqeHcoKFUX6T4HFNT5o9tUXJsVprkkXzaL0Fyd01gef-eF4lTKsKl3EAn2hPAbfT-HTiuOnXzm4Zmvb4S-Ef3loOgLuoIH8Ks7SzSGhy6U9qvRk4Y4IZjzHCtUHaGE5utuTTy9lff8h4HCzgCp92ots2PPXD4dGHN_yXs-EpARXGPR2RbWWnj4P3Pu8xeMBk7hDCa51ZweJ_xQBRvXHmSy0PkauDUbr4dlUf6QQa8RxSPsOSaVT8dtVIURZ9YP1p69ajSo98aHXqSBAouZGWkrWgmQsleNrSGcg", + CreationDate: time.Now(), + } + + g.Expect(testAADConfig.SetToken(key)).To(Succeed()) + + got, err := key.Decrypt() + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(got).To(Equal([]byte("foo"))) +} + +func TestMasterKey_EncryptDecrypt_RoundTrip(t *testing.T) { + g := NewWithT(t) + + key := &MasterKey{ + VaultURL: testVaultURL, + Name: testVaultKeyName, + Version: testVaultKeyVersion, + CreationDate: time.Now(), + } + + g.Expect(testAADConfig.SetToken(key)).To(Succeed()) + + 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 := &MasterKey{ + VaultURL: testVaultURL, + Name: testVaultKeyName, + Version: testVaultKeyVersion, + CreationDate: time.Now(), + } + g.Expect(testAADConfig.SetToken(encryptKey)).To(Succeed()) + + 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 := &MasterKey{ + VaultURL: testVaultURL, + Name: testVaultKeyName, + Version: testVaultKeyVersion, + EncryptedKey: encryptKey.EncryptedKey, + CreationDate: time.Now(), + } + g.Expect(testAADConfig.SetToken(decryptKey)).To(Succeed()) + + dec, err := decryptKey.Decrypt() + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(dec).To(Equal(dataKey)) +}