AWS STS Finally Lets You Write Trust Policies That Actually Mean Something

AWS STS Finally Lets You Write Trust Policies That Actually Mean Something

Table of Contents

If you’ve ever written an IAM trust policy for GitHub Actions OIDC federation, you’ve probably done the thing we all did. You set the sub condition to repo:my-org/my-repo:*, told yourself “that’s scoped enough,” and moved on with your day.

Here’s the uncomfortable part: that wildcard meant any branch, any environment, any workflow, any trigger type from that repository could assume your production deployment role. And if you’re being really honest with yourself, you might have a few roles out there where the sub condition is even broader than that.

You’re not alone. Datadog Security Labs found this pattern everywhere — including cases where the sub condition was missing entirely, meaning any GitHub Actions workflow on the planet could assume the role.

That’s… not good.

What Changed

AWS STS now supports validation of provider-specific OIDC claims as first-class condition keys in IAM trust policies. This landed in February 2026 across all commercial regions, and it covers four providers: GitHub Actions, Google, CircleCI, and Oracle Cloud Infrastructure.

The real story isn’t the announcement itself — it’s what it finally lets you do.

Instead of cramming your entire authorisation logic into a single sub claim string match, you can now write compound conditions against individual claims from the identity provider’s JWT token. Repository. Branch. Environment. Workflow. Actor. Each as a separate, composable condition key.

That’s a fundamental shift in how you write trust policies for OIDC federation.

The Problem With Packing Everything Into sub

GitHub Actions’ OIDC token actually contains over 30 claims. But until this feature, AWS STS only exposed the standard OIDC claims as condition keys — sub, aud, and amr. Everything else was invisible to your trust policy.

GitHub’s sub claim is a colon-delimited string that changes format based on the workflow trigger:

repo:my-org/my-repo:ref:refs/heads/main
repo:my-org/my-repo:environment:production
repo:my-org/my-repo:pull_request

This created three nasty problems:

  1. No compound conditions. You couldn’t say “this repo AND this branch AND this environment” because all that information was collapsed into a single string that varied based on trigger type.

  2. Wildcard abuse. To handle the varying formats, everyone used StringLike with wildcards — which was almost always more permissive than intended.

  3. Name mutability. Repository names can be changed. Username can be changed. Trust policies based on string names break silently when things get renamed.

The result? Most GitHub Actions trust policies in the wild were significantly more permissive than their authors believed.

Before and After: A Real Trust Policy

Before — the pattern almost everyone shipped:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
                },
                "StringLike": {
                    "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:*"
                }
            }
        }
    ]
}

Any branch. Any environment. Any workflow. Any actor. All allowed.

After — what you can write now:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
                    "token.actions.githubusercontent.com:repository_id": "123456789",
                    "token.actions.githubusercontent.com:ref": "refs/heads/main",
                    "token.actions.githubusercontent.com:environment": "production",
                    "token.actions.githubusercontent.com:job_workflow_ref": "my-org/platform/.github/workflows/deploy.yml@refs/heads/main"
                },
                "StringLike": {
                    "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:*"
                }
            }
        }
    ]
}

That’s defence in depth with actual teeth. You need the right repository (pinned by immutable ID), the right branch, the right environment, AND the right centrally-managed workflow template. Each condition is a separate key evaluated independently.

The sub claim is still required — AWS’s identity-provider controls mandate it — but it can now be the broad catch-all while the new keys do the real authorisation work.

The New Condition Keys Worth Knowing

GitHub Actions got the biggest expansion with 9 new condition keys. The ones I’d pay attention to:

  • repository_id — Immutable numeric ID. Survives repository renames. Use this instead of repository for production trust policies.
  • environment — Maps directly to GitHub Environments, which can require manual approval gates. This is your deployment governance layer.
  • job_workflow_ref — Full path to the reusable workflow file including the ref. This is how you enforce that only blessed, centrally-managed workflows can assume your deployment role. Developers can’t bypass it by copying workflow steps into their own file.
  • actor_id — Immutable numeric ID of the GitHub user who triggered the workflow. Survives username changes.
  • ref — The Git ref. Pin to refs/heads/main for production, refs/heads/develop for staging.

Google got organization_number for scoping access to a specific Google Cloud organisation. CircleCI got project_id for pinning to specific projects. Both useful, but GitHub Actions is where the real impact is.

Where This Gets Interesting: Supply Chain Controls

The job_workflow_ref key deserves special attention. Picture this scenario:

Your platform team maintains a centralised deployment workflow at my-org/platform/.github/workflows/deploy.yml. Every team is supposed to use it because it includes security scanning, approval gates, and audit logging. But nothing actually enforced that. A developer could copy the deployment steps into their own workflow, skip the security scanning, and still assume the production role.

With job_workflow_ref, the trust policy itself enforces the governance. Only the blessed workflow template can assume the role. That’s the difference between a policy you hope people follow and a control you can prove they can’t bypass.

Practical Migration Path

If you’re running GitHub Actions OIDC federation today, here’s what I’d do:

  1. Audit your existing trust policies. Look for StringLike with * on the sub claim. That’s your risk surface.
  2. Identify your immutable IDs. Get your repository_id values from the GitHub API. Start using these instead of string names.
  3. Layer in the new keys. Add environment, ref, and job_workflow_ref conditions to production deployment roles first.
  4. Keep the sub condition. It’s still mandatory. Make it the broad match and let the new keys do the precise work.
  5. Test in a non-production account first. Trust policy changes can lock out your CI/CD pipeline if you get a condition wrong.

What It Doesn’t Solve

Let’s be honest about the limitations:

Only four providers. GitHub, Google, CircleCI, and OCI. If you’re using Azure AD, Okta, GitLab, or Keycloak for OIDC federation — nothing changed for you. This feels like an iterative rollout, but there’s no timeline for broader coverage.

Trust-time only. All the new condition keys have Available in Session: No. You can use them in trust policies and resource control policies, but not in identity-based policies or permission boundaries after the role is assumed. The validation happens at role assumption time and then it’s gone.

Not all GitHub claims are mapped. GitHub’s OIDC token contains 33 claims. AWS maps 14 of them (5 standard + 9 new). Notable gaps include repository_owner, repository_owner_id, event_name, ref_protected, and repository_visibility. You still can’t write a trust policy that says “only public repositories” or “only protected branches.”

Existing roles are grandfathered. The identity-provider controls that mandate sub claim evaluation only apply to new or modified trust policies. Legacy roles without sub conditions remain vulnerable until you touch them.

The Bigger Picture

This is part of a broader shift I’ve been watching in how AWS approaches identity federation. The June 2025 identity-provider controls were the stick — forcing practitioners to include sub conditions. This feature is the carrot — giving you the granularity to actually write trust policies that express your intent.

The combination of immutable IDs (surviving renames), compound conditions (repository AND branch AND environment), and workflow-level governance (job_workflow_ref) means you can finally build OIDC trust policies that are both secure and maintainable.

If you’re running any CI/CD pipeline that uses OIDC federation with AWS — and you should be, long-lived credentials in GitHub secrets is a pattern that needs to die — this is worth an afternoon of your time to audit and upgrade your trust policies.

I hope someone else finds this useful.

Cheers

Share :