IAM Instance Profile

An IAM instance profile is a container for one IAM role that an EC2 instance assumes, so code on the machine gets rotating AWS credentials with no stored keys.

An IAM instance profile is an AWS container that holds one IAM role and can be attached to an EC2 instance, so that anything running on the machine can assume that role. The instance metadata service on the attached machine serves temporary credentials for the role, which means code on the instance authenticates to AWS without an access key stored on disk, in an environment variable, or in a secrets store.

For GitHub Actions this matters when jobs run on self-hosted runners backed by EC2. The runner machine can carry its permissions in its instance profile, and the workflow file that runs on it holds no AWS credentials at all.

Definition

An instance profile is a thin wrapper around a role. Five pieces have to line up before an instance can use one.

ObjectWhat it holdsWho reads it
IAM roleA name and an ARN, nothing executableIAM
Trust policy on the roleWhich principals may assume the roleAWS STS, when the assume call is made
Permissions policy on the roleThe allowed actions and resourcesEvery AWS API the instance calls
Instance profileExactly one role, plus its own ARNEC2, at attach time
iam:PassRole grantPermission to hand this role to a serviceIAM, when the instance is launched or modified

The trust policy is what makes the role usable by a machine. It has to name the EC2 service as the principal:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "ec2.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }
  ]
}

A role with the right permissions policy and the wrong trust policy attaches cleanly and then fails at credential time, which is a common first mistake.

An instance profile can contain only one role (AWS documentation on instance profiles, checked on 2026-08-13). A machine that needs two distinct permission sets therefore needs one merged role, or two machines. When a role is created in the AWS console for an EC2 use case, the console creates an instance profile with the same name automatically. When a role is created through the API, the CLI, or Terraform, the profile is a separate resource that has to be created and then associated:

aws iam create-instance-profile --instance-profile-name ci-runner-profile

aws iam add-role-to-instance-profile \
    --instance-profile-name ci-runner-profile \
    --role-name ci-runner-role

How the credentials reach the process

The instance metadata service, reachable from the instance at the link local address 169.254.169.254, exposes the credentials under a path keyed by the role name. With IMDSv2 the read takes a session token first:

TOKEN=$(curl -sX PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 300")

curl -s -H "X-aws-ec2-metadata-token: ${TOKEN}" \
  "http://169.254.169.254/latest/meta-data/iam/security-credentials/ci-runner-role"

The response is a small JSON document with AccessKeyId, SecretAccessKey, Token, and Expiration. AWS rotates those credentials and publishes the replacement set before the current one expires, so a long lived process on the instance keeps working without any rotation code of its own (AWS documentation on IAM roles for Amazon EC2, checked on 2026-08-13).

Almost nothing calls that endpoint by hand. The AWS SDKs and the AWS CLI walk a default credential provider chain: environment variables first, then the shared credentials file, then the container credential endpoint, then the instance metadata service. When a build step runs aws s3 cp on an instance with a profile attached and no credentials configured anywhere else, the chain falls through to the metadata service and the call succeeds with no configuration in the workflow.

The scope of the grant

The permissions belong to the machine and not to any one process on it. Every job, every step, and every side process on that instance shares the same identity for as long as the profile is attached. Two consequences follow.

The first is that the permission set has to be sized for the least trusted thing that will ever run on the machine. On a runner that executes code from pull requests, that is a strong argument for a narrow policy over a convenient one.

The second is that a profile is a per fleet decision. Instances that need write access to a deployment bucket and instances that only read test fixtures belong in separate fleets with separate profiles, so that neither inherits the other's reach.

An instance profile can be attached at launch or associated with a running instance later, and swapped with aws ec2 replace-iam-instance-profile-association. Equivalent mechanisms exist on the other major clouds: a Google Cloud VM runs as an attached service account, and an Azure virtual machine carries a managed identity. The shape is the same in all three cases, where the machine has an identity and the platform serves short lived credentials for it.

Example

A self-hosted GitHub Actions runner fleet needs to download a test fixture archive from one S3 bucket. The permissions policy on the role covers that bucket and nothing else:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::example-test-fixtures",
        "arn:aws:s3:::example-test-fixtures/*"
      ]
    }
  ]
}

The role is created with the EC2 trust policy above, the policy is attached, the role goes into the ci-runner-profile instance profile, and the profile is associated with the runner instances. The workflow then reads the fixtures with no credential configuration:

name: integration
on:
  push:
    branches: [main]

jobs:
  integration-tests:
    runs-on: [self-hosted, linux, x64]
    steps:
      - uses: actions/checkout@v4

      - name: Confirm the identity the runner is using
        run: aws sts get-caller-identity

      - name: Download fixtures
        run: |
          aws s3 cp \
            s3://example-test-fixtures/seed.tar.gz \
            "${RUNNER_TEMP}/seed.tar.gz"

      - run: ./scripts/load-fixtures.sh "${RUNNER_TEMP}/seed.tar.gz"

There is no aws-actions/configure-aws-credentials step, no AWS_ACCESS_KEY_ID secret, and no permissions grant for an identity token. The aws sts get-caller-identity step prints an assumed role ARN of the form arn:aws:sts::111122223333:assumed-role/ci-runner-role/i-0abc123def456789a, where the session name is the instance ID, which makes CloudTrail entries traceable back to the machine that made the call.

Now change one line and watch the boundary hold. A step that reaches for a different bucket:

      - run: aws s3 cp ./build.log s3://example-deploy-artifacts/build.log

fails with An error occurred (AccessDenied) when calling the PutObject operation, because the policy names one bucket and grants read actions only. The failure comes from IAM rather than from anything in the workflow file, so a contributor cannot widen the runner's reach by editing the workflow.

Two limits are worth stating plainly. Instance profiles work only on EC2 instances, so a job running on a hosted runner outside your account cannot use one. And because the identity belongs to the machine, a workflow that needs a per job or per branch identity uses an OIDC token exchange instead, where the branch condition is enforced in the role's trust policy against a claim GitHub signed.

FAQ

What is an IAM instance profile?

An IAM instance profile is an AWS container that holds a single IAM role and can be attached to an EC2 instance. Once attached, the instance metadata service on that machine hands out temporary credentials for the role, and the AWS SDKs and CLI pick them up automatically, so no access key has to be written to disk or stored as a secret.

What is the difference between an IAM role and an instance profile?

The role carries the permissions policy and the trust policy. The instance profile is the wrapper that lets EC2 pass that role to a machine. An instance profile holds at most one role, and the console creates a profile with the same name as the role, which is why the two are often confused.

Why does attaching an instance profile fail with a PassRole error?

The identity launching or modifying the instance needs iam:PassRole on the role inside the profile. Handing a role to a machine is a privilege escalation path, so AWS requires that grant separately from ec2:RunInstances, usually scoped with a condition on iam:PassedToService equal to ec2.amazonaws.com.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.