From e4c7aca6bda5b35b42080a9a4056230c8339285c Mon Sep 17 00:00:00 2001 From: Edward Klesel Date: Wed, 5 Aug 2026 10:11:45 +0100 Subject: [PATCH] azureblob: fix Entra ID server-side copy source authentication When using Microsoft Entra ID credentials, Azure Blob server-side copy uses a user delegation SAS URL for the private copy source. The SAS start time was set to the current local time. Azure Storage validates the copy source from the service side, and small clock differences can make that SAS appear not yet valid. The service then returns 403 CannotVerifyCopySource with AuthenticationFailed. Start the copy-source SAS 15 minutes in the past, matching Microsoft SAS guidance for clock skew. --- backend/azureblob/azureblob.go | 5 +++-- backend/azureblob/azureblob_internal_test.go | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/azureblob/azureblob.go b/backend/azureblob/azureblob.go index 878321097..6df71619c 100644 --- a/backend/azureblob/azureblob.go +++ b/backend/azureblob/azureblob.go @@ -70,6 +70,7 @@ const ( defaultChunkSize = 4 * fs.Mebi defaultAccessTier = blob.AccessTier("") // FIXME AccessTierNone sasCopyValidity = time.Hour // how long SAS should last when doing server side copy + sasCopyStartSkew = 15 * time.Minute // how far back SAS should start to allow for clock skew ) // setAcceptEncodingGzip is a per-call policy that sets Accept-Encoding: gzip @@ -1827,7 +1828,7 @@ func (f *Fs) getUserDelegation(ctx context.Context) (*service.UserDelegationCred } // Validity window - start := time.Now().UTC() + start := time.Now().UTC().Add(-sasCopyStartSkew) expiry := start.Add(2 * sasCopyValidity) startStr := start.Format(time.RFC3339) expiryStr := expiry.Format(time.RFC3339) @@ -1876,7 +1877,7 @@ func (o *Object) getAuth(ctx context.Context, noAuth bool) (srcURL string, err e // Build the SAS values perms := sas.BlobPermissions{Read: true} container, containerPath := o.split() - start := time.Now().UTC() + start := time.Now().UTC().Add(-sasCopyStartSkew) expiry := start.Add(sasCopyValidity) vals := sas.BlobSignatureValues{ StartTime: start, diff --git a/backend/azureblob/azureblob_internal_test.go b/backend/azureblob/azureblob_internal_test.go index acad39aad..c234463c2 100644 --- a/backend/azureblob/azureblob_internal_test.go +++ b/backend/azureblob/azureblob_internal_test.go @@ -54,6 +54,11 @@ func TestBlockIDCreator(t *testing.T) { assert.ErrorContains(t, bic2.checkID(chunkNumber, got), "random bytes") } +func TestCopySASTimingConstants(t *testing.T) { + require.Greater(t, sasCopyStartSkew, time.Duration(0)) + require.Greater(t, sasCopyValidity, sasCopyStartSkew) +} + func (f *Fs) testFeatures(t *testing.T) { // Check first feature flags are set on this remote enabled := f.Features().SetTier