-
Notifications
You must be signed in to change notification settings - Fork 12
/
Copy pathfetch_aws_keys.go
293 lines (225 loc) · 8.88 KB
/
fetch_aws_keys.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
// Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements;
// and to You under the Apache License, Version 2.0. See LICENSE in project root for full license + copyright.
package keynuker
import (
"fmt"
"log"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/aws/aws-sdk-go/service/iam/iamiface"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/pkg/errors"
uuid "github.com/satori/go.uuid"
"github.com/tleyden/keynuker/keynuker-go-common"
)
// Look up all the AWS keys associated with the AWS account corresponding to AwsAccessKeyId
// and return a document suitable for sticking into a Cloudant database.
//
// This is meant to be run in the context of an OpenWhisk Action
// (see https://tleyden.github.io/blog/2017/07/02/openwhisk-action-sequences/)
// and so nothing else except the JSON content can be written to standard output.
func FetchAwsKeys(params ParamsFetchAwsKeys) (docWrapper DocumentWrapperFetchAwsKeys, err error) {
docId := keynuker_go_common.GenerateDocId(
keynuker_go_common.DocIdPrefixAwsKeys,
params.KeyNukerOrg,
)
// Create output document + wrapepr
doc := DocumentFetchAwsKeys{
Id: docId,
AccessKeyMetadata: []FetchedAwsAccessKey{},
}
for _, targetAwsAccount := range params.TargetAwsAccounts {
fetchedAwsKeys, err := FetchAwsKeysTargetAccount(
params.InitiatingAwsAccountAssumeRole,
targetAwsAccount,
)
if err != nil {
log.Printf("Error fetching aws keys for target aws account. Err: %v", err)
continue
}
doc.AccessKeyMetadata = append(doc.AccessKeyMetadata, fetchedAwsKeys...)
}
docWrapper = DocumentWrapperFetchAwsKeys{
Doc: doc,
DocId: docId,
}
return docWrapper, nil
}
func FetchAwsKeysTargetAccount(initiatingAwsAccount AwsCredentials, targetAwsAccount TargetAwsAccount) (fetchedAwsKeys []FetchedAwsAccessKey, err error) {
fetchedAwsKeys = []FetchedAwsAccessKey{}
var sess *session.Session
switch targetAwsAccount.IsDirect() {
case true:
sess, err = session.NewSession(&aws.Config{
Credentials: credentials.NewCredentials(
&credentials.StaticProvider{Value: credentials.Value{
AccessKeyID: targetAwsAccount.AwsAccessKeyId,
SecretAccessKey: targetAwsAccount.AwsSecretAccessKey,
}},
),
})
case false:
log.Printf("Connecting via STS AssumeRole to target account: %v", targetAwsAccount.TargetAwsAccountId)
AWSCreds := credentials.NewStaticCredentials(
initiatingAwsAccount.AwsAccessKeyId,
initiatingAwsAccount.AwsSecretAccessKey,
"",
)
AWSConfig := &aws.Config{
Credentials: AWSCreds,
}
tempSession := session.New(AWSConfig)
uuid, _ := uuid.NewV4()
assumedConfig := &aws.Config{
Credentials: credentials.NewCredentials(&stscreds.AssumeRoleProvider{
// Client: sts.New(tempSession, &aws.Config{Region: aws.String(region)}),
Client: sts.New(tempSession, &aws.Config{}),
RoleARN: fmt.Sprintf(
"arn:aws:iam::%v:role/%v",
targetAwsAccount.TargetAwsAccountId,
targetAwsAccount.TargetRoleName,
),
RoleSessionName: uuid.String(),
ExternalID: aws.String(targetAwsAccount.AssumeRoleExternalId),
ExpiryWindow: 3600 * time.Second,
}),
}
sess = session.New(assumedConfig)
}
if err != nil {
return fetchedAwsKeys, fmt.Errorf("Error creating aws session: %v", err)
}
// Create IAM client with the session.
svc := iam.New(sess)
// Fetch list of IAM users
iamUsers, err := FetchIAMUsers(svc)
if err != nil {
// TODO: safely emit the targetAwsAccount.AwsAccessKeyId to the logs by truncating it or hashing it
return fetchedAwsKeys, fmt.Errorf("Error fetching list of IAM users. Error: %v", err)
}
for _, user := range iamUsers {
listAccessKeysInput := &iam.ListAccessKeysInput{
UserName: user.UserName,
MaxItems: aws.Int64(1000),
}
listAccessKeysOutput, err := svc.ListAccessKeys(listAccessKeysInput)
if err != nil {
return fetchedAwsKeys, fmt.Errorf("Error listing access keys for user: %v. Err: %v", user, err)
}
// Panic if more than 1K results, which is not handled
if *listAccessKeysOutput.IsTruncated {
// TODO: Put this in a paginated loop. Add unit tests against mocks
log.Printf("Error: Output is truncated and this code does not handle it")
}
for _, accessKeyMetadata := range listAccessKeysOutput.AccessKeyMetadata {
fetchedAwsAccessKey := NewFetchedAwsAccessKey(
accessKeyMetadata,
targetAwsAccount.AwsAccessKeyId,
)
fetchedAwsKeys = append(fetchedAwsKeys, *fetchedAwsAccessKey)
}
}
return fetchedAwsKeys, nil
}
func FetchIAMUsers(svc iamiface.IAMAPI) (users []*iam.User, err error) {
fetchedUsers := []*iam.User{}
var pageMarker *string
for {
// Discover list of IAM users in account
listUsersInput := &iam.ListUsersInput{
MaxItems: aws.Int64(1000),
Marker: pageMarker,
}
listUsersOutput, err := svc.ListUsers(listUsersInput)
if err != nil {
return []*iam.User{}, fmt.Errorf("Error listing users: %v", err)
}
if listUsersOutput == nil {
return []*iam.User{}, errors.Errorf("ListUsers returned nil output")
}
fetchedUsers = append(fetchedUsers, listUsersOutput.Users...)
// If the output isn't truncated, there are no more results, so break out of the for loop. Otherwise
// keep pulling users.
if *listUsersOutput.IsTruncated == false {
break
}
// Increment page marker so that next iteration will fetch next page of results
pageMarker = listUsersOutput.Marker
}
return fetchedUsers, nil
}
type AwsCredentials struct {
// The aws access key to connect as. This only needs permissions to list IAM users and access keys,
// and delete access keys (in the case they are nuked)
AwsAccessKeyId string
// The secret access key corresponding to AwsAccessKeyId
AwsSecretAccessKey string
}
type TargetAwsAccountAssumeRole struct {
// The target AWS account id. Eg, 012345
TargetAwsAccountId string
// The role on the target account, used to build the role-arn:
// arn:aws:iam::012345:role/KeyNuker
TargetRoleName string
// The ExternalID which provides a layer of security to avoid the "Confused Deputy" attack
// http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
AssumeRoleExternalId string
}
// The TargetAwsAccount can be connected to via two ways:
// - AwsCredentials: Either using direct credentials of a user with the required permissions
// - TargetAwsAccountAssumeRole: Using STS AssumeRole
type TargetAwsAccount struct {
AwsCredentials
TargetAwsAccountAssumeRole
}
func (t TargetAwsAccount) IsDirect() bool {
return t.TargetAwsAccountAssumeRole.TargetAwsAccountId == ""
}
type ParamsFetchAwsKeys struct {
// When using Cross-Account STS AssumeRole, this needs the credentials of the of the "connecting" aka "initiating"
// account that is being used to connect to the target account being monitored
InitiatingAwsAccountAssumeRole AwsCredentials
// The list of AWS accounts to fetch all the access keys for
TargetAwsAccounts []TargetAwsAccount
// This is the name of the KeyNuker "org/tenant". Defaults to "default", but allows to be extended multi-tenant.
KeyNukerOrg string
}
// This encapsulates all of the fields from iam.AccessKeyMetadata, as well as addding the FetcherAwsAccessKeyId
// that was used to fetch (and should be used to nuke key if needed)
type FetchedAwsAccessKey struct {
// The ID for this access key.
AccessKeyId *string `min:"16" type:"string"`
// The date when the access key was created.
CreateDate *time.Time `type:"timestamp" timestampFormat:"iso8601"`
// The status of the access key. Active means the key is valid for API calls;
// Inactive means it is not.
Status *string `type:"string" enum:"statusType"`
// The name of the IAM user that the key is associated with.
UserName *string `min:"1" type:"string"`
// The AWS access key used to monitor this AWS account's keys. Need to track since this same key will need to be used to nuke as well.
// TODO: this should be the sha1 hash of the key, not the access key itself. That would keep the access key out of the response json
MonitorAwsAccessKeyId string
}
// Create a new FetchedAwsAccessKey
func NewFetchedAwsAccessKey(accessKeyMetadata *iam.AccessKeyMetadata, monitorAwsAccessKeyId string) *FetchedAwsAccessKey {
return &FetchedAwsAccessKey{
AccessKeyId: accessKeyMetadata.AccessKeyId,
CreateDate: accessKeyMetadata.CreateDate,
Status: accessKeyMetadata.Status,
UserName: accessKeyMetadata.UserName,
MonitorAwsAccessKeyId: monitorAwsAccessKeyId,
}
}
type DocumentFetchAwsKeys struct {
Id string `json:"_id"`
AccessKeyMetadata []FetchedAwsAccessKey
}
type DocumentWrapperFetchAwsKeys struct {
// Serialize into a form that the cloudant db adapter expects
Doc DocumentFetchAwsKeys `json:"doc"`
DocId string `json:"docid"`
}