From 2c134eafe456ee77bbd46187040aa5041e5643ab Mon Sep 17 00:00:00 2001 From: Armando Ruocco Date: Thu, 8 Jan 2026 14:58:27 +0100 Subject: [PATCH] feat: add support for DefaultAzureCredential authentication mechanism (#681) This commit adds support for the DefaultAzureCredential authentication mechanism in Azure Blob Storage. Users can now use the `useDefaultAzureCredentials` option to enable Azure's default credential chain, which automatically discovers and uses available credentials in the following order 1. Environment Variables (Service Principal) 2. Managed Identity 3. Azure CLI 4. Azure PowerShell This is particularly useful when running on Azure Kubernetes Service (AKS) with Workload Identity, eliminating the need to explicitly store credentials in Kubernetes Secrets. Signed-off-by: Armando Ruocco Signed-off-by: Gabriele Fedi Signed-off-by: Marco Nenciarini Co-authored-by: Gabriele Fedi Co-authored-by: Marco Nenciarini --- .wordlist.txt | 4 + .../barmancloud.cnpg.io_objectstores.yaml | 5 + go.mod | 2 +- go.sum | 4 +- internal/cnpgi/operator/specs/secrets.go | 18 +- internal/cnpgi/operator/specs/secrets_test.go | 227 ++++++++++++++++++ internal/cnpgi/operator/specs/suite_test.go | 32 +++ manifest.yaml | 5 + web/docs/object_stores.md | 46 +++- 9 files changed, 327 insertions(+), 16 deletions(-) create mode 100644 internal/cnpgi/operator/specs/secrets_test.go create mode 100644 internal/cnpgi/operator/specs/suite_test.go diff --git a/.wordlist.txt b/.wordlist.txt index 246e667..3e59b45 100644 --- a/.wordlist.txt +++ b/.wordlist.txt @@ -1,3 +1,4 @@ +AKS AccessDenied AdditionalContainerArgs Akamai @@ -5,6 +6,7 @@ Azurite BarmanObjectStore BarmanObjectStoreConfiguration BarmanObjectStores +CLI CNCF CRD CloudNativePG @@ -38,6 +40,7 @@ PITR PoR PostgreSQL Postgres +PowerShell README RPO RTO @@ -45,6 +48,7 @@ RecoveryWindow ResourceRequirements RetentionPolicy SAS +SDK SFO SPDX SPDX diff --git a/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml b/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml index a141948..6abbd75 100644 --- a/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml +++ b/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml @@ -108,6 +108,11 @@ spec: - key - name type: object + useDefaultAzureCredentials: + description: |- + Use the default Azure authentication flow, which includes DefaultAzureCredential. + This allows authentication using environment variables and managed identities. + type: boolean type: object data: description: |- diff --git a/go.mod b/go.mod index cc36802..6b11108 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.25.5 require ( github.com/cert-manager/cert-manager v1.19.2 github.com/cloudnative-pg/api v1.28.0 - github.com/cloudnative-pg/barman-cloud v0.4.1-0.20251230211524-20b7e0e10b0f + github.com/cloudnative-pg/barman-cloud v0.4.1-0.20260108104508-ced266c145f5 github.com/cloudnative-pg/cloudnative-pg v1.28.0 github.com/cloudnative-pg/cnpg-i v0.3.1 github.com/cloudnative-pg/cnpg-i-machinery v0.4.2 diff --git a/go.sum b/go.sum index 709f14e..22d8a0b 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudnative-pg/api v1.28.0 h1:xElzHliO0eKkVQafkfMhDJo0aIRCmB1ItEt+SGh6B58= github.com/cloudnative-pg/api v1.28.0/go.mod h1:puXJBOsEaJd8JLgvCtxgl2TO/ZANap/z7bPepKRUgrk= -github.com/cloudnative-pg/barman-cloud v0.4.1-0.20251230211524-20b7e0e10b0f h1:4/PwIQOwQSTIxuncGRn3pX2V9CRwl7zJNXOVWOMSCCU= -github.com/cloudnative-pg/barman-cloud v0.4.1-0.20251230211524-20b7e0e10b0f/go.mod h1:qD0NtJOllNQbRB0MaleuHsZjFYaXtXfdg0HbFTbuHn0= +github.com/cloudnative-pg/barman-cloud v0.4.1-0.20260108104508-ced266c145f5 h1:wPB7VTNgTv6t9sl4QYOBakmVTqHnOdKUht7Q3aL+uns= +github.com/cloudnative-pg/barman-cloud v0.4.1-0.20260108104508-ced266c145f5/go.mod h1:qD0NtJOllNQbRB0MaleuHsZjFYaXtXfdg0HbFTbuHn0= github.com/cloudnative-pg/cloudnative-pg v1.28.0 h1:vkv0a0ewDSfJOPJrsyUr4uczsxheReAWf/k171V0Dm0= github.com/cloudnative-pg/cloudnative-pg v1.28.0/go.mod h1:209fkRR6m0vXUVQ9Q498eAPQqN2UlXECbXXtpGsZz3I= github.com/cloudnative-pg/cnpg-i v0.3.1 h1:fKj8NoToWI11HUL2UWYJBpkVzmaTvbs3kDMo7wQF8RU= diff --git a/internal/cnpgi/operator/specs/secrets.go b/internal/cnpgi/operator/specs/secrets.go index c1fd268..89811ad 100644 --- a/internal/cnpgi/operator/specs/secrets.go +++ b/internal/cnpgi/operator/specs/secrets.go @@ -37,13 +37,17 @@ func CollectSecretNamesFromCredentials(barmanCredentials *barmanapi.BarmanCreden ) } if barmanCredentials.Azure != nil { - references = append( - references, - barmanCredentials.Azure.ConnectionString, - barmanCredentials.Azure.StorageAccount, - barmanCredentials.Azure.StorageKey, - barmanCredentials.Azure.StorageSasToken, - ) + // When using default Azure credentials or managed identity, no secrets are required + if !barmanCredentials.Azure.UseDefaultAzureCredentials && + !barmanCredentials.Azure.InheritFromAzureAD { + references = append( + references, + barmanCredentials.Azure.ConnectionString, + barmanCredentials.Azure.StorageAccount, + barmanCredentials.Azure.StorageKey, + barmanCredentials.Azure.StorageSasToken, + ) + } } if barmanCredentials.Google != nil { references = append( diff --git a/internal/cnpgi/operator/specs/secrets_test.go b/internal/cnpgi/operator/specs/secrets_test.go new file mode 100644 index 0000000..d6fa706 --- /dev/null +++ b/internal/cnpgi/operator/specs/secrets_test.go @@ -0,0 +1,227 @@ +/* +Copyright © contributors to CloudNativePG, established as +CloudNativePG a Series of LF Projects, LLC. + +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. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package specs + +import ( + barmanapi "github.com/cloudnative-pg/barman-cloud/pkg/api" + machineryapi "github.com/cloudnative-pg/machinery/pkg/api" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("CollectSecretNamesFromCredentials", func() { + Context("when collecting secrets from AWS credentials", func() { + It("should return secret names from S3 credentials", func() { + credentials := &barmanapi.BarmanCredentials{ + AWS: &barmanapi.S3Credentials{ + AccessKeyIDReference: &machineryapi.SecretKeySelector{ + LocalObjectReference: machineryapi.LocalObjectReference{ + Name: "aws-secret", + }, + Key: "access-key-id", + }, + SecretAccessKeyReference: &machineryapi.SecretKeySelector{ + LocalObjectReference: machineryapi.LocalObjectReference{ + Name: "aws-secret", + }, + Key: "secret-access-key", + }, + }, + } + + secrets := CollectSecretNamesFromCredentials(credentials) + Expect(secrets).To(ContainElement("aws-secret")) + }) + + It("should handle nil AWS credentials", func() { + credentials := &barmanapi.BarmanCredentials{} + + secrets := CollectSecretNamesFromCredentials(credentials) + Expect(secrets).To(BeEmpty()) + }) + }) + + Context("when collecting secrets from Azure credentials", func() { + It("should return secret names when using explicit credentials", func() { + credentials := &barmanapi.BarmanCredentials{ + Azure: &barmanapi.AzureCredentials{ + ConnectionString: &machineryapi.SecretKeySelector{ + LocalObjectReference: machineryapi.LocalObjectReference{ + Name: "azure-secret", + }, + Key: "connection-string", + }, + }, + } + + secrets := CollectSecretNamesFromCredentials(credentials) + Expect(secrets).To(ContainElement("azure-secret")) + }) + + It("should return empty list when using UseDefaultAzureCredentials", func() { + credentials := &barmanapi.BarmanCredentials{ + Azure: &barmanapi.AzureCredentials{ + UseDefaultAzureCredentials: true, + ConnectionString: &machineryapi.SecretKeySelector{ + LocalObjectReference: machineryapi.LocalObjectReference{ + Name: "azure-secret", + }, + Key: "connection-string", + }, + }, + } + + secrets := CollectSecretNamesFromCredentials(credentials) + Expect(secrets).To(BeEmpty()) + }) + + It("should return empty list when using InheritFromAzureAD", func() { + credentials := &barmanapi.BarmanCredentials{ + Azure: &barmanapi.AzureCredentials{ + InheritFromAzureAD: true, + }, + } + + secrets := CollectSecretNamesFromCredentials(credentials) + Expect(secrets).To(BeEmpty()) + }) + + It("should return secret names for storage account and key", func() { + credentials := &barmanapi.BarmanCredentials{ + Azure: &barmanapi.AzureCredentials{ + StorageAccount: &machineryapi.SecretKeySelector{ + LocalObjectReference: machineryapi.LocalObjectReference{ + Name: "azure-storage", + }, + Key: "account-name", + }, + StorageKey: &machineryapi.SecretKeySelector{ + LocalObjectReference: machineryapi.LocalObjectReference{ + Name: "azure-storage", + }, + Key: "account-key", + }, + }, + } + + secrets := CollectSecretNamesFromCredentials(credentials) + Expect(secrets).To(ContainElement("azure-storage")) + }) + }) + + Context("when collecting secrets from Google credentials", func() { + It("should return secret names from Google credentials", func() { + credentials := &barmanapi.BarmanCredentials{ + Google: &barmanapi.GoogleCredentials{ + ApplicationCredentials: &machineryapi.SecretKeySelector{ + LocalObjectReference: machineryapi.LocalObjectReference{ + Name: "google-secret", + }, + Key: "credentials.json", + }, + }, + } + + secrets := CollectSecretNamesFromCredentials(credentials) + Expect(secrets).To(ContainElement("google-secret")) + }) + }) + + Context("when collecting secrets from multiple cloud providers", func() { + It("should return secret names from all providers", func() { + credentials := &barmanapi.BarmanCredentials{ + AWS: &barmanapi.S3Credentials{ + AccessKeyIDReference: &machineryapi.SecretKeySelector{ + LocalObjectReference: machineryapi.LocalObjectReference{ + Name: "aws-secret", + }, + Key: "access-key-id", + }, + }, + Azure: &barmanapi.AzureCredentials{ + ConnectionString: &machineryapi.SecretKeySelector{ + LocalObjectReference: machineryapi.LocalObjectReference{ + Name: "azure-secret", + }, + Key: "connection-string", + }, + }, + Google: &barmanapi.GoogleCredentials{ + ApplicationCredentials: &machineryapi.SecretKeySelector{ + LocalObjectReference: machineryapi.LocalObjectReference{ + Name: "google-secret", + }, + Key: "credentials.json", + }, + }, + } + + secrets := CollectSecretNamesFromCredentials(credentials) + Expect(secrets).To(ContainElements("aws-secret", "azure-secret", "google-secret")) + }) + + It("should skip Azure secrets when using UseDefaultAzureCredentials with other providers", func() { + credentials := &barmanapi.BarmanCredentials{ + AWS: &barmanapi.S3Credentials{ + AccessKeyIDReference: &machineryapi.SecretKeySelector{ + LocalObjectReference: machineryapi.LocalObjectReference{ + Name: "aws-secret", + }, + Key: "access-key-id", + }, + }, + Azure: &barmanapi.AzureCredentials{ + UseDefaultAzureCredentials: true, + ConnectionString: &machineryapi.SecretKeySelector{ + LocalObjectReference: machineryapi.LocalObjectReference{ + Name: "azure-secret", + }, + Key: "connection-string", + }, + }, + } + + secrets := CollectSecretNamesFromCredentials(credentials) + Expect(secrets).To(ContainElement("aws-secret")) + Expect(secrets).NotTo(ContainElement("azure-secret")) + }) + }) + + Context("when handling nil references", func() { + It("should skip nil secret references", func() { + credentials := &barmanapi.BarmanCredentials{ + AWS: &barmanapi.S3Credentials{ + AccessKeyIDReference: &machineryapi.SecretKeySelector{ + LocalObjectReference: machineryapi.LocalObjectReference{ + Name: "aws-secret", + }, + Key: "access-key-id", + }, + SecretAccessKeyReference: nil, + }, + } + + secrets := CollectSecretNamesFromCredentials(credentials) + Expect(secrets).To(ContainElement("aws-secret")) + Expect(len(secrets)).To(Equal(1)) + }) + }) +}) diff --git a/internal/cnpgi/operator/specs/suite_test.go b/internal/cnpgi/operator/specs/suite_test.go new file mode 100644 index 0000000..1dc0ae1 --- /dev/null +++ b/internal/cnpgi/operator/specs/suite_test.go @@ -0,0 +1,32 @@ +/* +Copyright © contributors to CloudNativePG, established as +CloudNativePG a Series of LF Projects, LLC. + +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. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package specs + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestSpecs(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Specs Suite") +} diff --git a/manifest.yaml b/manifest.yaml index 95ed059..b4f98a7 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -107,6 +107,11 @@ spec: - key - name type: object + useDefaultAzureCredentials: + description: |- + Use the default Azure authentication flow, which includes DefaultAzureCredential. + This allows authentication using environment variables and managed identities. + type: boolean type: object data: description: |- diff --git a/web/docs/object_stores.md b/web/docs/object_stores.md index f1714c9..4ccedac 100644 --- a/web/docs/object_stores.md +++ b/web/docs/object_stores.md @@ -230,14 +230,18 @@ is Microsoft’s cloud-based object storage solution. Barman Cloud supports the following authentication methods: - [Connection String](https://learn.microsoft.com/en-us/azure/storage/common/storage-configure-connection-string) -- Storage Account Name + [Access Key](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-keys-manage) -- Storage Account Name + [SAS Token](https://learn.microsoft.com/en-us/azure/storage/blobs/sas-service-create) -- [Azure AD Workload Identity](https://azure.github.io/azure-workload-identity/docs/introduction.html) +- Storage Account Name + [Storage Account Access Key](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-keys-manage) +- Storage Account Name + [Storage Account SAS Token](https://learn.microsoft.com/en-us/azure/storage/blobs/sas-service-create) +- [Azure AD Managed Identity](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) +- [Default Azure Credentials](https://learn.microsoft.com/en-us/dotnet/api/azure.identity.defaultazurecredential?view=azure-dotnet) -### Azure AD Workload Identity +### Azure AD Managed Identity -This method avoids storing credentials in Kubernetes via the -`.spec.configuration.inheritFromAzureAD` option: +This method avoids storing credentials in Kubernetes by enabling the +usage of [Azure Managed Identities](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) authentication mechanism. +This can be enabled by setting the `inheritFromAzureAD` option to `true`. +Managed Identity can be configured for the AKS Cluster by following +the [Azure documentation](https://learn.microsoft.com/en-us/azure/aks/use-managed-identity?pivots=system-assigned). ```yaml apiVersion: barmancloud.cnpg.io/v1 @@ -252,6 +256,36 @@ spec: [...] ``` +### Default Azure Credentials + +The `useDefaultAzureCredentials` option enables the default Azure credentials +flow, which uses [`DefaultAzureCredential`](https://learn.microsoft.com/en-us/python/api/azure-identity/azure.identity.defaultazurecredential) +to automatically discover and use available credentials in the following order: + +1. **Environment Variables** — `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, and `AZURE_TENANT_ID` for Service Principal authentication +2. **Managed Identity** — Uses the managed identity assigned to the pod +3. **Azure CLI** — Uses credentials from the Azure CLI if available +4. **Azure PowerShell** — Uses credentials from Azure PowerShell if available + +This approach is particularly useful for getting started with development and testing; it allows +the SDK to attempt multiple authentication mechanisms seamlessly across different environments. +However, this is not recommended for production. Please refer to the +[official Azure guidance](https://learn.microsoft.com/en-us/dotnet/azure/sdk/authentication/credential-chains?tabs=dac#usage-guidance-for-defaultazurecredential) +for a comprehensive understanding of `DefaultAzureCredential`. + +```yaml +apiVersion: barmancloud.cnpg.io/v1 +kind: ObjectStore +metadata: + name: azure-store +spec: + configuration: + destinationPath: "" + azureCredentials: + useDefaultAzureCredentials: true + [...] +``` + ### Access Key, SAS Token, or Connection String Store credentials in a Kubernetes secret: