Hedronite · Cert Lesson · Cert-Prep / HashiCorp · Thu 2026-08-27

Terraform Pro IAM Trust and Instance Profile — the role that is not a key

Trust answers who. The profile is the bridge. The instance does not take a key.

Lesson Class: Cert (Terraform Pro · Lab 08 IAM chain)
Cloud Referent: AWS IAM / STS — trust policy, instance profile, EC2 metadata assume
Paired Ops: Provider assume_role nest versus static access_key
Paired Dev: boto3 STS against a Terraform-exported role ARN
Grounding: Lab 08 real · sts.md / iam.md · Brikman Ch.7 referenced, not redone
The trust
Who may sts:AssumeRole. Lab 08 names ec2.amazonaws.com.
The bridge
iam_instance_profile takes a profile name, not a role name.
The passport
An access key in user data is the illegal provider block on an instance.
Trust answers who. The profile is the bridge. The instance does not take a key.

<!-- hal:authoritative:yaml -->

§I — Frame

Associate-003 is closed. Four Pro fires on this arc opened run tasks, remote unlock, test-run commands, and Lab 14's sockets. Lab 08 is the leftover that matches today's AWS overlay.

Lab 08's README is the exam: fix the IAM trust policy, create a working permissions policy, attach the policy to the role, create the instance profile, attach the instance profile to EC2. The starter already names every type. The lab is not "create an EC2 instance." The lab is the chain.

Coin it: the role that is not a key. On Ops the visa sits in a provider nest. On Dev the visa sits in boto3. On Lab 08 the visa sits on an instance, and the footgun is an access key baked into user data.

This is not 08-24. That lesson's command was validate versus a module call that forgot the providers map. Today's command is plan versus a chain that forgot the profile, or attached a profile whose trust names the wrong principal.

This is not 08-21. That lesson's command was plan versus apply. Today's command is "who may assume" versus "what the session may do."

This is not 08-03. OIDC is a different trust principal. Lab 08's principal is ec2.amazonaws.com. If a stem writes token.actions.githubusercontent.com:sub, it is a different lab.

§II — Domain Foundations: four objects, four owners

The Bootcamp IAM notes split the world into users, groups, roles, and policies. Lab 08 uses two of those, plus a bridge the notes do not name: the instance profile. Know the four objects before you touch the starter.

The trust policy. Lives on the role as assume_role_policy. Answers who may call sts:AssumeRole against this role. For Lab 08 the principal type is Service and the identifier is ec2.amazonaws.com. The action is sts:AssumeRole. A trust policy that names s3.amazonaws.com, or names an account that is not this one, or names AWS = "*" , is a different visa. The instance will start. The metadata service will ask STS. STS will refuse. The instance will have a role name in the console and no session.

The Bootcamp STS notes say the same sentence another way: the trust policy specifies who can assume a role. Everybody who assumes it gets the same permission policy. You cannot cancel a session by hand. You can attach AWSRevokeOlderSessions. Changing the permission policy mid-flight punishes the legitimate holders too.

The permission policy. Lives as an inline aws_iam_role_policy or as a managed policy attachment. Answers what the session may do after the assume. Lab 08's starter allows s3:ListBucket on "*". That is a plan-clean shirt and a review failure. ListBucket is a bucket-level action. The resource should be a bucket ARN, not *, and not an object ARN. A stem that puts s3:GetObject on a bucket ARN without /* is the twin mistake. Trust will not save you. Permission will not save a bad trust.

The instance profile. Lives as aws_iam_instance_profile. Holds a role name. It is the only object aws_instance.iam_instance_profile will accept. Passing aws_iam_role.app.name straight into iam_instance_profile is a type error the exam loves. The API wants a profile name, not a role name, even when you made them the same string. Make the profile. Point it at the role. Point the instance at the profile.

The instance. Lives as aws_instance. Receives the profile name. At boot the instance asks the metadata service for credentials. The metadata service assumes the role through the profile. Temporary credentials appear at the metadata path. No access_key attribute exists on aws_instance. A user_data that writes AWS_ACCESS_KEY_ID=AKIA... is the illegal provider block wearing a boot script. The IAM notes say never write credentials in code. User data is code.

Brikman, Working with Multiple AWS Accounts, is the provider-side shirt. A human or a pipeline assumes OrganizationAccountAccessRole. Lab 08 is the service-side shirt. EC2 assumes a role whose trust names EC2. Same verb. Different principal. The Pro item will not tell you which shirt you are wearing. Read the principal.

§III — Lab 08 Flavor: the chain the README named

The starter, reduced to the chain:

data "aws_iam_policy_document" "trust" {
  statement {
    effect = "Allow"
    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "app" {
  name               = "tfpro-iam-role"
  assume_role_policy = data.aws_iam_policy_document.trust.json
}

data "aws_iam_policy_document" "permissions" {
  statement {
    effect    = "Allow"
    actions   = ["s3:ListBucket"]
    resources = ["*"]
  }
}

resource "aws_iam_role_policy" "app" {
  name   = "tfpro-inline-policy"
  role   = aws_iam_role.app.id
  policy = data.aws_iam_policy_document.permissions.json
}

resource "aws_iam_instance_profile" "app" {
  name = "tfpro-instance-profile"
  role = aws_iam_role.app.name
}

resource "aws_instance" "app" {
  ami                  = data.aws_ami.al2023.id
  instance_type        = "t3.micro"
  iam_instance_profile = aws_iam_instance_profile.app.name
}

Five attachments. Miss one and the lab is not done.

  1. Role consumes trust JSON. A role with assume_role_policy = "{}" will plan if the provider is lenient and fail in IAM. A role that puts the permissions JSON in assume_role_policy has swapped who and what. STS will look for a principal in a document that named s3:ListBucket. Refuse.
  1. Inline policy consumes permissions JSON and a role id. role = aws_iam_role.app.name often works because the provider accepts both. Prefer .id here and .name on the profile. Be consistent with the type the resource documents.
  1. Profile consumes the role name. An empty profile, or a profile that names a role you never created, fails apply. Validate may pass. The API enforces the world.
  1. Instance consumes the profile name. iam_instance_profile = aws_iam_role.app.name is the exam trap when the two strings differ. When they are the same string, apply can still fail because the instance API looks up a profile, not a role.
  1. AMI data source is not the chain. It is how the instance boots. Do not spend this fire on AMI filters. A most-recent Amazon Linux 2023 is enough. If the starter's filter is too wide, tighten it. That is hygiene, not the lab.

Lab 08 success mode is plan. A clean plan of a swapped trust/permission pair can still be green. The README's target end state is stronger than the success mode: trust valid for EC2, policy attached, profile wired, instance using the profile. Read the target. Do not stop at "plan has no error."

Lab 06 is the other shirt in the same aisle. Two provider "aws" blocks, two regions, no module, no assume_role nest, no IAM role. 08-24 referenced it. 07-28 spent the Associate form. A candidate who "fixes Lab 08" by adding alias = "secondary" has walked into the wrong room.

§IV — Mechanism: who asks, who answers

At apply time Terraform writes four IAM objects and one instance. STS is not called by Terraform for the instance session. Terraform calls IAM to put the documents in place. EC2 calls STS later, when the instance asks the metadata service. If/then: if you terraform apply and immediately aws sts get-caller-identity from your laptop, you are still the laptop. The instance has not spoken. Proof that the chain works is on the instance, or in a later Dev-shaped script that assumes the same role from a principal the trust allows.

The laptop proof is a different visa. Ops taught it: a provider assume_role nest, aws_caller_identity on that process. Dev taught it: boto3 assume_role with the exported ARN. Lab 08's trust names EC2, not your IAM user. If you take the Lab 08 role ARN and call assume_role from the laptop, STS refuses unless you also named your user or role in the trust. That refusal is correct. Do not "fix" Lab 08 by adding your user to the trust so a local script can assume. That is a different lab, and it widens the visa.

ExternalId does not belong on this shirt. Confused-deputy is a third-party assume. EC2 is not a vendor. If a stem adds condition { test = "StringEquals" variable = "sts:ExternalId" } to Lab 08's trust, the instance cannot pass that condition. The metadata assume has no ExternalId. The instance starts. The session never arrives.

Role chaining is the other professional trap, and it is also the wrong shirt for this lab. The instance assumes once. It does not assume a second role from inside the first session unless your application code does that. Application code that assumes a second role is a Dev-shaped question, not Lab 08.

§V — Worked Example: one Lab 08 fix, one stem

Fix. Keep the trust principal as ec2.amazonaws.com. Keep actions = ["sts:AssumeRole"]. Point the inline policy at a real bucket ARN if the lab's review bar requires it:

resources = [aws_s3_bucket.logs.arn]

Create the bucket in the same root or consume it as a data source. Do not leave resources = ["*"] if the README called the permission "working" in the sense of least privilege. The starter is plan-clean with *. The Pro stem will ask why ListBucket on * is accepted and still wrong.

Keep the profile. Keep iam_instance_profile = aws_iam_instance_profile.app.name. Export instance_profile_name and instance_id as the starter does. terraform validate must pass. terraform plan must show the four IAM objects and the instance. The lab is the path the name took: trust to role, role to policy, role to profile, profile to instance.

Stem. A candidate sets iam_instance_profile = aws_iam_role.app.name. The role and the profile were created with the same string tfpro-iam-role. Apply fails with an API error about an instance profile that does not exist. What is the smallest legal change?

Point the instance at the profile resource:

iam_instance_profile = aws_iam_instance_profile.app.name

Do not rename the role to match. Do not delete the profile. Do not put an access key on the instance to "unblock" the plan.

A second stem, because Pro likes a pair. The trust document uses principals { type = "Service" identifiers = ["lambda.amazonaws.com"] }. The rest of the chain is perfect. The instance starts. curl to the metadata credentials path returns an assume error. What is the smallest legal change?

Change the identifier to ec2.amazonaws.com. Do not add a second statement that names every compute service. Do not attach AmazonEC2FullAccess to "make metadata work." Metadata does not need more permission. It needs a trust principal that is EC2.

A third stem, because the Ops shirt will try to leak in. The same role must also be assumable by a parent account's Terraform process. The candidate adds a second statement to the trust:

principals {
  type        = "AWS"
  identifiers = ["arn:aws:iam::111111111111:root"]
}

That is a legal IAM document. It is a wider visa. It is not Lab 08's target. If the exam asked for EC2-only, the extra statement is overreach. If the exam asked for EC2 plus a named parent, both statements belong. Read the stem. Do not copy Brikman's Organizations role into an EC2 lab by habit.

§VI — Connection to Today's Ops + Dev Lessons

Ops: assume_role.role_arn on a provider is a visa the plugin requests. access_key in that block is a passport. aws_caller_identity is the stamp.

Dev: terraform output -json plus boto3 assume_role is a visa Python requests. A static AWS_ACCESS_KEY_ID without a session token is the same passport. get_caller_identity is the stamp.

Cert: Lab 08 is the visa an instance requests. The instance profile is the bridge. The trust principal is EC2. An access key in user data is the same passport.

One theme, three askers, one leftover the Associate day did not reach. 08-24's sockets still matter when this chain lives in a child module. Today's fire does not reopen the map. Put the chain in the root. Pass the plugin later.

08-18's force-unlock is a burial. Today's failed metadata assume is a diagnosis. Do not force-unlock a state to make a trust principal exist.

08-21's plan run still reads last apply. Today's aws_iam_role.arn output is last-apply in that same sense. A Dev wrapper that assumes the exported ARN after you changed the trust and did not apply will hear yesterday's document. Apply first.

08-15's speculative plan still cannot apply. Today's validate still cannot see that lambda.amazonaws.com is the wrong principal for EC2. Validate answers as a document. IAM answers as a world. The instance answers as a session.

§VII — Practice Questions

Question 1
Lab 08's starter plans cleanly with a trust document, a role, an inline policy, a profile, and an instance. Has the lab been completed? Why?
tap to reveal
Only if the chain matches the README target: trust valid for EC2, policy attached, profile wired, instance using the profile. A clean plan that points iam_instance_profile at a role name, or that puts permissions JSON in assume_role_policy, is not completion. Read the target end state, not the success mode alone.
Question 2
A candidate sets iam_instance_profile = aws_iam_role.app.name. The role and profile share the same string. Apply fails. What is missing?
tap to reveal
The instance API looks up an instance profile, not a role. Point iam_instance_profile at aws_iam_instance_profile.app.name. Matching strings do not merge the two types.
Question 3
Trust names lambda.amazonaws.com. Permission allows s3:ListBucket on a real bucket ARN. Profile and instance are wired. The instance starts. Metadata credentials fail. What is the smallest fix?
tap to reveal
Change the trust identifier to ec2.amazonaws.com. Permission is not the failure. The principal is.
Question 4
A laptop script calls sts.assume_role on the Lab 08 role ARN and is refused. The instance can assume. Is the role broken?
tap to reveal
No. Trust names EC2, not the laptop principal. Refusal from the laptop is correct. Do not widen trust to make a local script pass unless the stem asked for both.
Question 5
Brikman, Working with Multiple AWS Accounts: a provider assume_role nest points at OrganizationAccountAccessRole. Is that Lab 08?
tap to reveal
No. That shirt is a pipeline or parent account assuming a child account role. Lab 08 is EC2 assuming a role whose trust names the EC2 service. Same verb, different principal, different object chain (no instance profile on the Brikman shirt).

§VIII — Closing

Trust answers who. Permission answers what. The instance profile is the bridge the instance API will accept. The instance does not take a key. Metadata asks STS. STS reads the trust. The session lands, or it does not.

Name it when you see it. The role that is not a key. Write the four objects. Point each at the next. Leave the access key out of user data. Ask EC2 to assume. Prove on the instance, not on the laptop.

Examine well. The instance name will still be pretty. The door is the trust principal. The bridge is the profile. The plugin writes the documents. STS is the one who answers later.

Related