Cloud storage and access · from our own builds

AWS IAM role assumption with ExternalId: connecting customer AWS accounts without access keys

Keyless AWS onboarding takes a CloudFormation template the customer runs in their own account, a role in it that trusts one role of yours only when it presents an ExternalId your server minted for that connection, and server code that assumes the role through STS and refreshes one-hour credentials. Storafleet does this with 37 S3 permissions on a single bucket and two STS probes that must pass before anything is saved. As of 29 September 2026 the first hop of the chain is proven from production; the second still needs a run against a real customer-style stack, and no customer connection uses the role method yet.

Updated 20 October 2026 · checked against our code and the vendors' documentation

Where have we built this?

  1. StorafleetA connect wizard with a CloudFormation quick-create link, an ExternalId per connection, two STS probes before saving, a two-hop STS chain and an in-place upgrade from stored keys to a role that discards the keys.
  2. StorafleetThe customer-side template: a role trusting one Storafleet role on the ExternalId condition, a one-hour maximum session, 37 S3 actions on one bucket, and an optional bucket created hardened and kept if the stack is deleted.

What are the numbers and limits to know?

Role chaining
Using a role to assume a second role caps the session at one hour; asking for more than an hour in a chain failssource, checked 20 October 2026
AssumeRole session length
DurationSeconds from 900 to 43,200 (12 hours), limited by the role's maximum session settingsource, checked 20 October 2026
ExternalId format
2 to 1,224 characters: letters, digits and +=,.@:/-source, checked 20 October 2026
RoleSessionName format
2 to 64 characters: letters, digits and _+=,.@-source, checked 20 October 2026
Who controls the ExternalId
It must be unique among the third party's customers and controlled by the third party, not the customer; a role ARN is not a secretsource, checked 20 October 2026
Price of IAM and STS
Offered at no additional charge; you pay only for the services the credentials are used onsource, checked 20 October 2026
Quick-create links
templateURL must point to the template in an Amazon S3 bucket; parameters are passed as param_<Name>; templates with IAM resources need the user's acknowledgementsource, checked 20 October 2026
Presigned URLs from role credentials
Expire when the role session expires, even if a longer expiry was setsource, checked 20 October 2026

How do you integrate AWS IAM role assumption with ExternalId, step by step?

  1. 1

    Create a trust-anchor role in your own account

    A caller identity whose only permission is sts:AssumeRole assumes one dedicated role, storafleet-sts. Customer stacks trust that role, not your account root and not the user, so your own key can be rotated or replaced without any customer re-running a stack.

  2. 2

    Mint an ExternalId for each connection on the server

    The connect wizard asks the API for a setup. The server creates a UUIDv4 ExternalId and stores it in a setup record that expires after one hour and can be used once. The customer never chooses it.

  3. 3

    Send the customer a quick-create link

    The server builds the CloudFormation link with the template's direct S3 URL, a stack name, the ExternalId, your role's ARN, the bucket name and a create-the-bucket switch. The wizard warns the customer to tick the IAM acknowledgement, which AWS does not let anyone pre-tick.

  4. 4

    Ship a tight trust policy and a hardened bucket

    The role's principal is your one role, with a StringEquals condition on sts:ExternalId, and its maximum session is 3,600 seconds. A bucket the stack creates gets versioning, default encryption, all four public-access blocks and a Retain policy, so deleting the stack never deletes data.

  5. 5

    Derive the permissions from your code

    List every S3 command the server uses, map each to its IAM action and split bucket-level from object-level. Storafleet's list is 37 actions, 24 on the bucket and 13 on its objects, with no CreateBucket, DeleteBucket or BypassGovernanceRetention.

  6. 6

    Verify by trying to break in first

    Before saving, the server assumes the pasted role without the ExternalId, which must fail, then with it, which must succeed. Only then is the bucket saved and the head, list, put and delete checks run.

  7. 7

    Give the SDK a credentials provider

    For a role connection the bucket's S3 config returns a chained fromTemporaryCredentials provider instead of keys. The SDK refreshes the session before it expires, so roughly 30 places that build an S3 client did not change.

  8. 8

    Prove each hop on real infrastructure

    Hop one is proven with the application's own provider code from production. Hop two can only be proven by the wizard's real CloudFormation flow against a disposable bucket; until that run, call the feature built, not proven.

What went wrong, and how was it fixed?

One hop in the code, two in the plan

The first build assumed the customer's role directly as the IAM user, while the template trusts the storafleet-sts role. Every verification would have failed with AccessDenied.

Fix: Caught by an audit of the code against the plan on 12 June 2026, before any AWS infrastructure existed, and rebuilt as a two-hop chain.

Five permissions for forty API calls

The first template granted five S3 actions. The product calls around 40 S3 operations, so lifecycle, CORS, versioning, tagging and multipart calls would all have failed with AccessDenied on role connections.

Fix: Enumerated every S3 command in the server and mapped each to its action. Some are not obvious: deleting lifecycle rules or CORS is authorised by the Put action. The template went to 37 actions while it still had no customers.

A session name built from a whole database document

The STS session name was built from the bucket's owner field, which was sometimes a populated document rather than an id. Role buckets failed with 'Failed to list bucket contents'.

Fix: Fixed on 24 June 2026: the name uses the owner id only and is cut down to the characters and 64-character length STS accepts.

A template served from a CDN

We planned to host the template on a CloudFront custom domain. CloudFormation's quick-create flow wants a direct Amazon S3 URL and rejects others.

Fix: Templates are published to S3 under versioned, immutable names, and the link points at the S3 URL.

Your account ID is baked into every customer stack

Each customer's trust policy names your role's ARN, account number included, so moving the STS identity to another AWS account forces every customer to re-run their stack. Our runbook recommended a dedicated account; we used a shared one.

Fix: On 29 September 2026 the identity was rebuilt in a new account. No customer used the role method yet, so nobody had to re-run anything. Start in a dedicated account.

'Configured' is not 'working'

The connect dialog offers the role method when the settings exist, not when the credential works. When the caller key died, the feature was still advertised and every attempt failed at the probe.

Fix: Open: check the credential with GetCallerIdentity at boot and in the health check, and hide the option when it fails.

An environment file that held the key twice

On the day of the rebuild, our env files carried the STS key lines twice. The new values were pasted over the second copy, and dotenv reads the first.

Fix: Read every environment change back with dotenv.parse before restarting.

What should you check before going live?

  • ExternalId minted by your server, one per connection, never chosen by the customer
  • Trust policy names one role, not your account root
  • No-ExternalId probe must fail before anything is saved
  • Template URL is a direct S3 URL with a versioned name
  • Permission list derived from the SDK calls in your code
  • Region set explicitly on the STS and S3 clients
  • Presigned URL lifetimes shorter than the role session
  • STS identity in a dedicated AWS account
  • Credential checked with GetCallerIdentity, not only for presence
  • Both hops proven with a real stack before launch

Questions about AWS IAM role assumption with ExternalId

Is an ExternalId a secret?

Not in the password sense, but it must be unique to each connection and controlled by your server, never typed in by the customer. Storafleet returns it once during setup and never in the bucket data its API sends afterwards. AWS notes that a role ARN is not a secret either; the ExternalId is what stops another customer reusing it.

How long do assumed-role credentials last in a chain?

One hour at most. AWS caps chained role sessions at an hour whatever the role allows. Storafleet asks for 3,600 seconds and lets the SDK refresh the session before it expires, and presigned URLs signed with those credentials stop working when the session ends.

What permissions should a cross-account S3 role ask for?

Exactly what your code calls, on one bucket. We derived Storafleet's 37 actions from every S3 command in the server and left out creating or deleting buckets and bypassing object-lock governance.

Can the customer revoke access on their own?

Yes. They delete the role or the stack in their own account and access ends. A bucket the stack created is kept if the stack is deleted.

What does the customer see when the role is set up wrong?

A named reason. Storafleet refuses a role that works without the ExternalId and reports an organisation policy block or an ExternalId mismatch when STS says so. Because STS often will not say whether the role is missing, untrusted or expecting a different ExternalId, the last message asks them to check all three.

Should a SaaS product still accept access keys?

Storafleet does, encrypted, because some customers want them. But a key is a standing credential outside the customer's account that works until someone notices a leak, while a role stores nothing secret on our side and every session expires within an hour.

Need AWS IAM role assumption with ExternalId in your app?

We have built it in production code, including the parts that went wrong. Tell us what your app needs and we'll say what it takes.

Ask about this integration