My infrastructure setup is as follows:
There are two tenants – Tenant A and Tenant B.
In Tenant A, I have a resource group with a k8s cluster, a managed identity, a storage account with a container, and other resources. In Tenant B, I have a resource group with only a managed identity and a storage account with a container.
I can successfully write from an app running in the k8s cluster in Tenant A to the storage container in Tenant B. I am able to do this because in Tenant B, I gave Storage Account Contributor
and Storage Blob Data Owner
roles to the managed identity in the same resource group. Then, I created a federated credential that allows the k8s cluster in tenant A to auth to the managed identity in tenant B, by using the following command:
az identity federated-credential create
--name fc-x-tenant-test
--identity-name tenantB-managedIdentity
--resource-group tenantB-rg
--issuer <Tenant A AKS Cluster OIDC Provider URL>
--subject system:serviceaccount:default:<Tenant A Service Account name>
(I followed https://paulyu.dev/article/cross-tenant-workload-identity-on-aks/ to set this up.)
Then, using azure-sdk-for-go, I create a client
url := fmt.Sprintf("https://%s.blob.core.windows.net", accountName)
opts := &azidentity.WorkloadIdentityCredentialOptions{
TenantID: *tenantBID,
ClientID: *tenantBmanagedIdentityClientID,
}
cred, err := azidentity.NewWorkloadIdentityCredential(opts)
if err != nil {
return nil, connectors.ErrConnection("customer tenant credential err %s", err)
}
serviceClient := client.ServiceClient()
containerClient := serviceClient.NewContainerClient(containerName)
From there, I can successfully read and write to the container. Note: I need to specify the tenantID and managed identity client ID from Tenant B when I create the credential, otherwise the SDK would try to authenticate with the Tenant A – the tenant that the AKS Cluster is in!
However, when I try to generate a pre-signed URL for a client to be able to PUT objects to this container, it does not work. The signed URL looks like this https://<Tenant B Storage Account Name (redacted)>.blob.core.windows.net/<Tenant B Container Name>/path/to/object /metadata.json?se=2024-05-03T01%3A33%3A10Z&sig=KL3t1EGRODo4%2FUUu5Irtaw11vXwIEpBkO2sYTqrTg5o%3D&sp=w&spr=https&sr=b&sv=2023-11-03
.
And I get back a 403 error with
ErrorCode:AuthenticationFailed
authenticationerrordetail:Signature did not match
I create the pre-signed URL with the following code (using a client
created as shown above, with Tenant B’s tenantID and managed identity Client ID).
serviceClient := client.ServiceClient()
now := time.Now().UTC().Add(-10 * time.Second)
expiry := now.Add(24 * time.Hour)
info := azblobService.KeyInfo{
Start: to.Ptr(now.UTC().Format(sas.TimeFormat)),
Expiry: to.Ptr(expiry.UTC().Format(sas.TimeFormat)),
}
udc, err := serviceClient.GetUserDelegationCredential(ctx, info, nil)
if err != nil {
return "", err
}
sigVals := sas.BlobSignatureValues{
Protocol: sas.ProtocolHTTPS,
ExpiryTime: time.Now().Add(24 * time.Hour).UTC(),
Permissions: perm.String(),
ContainerName: containerName,
BlobName: blobName,
}
query, err := sigVals.SignWithUserDelegation(udc)
I then call query.Encode()
while generating the signed URL.
It’s also important to note, that this same code worked before in a previous setup I had where the target bucket was in Tenant A (not in a different tenant), and I was creating the client
using an EnvironmentCredential to authenticate as a service principal that the storage account had given access to with RBAC.
tl/dr; I suspect that the signedTenantId
for https://learn.microsoft.com/en-us/rest/api/storageservices/create-user-delegation-sas#construct-a-user-delegation-sas is not being set correctly using the azure-sdk-for-go when the client that you call GetUserDelegationCredential
on is a client that authenticates to another tenant. However, I really do not know what the problem is here, and will appreciate any help pointing me in the right direction. Thank you.
Andrew Levin is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.