Project

General

Profile

Task #150 » github-oidc-roles.yaml

YAML file: AWSTemplateFormatVersion: "2010-09-09" - Akshath Shettigara, 10/15/2026 04:35 PM

 
AWSTemplateFormatVersion: "2010-09-09"
Description: >
One IAM role letting GitHub Actions (canofytech/canofy-NSDL-data-extraction)
deploy this project's SAM stack, for ONE environment (TEST or PROD), via
OIDC -- no long-lived AWS keys stored in GitHub. Deploy this template once
per environment (same pattern as the main SAM template.yaml) so each
role's permissions are scoped to only that environment's own stack/
resource names -- the test pipeline can never touch prod-named resources
and vice versa. Assumes the account's GitHub OIDC provider
(token.actions.githubusercontent.com) already exists.

Trust is scoped via the OIDC token's "environment" claim (GitHub's own
recommended pattern for this), matching the workflow's `environment:` key
-- NOT a branch/ref pattern, which is easier to spoof via a
differently-named branch.

Parameters:
Environment:
Type: String
AllowedValues: [test, production]
Description: >
Which GitHub Environment (Settings -> Environments) this role trusts,
and which stack-name prefix its permissions are scoped to. Deploy
with "test" now; deploy again with "production" only once a prod
stack/environment actually exists.
GitHubOrg:
Type: String
Default: canofytech
GitHubRepo:
Type: String
Default: canofy-NSDL-data-extraction
StackNamePrefix:
Type: String
Description: >
The SAM stack name this role may manage, e.g.
nsdl-sez-boe-automation-test or nsdl-sez-boe-automation-prod. Must
match what --stack-name the corresponding workflow actually deploys.

Conditions:
IsTestEnvironment: !Equals [!Ref Environment, test]

Resources:
SamDeployPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
ManagedPolicyName: !Sub "nsdl-sez-boe-sam-deploy-policy-${Environment}"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: CloudFormationAccess
Effect: Allow
Action:
- cloudformation:CreateStack
- cloudformation:UpdateStack
- cloudformation:DeleteStack
- cloudformation:DescribeStacks
- cloudformation:DescribeStackEvents
- cloudformation:DescribeStackResource
- cloudformation:DescribeStackResources
- cloudformation:GetTemplate
- cloudformation:GetTemplateSummary
- cloudformation:ListStackResources
- cloudformation:CreateChangeSet
- cloudformation:DescribeChangeSet
- cloudformation:ExecuteChangeSet
- cloudformation:DeleteChangeSet
- cloudformation:ListChangeSets
- cloudformation:ValidateTemplate
Resource:
- !Sub "arn:aws:cloudformation:*:${AWS::AccountId}:stack/${StackNamePrefix}/*"

# SAM templates use the AWS::Serverless-2016-10-31 transform;
# CloudFormation requires CreateChangeSet on the transform itself
# (an AWS-owned resource, no account ID) in addition to the stack.
- Sid: SamServerlessTransform
Effect: Allow
Action:
- cloudformation:CreateChangeSet
Resource:
- "arn:aws:cloudformation:*:aws:transform/Serverless-2016-10-31"

- Sid: SamCliManagedBucket
Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
- s3:GetBucketLocation
- s3:ListBucket
Resource:
- "arn:aws:s3:::aws-sam-cli-managed-default*"
- "arn:aws:s3:::aws-sam-cli-managed-default*/*"

- Sid: ProjectS3Bucket
Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
- s3:GetBucketLocation
- s3:ListBucket
- s3:PutBucketPolicy
- s3:GetBucketPolicy
- s3:PutEncryptionConfiguration
- s3:PutBucketPublicAccessBlock
- s3:PutLifecycleConfiguration
- s3:GetLifecycleConfiguration
- s3:CreateBucket
- s3:DeleteBucket
- s3:PutBucketTagging
Resource:
- !Sub "arn:aws:s3:::${StackNamePrefix}-*"
- !Sub "arn:aws:s3:::${StackNamePrefix}-*/*"

# ECR repo names carry a random hash suffix SAM generates at
# build time, and GetAuthorizationToken is only ever offered
# account-wide by AWS -- can't be scoped by environment.
- Sid: EcrAccess
Effect: Allow
Action:
- ecr:CreateRepository
- ecr:DescribeRepositories
- ecr:GetAuthorizationToken
- ecr:BatchCheckLayerAvailability
- ecr:GetDownloadUrlForLayer
- ecr:BatchGetImage
- ecr:PutImage
- ecr:InitiateLayerUpload
- ecr:UploadLayerPart
- ecr:CompleteLayerUpload
- ecr:SetRepositoryPolicy
- ecr:DeleteRepository
- ecr:TagResource
Resource: "*"

- Sid: LambdaAccess
Effect: Allow
Action:
- lambda:CreateFunction
- lambda:UpdateFunctionCode
- lambda:UpdateFunctionConfiguration
- lambda:GetFunction
- lambda:GetFunctionConfiguration
- lambda:DeleteFunction
- lambda:AddPermission
- lambda:RemovePermission
- lambda:GetPolicy
- lambda:ListVersionsByFunction
- lambda:PublishVersion
- lambda:PutFunctionEventInvokeConfig
- lambda:UpdateFunctionEventInvokeConfig
- lambda:GetFunctionEventInvokeConfig
- lambda:DeleteFunctionEventInvokeConfig
- lambda:TagResource
- lambda:UntagResource
- lambda:ListTags
Resource:
- !Sub "arn:aws:lambda:*:${AWS::AccountId}:function:${StackNamePrefix}-*"
# The watchdog is named explicitly in template.yaml
# (nsdl-boe-watchdog-<ENV>), not with the stack-name prefix.
- !Sub "arn:aws:lambda:*:${AWS::AccountId}:function:nsdl-boe-watchdog-*"

- Sid: DynamoDbAccess
Effect: Allow
Action:
- dynamodb:CreateTable
- dynamodb:UpdateTable
- dynamodb:DeleteTable
- dynamodb:DescribeTable
- dynamodb:DescribeTimeToLive
- dynamodb:UpdateTimeToLive
- dynamodb:TagResource
- dynamodb:ListTagsOfResource
Resource:
- !Sub "arn:aws:dynamodb:*:${AWS::AccountId}:table/${StackNamePrefix}-*"

# API Gateway's IAM model doesn't support scoping by resource name
# at creation time -- restricted to just the resource types this
# stack actually uses, not by environment.
- Sid: ApiGatewayAccess
Effect: Allow
Action:
- apigateway:*
Resource:
- "arn:aws:apigateway:*::/restapis*"
- "arn:aws:apigateway:*::/apikeys*"
- "arn:aws:apigateway:*::/usageplans*"

- Sid: SnsSqsAccess
Effect: Allow
Action:
- sns:CreateTopic
- sns:DeleteTopic
- sns:GetTopicAttributes
- sns:SetTopicAttributes
- sns:TagResource
- sqs:CreateQueue
- sqs:DeleteQueue
- sqs:GetQueueAttributes
- sqs:SetQueueAttributes
- sqs:TagQueue
Resource:
- !Sub "arn:aws:sns:*:${AWS::AccountId}:${StackNamePrefix}-*"
- !Sub "arn:aws:sqs:*:${AWS::AccountId}:${StackNamePrefix}-*"
# Topic/queue are named explicitly in template.yaml
# (nsdl-boe-alerts-<ENV>, nsdl-boe-phase-b-dlq-<ENV>).
- !Sub "arn:aws:sns:*:${AWS::AccountId}:nsdl-boe-alerts-*"
- !Sub "arn:aws:sqs:*:${AWS::AccountId}:nsdl-boe-phase-b-dlq-*"

- Sid: CloudWatchAlarms
Effect: Allow
Action:
- cloudwatch:PutMetricAlarm
- cloudwatch:DeleteAlarms
- cloudwatch:DescribeAlarms
Resource:
- !Sub "arn:aws:cloudwatch:*:${AWS::AccountId}:alarm:${StackNamePrefix}-*"
# Log group creation/retention can't be scoped ahead of creation.
- Sid: CloudWatchLogs
Effect: Allow
Action:
- logs:CreateLogGroup
- logs:DeleteLogGroup
- logs:PutRetentionPolicy
- logs:DescribeLogGroups
- logs:TagResource
Resource: "*"

- Sid: EventsSchedulerAccess
Effect: Allow
Action:
- events:PutRule
- events:DeleteRule
- events:DescribeRule
- events:PutTargets
- events:RemoveTargets
- events:ListTargetsByRule
- events:TagResource
Resource:
- !Sub "arn:aws:events:*:${AWS::AccountId}:rule/${StackNamePrefix}-*"
# SAM truncates the stack-name portion of schedule rule names
# to 25 characters (e.g. nsdl-sez-boe-automation-t-PhaseAFunction
# DailySchedu-<hash>), so the full-prefix pattern above never
# matches the real rules. Scoped per environment so the test role
# still cannot touch prod-named rules.
- !If
- IsTestEnvironment
- !Sub "arn:aws:events:*:${AWS::AccountId}:rule/nsdl-sez-boe-automation-t*"
- !Sub "arn:aws:events:*:${AWS::AccountId}:rule/nsdl-sez-boe-automation-p*"

# Deploy never reads the secret VALUE -- CloudFormation only
# needs to confirm the secret resource exists/is referenceable.
- Sid: SecretsManagerReadOnly
Effect: Allow
Action:
- secretsmanager:DescribeSecret
- secretsmanager:GetResourcePolicy
Resource:
- !Sub "arn:aws:secretsmanager:*:${AWS::AccountId}:secret:nsdl-boe-automation-secrets-*"

# The most sensitive statement here: SAM creates one execution
# role per Lambda function as part of this same stack. Scoped to
# this environment's own stack-name prefix -- cannot create or
# pass roles outside it, and cannot touch the other environment's
# roles even if this policy were somehow attached there.
- Sid: IamManageLambdaExecutionRoles
Effect: Allow
Action:
- iam:CreateRole
- iam:DeleteRole
- iam:GetRole
- iam:PutRolePolicy
- iam:DeleteRolePolicy
- iam:GetRolePolicy
- iam:AttachRolePolicy
- iam:DetachRolePolicy
- iam:TagRole
- iam:PassRole
Resource:
- !Sub "arn:aws:iam::${AWS::AccountId}:role/${StackNamePrefix}-*"

GitHubActionsDeployRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub "nsdl-sez-boe-github-actions-${Environment}-deploy"
MaxSessionDuration: 3600
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Federated: !Sub "arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com"
Action: sts:AssumeRoleWithWebIdentity
Condition:
StringEquals:
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
"token.actions.githubusercontent.com:sub": !Sub "repo:${GitHubOrg}/${GitHubRepo}:environment:${Environment}"
ManagedPolicyArns:
- !Ref SamDeployPolicy

Outputs:
RoleArn:
Value: !GetAtt GitHubActionsDeployRole.Arn
Description: >
Set as this repo's AWS_DEPLOY_ROLE_ARN (test) or
AWS_DEPLOY_ROLE_ARN_PROD (production) secret, under
Settings -> Environments -> <this Environment>.
(7-7/7)