feat(providers): Azure Federated Identity support (#4728)

Add authentication support for Azure Federated Identity (AFI)

Authored-by: @alisonb-veeam
Authored-by: Alison Burgess <a.burgess@veeam.com>
This commit is contained in:
Julio Lopez
2025-07-19 12:24:03 -07:00
committed by GitHub
parent 3a38279bcd
commit 3ae1c0e225
4 changed files with 69 additions and 13 deletions

View File

@@ -26,6 +26,7 @@ func (c *storageAzureFlags) Setup(svc StorageProviderServices, cmd *kingpin.CmdC
cmd.Flag("client-id", "Azure service principle client ID (overrides AZURE_CLIENT_ID environment variable)").Envar(svc.EnvName("AZURE_CLIENT_ID")).StringVar(&c.azOptions.ClientID)
cmd.Flag("client-secret", "Azure service principle client secret (overrides AZURE_CLIENT_SECRET environment variable)").Envar(svc.EnvName("AZURE_CLIENT_SECRET")).StringVar(&c.azOptions.ClientSecret)
cmd.Flag("client-cert", "Azure client certificate (overrides AZURE_CLIENT_CERTIFICATE environment variable)").Envar(svc.EnvName("AZURE_CLIENT_CERTIFICATE")).StringVar(&c.azOptions.ClientCertificate)
cmd.Flag("azure-federated-token-file", "Path to a file containing an Azure Federated Token (overrides AZURE_FEDERATED_TOKEN_FILE environment variable)").Envar(svc.EnvName("AZURE_FEDERATED_TOKEN_FILE")).StringVar(&c.azOptions.AzureFederatedTokenFile)
commonThrottlingFlags(cmd, &c.azOptions.Limits)

View File

@@ -31,6 +31,9 @@ type Options struct {
// ClientCertificate are used for creating ClientCertificateCredentials
ClientCertificate string `json:"clientCertificate,omitempty" kopia:"sensitive"`
// AzureFederatedTokenFile is the path to a file containing an Azure Federated Token.
AzureFederatedTokenFile string `json:"azureFederatedTokenFile,omitempty"`
StorageDomain string `json:"storageDomain,omitempty"`
throttling.Limits

View File

@@ -403,6 +403,7 @@ func New(ctx context.Context, opt *Options, isCreate bool) (blob.Storage, error)
return az, nil
}
//nolint:gocyclo
func getAZService(opt *Options, storageHostname string) (*azblob.Client, error) {
var (
service *azblob.Client
@@ -445,9 +446,21 @@ func getAZService(opt *Options, storageHostname string) (*azblob.Client, error)
return nil, errors.Wrap(credErr, "unable to initialize client cert credential")
}
service, serviceErr = azblob.NewClient(fmt.Sprintf("https://%s/", storageHostname), cred, nil)
// Azure Federated Token
case opt.TenantID != "" && opt.ClientID != "" && opt.AzureFederatedTokenFile != "":
cred, err := azidentity.NewWorkloadIdentityCredential(&azidentity.WorkloadIdentityCredentialOptions{
ClientID: opt.ClientID,
TenantID: opt.TenantID,
TokenFilePath: opt.AzureFederatedTokenFile,
})
if err != nil {
return nil, errors.Wrap(err, "unable to initialize Azure Federated Identity workload identity credential")
}
service, serviceErr = azblob.NewClient(fmt.Sprintf("https://%s/", storageHostname), cred, nil)
default:
return nil, errors.New("one of the storage key, SAS token, client secret or client certificate must be provided")
return nil, errors.New("one of the storage key, SAS token, client secret, client certificate, or Azure Federated Token must be provided")
}
return service, errors.Wrap(serviceErr, "unable to create azure client")

View File

@@ -22,18 +22,19 @@
)
const (
testContainerEnv = "KOPIA_AZURE_TEST_CONTAINER"
testStorageAccountEnv = "KOPIA_AZURE_TEST_STORAGE_ACCOUNT"
testStorageKeyEnv = "KOPIA_AZURE_TEST_STORAGE_KEY"
testStorageSASTokenEnv = "KOPIA_AZURE_TEST_SAS_TOKEN"
testImmutableContainerEnv = "KOPIA_AZURE_TEST_IMMUTABLE_CONTAINER"
testImmutableStorageAccountEnv = "KOPIA_AZURE_TEST_IMMUTABLE_STORAGE_ACCOUNT"
testImmutableStorageKeyEnv = "KOPIA_AZURE_TEST_IMMUTABLE_STORAGE_KEY"
testImmutableStorageSASTokenEnv = "KOPIA_AZURE_TEST_IMMUTABLE_SAS_TOKEN"
testStorageTenantIDEnv = "KOPIA_AZURE_TEST_TENANT_ID"
testStorageClientIDEnv = "KOPIA_AZURE_TEST_CLIENT_ID"
testStorageClientSecretEnv = "KOPIA_AZURE_TEST_CLIENT_SECRET"
testStorageClientCertEnv = "KOPIA_AZURE_TEST_CLIENT_CERTIFICATE"
testContainerEnv = "KOPIA_AZURE_TEST_CONTAINER"
testStorageAccountEnv = "KOPIA_AZURE_TEST_STORAGE_ACCOUNT"
testStorageKeyEnv = "KOPIA_AZURE_TEST_STORAGE_KEY"
testStorageSASTokenEnv = "KOPIA_AZURE_TEST_SAS_TOKEN"
testImmutableContainerEnv = "KOPIA_AZURE_TEST_IMMUTABLE_CONTAINER"
testImmutableStorageAccountEnv = "KOPIA_AZURE_TEST_IMMUTABLE_STORAGE_ACCOUNT"
testImmutableStorageKeyEnv = "KOPIA_AZURE_TEST_IMMUTABLE_STORAGE_KEY"
testImmutableStorageSASTokenEnv = "KOPIA_AZURE_TEST_IMMUTABLE_SAS_TOKEN"
testStorageTenantIDEnv = "KOPIA_AZURE_TEST_TENANT_ID"
testStorageClientIDEnv = "KOPIA_AZURE_TEST_CLIENT_ID"
testStorageClientSecretEnv = "KOPIA_AZURE_TEST_CLIENT_SECRET"
testStorageClientCertEnv = "KOPIA_AZURE_TEST_CLIENT_CERTIFICATE"
testAzureFederatedIdentityFilePathEnv = "KOPIA_AZURE_FEDERATED_IDENTITY_FILE_PATH"
)
func getEnvOrSkip(t *testing.T, name string) string {
@@ -240,6 +241,44 @@ func TestAzureStorageClientCertificate(t *testing.T) {
require.NoError(t, providervalidation.ValidateProvider(ctx, st, blobtesting.TestValidationOptions))
}
func TestAzureFederatedIdentity(t *testing.T) {
t.Parallel()
testutil.ProviderTest(t)
container := getEnvOrSkip(t, testContainerEnv)
storageAccount := getEnvOrSkip(t, testStorageAccountEnv)
tenantID := getEnvOrSkip(t, testStorageTenantIDEnv)
clientID := getEnvOrSkip(t, testStorageClientIDEnv)
azureFederatedTokenFilePath := getEnvOrSkip(t, testAzureFederatedIdentityFilePathEnv)
data := make([]byte, 8)
rand.Read(data)
ctx := testlogging.Context(t)
// use context that gets canceled after storage is initialize,
// to verify we do not depend on the original context past initialization.
newctx, cancel := context.WithCancel(ctx)
st, err := azure.New(newctx, &azure.Options{
Container: container,
StorageAccount: storageAccount,
TenantID: tenantID,
ClientID: clientID,
AzureFederatedTokenFile: azureFederatedTokenFilePath,
Prefix: fmt.Sprintf("sastest-%v-%x/", clock.Now().Unix(), data),
}, false)
require.NoError(t, err)
cancel()
defer st.Close(ctx)
defer blobtesting.CleanupOldData(ctx, t, st, 0)
blobtesting.VerifyStorage(ctx, t, st, blob.PutOptions{})
blobtesting.AssertConnectionInfoRoundTrips(ctx, t, st)
require.NoError(t, providervalidation.ValidateProvider(ctx, st, blobtesting.TestValidationOptions))
}
func TestAzureStorageInvalidBlob(t *testing.T) {
testutil.ProviderTest(t)