GitHub OIDC and AWS-Owned Control

Use this pattern when GitHub should authorize a bounded AWS control action without making an interactive browser, MCP, SSO, or runner session responsible for long execution.

Core separation

Keep five concerns separate:

  1. Repository access — GitHub App or normal GitHub permissions.
  2. Workflow definition — GitHub Actions.
  3. Execution substrate — GitHub-hosted runner, CodeBuild-hosted runner, or another reviewed runner.
  4. AWS control identity — GitHub OIDC to a bounded IAM role.
  5. Long execution — an AWS-owned service such as SSM Automation, CodeBuild, CodePipeline, Step Functions, Image Builder, or another appropriate native service.

The runner service role is not the workload/control role.

Preferred office control path

For durable office AWS automation, prefer:

GitHub request -> GitHub Actions -> runner -> GitHub OIDC -> bounded AWS role -> short control action -> AWS-owned execution

A CodeBuild-hosted GitHub Actions runner is useful when AWS-local execution, VPC access, runner scheduling, or GitHub-hosted-runner constraints justify it. Do not make it universal when a normal GitHub-hosted runner is sufficient.

The GitHub job should normally remain short:

authenticate -> verify repo/ref -> verify account -> verify Region -> verify exact target -> start one bounded AWS operation -> persist execution ID -> exit

Durable execution checkpoint

Before the short controller exits, persist enough state for another session or worker to resume safely:

Examples include:

Before retrying a mutation, read the existing execution state first. Never blindly start a duplicate mutation merely because a ChatGPT, MCP, SSO, runner, or browser session ended.

Fail-closed mutation gate

Before a write, verify at minimum:

For Terraform-controlled changes prefer:

import/read live state -> plan -> machine gate -> review gate when required -> apply exact reviewed plan -> provider/Terraform readback

Interactive tools versus durable control

AWS MCP, CLI, SSO, AgentCore Harness, or another interactive operator surface may be appropriate for:

They should not be the durable office mutation dependency when their credentials can expire mid-work.

A useful separation is:

operator surface -> detect/explain/prepare -> durable approved request -> GitHub/OIDC controller -> AWS-owned execution -> provider readback

Connector expiry is a credential event, not a planning reset:

checkpoint in GitHub -> continue repository-only work -> reconnect only when live AWS is the blocker -> verify identity -> resume from execution state

SSM does not imply EC2

Do not create one EC2 instance per account merely to gain control.

For compliance and remediation prefer:

native AWS API -> AWS-managed SSM Automation -> custom SSM Automation -> Lambda only when justified

SSM Automation can invoke AWS APIs without an EC2 managed node.

Use EC2 plus SSM only when a real host or VPC-local requirement exists, such as:

Multi-account default

Prefer one durable controller and the same narrow target-role contract in each account:

GitHub OIDC controller -> bounded controller role -> STS AssumeRole -> target role -> AWS-owned execution

Re-verify target identity after every role assumption.

Do not require one runner or one EC2 instance per account unless network locality requires it.

Troubleshooting order

Use this order:

  1. runner assigned?
  2. workflow step reached?
  3. OIDC token exchange reached?
  4. STS caller identity correct?
  5. target account/Region correct?
  6. AWS API permitted?
  7. execution ID created?
  8. AWS-owned execution succeeded?
  9. provider readback correct?

Do not classify RUNNER_NOT_ASSIGNED as an OIDC or IAM failure.

Reuse rule

Generalize the controller substrate; keep workload-specific behavior in repository-owned configuration; keep permissions bounded.

Avoid creating a new broad role, runner, or workflow for every workload when the execution shape is materially the same.

Citations