GitHub Actions OIDC on AWS: IAM Trust Policy Mistake That Exposes Your Role

8 min read Original article ↗

TL;DR: If your GitHub Actions OIDC trust policy checks aud but not sub, any GitHub repo can assume your AWS role. Here’s what to look for and how to fix it.

Vulnerable - missing sub condition:

"Condition": {
  "StringEquals": {
    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
  }
}

Secure - restricts to your repo:

"Condition": {
  "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" },
  "StringLike": { "token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:*" }
}

Detect it now: aws iam list-roles --output json | jq -r '.Roles[] | select(.AssumeRolePolicyDocument.Statement[] | select(.Principal.Federated? // empty | endswith("token.actions.githubusercontent.com")) | (.Condition.StringEquals["token.actions.githubusercontent.com:sub"] // .Condition.StringLike["token.actions.githubusercontent.com:sub"]) == null) | "\(.RoleName) -- VULNERABLE"'


The title sounds scary and clickbait, right? Unfortunately, only the second part of the question is false. It’s not clickbait. Last week, Google published details about a threat group called UNC6426. A single compromised npm package allowed access to full AWS admin within 72 hours. How was this possible? Well, a poisoned npm package stole the developer’s GitHub token. From there, the path was clear - going directly to production on AWS, password-free and alert-free.

The door they used? It’s probably open in your account right now.

How a single npm install led to AWS admin

Let’s take a look at the attack process and try to understand it in simple terms. One developer came to work on Monday morning and made a to-do list for the day. The first task required installing an npm package, just like any other, from a trusted registry. The problem was that this package contained a credential-stealing script called QUIETVAULT. It worked by silently extracting the developer’s personal GitHub token.

The attackers intercepted the token and easily used it to gain access to the organization’s GitHub repository. The next step was to use the open-source Nord Stream tool to extract secrets from CI/CD. Further, after searching, they found the GitHub Actions workflow deployed to AWS using OIDC. OIDC is a “modern” and secure authentication method without the need to store access keys.

Sound bad? We’re just getting started. The AWS rule used by GitHub Actions was configured so that any GitHub repo could use it. ALL of them, not just those belonging to the organization.

So what did the attackers do with this? They generated temporary AWS credentials by exploiting a misconfigured OIDC. Next, CloudFormation was deployed with the ability to create a completely new IAM role with admin access. There were no login credentials? So they created their own.

All this took less than 72 hours.

Datadog Security Labs detected over 500 roles with the exact same misconfiguration across ~275 AWS accounts. You know how? By scanning public GitHub workflows. One of them belonged to the British government’s digital service…

What’s OIDC and why should you care

Anyone with a passing understanding of security knows to use OIDC when connecting GitHub Actions to AWS. This approach allows communication without the need to store long-term confidential information. And that’s great, that’s the point. It just needs to be configured correctly. If you’ve ever hit a confusing Access Denied error when assuming a role, misconfigured OIDC trust policies are one of the most common causes.

You’re only as secure as your permission rules that control who can use them. Configuring them incorrectly? You’ve left the door wide open to a potential burglar.

Consider a real-life analogy. You installed the most armor-resistant door in your house. Not even an explosive device can break it down. And then you hung the key to that door on the doorknob.

The vulnerability - one missing line

Here’s what I find in roughly 8 out of 10 client accounts. Look at the Condition block:

"Condition": {
  "StringEquals": {
    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
  }
}

Professional and secure, eh? Well, almost, because there’s only one condition to check - audience. This only confirms that the token is intended for AWS, but does it mention who’s presenting it?

Now look at the secure version:

"Condition": {
  "StringEquals": {
    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
    "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main"
  }
}

One line. Adding the sub claim condition locks the role down to a specific repository and branch.

Without that, you can think of it like this: you go to a concert, go through a series of personal checks, and then hand in your ticket for verification. The security guard looks at you - you have a ticket, come on in. He just didn’t check if it was a ticket for this concert…

Check your account in 60 seconds

Stop reading and run this. Find all roles that trust GitHub’s OIDC provider but are missing the sub condition:

aws iam list-roles --output json | jq -r '
  .Roles[]
  | select(
      .AssumeRolePolicyDocument.Statement[]
      | select(.Principal.Federated? // empty
        | endswith("token.actions.githubusercontent.com"))
      | (.Condition.StringEquals["token.actions.githubusercontent.com:sub"] //
         .Condition.StringLike["token.actions.githubusercontent.com:sub"]) == null
    )
  | "\(.RoleName) -- VULNERABLE"'

If you see output - you have a problem.

To inspect a specific role:

aws iam get-role --role-name YOUR_ROLE_NAME \
  --query 'Role.AssumeRolePolicyDocument' --output json | jq .

No sub condition in the output = vulnerable.

The Terraform fix

Don’t use jsonencode() for this policy. Duplicate map keys in HCL silently overwrite each other - this exact bug hit the UK Government Digital Service. Use aws_iam_policy_document instead:

data "aws_iam_policy_document" "github_actions_trust" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRoleWithWebIdentity"]

    principals {
      type        = "Federated"
      identifiers = [aws_iam_openid_connect_provider.github.arn]
    }

    condition {
      test     = "StringEquals"
      variable = "token.actions.githubusercontent.com:aud"
      values   = ["sts.amazonaws.com"]
    }

    condition {
      test     = "StringLike"
      variable = "token.actions.githubusercontent.com:sub"
      values   = ["repo:YOUR_ORG/YOUR_REPO:ref:refs/heads/main"]
    }
  }
}

resource "aws_iam_role" "github_actions" {
  name               = "GitHubActionsRole"
  assume_role_policy = data.aws_iam_policy_document.github_actions_trust.json
}

Two separate condition blocks. No silent overwrites. No surprises.

What AWS fixed (and what they didn’t)

Back in June 2025, AWS introduced an additional security measure that blocks the creation of new roles without this condition. If you configure it incorrectly, it’s an error.

That’s probably a no-brainer, right?

No, exactly. This security measure only applies to new roles. Pay attention to your OIDC roles created before June 2025. If you didn’t fix it yourself, AWS didn’t fix it for you either.

Why this matters more than you think

The scale of this problem is larger than most people realize. Datadog Security Labs analyzed public GitHub Actions workflows and found over 500 vulnerable IAM role ARNs across 275+ AWS accounts. These are not test accounts - they are production environments belonging to companies that followed standard setup guides and never revisited their trust policies.

The attack surface is enormous because GitHub Actions OIDC tokens are available to every workflow on the platform. Any public repository can request a token with the sts.amazonaws.com audience. If your role trusts the GitHub OIDC provider without restricting the sub claim, that token is accepted. No stolen credentials needed. No phishing. No supply chain compromise of your dependencies. Just a correctly formatted API call from any GitHub Actions runner in the world.

What makes this particularly dangerous is that traditional monitoring tools do not flag it. The AssumeRoleWithWebIdentity call comes from a legitimate identity provider (GitHub) with a valid token. CloudTrail logs it as a normal federation event. GuardDuty does not alert on it by default. Unless you specifically query CloudTrail for role assumptions from repos outside your organization, you will never know it happened.

Did someone already exploit this?

If you use CloudTrail Lake, run this query to find any role assumptions from repos outside your organization:

SELECT eventTime, userIdentity.username AS github_subject,
       sourceIPAddress
FROM <your-event-data-store-id>
WHERE eventSource = 'sts.amazonaws.com'
  AND eventName = 'AssumeRoleWithWebIdentity'
  AND userIdentity.username NOT LIKE 'repo:YOUR-GITHUB-ORG/%'

If you see results - someone outside your org already used your role. Time to rotate credentials and check what they accessed.

One more thing

I’m currently working on additional functionality to detect this configuration in my AWS cloud-audit security scanner (it’s completely open source). Any detection of this error will be included in a report as the OIDC trust policy check, along with comments on how to fix it. If you’d like, please add a star to the repo; it will help me develop and encourage further work.

This year, I’ve audited dozens of accounts, and the ratio of vulnerable to secure is alarming - I bet most of you won’t like the answer.

If you want to scan your entire AWS account for misconfigurations like this, here’s how the major scanners compare - cloud-audit catches the OIDC issue along with 46 other checks.


Sources: Datadog Security Labs, Google Cloud Threat Horizons H1 2026, AWS Security Blog, Wiz Blog


Worried about OIDC and other IAM misconfigurations in your AWS accounts? I offer a free initial AWS security review covering IAM, networking, encryption, and logging.