Skip to content

[Bug]: files_external AmazonS3::test() uses HeadBucket which is rejected by Backblaze B2 bucket-restricted application keys #60791

@jg-shinji-ueda

Description

@jg-shinji-ueda

⚠️ This issue respects the following points: ⚠️

Bug description

Summary

apps/files_external/lib/Lib/Storage/AmazonS3.php::test() calls headBucket() to verify the configured external S3 storage is reachable. The Availability storage wrapper invokes test() and, when it throws, records available=false in the storage availability cache. Subsequent file operations short-circuit to HTTP 503 until RECHECK_TTL_SEC (10 min) and the next test() again succeeds.

Backblaze B2 (which Nextcloud supports as an S3-compatible target via the AmazonS3 external storage backend) returns HTTP 403 Forbidden for HeadBucket on any application key that has a bucket restriction (i.e., any key created with bucketIds), regardless of the key's capabilities. Therefore an external storage mount backed by a B2 bucket-restricted key is permanently marked unavailable, even though the key can perform all relevant file operations (Get / Put / List / Delete) on that bucket.

Affected version

Nextcloud Server 32.0.9 (confirmed). The same code path exists in older versions and main.

Steps to reproduce

  1. Create a Backblaze B2 application key restricted to a single bucket, e.g.:
    b2 key create --bucket <bucket-name> mykey listFiles readFiles writeFiles deleteFiles readBuckets writeBuckets listAllBucketNames
  2. In Nextcloud, configure an external storage mount with the "Amazon S3" backend:
    • Bucket: <bucket-name>
    • Hostname: s3.<region>.backblazeb2.com
    • Region: B2 region (e.g., us-west-004)
    • Use SSL / use path-style: enabled
    • Authentication: Access key with the application key id and key from step 1
  3. Open the Files app or run occ files_external:verify <mount-id>.

Expected behavior

The mount is usable. test() succeeds whenever the underlying credentials can list / read / write objects in the configured bucket, even if the S3-compatible service does not grant HeadBucket on that credential.

Current behavior

occ files_external:verify <mount-id>:

- status: error
- code: 1
- message: Aws\S3\Exception\S3Exception: Error executing "HeadBucket" on "https://s3.us-west-004.backblazeb2.com/<bucket>":
   (client): 403 Forbidden (Request-ID: ...)

nextcloud.log:

files_external · OCP\Files\StorageNotAvailableException
Creation of bucket "<bucket>" failed. Error executing "CreateBucket" on "https://s3.us-west-004.backblazeb2.com/<bucket>":
AWS HTTP error: AccessDenied (client): not entitled

(The CreateBucket attempt is S3ConnectionTrait::getConnection()'s autocreate branch, separately fixable via verify_bucket_exists=false. After fixing that, the Availability wrapper still fails on test() -> HeadBucket and permanently marks the storage unavailable.)

Root cause

apps/files_external/lib/Lib/Storage/AmazonS3.php::test():

public function test(): bool {
    $this->getConnection()->headBucket([
        'Bucket' => $this->bucket,
    ]);
    return true;
}

lib/private/Files/Storage/Wrapper/Availability.php calls test() and records unavailability on exception. RECHECK_TTL_SEC = 600.

On Backblaze B2, the S3-compatible API returns 403 Forbidden for HeadBucket on any application key with a bucket restriction. Verified on a real B2 account with five key shapes (master, MBAK 4-bucket, SBAK 1-bucket full caps, SBAK 1-bucket without listAllBucketNames, SBAK 1-bucket with only readBuckets); HeadBucket returned 403 in all cases. The same keys returned 200 for ListObjectsV2 (MaxKeys=1), GetBucketLocation, PutObject, GetObject, DeleteObject on the same bucket.

Proposed fix

Replace HeadBucket in test() with a check that does not require the HeadBucket privilege but that any working external-storage credential already has. ListObjectsV2(MaxKeys=1) works across AWS S3, MinIO, Wasabi, and Backblaze B2:

public function test(): bool {
    $this->getConnection()->listObjectsV2([
        'Bucket' => $this->bucket,
        'MaxKeys' => 1,
    ]);
    return true;
}

GetBucketLocation is also a viable equivalent on B2; ListObjectsV2 is more universally accepted across S3-compatible services and matches how the existing getFolderContents() and unlink() paths in the same file already exercise the bucket.

Workaround in user environment

Until this is fixed upstream, B2-backed external storage mounts are unusable in Nextcloud. The B2 bucket can still be used as the primary object store (config.php objectstore section) because lib/private/Files/ObjectStore/S3ConnectionTrait uses a separate verify_bucket_exists flag for the bucket-existence check and does not invoke headBucket from a Storage::test()-equivalent path.

Additional references

  • apps/files_external/lib/Lib/Storage/AmazonS3.php (line ~594, current test())
  • lib/private/Files/Storage/Wrapper/Availability.php (RECHECK_TTL_SEC)
  • lib/private/Files/ObjectStore/S3ConnectionTrait.php (parallel pattern for object store, uses verify_bucket_exists rather than calling HeadBucket unconditionally)
  • Backblaze B2 application key documentation (bucket restriction limits HeadBucket on S3-compatible API)

Steps to reproduce

  1. use external strage. backblaze. B2
  2. setup external strage

Expected behavior

can upload files.

Nextcloud Server version

32

Operating system

None

PHP engine version

None

Web server

None

Database engine version

None

Is this bug present after an update or on a fresh install?

None

Are you using the Nextcloud Server Encryption module?

None

What user-backends are you using?

  • Default user-backend (database)
  • LDAP/ Active Directory
  • SSO - SAML
  • Other

Configuration report

List of activated Apps

Nextcloud Signing status

Nextcloud Logs

Additional info

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No fields configured for Bug.

    Projects

    Status

    To triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions