Connecting a bucket should not require handing over an IAM user’s long-lived access key. BucketDesk uses customer-owned IAM roles and temporary AWS STS credentials for customer storage access. You decide what the role can do; BucketDesk requests a limited session when authorized work needs it.

This article explains the connection security model and the verification checks it should enforce. The Java AWS SDK v2 example illustrates the protocol; it is not a claim that BucketDesk’s Laravel application runs Java. The onboarding and negative tests below are implementation requirements, not a report of a completed production security audit.

The customer owns the role and its permissions

In the example, BucketDesk runs in AWS account 111111111111. Acme owns account 222222222222 and creates BucketDeskDocumentChatRole there. The account numbers, role names and external ID below are illustrative; use the production principal and generated value supplied during onboarding.

A trust policy controls who can assume the customer role. A separate permissions policy controls its allowed operations and resources. BucketDesk’s own execution role also needs permission to call sts:AssumeRole on that customer role. Trust alone does not grant S3 access.

For a document-reading connection, grant s3:GetObject on arn:aws:s3:::acme-archive/documents/* and, if browsing is needed, s3:ListBucket on the bucket ARN with an s3:prefix condition scoped to documents/. Do not add write or delete permissions to this reading role. SSE-KMS objects may also require kms:Decrypt and an appropriate KMS key policy. Effective access remains subject to other applicable AWS policies and explicit denies.

Authorized workspace request
  → BucketDesk execution role
  → STS AssumeRole(customer role ARN, connection ExternalId)
  → Temporary customer-role credentials
  → S3 operation within the approved bucket and prefix

No shared long-lived customer access keys

STS returns an access key ID, secret access key and session token with an expiration time. “Without sharing access keys” means customers do not create and give BucketDesk long-lived IAM user keys. AWS still issues temporary credentials behind the scenes.

BucketDesk authenticates to STS using its own workload identity. The resulting customer-role credentials belong in the server-side execution path, not browser storage or ordinary logs. When they expire, AWS rejects their use; another session requires another authorized AssumeRole request.

The confused-deputy problem

Imagine a second customer submits Acme’s role ARN as their own connection. An ARN is an identifier, not a password. If BucketDesk trusts that input and Acme’s role only checks the BucketDesk principal, the service could assume Acme’s role while answering the second customer’s request. BucketDesk becomes the confused deputy: a trusted service tricked into using its authority for someone else.

An ExternalId condition adds the connection context. When the second customer supplies Acme’s ARN, BucketDesk must still send the external ID assigned to the second customer’s connection. Acme’s policy expects Acme’s value, so that request fails. This protection depends on BucketDesk controlling the mapping and never accepting a caller-selected external ID for a storage operation.

ExternalId is not a secret

AWS explicitly does not treat ExternalId as a secret: someone permitted to view the role can read it. It is neither a password nor a substitute for the trusted AWS principal. Knowing it alone does not authorize an arbitrary AWS identity to assume the role.

AWS recommends unique provider-controlled customer identifiers, including one per customer AWS account. BucketDesk’s more granular design should generate a fresh UUID-based value per connection, enforce global uniqueness and show it as read-only during setup. Separate connections to the same AWS account can then have independent lifecycles. This is a BucketDesk design choice, not an AWS requirement for one ID per bucket.

Prefer a separate customer role for each independently managed connection. If one role explicitly accepts several connection IDs, those IDs reach the same role permissions; ExternalId does not create separate S3 permission scopes. Never let users copy another connection’s external ID into an editable field.

An example customer trust policy

Attach this trust policy to the example role in Acme’s account. Replace the BucketDesk principal and external ID with the actual onboarding values. StringEquals requires the expected value on this Allow statement. Review the whole policy: another Allow that trusts the same caller without the condition can bypass this protection.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::111111111111:role/BucketDeskProduction"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "bd-conn-7f21c930-47c1-4e52-a97b-162d5b798d42"
        }
      }
    }
  ]
}

Bind the workspace to a verified connection

The application should resolve the role ARN and external ID together from a connection belonging to the authenticated workspace. Do not look up a connection globally by a submitted role ARN, or accept an external ID from the document request. An external ID cannot repair broken workspace authorization.

Before calling STS, check membership, the requested action, connection status, bucket and prefix. Background jobs need the same checks at execution time. Scope credential caches by workspace, connection and permission context, respect expiration, and invalidate them when a connection changes. Never reuse a session simply because two requests mention the same bucket.

Workspace: Acme
└── Connection: conn-42 (verified)
    ├── workspaceId: acme
    ├── awsAccountId: 222222222222
    ├── roleArn: arn:aws:iam::222222222222:role/BucketDeskDocumentChatRole
    ├── externalId: bd-conn-7f21c930-47c1-4e52-a97b-162d5b798d42
    ├── bucket: acme-archive
    └── prefix: documents/

AssumeRole with the Java AWS SDK v2

This self-contained helper uses the SDK’s default credential provider chain for BucketDesk’s workload identity. Its arguments must come from the authorized, verified connection record described above. Add the software.amazon.awssdk:sts module through your project’s AWS SDK v2 dependency management and configure the deployment region.

The request uses a 900-second session, the STS minimum for AssumeRole. The role’s configured maximum and other session constraints still apply; role chaining is limited to one hour. Never retry a failure by dropping ExternalId. Treat AccessDenied as a connection or authorization failure.

The returned Credentials contain all three temporary credential fields and expiration. For an S3 client, construct AwsSessionCredentials and supply an appropriate credentials provider. A static provider does not refresh an expired session; production code needs an expiration-aware lifecycle. Do not print the returned Credentials.

import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.sts.StsClient;
import software.amazon.awssdk.services.sts.model.AssumeRoleRequest;
import software.amazon.awssdk.services.sts.model.Credentials;

public final class CustomerRoleSession {
    private CustomerRoleSession() {}

    // Call only after authorizing and loading the connection server-side.
    public static Credentials assume(
            String roleArn, String externalId, Region region) {
        try (StsClient sts = StsClient.builder().region(region).build()) {
            AssumeRoleRequest request = AssumeRoleRequest.builder()
                    .roleArn(roleArn)
                    .roleSessionName("bucketdesk-document-chat")
                    .externalId(externalId)
                    .durationSeconds(900)
                    .build();
            return sts.assumeRole(request).credentials();
        }
    }
}

Onboarding should prove the trust condition

  • An authorized workspace administrator starts a connection. BucketDesk generates the unique external ID and supplies its trusted principal plus a narrowly scoped IAM template.
  • The customer reviews the template, creates the role in their AWS account and submits the role ARN for verification. Keep unverified setup separate from usable connections; do not register the ARN as an operational connection yet.
  • Using the actual BucketDesk execution identity and the same candidate role, attempt AssumeRole with no ExternalId, with a different valid ExternalId, and with the assigned ExternalId. Only the last attempt may succeed.
  • If either negative attempt succeeds, reject setup and require a corrected trust policy. A timeout or throttling error is inconclusive, not a passing denial. If the positive attempt fails, diagnose permissions and policy propagation, then rerun all checks.
  • Use the correctly obtained session for a minimal, customer-approved read/list probe within the selected scope. Check that the returned assumed-role identity matches the expected account and role. Register the verified role mapping only after these checks pass.
  • Reverify after role or trust changes. Disabling a connection should stop new work and session issuance. Removing trust blocks future assumptions; already issued sessions need separate handling and should not be assumed instantly revoked.

The security test that must pass

Run this matrix against a controlled customer test role with the intended BucketDesk principal. The positive control matters: three denied requests could simply mean the principal has no access. These are expected acceptance results, not production test results from writing this article.

Same trusted principal + same customer role:
  ExternalId omitted        → AccessDenied
  Wrong valid ExternalId    → AccessDenied
  Assigned ExternalId       → Success

Application isolation checks:
  Workspace B requests workspace A connection → Reject before STS
  Connection B supplies role A ARN + ID B      → AccessDenied
  Request attempts to override stored ID      → Reject/ignore override
  • Also test a controlled role with an unconditional trust Allow; onboarding must reject it even if the correctly identified request succeeds.
  • Test allowed reads and denied out-of-scope reads using fixtures. Verify write/delete denial through policy review or a disposable test environment, not destructive calls against customer files.
  • Record the connection, actor, expected outcome and AWS request ID for diagnosis. Keep temporary keys and session tokens out of logs.
THE DECISION

Customer-owned roles bound AWS permissions. Temporary sessions bound credential lifetime. Provider-controlled External IDs bind a role assumption to its connection context. Workspace authorization ties the entire operation to the person or job allowed to request it.