Skip to content

feat(secrets): add Azure Key Vault provider#5428

Merged
kodiakhq[bot] merged 6 commits into
v2from
feat/azure-key-vault-secrets
Feb 28, 2026
Merged

feat(secrets): add Azure Key Vault provider#5428
kodiakhq[bot] merged 6 commits into
v2from
feat/azure-key-vault-secrets

Conversation

@markphelps

Copy link
Copy Markdown
Collaborator

Summary

  • Adds Azure Key Vault as a secret provider for Flipt, following the same patterns as the existing AWS Secrets Manager and GCP Secret Manager providers
  • Uses the Azure SDK for Go (azidentity + azsecrets) with DefaultAzureCredential for flexible authentication (managed identity, env vars, Azure CLI, etc.)
  • Supports emulator mode via AZURE_KEYVAULT_EMULATOR env var for integration testing with Lowkey Vault

Changes

  • Config: Added AzureProviderConfig with vault_url field, validation, defaults, and schema (CUE + JSON)
  • Provider: Implemented secrets.Provider interface with GetSecret and ListSecrets using the Azure Key Vault SDK
  • Wiring: Registered provider factory via init(), added manager initialization block, and blank import in main.go
  • Tests: Unit tests for config validation, provider registration, manager lifecycle, and secret reference resolution
  • Integration: Added signing/azure test case using Lowkey Vault emulator via Dagger

Add Azure Key Vault as a secret provider for Flipt, following the same
patterns as the existing GCP and AWS providers.

- Add AzureProviderConfig with vault_url field and validation
- Implement provider using Azure SDK (azidentity + azsecrets)
- Support emulator mode via AZURE_KEYVAULT_EMULATOR env var
- Wire provider into secrets manager and main.go
- Add CUE and JSON schema entries
- Add unit tests for config, provider, manager, and secret resolution
- Add integration test using Lowkey Vault emulator

Signed-off-by: Mark Phelps <[email protected]>
@markphelps markphelps added the v2 Flipt v2 label Feb 25, 2026
Comment thread internal/coss/secrets/azure/provider.go Fixed
@codecov

codecov Bot commented Feb 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.29213% with 30 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.40%. Comparing base (660efb9) to head (04f1b6c).
⚠️ Report is 5 commits behind head on v2.

Files with missing lines Patch % Lines
internal/coss/secrets/azure/provider.go 65.27% 22 Missing and 3 partials ⚠️
internal/secrets/manager.go 63.63% 2 Missing and 2 partials ⚠️
internal/config/secrets.go 83.33% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##               v2    #5428      +/-   ##
==========================================
+ Coverage   60.36%   60.40%   +0.03%     
==========================================
  Files         140      141       +1     
  Lines       13906    13995      +89     
==========================================
+ Hits         8394     8453      +59     
- Misses       4796     4821      +25     
- Partials      716      721       +5     
Flag Coverage Δ
integrationtests 34.32% <57.30%> (+0.15%) ⬆️
unittests 51.53% <17.97%> (-0.22%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

- Run go mod tidy to clean up dependencies
- Use existing Lowkey Vault image tag (7.1.13) instead of non-existent 2.4.108

Signed-off-by: Mark Phelps <[email protected]>
The Azure SDK validates that the challenge resource URL matches the
requested domain, which fails with the Lowkey Vault emulator. Set
DisableChallengeResourceVerification to true in emulator mode.

Signed-off-by: Mark Phelps <[email protected]>
Transport: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, //nolint:gosec // emulator only

Check failure

Code scanning / CodeQL

Disabled TLS certificate check High

InsecureSkipVerify should not be used in production code.

Copilot Autofix

AI 5 months ago

In general, the fix is to avoid setting InsecureSkipVerify: true and instead configure TLS so that the emulator’s certificate can be validated. This usually means loading the emulator’s CA certificate into a CertPool and setting RootCAs on the tls.Config, leaving verification enabled.

Concretely for internal/coss/secrets/azure/provider.go, we can keep the special emulator branch, but replace the InsecureSkipVerify: true config with one that loads a CA certificate from a file path specified by an environment variable (e.g., AZURE_KEYVAULT_EMULATOR_CA_CERT). If that variable is not set or the CA file cannot be loaded, we should fail early instead of silently falling back to insecure TLS. This preserves existing functionality (connecting to an emulator with a self‑signed cert) while ensuring proper certificate validation.

To implement this:

  • Add imports for crypto/x509, encoding/pem, and io/ioutil (or os.ReadFile alone; here we’ll use os.ReadFile to avoid an extra import) to parse a PEM CA file.
  • In the emulator branch (inside if os.Getenv("AZURE_KEYVAULT_EMULATOR") != "" { ... }), before constructing clientOpts, read the CA path from an env var like AZURE_KEYVAULT_EMULATOR_CA_CERT.
  • Read and parse the CA certificate, add it to a new x509.CertPool, and set TLSClientConfig: &tls.Config{RootCAs: caPool} without InsecureSkipVerify.
  • If the CA env var is missing or parsing fails, return an error from NewProvider so misconfigurations don’t lead to insecure defaults.

All changes are confined to internal/coss/secrets/azure/provider.go in the shown snippet.

Suggested changeset 1
internal/coss/secrets/azure/provider.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/internal/coss/secrets/azure/provider.go b/internal/coss/secrets/azure/provider.go
--- a/internal/coss/secrets/azure/provider.go
+++ b/internal/coss/secrets/azure/provider.go
@@ -8,6 +8,8 @@
 import (
 	"context"
 	"crypto/tls"
+	"crypto/x509"
+	"encoding/pem"
 	"fmt"
 	"net/http"
 	"os"
@@ -55,12 +57,37 @@
 
 		// Use fake credential for emulator testing
 		cred = &fakeCredential{}
+
+		// Configure TLS to trust the emulator's CA certificate instead of disabling verification.
+		emulatorCACertPath := os.Getenv("AZURE_KEYVAULT_EMULATOR_CA_CERT")
+		if emulatorCACertPath == "" {
+			return nil, fmt.Errorf("AZURE_KEYVAULT_EMULATOR_CA_CERT must be set when using AZURE_KEYVAULT_EMULATOR")
+		}
+
+		caData, err := os.ReadFile(emulatorCACertPath)
+		if err != nil {
+			return nil, fmt.Errorf("reading emulator CA certificate from %s: %w", emulatorCACertPath, err)
+		}
+
+		block, _ := pem.Decode(caData)
+		if block == nil {
+			return nil, fmt.Errorf("failed to decode PEM for emulator CA certificate at %s", emulatorCACertPath)
+		}
+
+		cert, err := x509.ParseCertificate(block.Bytes)
+		if err != nil {
+			return nil, fmt.Errorf("parsing emulator CA certificate at %s: %w", emulatorCACertPath, err)
+		}
+
+		caPool := x509.NewCertPool()
+		caPool.AddCert(cert)
+
 		clientOpts = &azsecrets.ClientOptions{
 			ClientOptions: azcore.ClientOptions{
 				Transport: &http.Client{
 					Transport: &http.Transport{
 						TLSClientConfig: &tls.Config{
-							InsecureSkipVerify: true, //nolint:gosec // emulator only
+							RootCAs: caPool,
 						},
 					},
 				},
EOF
@@ -8,6 +8,8 @@
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"net/http"
"os"
@@ -55,12 +57,37 @@

// Use fake credential for emulator testing
cred = &fakeCredential{}

// Configure TLS to trust the emulator's CA certificate instead of disabling verification.
emulatorCACertPath := os.Getenv("AZURE_KEYVAULT_EMULATOR_CA_CERT")
if emulatorCACertPath == "" {
return nil, fmt.Errorf("AZURE_KEYVAULT_EMULATOR_CA_CERT must be set when using AZURE_KEYVAULT_EMULATOR")
}

caData, err := os.ReadFile(emulatorCACertPath)
if err != nil {
return nil, fmt.Errorf("reading emulator CA certificate from %s: %w", emulatorCACertPath, err)
}

block, _ := pem.Decode(caData)
if block == nil {
return nil, fmt.Errorf("failed to decode PEM for emulator CA certificate at %s", emulatorCACertPath)
}

cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("parsing emulator CA certificate at %s: %w", emulatorCACertPath, err)
}

caPool := x509.NewCertPool()
caPool.AddCert(cert)

clientOpts = &azsecrets.ClientOptions{
ClientOptions: azcore.ClientOptions{
Transport: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, //nolint:gosec // emulator only
RootCAs: caPool,
},
},
},
Copilot is powered by AI and may make mistakes. Always verify output.
The Lowkey Vault emulator creates vaults at https://<name>.localhost:<port>
by default, but the Dagger service binding exposes it as "lowkey-vault".
Register a vault alias so the emulator recognizes requests to
https://lowkey-vault:8443 as a valid vault URI.

Fixes "Unable to find active vault: https://lowkey-vault:8443" error
in the signing/azure integration test.

Signed-off-by: Mark Phelps <[email protected]>
@markphelps
markphelps marked this pull request as ready for review February 27, 2026 17:58
@markphelps
markphelps requested a review from a team as a code owner February 27, 2026 17:58
@dosubot dosubot Bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label Feb 27, 2026
@dosubot

dosubot Bot commented Feb 27, 2026

Copy link
Copy Markdown

Related Documentation

Checked 4 published document(s) in 1 knowledge base(s). No updates required.

How did I do? Any feedback?  Join Discord

Comment thread build/testing/integration.go

@markphelps markphelps left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<port> is correct here — it's a Lowkey Vault placeholder that gets substituted at runtime with the actual --server.port value (8443 in this case). I've added a clarifying comment in the code.

Add comment explaining that <port> is a Lowkey Vault runtime placeholder,
not a literal value, to avoid confusion during code review.

Signed-off-by: Mark Phelps <[email protected]>
@markphelps markphelps added the automerge Used by Kodiak bot to automerge PRs label Feb 28, 2026
@kodiakhq
kodiakhq Bot merged commit 0d039fd into v2 Feb 28, 2026
32 of 33 checks passed
@kodiakhq
kodiakhq Bot deleted the feat/azure-key-vault-secrets branch February 28, 2026 00:43
@github-project-automation github-project-automation Bot moved this to Done in Flipt V2 Feb 28, 2026
@markphelps markphelps added the needs docs Requires documentation updates label Mar 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automerge Used by Kodiak bot to automerge PRs needs docs Requires documentation updates size:XL This PR changes 500-999 lines, ignoring generated files. v2 Flipt v2

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants