AWS IAM Role Trust Relationship for GitHub Actions


Description

This policy ensures that IAM roles used with GitHub Actions are securely configured to prevent unauthorized access to AWS resources. It validates trust relationships by checking for proper audience (aud) restrictions, ensuring it is set to sts.amazonaws.com, and subject (sub) conditions, confirming they are scoped to specific repositories or environments. Misconfigurations, such as overly permissive wildcards or missing conditions, can allow unauthorized repositories to assume roles, leading to potential data breaches or compliance violations. By enforcing these checks, the policy mitigates risks of exploitation, enhances security posture, and protects critical AWS resources from external threats.

Query · python

import json

from panther_base_helpers import deep_get

# Add/remove pairs as desired
ALLOWED_ORG_REPO_PAIRS = ["org/repo", "allowed-org-example/allowed-repo-example"]


def policy(resource):
    # check if resource.AssumRolePolicyDocument is a string, and if so convert to json
    if isinstance(resource.get("AssumeRolePolicyDocument"), str):
        policy_document = json.loads(resource.get("AssumeRolePolicyDocument", {}))
    else:
        policy_document = resource.get("AssumeRolePolicyDocument", {})
    assume_role_policy = policy_document.get("Statement", [])

    for statement in assume_role_policy:
        # only check for Allow sts:AssumeRoleWithWebIdentity
        if (
            statement.get("Effect") != "Allow"
            or statement.get("Action") != "sts:AssumeRoleWithWebIdentity"
        ):
            continue

        principal = deep_get(statement, "Principal", "Federated")
        audience = deep_get(
            statement, "Condition", "StringEquals", "token.actions.githubusercontent.com:aud"
        )
        subject = deep_get(
            statement,
            "Condition",
            "StringLike",
            "token.actions.githubusercontent.com:sub",
            default="",
        ) or deep_get(
            statement,
            "Condition",
            "StringEquals",
            "token.actions.githubusercontent.com:sub",
            default="",
        )

        if subject.startswith("repo:"):
            # repo subjects must have github as the principal and sts.amazonaws.com as the audience
            if any(
                [
                    "oidc-provider/token.actions.githubusercontent.com" not in principal,
                    audience != "sts.amazonaws.com",
                    (
                        "*" in subject
                        and not any(
                            subject.startswith(f"repo:{org_repo}:*")
                            for org_repo in ALLOWED_ORG_REPO_PAIRS
                        )
                    ),
                ]
            ):
                return False
        else:
            # non-repo subjects must not have github as the principal
            if "oidc-provider/token.actions.githubusercontent.com" in principal:
                return False

    return True

Analyst notes

To fix roles flagged by this policy: 1. Update the trust relationship of the flagged IAM role in the AWS Management Console or CLI. 2. Add a Condition block with 'StringLike' or 'StringEquals' for 'token.actions.githubusercontent.com:sub'. 3. Ensure the audience is set to 'sts.amazonaws.com'. 4. Avoid overly permissive wildcards in the sub condition.

Raw source AWS IAM Role Trust Relationship for GitHub Actions · Panther Python
Esc
Published by panther-labs/panther-analysis ↗, licensed under Apache 2.0 ↗. Reproduced here unmodified.
AnalysisType: policy
Filename: aws_iam_role_github_actions_trust.py
PolicyID: "AWS.IAM.Role.GitHubActionsTrust"
DisplayName: "AWS IAM Role Trust Relationship for GitHub Actions"
Enabled: false
ResourceTypes:
  - AWS.IAM.Role
Tags:
  - AWS
  - GitHub Actions
  - Identity & Access Management
  - Configuration Required
Severity: High
Description: >
  This policy ensures that IAM roles used with GitHub Actions are securely configured to prevent unauthorized access to AWS resources. 
  It validates trust relationships by checking for proper audience (aud) restrictions, ensuring it is set to sts.amazonaws.com, and subject (sub) conditions, 
  confirming they are scoped to specific repositories or environments. Misconfigurations, such as overly permissive wildcards or missing conditions, 
  can allow unauthorized repositories to assume roles, leading to potential data breaches or compliance violations. 
  By enforcing these checks, the policy mitigates risks of exploitation, enhances security posture, and protects critical AWS resources from external threats.
Runbook: >
  To fix roles flagged by this policy:
  1. Update the trust relationship of the flagged IAM role in the AWS Management Console or CLI.
  2. Add a Condition block with 'StringLike' or 'StringEquals' for 'token.actions.githubusercontent.com:sub'.
  3. Ensure the audience is set to 'sts.amazonaws.com'.
  4. Avoid overly permissive wildcards in the sub condition.
Reference: >
  - https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html
  - https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-cloud-providers
  - https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services
Tests:
  - Name: Valid GitHub Actions Trust Relationship
    ExpectedResult: true
    Resource:
      {
        "AssumeRolePolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Action": "sts:AssumeRoleWithWebIdentity",
              "Principal": {
                "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
              },
              "Condition": {
                "StringEquals": {
                  "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
                },
                "StringLike": {
                  "token.actions.githubusercontent.com:sub": "repo:org/repo:*"
                }
              }
            }
          ]
        }
      }

  - Name: Missing Audience Condition
    ExpectedResult: false
    Resource:
      {
        "AssumeRolePolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Action": "sts:AssumeRoleWithWebIdentity",
              "Principal": {
                "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
              },
              "Condition": {
                "StringLike": {
                  "token.actions.githubusercontent.com:sub": "repo:org/repo:*"
                }
              }
            }
          ]
        }
      }

  - Name: Missing Subject Restriction
    ExpectedResult: false
    Resource:
      {
        "AssumeRolePolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Action": "sts:AssumeRoleWithWebIdentity",
              "Principal": {
                "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
              },
              "Condition": {
                "StringEquals": {
                  "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
                }
              }
            }
          ]
        }
      }

  - Name: Overly Permissive Wildcard in Subject
    ExpectedResult: false
    Resource:
      {
        "AssumeRolePolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Action": "sts:AssumeRoleWithWebIdentity",
              "Principal": {
                "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
              },
              "Condition": {
                "StringEquals": {
                  "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
                },
                "StringLike": {
                  "token.actions.githubusercontent.com:sub": "*"
                }
              }
            }
          ]
        }
      }

  - Name: Valid Subject Restriction with Specific Environment
    ExpectedResult: true
    Resource:
      {
        "AssumeRolePolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Action": "sts:AssumeRoleWithWebIdentity",
              "Principal": {
                "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
              },
              "Condition": {
                "StringEquals": {
                  "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
                  "token.actions.githubusercontent.com:sub": "repo:org/repo:environment:prod"
                }
              }
            }
          ]
        }
      }

  - Name: Invalid Principal as Wildcard
    ExpectedResult: false
    Resource:
      {
        "AssumeRolePolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Action": "sts:AssumeRoleWithWebIdentity",
              "Principal": {
                "Federated": "*"
              },
              "Condition": {
                "StringEquals": {
                  "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
                },
                "StringLike": {
                  "token.actions.githubusercontent.com:sub": "repo:org/repo:*"
                }
              }
            }
          ]
        }
      }

  - Name: Non-GitHub OIDC Principal
    ExpectedResult: false
    Resource:
      {
        "AssumeRolePolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Action": "sts:AssumeRoleWithWebIdentity",
              "Principal": {
                "Federated": "arn:aws:iam::123456789012:oidc-provider/accounts.google.com"
              },
              "Condition": {
                "StringEquals": {
                  "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
                },
                "StringLike": {
                  "token.actions.githubusercontent.com:sub": "repo:org/repo:*"
                }
              }
            }
          ]
        }
      }

  - Name: Non-GitHub IAM Role
    ExpectedResult: true
    Resource:
      {
      "AccountId": "123412341233",
      "Arn": "arn:aws:iam::123412341233:role/DevAdministrator",
      "AssumeRolePolicyDocument": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"arn:aws:iam::12341523456:root\",\"arn:aws:iam::123412341233:root\"]},\"Action\":\"sts:AssumeRole\",\"Condition\":{\"Bool\":{\"aws:MultiFactorAuthPresent\":\"true\",\"aws:SecureTransport\":\"true\"},\"NumericLessThan\":{\"aws:MultiFactorAuthAge\":\"28800\"}}}]}",
      "ManagedPolicyARNs": [
        "arn:aws:iam::aws:policy/AdministratorAccess"
      ],
      "ManagedPolicyNames": [
        "AdministratorAccess"
      ],
      "MaxSessionDuration": 28800,
      "Name": "DevAdministrator",
      "Path": "/",
      "Region": "global",
      "ResourceId": "arn:aws:iam::123412341233:role/DevAdministrator",
      "ResourceType": "AWS.IAM.Role",
      "TimeCreated": "2023-11-08T23:50:46Z"
    }
  - Name: Allowed repo
    ExpectedResult: true
    Resource:
      {
        "AssumeRolePolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Action": "sts:AssumeRoleWithWebIdentity",
              "Principal": {
                "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
              },
              "Condition": {
                "StringEquals": {
                  "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
                  "token.actions.githubusercontent.com:sub": "repo:allowed-org-example/allowed-repo-example:*"
                }
              }
            }
          ]
        }
      }

# ------ paired body: aws_iam_role_github_actions_trust.py ------

import json

from panther_base_helpers import deep_get

# Add/remove pairs as desired
ALLOWED_ORG_REPO_PAIRS = ["org/repo", "allowed-org-example/allowed-repo-example"]


def policy(resource):
    # check if resource.AssumRolePolicyDocument is a string, and if so convert to json
    if isinstance(resource.get("AssumeRolePolicyDocument"), str):
        policy_document = json.loads(resource.get("AssumeRolePolicyDocument", {}))
    else:
        policy_document = resource.get("AssumeRolePolicyDocument", {})
    assume_role_policy = policy_document.get("Statement", [])

    for statement in assume_role_policy:
        # only check for Allow sts:AssumeRoleWithWebIdentity
        if (
            statement.get("Effect") != "Allow"
            or statement.get("Action") != "sts:AssumeRoleWithWebIdentity"
        ):
            continue

        principal = deep_get(statement, "Principal", "Federated")
        audience = deep_get(
            statement, "Condition", "StringEquals", "token.actions.githubusercontent.com:aud"
        )
        subject = deep_get(
            statement,
            "Condition",
            "StringLike",
            "token.actions.githubusercontent.com:sub",
            default="",
        ) or deep_get(
            statement,
            "Condition",
            "StringEquals",
            "token.actions.githubusercontent.com:sub",
            default="",
        )

        if subject.startswith("repo:"):
            # repo subjects must have github as the principal and sts.amazonaws.com as the audience
            if any(
                [
                    "oidc-provider/token.actions.githubusercontent.com" not in principal,
                    audience != "sts.amazonaws.com",
                    (
                        "*" in subject
                        and not any(
                            subject.startswith(f"repo:{org_repo}:*")
                            for org_repo in ALLOWED_ORG_REPO_PAIRS
                        )
                    ),
                ]
            ):
                return False
        else:
            # non-repo subjects must not have github as the principal
            if "oidc-provider/token.actions.githubusercontent.com" in principal:
                return False

    return True

Detection rules belong to the projects that publish them and remain under their own licenses. This site indexes and links to them; it claims no rights in them.