IAM Policy Setup

Configure AWS IAM roles and policies to grant StackSage read-only access to your AWS resources.

Privacy-First Architecture

StackSage runs entirely in your GitHub Actions environment. Your AWS credentials never leave your infrastructure.

Overview

StackSage requires an IAM role in your AWS account with read-only permissions. This role will be assumed by GitHub Actions using OIDC (OpenID Connect) - no long-lived credentials needed.

Step 1: Create Trust Policy

Create a file trust-policy.json that allows GitHub Actions to assume the role:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::YOUR_ACCOUNT_ID: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:YOUR_ORG/YOUR_REPO:*"
        }
      }
    }
  ]
}

First-Time Setup

If you haven't set up GitHub OIDC before, you'll need to create the OIDC provider first. See GitHub's documentation.

Step 2: Create IAM Role

Create the IAM role using the trust policy:

aws iam create-role \
  --role-name StackSageAuditor \
  --assume-role-policy-document file://trust-policy.json \
  --description "Read-only role for StackSage AWS audits"

Step 3: Create Custom Policy

Create a file stacksage-policy.json with the required permissions:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "StackSageFinOpsAndFuturePlanning",
      "Effect": "Allow",
      "Action": [
        "access-analyzer:ListAnalyzers",
        "access-analyzer:ListFindings",
        "budgets:DescribeBudgets",
        "ce:GetAnomalyMonitors",
        "ce:GetAnomalySubscriptions",
        "ce:GetCostAndUsage",
        "ce:GetDimensionValues",
        "cloudfront:ListDistributions",
        "cloudtrail:DescribeTrails",
        "cloudtrail:GetTrailStatus",
        "cloudwatch:DescribeAlarms",
        "cloudwatch:GetMetricStatistics",
        "cloudwatch:ListMetrics",
        "config:DescribeConfigurationRecorderStatus",
        "dynamodb:DescribeTable",
        "dynamodb:ListTables",
        "ec2:DescribeAddresses",
        "ec2:DescribeImages",
        "ec2:DescribeInstances",
        "ec2:DescribeNatGateways",
        "ec2:DescribeRegions",
        "ec2:DescribeSecurityGroups",
        "ec2:DescribeSnapshots",
        "ec2:DescribeSubnets",
        "ec2:DescribeVolumes",
        "ec2:DescribeVpcEndpoints",
        "ec2:DescribeVpcs",
        "ec2:GetEbsEncryptionByDefault",
        "ecs:DescribeClusters",
        "ecs:DescribeServices",
        "ecs:DescribeTaskDefinition",
        "ecs:ListClusters",
        "ecs:ListServices",
        "ecs:ListTaskDefinitions",
        "elasticache:DescribeCacheClusters",
        "elasticache:DescribeReplicationGroups",
        "elasticloadbalancing:DescribeListeners",
        "elasticloadbalancing:DescribeLoadBalancers",
        "elasticloadbalancing:DescribeTargetGroups",
        "elasticloadbalancing:DescribeTargetHealth",
        "guardduty:ListDetectors",
        "iam:GetAccessKeyLastUsed",
        "iam:GetAccountPasswordPolicy",
        "iam:GetAccountSummary",
        "iam:ListAccessKeys",
        "iam:ListUsers",
        "logs:DescribeLogGroups",
        "logs:DescribeLogStreams",
        "pricing:GetProducts",
        "rds:DescribeDBClusters",
        "rds:DescribeDBInstances",
        "rds:DescribeDBSnapshots",
        "rds:ListTagsForResource",
        "route53:ListHostedZones",
        "s3:GetBucketEncryption",
        "s3:GetBucketLocation",
        "s3:GetBucketLogging",
        "s3:GetBucketPolicy",
        "s3:GetBucketPolicyStatus",
        "s3:GetBucketTagging",
        "s3:GetBucketVersioning",
        "s3:GetLifecycleConfiguration",
        "s3:ListAllMyBuckets",
        "s3control:GetPublicAccessBlock",
        "securityhub:DescribeHub",
        "sts:GetCallerIdentity",
        "tag:GetResources"
      ],
      "Resource": "*"
    }
  ]
}

Step 4: Attach Policy

Create and attach the custom policy to the role:

# Create the policy
aws iam create-policy \
  --policy-name StackSageAuditPolicy \
  --policy-document file://stacksage-policy.json

# Attach to role
aws iam attach-role-policy \
  --role-name StackSageAuditor \
  --policy-arn arn:aws:iam::YOUR_ACCOUNT_ID:policy/StackSageAuditPolicy

Step 5: Configure GitHub Secrets

Add the role ARN to your GitHub repository secrets:

  1. Go to your repository → Settings → Secrets and variables → Actions
  2. Click "New repository secret"
  3. Name: AWS_ROLE_ARN
  4. Value: arn:aws:iam::YOUR_ACCOUNT_ID:role/StackSageAuditor

Alternative: Using AWS Managed Policies

For quick setup, you can use AWS managed policies (though custom policies are more restrictive):

aws iam attach-role-policy \
  --role-name StackSageAuditor \
  --policy-arn arn:aws:iam::aws:policy/ReadOnlyAccess

aws iam attach-role-policy \
  --role-name StackSageAuditor \
  --policy-arn arn:aws:iam::aws:policy/CloudWatchReadOnlyAccess

Verification

Test your IAM setup by running the workflow manually:

  1. Go to your repository's Actions tab
  2. Select "StackSage AWS Audit"
  3. Click "Run workflow"
  4. Check for authentication errors in the logs

Multi-Account Setup

To audit multiple AWS accounts, create the role in each account with the same name, then configure your workflow to loop through accounts:

strategy:
  matrix:
    account:
      - name: Production
        role: arn:aws:iam::111111111111:role/StackSageAuditor
      - name: Staging
        role: arn:aws:iam::222222222222:role/StackSageAuditor
steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: ${{ matrix.account.role }}

Security Best Practices

  • Use OIDC: Avoid long-lived access keys
  • Scope trust policy: Limit to specific repositories
  • Custom policies: Use least-privilege custom policies over managed policies
  • Regular audits: Review role permissions quarterly
  • CloudTrail: Monitor AssumeRole API calls for the StackSage role

Troubleshooting

Error: "User is not authorized to perform: sts:AssumeRoleWithWebIdentity"

The OIDC provider is not configured or the trust policy is incorrect. Verify:

  • OIDC provider exists in IAM console
  • Trust policy references correct GitHub org/repo
  • Trust policy condition matches sts.amazonaws.com

Error: "Access Denied" on specific API calls

The attached policy is missing required permissions. Compare against the custom policy above.

Related Topics