DEV Community

Mariusz Gębala
Mariusz Gębala

Posted on • Originally published at haitmg.pl

I Audit AWS Accounts. 8 Out of 10 Have This GitHub Actions Backdoor.

TL;DR: Configuring GitHub Actions OIDC is very convenient and useful, but often dangerous. If you didn't consider one specific IAM requirement and created a role before June 2025, you're almost certainly vulnerable to an attack that would allow ANY GitHub repository to assume your AWS deployment role.


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.

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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"'
Enter fullscreen mode Exit fullscreen mode

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 .
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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.

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/%'
Enter fullscreen mode Exit fullscreen mode

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, 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.


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

Top comments (0)