forked from 99designs/aws-vault
-
Notifications
You must be signed in to change notification settings - Fork 0
/
rotator.go
241 lines (195 loc) · 6.21 KB
/
rotator.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
package vault
import (
"fmt"
"log"
"regexp"
"strings"
"time"
"github.com/99designs/aws-vault/prompt"
"github.com/99designs/keyring"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/iam"
)
type Rotator struct {
Keyring keyring.Keyring
MfaToken string
MfaPrompt prompt.PromptFunc
Config *Config
}
// Rotate creates a new key and deletes the old one
func (r *Rotator) Rotate(profile string) error {
var err error
source, _ := r.Config.SourceProfile(profile)
// --------------------------------
// Get the existing credentials
provider := &KeyringProvider{
Keyring: r.Keyring,
Profile: source.Name,
Region: source.Region,
}
oldMasterCreds, err := provider.Retrieve()
if err != nil {
return err
}
oldSess := session.New(&aws.Config{Region: aws.String(provider.Region),
Credentials: credentials.NewCredentials(&credentials.StaticProvider{Value: oldMasterCreds}),
})
currentUserName, err := GetUsernameFromSession(oldSess)
if err != nil {
return err
}
log.Printf("Found old access key ****************%s for user %s",
oldMasterCreds.AccessKeyID[len(oldMasterCreds.AccessKeyID)-4:],
currentUserName)
oldSessionCreds, err := NewVaultCredentials(r.Keyring, profile, VaultOptions{
MfaToken: r.MfaToken,
MfaPrompt: r.MfaPrompt,
Config: r.Config,
NoSession: !r.needsSessionToRotate(profile),
MasterCreds: &oldMasterCreds,
})
if err != nil {
return err
}
oldSessionVal, err := oldSessionCreds.Get()
if err != nil {
return err
}
// --------------------------------
// Create new access key
log.Println("Using old credentials to create a new access key")
var iamUserName *string
// A username is needed for some IAM calls if the credentials have assumed a role
if oldSessionVal.SessionToken != "" || currentUserName != "root" {
iamUserName = aws.String(currentUserName)
}
oldSessionClient := iam.New(session.New(&aws.Config{Region: aws.String(provider.Region),
Credentials: credentials.NewCredentials(&credentials.StaticProvider{Value: oldSessionVal}),
}))
createOut, err := oldSessionClient.CreateAccessKey(&iam.CreateAccessKeyInput{
UserName: iamUserName,
})
if err != nil {
return err
}
log.Println("Created new access key")
newMasterCreds := credentials.Value{
AccessKeyID: *createOut.AccessKey.AccessKeyId,
SecretAccessKey: *createOut.AccessKey.SecretAccessKey,
}
if err := provider.Store(newMasterCreds); err != nil {
return fmt.Errorf("Error storing new access key %v: %v",
newMasterCreds.AccessKeyID, err)
}
// --------------------------------
// Use new credentials to delete old access key
log.Println("Using new credentials to delete the old new access key")
newSessionCreds, err := NewVaultCredentials(r.Keyring, profile, VaultOptions{
MfaToken: r.MfaToken,
MfaPrompt: r.MfaPrompt,
Config: r.Config,
NoSession: !r.needsSessionToRotate(profile),
MasterCreds: &newMasterCreds,
})
if err != nil {
return err
}
log.Printf("Waiting for new IAM credentials to propagate (takes up to 10 seconds)")
err = retry(time.Second*20, time.Second*5, func() error {
newVal, err := newSessionCreds.Get()
if err != nil {
return err
}
newClient := iam.New(session.New(&aws.Config{Region: aws.String(provider.Region),
Credentials: credentials.NewCredentials(&credentials.StaticProvider{Value: newVal}),
}))
_, err = newClient.DeleteAccessKey(&iam.DeleteAccessKeyInput{
AccessKeyId: aws.String(oldMasterCreds.AccessKeyID),
UserName: iamUserName,
})
return err
})
if err != nil {
return fmt.Errorf("Can't delete old access key %v: %v", oldMasterCreds.AccessKeyID, err)
}
// --------------------------------
// Delete old sessions
sessions, err := NewKeyringSessions(r.Keyring, r.Config)
if err != nil {
return err
}
if n, _ := sessions.Delete(profile); n > 0 {
log.Printf("Deleted %d existing sessions.", n)
}
log.Printf("Rotated credentials for profile %q in vault", profile)
return nil
}
var (
getUserErrorRegexp = regexp.MustCompile(`^AccessDenied: User: arn:aws:iam::(\d+):user/(.+) is not`)
)
// GetUsernameFromSession returns the IAM username (or root) associated with the current aws session
func GetUsernameFromSession(sess *session.Session) (string, error) {
client := iam.New(sess)
resp, err := client.GetUser(&iam.GetUserInput{})
if err != nil {
// Even if GetUser fails, the current user is included in the error. This happens when you have o IAM permissions
// on the master credentials, but have permission to use assumeRole later
matches := getUserErrorRegexp.FindStringSubmatch(err.Error())
if len(matches) > 0 {
pathParts := strings.Split(matches[2], "/")
return pathParts[len(pathParts)-1], nil
}
return "", err
}
if resp.User.UserName != nil {
return *resp.User.UserName, nil
}
if resp.User.Arn != nil {
arnParts := strings.Split(*resp.User.Arn, ":")
return arnParts[len(arnParts)-1], nil
}
return "", fmt.Errorf("Couldn't determine current username")
}
func retry(duration time.Duration, sleep time.Duration, callback func() error) (err error) {
t0 := time.Now()
i := 0
for {
i++
err = callback()
if err == nil {
return
}
delta := time.Now().Sub(t0)
if delta > duration {
return fmt.Errorf("After %d attempts (during %s), last error: %s", i, delta, err)
}
time.Sleep(sleep)
log.Println("Retrying after error:", err)
}
}
// needsSessionToRotate attempts to resolve the dilemma around whether or not
// profiles should use a session to be able to rotate.
//
// Some profiles require assuming a role to get permission to create new
// credentials. Alas, others which don't use a role are pure IAM and will
// fail to create credentials when using an STS role, because AWS's IAM
// systems hard-fail early when given STS credentials.
//
// This is a heuristic which might need to continue to evolve. :(
func (r *Rotator) needsSessionToRotate(profileName string) bool {
if r.MfaToken != "" {
return true
}
sourceProfile, known := r.Config.SourceProfile(profileName)
if !known {
// best guess
return false
}
if sourceProfile.Name != profileName {
// TODO: should this comparison be case-insensitive?
return true
}
return false
}