Module 2: Domain 1 Secure Architectures

3. Task 1.1 cont.: federation, STS, and cross-account roles

Description

Lesson 2 closed out the single-account IAM model, and the multi-account administrative piece (Identity Center, SCPs). This lesson closes Task 1.1 with the missing half: how an identity proves who it is without using a long-lived credential, the exact mechanism behind sts:AssumeRole, and the specific cross-account access pattern the exam tests most often — including one no earlier guide in the ecosystem needed: access from an external third party into your account.

Connection to the module

If you came from cloud-security-and-guardrails-guide M2, you already built the full OIDC federation mechanism: the JWT, the identity provider, the trust policy with its exact conditions on aud and sub. This lesson doesn't reteach that — it rereads it as a specific instance of a broader concept (AWS STS and its different ways of issuing temporary credentials), and adds the piece cloud-security-and-guardrails-guide never needed: cross-account access with a third party outside your organization.


Analogy: the check-in desk that never hands out a permanent key

Picture a hotel that never gives anyone a long-lived physical key —not even a guest staying for a month—. Instead, every time someone needs to enter a room, they present ID at the front desk, the desk verifies who they are and which room they're assigned, and issues a card that expires on its own in a few hours. That desk is AWS STS (Security Token Service): it never hands out a permanent key, it always issues a temporary card, after verifying identity against some proof —a person's ID, an employee badge, a letter from an external partner— and checking which room (which role, with which permissions) corresponds to that specific proof.


AWS STS: one service, three ways to request temporary credentials

You've already used STS, even if the exact name of the call was different each time. The three forms the exam expects you to distinguish:

STS operationWho uses itProof of identity it presentsExample already built in the ecosystem
sts:AssumeRoleAn IAM identity (user or role) that already exists in AWSIts own IAM credentialsAn Andes Cargo engineer switching roles inside the console
sts:AssumeRoleWithWebIdentityAn external identity authenticated by an OIDC provider (GitHub Actions, Google, an application identity provider)A signed JSON Web Token (JWT) from that providerAndesCargoDeployRole, assumed by the GitHub Actions pipeline (cloud-security M2)
sts:AssumeRoleWithSAMLAn identity from a corporate directory (Active Directory, Okta) federated via SAML 2.0A SAML assertion signed by the identity providerNot built in the ecosystem — mentioned here for exam completeness

All three calls do, conceptually, the same thing: they receive a proof of identity, verify it against a trust policy, and return temporary credentials (AccessKeyId, SecretAccessKey, SessionToken) with a short expiration. The difference between them is exclusively what kind of identity proof they accept — and that's, precisely, the question that decides which one to use in an exam scenario: does the identity requesting access already live in IAM (AssumeRole), come from an external OIDC provider (AssumeRoleWithWebIdentity), or come from a corporate SAML directory (AssumeRoleWithSAML)?


Cross-account roles: the pattern within your own organization

The simplest case of sts:AssumeRole: a user or role in Account A needs to act in Account B. Account B creates a role whose trust policy names, as Principal, the ARN of the identity (or of the entire account) in Account A. The identity in Account A calls sts:AssumeRole against that role, and receives temporary credentials valid inside Account B, with exactly the permissions the role defines — never more.

   CROSS-ACCOUNT ROLE, WITHIN THE SAME ORGANIZATION

   Account A (andes-cargo-prod)                Account B (andes-cargo-staging)
   ┌─────────────────────────┐                ┌─────────────────────────────┐
   │  Source user or role       │  sts:AssumeRole │  StagingReadOnlyRole          │
   │  (identity already in IAM) │ ──────────────▶ │  Trust policy:                 │
   └─────────────────────────┘                │    Principal: Account A's arn  │
                                               │  Permission policy:            │
                                               │    read-only over S3/DynamoDB  │
                                               └─────────────────────────────┘

This is exactly the pattern IAM Identity Center automates at scale with permission sets (lesson 2) — but nothing stops you from building it by hand, role by role, when the number of accounts is small and the case is simple.


Third-party access: the "confused deputy" problem and ExternalId

Here's the piece no earlier guide in the ecosystem needed, because Andes Cargo never gave an external company access to its account. The scenario: Andes Cargo hires a FinOps consultancy to monitor costs in its account. The consultancy manages many clients' accounts, not just Andes Cargo's — and that's where a real risk shows up, one AWS documents with its own name: the confused deputy problem. If the role Andes Cargo gives the consultancy simply trusts "the consultancy's AWS account" with no additional condition, a malicious employee at another client of that same consultancy —who also has access to the consultancy's account— could, in theory, try to assume Andes Cargo's role without having been authorized by Andes Cargo specifically.

AWS's official solution is an sts:ExternalId condition in the trust policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::<consultora-account-id>:root" },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": { "sts:ExternalId": "andes-cargo-finops-2026-08" }
      }
    }
  ]
}

The ExternalId is a value unique per client, generated by the consultancy —not by Andes Cargo—, which the consultancy must include in every sts:AssumeRole call it makes on Andes Cargo's behalf. A consultancy employee who tries to use Andes Cargo's role to access, by mistake or bad intent, on behalf of another client, fails the condition and the call is rejected. Official documentation is explicit on a point that surprises people the first time they read it: the ExternalId is not a secret — anyone with permission to view the role can read it. Its function isn't to hide anything; it's to force the role assumption to only happen in the correct context, verified by both parties.

The exam decision: when a scenario describes access from an external third party (a consultancy, a SaaS provider, a business partner) to your AWS account, and in particular when that third party manages access for multiple clients from the same AWS account, the correct answer is a cross-account role with ExternalId in the condition — never a shared IAM user with the third party, and not a cross-account role without a condition (vulnerable to the confused deputy problem).


AssumeRole vs. console role switching: the same mechanism, two interfaces

An exam detail that tends to cause confusion: when someone "switches roles" in the AWS web console (the account/role selector in the top-right corner), they're not using a different mechanism — behind the scenes, they're making exactly the same sts:AssumeRole call a script or a pipeline would make via API. The only difference is the interface: a human clicks a menu; a program calls the SDK or the CLI. The result —temporary credentials, with an expiration, limited by the role's trust policy and permissions policy— is identical in both cases.


Common mistakes

Confusing AssumeRole with AssumeRoleWithWebIdentity (terminology). What happens: someone describes GitHub Actions' OIDC flow using the generic name AssumeRole. How to spot it: if your description of the OIDC mechanism doesn't mention the JWT as the proof of identity. How to fix it: AssumeRole is for identities that already exist in IAM; AssumeRoleWithWebIdentity is specifically for external identities proven with an OIDC token. They're two distinct operations of the same STS API, not synonyms.

Omitting ExternalId in a third-party access scenario (the mistake AWS itself cites most often about this pattern). What happens: someone designs a cross-account role for an external provider using only the provider's account Principal, with no additional condition. How to spot it: if your trust policy for a third party doesn't include any Condition block. How to fix it: without ExternalId, the role is exposed to the confused deputy problem in any scenario where the third party manages multiple clients from the same account — AWS's official documentation names this pattern specifically as the reason ExternalId exists.


❓ Practice question — Domain 1 (Secure)

Scenario: A fintech (Prisma Pagos) hires an external audit firm (Contadores Vega) to review, quarterly, transaction records stored in an S3 bucket. Contadores Vega uses the same AWS account to audit dozens of other clients. Prisma Pagos needs to guarantee that no Contadores Vega employee could, through a misconfiguration for another client, end up accessing Prisma Pagos's data without Prisma Pagos having explicitly authorized that specific client.

Question: What's the most secure way to grant this access?

A. Create an IAM user in Prisma Pagos's account and share its access credentials with Contadores Vega. B. Create a role in Prisma Pagos's account whose trust policy trusts Contadores Vega's account ARN, with no additional condition, and share the role's ARN. C. Create a role in Prisma Pagos's account whose trust policy trusts Contadores Vega's account ARN and requires, via an sts:ExternalId condition, a unique identifier generated by Contadores Vega for Prisma Pagos. D. Federate Contadores Vega's identity using AssumeRoleWithWebIdentity, treating Contadores Vega as an OIDC provider.


✅ Correct answer: C

Why it's correct: this is, precisely, the canonical scenario AWS's official documentation uses to justify ExternalId — a third party that manages access for multiple clients from the same AWS account. The sts:ExternalId condition, with a value unique per client generated by Contadores Vega (never by Prisma Pagos), guarantees that only a call including that specific value can assume the role — directly mitigating the confused deputy problem the scenario describes.

Why the others fail:

  • A: shares a long-lived credential with an external third party, directly violates the principle of least privilege and temporary identity, and offers no way to distinguish Prisma Pagos's access from any other client's if those credentials leak or get reused.
  • B: is a functionally correct cross-account role, but without the ExternalId condition it lands exactly in the risk scenario the confused deputy problem describes — any identity in Contadores Vega's account, including one misconfigured for another client, could assume the role.
  • D: AssumeRoleWithWebIdentity is the mechanism for OIDC identity providers (like GitHub Actions or Google), which issue JWTs — it's not the mechanism designed for cross-account access between two AWS accounts; treating an audit firm's account as an "OIDC provider" doesn't match any real AWS pattern.

Resources

  1. AWS Docs — Access to AWS accounts owned by third parties (ExternalId) — full official source for the cross-account pattern with third parties and the confused deputy problem.
  2. AWS Docs — Compare AWS STS credentials — official comparison of AssumeRole, AssumeRoleWithWebIdentity, and AssumeRoleWithSAML.
  3. AWS Docs — The confused deputy problem — the formal definition of the risk ExternalId mitigates.