Skip to content

Commit b23724e

Browse files
PaulDuvallclaude
andcommitted
feat: add IAM setup script for GitHub Actions NPM publishing
Created Python script to automate AWS IAM role setup with: - OIDC provider configuration for GitHub Actions - IAM role with SSM Parameter Store permissions - Specific permissions for NPM token management - KMS encryption support for SecureString parameters - Limited scope to /npm/tokens/*, /github/tokens/*, and /ci/tokens/* paths The script auto-detects the GitHub repository and creates a properly scoped role that can only be assumed by the specific repository. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent aa1d5ef commit b23724e

File tree

1 file changed

+327
-0
lines changed

1 file changed

+327
-0
lines changed
Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
#!/usr/bin/env python3
2+
"""
3+
AWS IAM Role Setup for GitHub Actions - NPM Publishing
4+
Creates the necessary IAM role and OIDC provider for GitHub Actions to publish NPM packages.
5+
"""
6+
7+
import boto3
8+
import json
9+
import sys
10+
from botocore.exceptions import ClientError
11+
12+
13+
def get_github_repo_info():
14+
"""Get GitHub repository information from git remote."""
15+
print("📋 Detecting GitHub Repository Information...")
16+
17+
try:
18+
import subprocess
19+
20+
# Get remote URL
21+
result = subprocess.run(['git', 'remote', 'get-url', 'origin'],
22+
capture_output=True, text=True, check=True)
23+
remote_url = result.stdout.strip()
24+
25+
# Parse GitHub URL
26+
if 'github.com' in remote_url:
27+
# Handle both SSH and HTTPS URLs
28+
if remote_url.startswith('git@github.com:'):
29+
# SSH: git@github.com:owner/repo.git
30+
repo_path = remote_url.replace('git@github.com:', '').replace('.git', '')
31+
elif 'github.com/' in remote_url:
32+
# HTTPS: https://github.com/owner/repo.git
33+
repo_path = remote_url.split('github.com/')[-1].replace('.git', '')
34+
else:
35+
raise ValueError("Cannot parse GitHub URL format")
36+
37+
github_org, github_repo = repo_path.split('/', 1)
38+
39+
print(f"✅ Detected GitHub repository: {github_org}/{github_repo}")
40+
return github_org, github_repo
41+
else:
42+
raise ValueError("Remote is not a GitHub repository")
43+
44+
except Exception as e:
45+
print(f"❌ Could not auto-detect GitHub repository: {e}")
46+
print("Please ensure you're in a git repository with GitHub remote")
47+
sys.exit(1)
48+
49+
50+
def create_oidc_provider(iam_client):
51+
"""Create GitHub OIDC identity provider if it doesn't exist."""
52+
print("\n🔐 Setting up GitHub OIDC Provider...")
53+
54+
github_oidc_url = "https://token.actions.githubusercontent.com"
55+
github_thumbprint = "6938fd4d98bab03faadb97b34396831e3780aea1" # GitHub's thumbprint
56+
57+
try:
58+
# Check if OIDC provider already exists
59+
response = iam_client.list_open_id_connect_providers()
60+
for provider in response['OpenIDConnectProviderList']:
61+
if github_oidc_url in provider['Arn']:
62+
print(f"✅ GitHub OIDC provider already exists: {provider['Arn']}")
63+
return provider['Arn']
64+
65+
# Create OIDC provider
66+
response = iam_client.create_open_id_connect_provider(
67+
Url=github_oidc_url,
68+
ThumbprintList=[github_thumbprint],
69+
ClientIDList=['sts.amazonaws.com']
70+
)
71+
72+
oidc_arn = response['OpenIDConnectProviderArn']
73+
print(f"✅ Created GitHub OIDC provider: {oidc_arn}")
74+
return oidc_arn
75+
76+
except ClientError as e:
77+
if 'EntityAlreadyExists' in str(e):
78+
print("✅ GitHub OIDC provider already exists")
79+
# Get the ARN
80+
response = iam_client.list_open_id_connect_providers()
81+
for provider in response['OpenIDConnectProviderList']:
82+
if github_oidc_url in provider['Arn']:
83+
return provider['Arn']
84+
else:
85+
print(f"❌ Error creating OIDC provider: {e}")
86+
sys.exit(1)
87+
88+
89+
def create_trust_policy(account_id, github_org, github_repo):
90+
"""Create trust policy for GitHub Actions."""
91+
return {
92+
"Version": "2012-10-17",
93+
"Statement": [
94+
{
95+
"Effect": "Allow",
96+
"Principal": {
97+
"Federated": f"arn:aws:iam::{account_id}:oidc-provider/token.actions.githubusercontent.com"
98+
},
99+
"Action": "sts:AssumeRoleWithWebIdentity",
100+
"Condition": {
101+
"StringEquals": {
102+
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
103+
},
104+
"StringLike": {
105+
"token.actions.githubusercontent.com:sub": [
106+
f"repo:{github_org}/{github_repo}:*"
107+
]
108+
}
109+
}
110+
}
111+
]
112+
}
113+
114+
115+
def create_npm_publish_policy():
116+
"""Create IAM policy for NPM publishing permissions."""
117+
return {
118+
"Version": "2012-10-17",
119+
"Statement": [
120+
{
121+
"Sid": "SSMParameterStoreAccess",
122+
"Effect": "Allow",
123+
"Action": [
124+
# SSM Parameter Store permissions for NPM token management
125+
"ssm:PutParameter",
126+
"ssm:GetParameter",
127+
"ssm:GetParameters",
128+
"ssm:DeleteParameter",
129+
"ssm:DescribeParameters",
130+
"ssm:GetParameterHistory",
131+
"ssm:GetParametersByPath",
132+
"ssm:AddTagsToResource",
133+
"ssm:RemoveTagsFromResource",
134+
"ssm:ListTagsForResource"
135+
],
136+
"Resource": [
137+
# Specific to NPM token parameters
138+
"arn:aws:ssm:*:*:parameter/npm/tokens/*",
139+
"arn:aws:ssm:*:*:parameter/github/tokens/*",
140+
"arn:aws:ssm:*:*:parameter/ci/tokens/*"
141+
]
142+
},
143+
{
144+
"Sid": "SSMDescribeAccess",
145+
"Effect": "Allow",
146+
"Action": [
147+
"ssm:DescribeParameters"
148+
],
149+
"Resource": "*"
150+
},
151+
{
152+
"Sid": "KMSKeyAccess",
153+
"Effect": "Allow",
154+
"Action": [
155+
# KMS permissions for SecureString parameters
156+
"kms:Decrypt",
157+
"kms:Encrypt",
158+
"kms:GenerateDataKey",
159+
"kms:DescribeKey"
160+
],
161+
"Resource": "*",
162+
"Condition": {
163+
"StringLike": {
164+
"kms:ViaService": [
165+
"ssm.*.amazonaws.com"
166+
]
167+
}
168+
}
169+
},
170+
{
171+
"Sid": "CloudWatchLogsAccess",
172+
"Effect": "Allow",
173+
"Action": [
174+
# CloudWatch Logs for GitHub Actions debugging
175+
"logs:CreateLogGroup",
176+
"logs:CreateLogStream",
177+
"logs:PutLogEvents",
178+
"logs:DescribeLogGroups",
179+
"logs:DescribeLogStreams"
180+
],
181+
"Resource": [
182+
"arn:aws:logs:*:*:log-group:/github-actions/*",
183+
"arn:aws:logs:*:*:log-group:/aws/lambda/github-actions-*"
184+
]
185+
},
186+
{
187+
"Sid": "STSAssumeRole",
188+
"Effect": "Allow",
189+
"Action": [
190+
# STS permissions for role assumption
191+
"sts:GetCallerIdentity",
192+
"sts:GetSessionToken"
193+
],
194+
"Resource": "*"
195+
}
196+
]
197+
}
198+
199+
200+
def create_npm_role(iam_client, role_name, github_org, github_repo, account_id):
201+
"""Create the NPM publishing role for GitHub Actions."""
202+
print(f"\n🔧 Creating NPM publishing role: {role_name}")
203+
204+
trust_policy = create_trust_policy(account_id, github_org, github_repo)
205+
206+
try:
207+
# Create the role
208+
response = iam_client.create_role(
209+
RoleName=role_name,
210+
AssumeRolePolicyDocument=json.dumps(trust_policy),
211+
Description=f"NPM publishing role for GitHub Actions in {github_org}/{github_repo}",
212+
MaxSessionDuration=3600 # 1 hour
213+
)
214+
215+
role_arn = response['Role']['Arn']
216+
print(f"✅ Created role: {role_arn}")
217+
218+
# Create and attach the NPM publishing policy
219+
policy_name = f"{role_name}-NPMPublishPolicy"
220+
npm_policy = create_npm_publish_policy()
221+
222+
iam_client.put_role_policy(
223+
RoleName=role_name,
224+
PolicyName=policy_name,
225+
PolicyDocument=json.dumps(npm_policy)
226+
)
227+
228+
print(f"✅ Attached NPM publishing policy: {policy_name}")
229+
return role_arn
230+
231+
except ClientError as e:
232+
if 'EntityAlreadyExists' in str(e):
233+
print(f"✅ Role {role_name} already exists")
234+
response = iam_client.get_role(RoleName=role_name)
235+
role_arn = response['Role']['Arn']
236+
237+
# Update the policy even if role exists
238+
policy_name = f"{role_name}-NPMPublishPolicy"
239+
npm_policy = create_npm_publish_policy()
240+
241+
iam_client.put_role_policy(
242+
RoleName=role_name,
243+
PolicyName=policy_name,
244+
PolicyDocument=json.dumps(npm_policy)
245+
)
246+
247+
print(f"✅ Updated NPM publishing policy: {policy_name}")
248+
return role_arn
249+
else:
250+
print(f"❌ Error creating role: {e}")
251+
sys.exit(1)
252+
253+
254+
def main():
255+
"""Main function to set up GitHub Actions AWS integration for NPM publishing."""
256+
print("🚀 AWS IAM Role Setup for GitHub Actions - NPM Publishing")
257+
print("=" * 50)
258+
259+
# Initialize AWS clients
260+
try:
261+
sts_client = boto3.client('sts')
262+
iam_client = boto3.client('iam')
263+
264+
# Get current AWS account info
265+
identity = sts_client.get_caller_identity()
266+
account_id = identity['Account']
267+
current_user = identity['Arn']
268+
269+
print(f"📋 AWS Account: {account_id}")
270+
print(f"📋 Current User: {current_user}")
271+
272+
except Exception as e:
273+
print(f"❌ AWS authentication failed: {e}")
274+
print("Make sure your AWS credentials are configured (aws configure)")
275+
sys.exit(1)
276+
277+
# Get GitHub repository information
278+
github_org, github_repo = get_github_repo_info()
279+
280+
# Set up role name
281+
role_name = f"github-actions-{github_org}-{github_repo}-npm-access"
282+
283+
print(f"\n📋 Setup Summary:")
284+
print(f" AWS Account: {account_id}")
285+
print(f" GitHub Repo: {github_org}/{github_repo}")
286+
print(f" Role Name: {role_name}")
287+
288+
# Auto-proceed (non-interactive)
289+
print("\n🚀 Proceeding with automatic setup...")
290+
291+
# Create OIDC provider
292+
oidc_arn = create_oidc_provider(iam_client)
293+
294+
# Create NPM publishing role
295+
role_arn = create_npm_role(iam_client, role_name, github_org, github_repo, account_id)
296+
297+
print("\n" + "=" * 50)
298+
print("✅ Setup Complete!")
299+
print("=" * 50)
300+
301+
print(f"\n📋 GitHub Repository Variable to add:")
302+
print(f" Repository: https://github.com/{github_org}/{github_repo}/settings/variables/actions")
303+
print(f" ")
304+
print(f" Variable Name: AWS_DEPLOYMENT_ROLE")
305+
print(f" Variable Value: {role_arn}")
306+
print(f" ")
307+
print(f" Note: This should be a Repository Variable, not a Secret")
308+
309+
print(f"\n🔗 Next Steps:")
310+
print(f" 1. Add the repository variable above to your GitHub repository")
311+
print(f" 2. Store your NPM token in the workflow when running it")
312+
print(f" 3. The workflow will save the token to SSM for future use")
313+
314+
print(f"\n🔐 Security Notes:")
315+
print(f" - Role can only be assumed by your specific GitHub repository")
316+
print(f" - Role has limited permissions for SSM Parameter Store only")
317+
print(f" - Can only access parameters under /npm/tokens/*, /github/tokens/*, and /ci/tokens/*")
318+
print(f" - Sessions are limited to 1 hour")
319+
320+
print(f"\n📦 NPM Token Storage:")
321+
print(f" - Tokens will be stored in: /npm/tokens/{github_org}-{github_repo}")
322+
print(f" - Tokens are encrypted using AWS KMS")
323+
print(f" - Only this GitHub repository can access these tokens")
324+
325+
326+
if __name__ == '__main__':
327+
main()

0 commit comments

Comments
 (0)