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.
| Prerequisite | Who creates it | Detail |
|---|---|---|
| IAM role trusted by EC2 | You, in AWS | Trust policy names ec2.amazonaws.com as the principal (AWS IAM roles for EC2) |
| Instance profile holding that role | You, in AWS | The 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 name | WarpBuild CloudFormation connection stack | Read it from the BYOC connections page; the name follows the pattern warpbuild-<UUID> |
iam:PassRole on the runner role | You, added to the connection role | Conditioned on iam:PassedToService equal to ec2.amazonaws.com |
| Instance Profile ARN on the runner set | You, in the WarpBuild dashboard | Custom 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.
| Object | Owner | Lifetime |
|---|---|---|
Connection role warpbuild-<UUID> | Created by the WarpBuild CloudFormation connection stack, held in your account | Lives as long as the cloud connection |
| Runner IAM role and its policy | You | Lives until you delete it |
| Instance profile | You | Lives until you delete it |
| Credentials inside the job | EC2 instance metadata service | Rotate 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).
| Dimension | Instance profile on the runner set | OIDC exchange in the workflow |
|---|---|---|
| Where the trust decision lives | Runner set configuration plus a role trusted by EC2 | Role trust policy with conditions on the GitHub claims |
| Granularity | Every job that targets the label | The job that runs the exchange |
| Workflow edit required | None | An extra step and id-token: write permission |
| Available on GitHub-hosted runners | No, BYOC EC2 instances only | Yes |
| Who can obtain the credentials | Anyone who can queue a job on that label | Anyone 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-roleGrant 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 }}/ --recursiveOperations
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.comIf 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.