Skip to content

Commit 5ff7f28

Browse files
authored
Merge pull request #18 from gpuaudit/feature/multi-account-scanning
Add multi-account AWS scanning
2 parents ca8c936 + 91383c6 commit 5ff7f28

15 files changed

Lines changed: 2981 additions & 135 deletions

File tree

README.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,106 @@ gpuaudit diff scan-apr-08.json scan-apr-15.json
118118

119119
Matches instances by ID. Reports added, removed, and changed instances with per-field diffs (instance type, pricing model, cost, state, GPU allocation, waste severity).
120120

121+
## Multi-Account Scanning
122+
123+
Scan multiple AWS accounts in a single invocation using STS AssumeRole.
124+
125+
### Prerequisites
126+
127+
Deploy a read-only IAM role (`gpuaudit-reader`) to each target account. See [Cross-Account Role Setup](#cross-account-role-setup) below.
128+
129+
### Usage
130+
131+
```bash
132+
# Scan specific accounts
133+
gpuaudit scan --targets 111111111111,222222222222 --role gpuaudit-reader
134+
135+
# Scan entire AWS Organization
136+
gpuaudit scan --org --role gpuaudit-reader
137+
138+
# Exclude management account
139+
gpuaudit scan --org --role gpuaudit-reader --skip-self
140+
141+
# With external ID
142+
gpuaudit scan --targets 111111111111 --role gpuaudit-reader --external-id my-secret
143+
```
144+
145+
### Cross-Account Role Setup
146+
147+
#### Terraform
148+
149+
```hcl
150+
variable "management_account_id" {
151+
description = "AWS account ID where gpuaudit runs"
152+
type = string
153+
}
154+
155+
resource "aws_iam_role" "gpuaudit_reader" {
156+
name = "gpuaudit-reader"
157+
assume_role_policy = jsonencode({
158+
Version = "2012-10-17"
159+
Statement = [{
160+
Effect = "Allow"
161+
Principal = { AWS = "arn:aws:iam::${var.management_account_id}:root" }
162+
Action = "sts:AssumeRole"
163+
}]
164+
})
165+
}
166+
167+
resource "aws_iam_role_policy" "gpuaudit_reader" {
168+
name = "gpuaudit-policy"
169+
role = aws_iam_role.gpuaudit_reader.id
170+
policy = file("gpuaudit-policy.json") # from: gpuaudit iam-policy > gpuaudit-policy.json
171+
}
172+
```
173+
174+
Deploy to all accounts using Terraform workspaces or CloudFormation StackSets.
175+
176+
#### CloudFormation StackSet
177+
178+
```yaml
179+
AWSTemplateFormatVersion: "2010-09-09"
180+
Parameters:
181+
ManagementAccountId:
182+
Type: String
183+
Resources:
184+
GpuAuditRole:
185+
Type: AWS::IAM::Role
186+
Properties:
187+
RoleName: gpuaudit-reader
188+
AssumeRolePolicyDocument:
189+
Version: "2012-10-17"
190+
Statement:
191+
- Effect: Allow
192+
Principal:
193+
AWS: !Sub "arn:aws:iam::${ManagementAccountId}:root"
194+
Action: sts:AssumeRole
195+
Policies:
196+
- PolicyName: gpuaudit-policy
197+
PolicyDocument:
198+
Version: "2012-10-17"
199+
Statement:
200+
- Effect: Allow
201+
Action:
202+
- ec2:DescribeInstances
203+
- ec2:DescribeInstanceTypes
204+
- ec2:DescribeRegions
205+
- sagemaker:ListEndpoints
206+
- sagemaker:DescribeEndpoint
207+
- sagemaker:DescribeEndpointConfig
208+
- eks:ListClusters
209+
- eks:ListNodegroups
210+
- eks:DescribeNodegroup
211+
- cloudwatch:GetMetricData
212+
- cloudwatch:GetMetricStatistics
213+
- cloudwatch:ListMetrics
214+
- ce:GetCostAndUsage
215+
- ce:GetReservationUtilization
216+
- ce:GetSavingsPlansUtilization
217+
- pricing:GetProducts
218+
Resource: "*"
219+
```
220+
121221
## IAM permissions
122222
123223
gpuaudit is read-only. It never modifies your infrastructure. Generate the minimal IAM policy:

cmd/gpuaudit/main.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ var (
5252
scanKubeContext string
5353
scanExcludeTags []string
5454
scanMinUptimeDays int
55+
scanTargets []string
56+
scanRole string
57+
scanExternalID string
58+
scanOrg bool
59+
scanSkipSelf bool
5560
)
5661

5762
// --- diff command ---
@@ -85,6 +90,12 @@ func init() {
8590
scanCmd.Flags().StringVar(&scanKubeContext, "kube-context", "", "Kubernetes context to use (default: current context)")
8691
scanCmd.Flags().StringSliceVar(&scanExcludeTags, "exclude-tag", nil, "Exclude instances matching tag (key=value, repeatable)")
8792
scanCmd.Flags().IntVar(&scanMinUptimeDays, "min-uptime-days", 0, "Only flag instances running for at least this many days")
93+
scanCmd.Flags().StringSliceVar(&scanTargets, "targets", nil, "Account IDs to scan (comma-separated)")
94+
scanCmd.Flags().StringVar(&scanRole, "role", "", "IAM role name to assume in each target")
95+
scanCmd.Flags().StringVar(&scanExternalID, "external-id", "", "STS external ID for cross-account role assumption")
96+
scanCmd.Flags().BoolVar(&scanOrg, "org", false, "Auto-discover all accounts from AWS Organizations")
97+
scanCmd.Flags().BoolVar(&scanSkipSelf, "skip-self", false, "Exclude the caller's own account from the scan")
98+
scanCmd.MarkFlagsMutuallyExclusive("targets", "org")
8899

89100
diffCmd.Flags().StringVar(&diffFormat, "format", "table", "Output format: table, json")
90101

@@ -98,6 +109,10 @@ func init() {
98109
func runScan(cmd *cobra.Command, args []string) error {
99110
ctx := context.Background()
100111

112+
if (len(scanTargets) > 0 || scanOrg) && scanRole == "" {
113+
return fmt.Errorf("--role is required when using --targets or --org")
114+
}
115+
101116
opts := awsprovider.DefaultScanOptions()
102117
opts.Profile = scanProfile
103118
opts.Regions = scanRegions
@@ -107,6 +122,11 @@ func runScan(cmd *cobra.Command, args []string) error {
107122
opts.SkipCosts = scanSkipCosts
108123
opts.ExcludeTags = parseExcludeTags(scanExcludeTags)
109124
opts.MinUptimeDays = scanMinUptimeDays
125+
opts.Targets = scanTargets
126+
opts.Role = scanRole
127+
opts.ExternalID = scanExternalID
128+
opts.OrgScan = scanOrg
129+
opts.SkipSelf = scanSkipSelf
110130

111131
result, err := awsprovider.Scan(ctx, opts)
112132
if err != nil {
@@ -322,8 +342,21 @@ var iamPolicyCmd = &cobra.Command{
322342
},
323343
"Resource": "*",
324344
},
345+
{
346+
"Sid": "GPUAuditCrossAccount",
347+
"Effect": "Allow",
348+
"Action": "sts:AssumeRole",
349+
"Resource": "arn:aws:iam::*:role/gpuaudit-reader",
350+
},
351+
{
352+
"Sid": "GPUAuditOrganizations",
353+
"Effect": "Allow",
354+
"Action": "organizations:ListAccounts",
355+
"Resource": "*",
356+
},
325357
},
326358
}
359+
fmt.Fprintln(os.Stdout, "// The last two statements (CrossAccount, Organizations) are only needed for --targets or --org scanning.")
327360
enc := json.NewEncoder(os.Stdout)
328361
enc.SetIndent("", " ")
329362
enc.Encode(policy)

0 commit comments

Comments
 (0)