Instance Profiles for BYOC Runners

Attach an IAM instance profile to WarpBuild BYOC runners on AWS so every GitHub Actions job on a runner set gets the AWS permissions it needs, and no more.

Attach an IAM instance profile to WarpBuild BYOC runners on AWS in three moves: create an IAM role that EC2 can assume, grant iam:PassRole on that role to the WarpBuild connection role, then set the instance profile ARN on the custom runner. Every GitHub Actions job that lands on that runner set reads short-lived AWS credentials from the instance metadata service, so no AWS access keys go into GitHub secrets.

This page covers the documented prerequisites, the setup commands, a least-privilege policy scoped to one bucket and one registry, and where an instance profile beats an OIDC exchange run from inside the workflow.

Overview

Runner instances in a BYOC AWS stack are EC2 instances in your own account, so they take AWS identity the way any other EC2 instance does: through an instance profile that wraps an IAM role. WarpBuild adds no permissions to runner workloads, so what a job can reach in AWS is exactly what the attached profile grants, as stated in the BYOC security hardening guide.

Scoping is per runner set. The Instance Profile ARN field sits on the Custom Runner configuration rather than on the stack, so permissions follow the runs-on label instead of the whole fleet.

PrerequisiteWho creates itDetail
IAM role trusted by EC2You, in AWSTrust policy names ec2.amazonaws.com as the principal (AWS IAM roles for EC2)
Instance profile holding that roleYou, in AWSThe console creates one implicitly with the role; the CLI needs create-instance-profile plus add-role-to-instance-profile (AWS instance profiles)
WarpBuild connection role nameWarpBuild CloudFormation connection stackRead it from the BYOC connections page; the name follows the pattern warpbuild-<UUID>
iam:PassRole on the runner roleYou, added to the connection roleConditioned on iam:PassedToService equal to ec2.amazonaws.com
Instance Profile ARN on the runner setYou, in the WarpBuild dashboardCustom Runner configuration, one ARN per runner set

BYOC runs on AWS, GCP, and Azure. Instance profiles are the AWS mechanism, and everything below is AWS only.

Architecture

Two IAM roles are in play and they belong to different parties. The connection role, warpbuild-<UUID>, is what the WarpBuild control plane assumes to call RunInstances in your account. The runner role is yours, and its credentials are what your build steps use.

Launching an instance that carries a role means passing an IAM role to the EC2 service, and AWS gates that action behind iam:PassRole on the passed role. That grant is the one step that surprises teams, because every other runner setting is picked from a dropdown.

ObjectOwnerLifetime
Connection role warpbuild-<UUID>Created by the WarpBuild CloudFormation connection stack, held in your accountLives as long as the cloud connection
Runner IAM role and its policyYouLives until you delete it
Instance profileYouLives until you delete it
Credentials inside the jobEC2 instance metadata serviceRotate automatically, and disappear when the instance is terminated after the job

Because each job runs on a freshly provisioned instance that is terminated when the job ends, the credential material never outlives the job, and a policy edit applies to the next job that starts. There is no long-running fleet to roll and no secret in GitHub to rotate.

The other route to AWS credentials in GitHub Actions is an OIDC exchange inside the workflow: GitHub mints a token for the job and aws-actions/configure-aws-credentials trades it through sts:AssumeRoleWithWebIdentity for role credentials, with the trust policy conditioned on repository, branch, or environment claims (GitHub docs on OpenID Connect in AWS).

DimensionInstance profile on the runner setOIDC exchange in the workflow
Where the trust decision livesRunner set configuration plus a role trusted by EC2Role trust policy with conditions on the GitHub claims
GranularityEvery job that targets the labelThe job that runs the exchange
Workflow edit requiredNoneAn extra step and id-token: write permission
Available on GitHub-hosted runnersNo, BYOC EC2 instances onlyYes
Who can obtain the credentialsAnyone who can queue a job on that labelAnyone whose workflow satisfies the trust conditions

The case for the instance profile is standing access that belongs to the machine: cache buckets, base image pulls, internal package mirrors, and tools that read the default AWS credential chain with no configuration. The case for the OIDC exchange is access that belongs to a workflow rather than a runner set, and the branch and environment conditions it can carry. They compose: run the deploy job on a runner set with a narrow profile, and still exchange for the production role inside that job.

Configuration

Create the role with a trust policy naming EC2.

aws iam create-role \
    --role-name ci-runner-role \
    --assume-role-policy-document '{
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Principal": {"Service": "ec2.amazonaws.com"},
                "Action": "sts:AssumeRole"
            }
        ]
    }'

Attach a policy scoped to the resources the jobs on this label actually touch. The policy below reaches one artifact bucket and one ECR repository, and nothing else. ecr:GetAuthorizationToken is the one action that takes "Resource": "*", because the token is account level.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ArtifactBucket",
      "Effect": "Allow",
      "Action": ["s3:PutObject", "s3:GetObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::acme-ci-artifacts",
        "arn:aws:s3:::acme-ci-artifacts/*"
      ]
    },
    {
      "Sid": "RegistryAuth",
      "Effect": "Allow",
      "Action": "ecr:GetAuthorizationToken",
      "Resource": "*"
    },
    {
      "Sid": "OneRepository",
      "Effect": "Allow",
      "Action": [
        "ecr:BatchCheckLayerAvailability",
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage",
        "ecr:PutImage",
        "ecr:InitiateLayerUpload",
        "ecr:UploadLayerPart",
        "ecr:CompleteLayerUpload"
      ],
      "Resource": "arn:aws:ecr:us-east-1:123456789012:repository/acme-api"
    }
  ]
}
aws iam put-role-policy \
    --role-name ci-runner-role \
    --policy-name ci-runner-policy \
    --policy-document file://policy.json

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

Grant iam:PassRole on that role to the WarpBuild connection role, exactly as the instance profile setup guide documents.

aws iam put-role-policy \
    --role-name warpbuild-<UUID> \
    --policy-name PassRolePolicy \
    --policy-document '{
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": "iam:PassRole",
                "Resource": "arn:aws:iam::123456789012:role/ci-runner-role",
                "Condition": {
                    "StringEquals": {
                        "iam:PassedToService": "ec2.amazonaws.com"
                    }
                }
            }
        ]
    }'

Paste the instance profile ARN, arn:aws:iam::123456789012:instance-profile/ci-runner-profile, into Custom Runner configuration in the WarpBuild dashboard. The runner set registers a label formed as warp-custom- plus the runner name, and workflows use it with no credential step at all.

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

jobs:
  image:
    runs-on: warp-custom-ci-linux-large
    steps:
      - uses: actions/checkout@v4
      - name: Confirm the runner identity
        run: aws sts get-caller-identity
      - name: Log in to the registry
        run: |
          aws ecr get-login-password --region us-east-1 \
            | docker login --username AWS --password-stdin \
              123456789012.dkr.ecr.us-east-1.amazonaws.com
      - run: docker build -t 123456789012.dkr.ecr.us-east-1.amazonaws.com/acme-api:${{ github.sha }} .
      - run: docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/acme-api:${{ github.sha }}
      - run: aws s3 cp ./dist s3://acme-ci-artifacts/${{ github.sha }}/ --recursive

Operations

Verify the iam:PassRole grant before you wait on a queued job. A simulation answers in one call.

aws iam simulate-principal-policy \
    --policy-source-arn arn:aws:iam::123456789012:role/warpbuild-<UUID> \
    --action-names iam:PassRole \
    --resource-arns arn:aws:iam::123456789012:role/ci-runner-role \
    --context-entries ContextKeyName=iam:PassedToService,ContextKeyType=string,ContextKeyValues=ec2.amazonaws.com

If jobs sit queued after a profile change, the pass-role grant and a typo in the ARN are the first two things to check. aws sts get-caller-identity as an early step tells you which role a job actually received.

Require IMDSv2 on runner sets that carry a profile. The toggle is under Runner Specs in the AWS BYOC configuration guide, and it blocks the request shape that server-side request forgery in a build step would use to read instance credentials.

Split labels by permission rather than by team. One runner set with a read-only profile for pull request builds and a second with write access for deploys gives you a boundary that shows up in code review, since promoting a job to the wider permission set means editing runs-on in a workflow file. On AWS BYOC the Linux and Windows sets are the ones that take an instance profile.

Attaching a profile changes no line on your WarpBuild invoice. BYOC runners are billed at $0.002 per runner minute for Linux and $0.002 per runner minute for Windows, with EC2 and EBS charged to your own AWS account at your rates, checked on 2026-08-13 against the WarpBuild pricing page.

For the audit question that follows this one in most reviews: SOC 2 Type 2, with trust.warpbuild.com as the linked evidence.

Where to go next depends on which role you are shaping. The permissions on the WarpBuild connection role itself are covered in IAM permissions for WarpBuild BYOC on AWS. The vendor-neutral definition of the object sits in IAM instance profile, defined. The short version of this page is Can I attach an IAM role to a BYOC runner?. The full AWS setup, from cloud connection to first label, is on BYOC runners on AWS for GitHub Actions.

FAQ

Why does the WarpBuild connection role need iam:PassRole?

AWS gates handing an IAM role to a service. The WarpBuild connection role calls RunInstances in your account, so to launch an instance carrying your runner role it needs iam:PassRole on that role ARN, scoped with a condition on iam:PassedToService equal to ec2.amazonaws.com. Without the grant the launch fails and jobs stay queued.

Can two runner sets carry different instance profiles?

Yes. The Instance Profile ARN is set on the Custom Runner configuration, so each runner set has its own. A deploy runner set can hold a role that a runner set running pull request tests never sees, and the split shows up in the workflow as two different runs-on labels.

Should I use an instance profile or an OIDC exchange in the workflow?

Use an instance profile for permissions every job on a runner set needs, such as reading a cache bucket or pulling a base image. Use an OIDC exchange when the permission belongs to one workflow rather than one runner set, when the same workflow also runs on GitHub-hosted runners, or when you want trust conditions on branch or environment. The two compose in the same job.

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.