/
generate_credentials.go
304 lines (259 loc) · 10.3 KB
/
generate_credentials.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
294
295
296
297
298
299
300
301
302
303
304
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package awsutilv2
import (
"context"
"fmt"
"net/http"
"os"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
"github.com/aws/aws-sdk-go-v2/service/sts/types"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-hclog"
)
const iamServerIdHeader = "X-Vault-AWS-IAM-Server-ID"
type CredentialsConfig struct {
// The access key if static credentials are being used
AccessKey string
// The secret key if static credentials are being used
SecretKey string
// The session token if it is being used
SessionToken string
// The IAM endpoint to use; if not set will use the default
IAMEndpoint string
// The STS endpoint to use; if not set will use the default
STSEndpoint string
// If specified, the region will be provided to the config of the
// EC2RoleProvider's client. This may be useful if you want to e.g. reuse
// the client elsewhere. If not specified, the region will be determined
// by the environment variables "AWS_REGION" or "AWS_DEFAULT_REGION".
// Otherwise the default value is us-east-1.
Region string
// The filename for the shared credentials provider, if being used
Filename string
// The profile for the shared credentials provider, if being used
Profile string
// The role arn to use when creating either a web identity role provider
// or a ec2-instance role provider.
RoleARN string
// The role session name to use when creating either a web identity role provider
// or a ec2-instance role provider.
RoleSessionName string
// The role external ID to use when creating a ec2-instance role provider.
RoleExternalId string
// The role tags to use when creating a ec2-instance role provider.
RoleTags map[string]string
// The web identity token file to use if using the web identity token provider
WebIdentityTokenFile string
// The web identity token (contents, not the file path) to use with the web
// identity token provider
WebIdentityToken string
// The http.Client to use, or nil for the client to use its default
HTTPClient *http.Client
// The max retries to set on the client. This is a pointer because the zero
// value has meaning. A nil pointer will use the default value.
MaxRetries *int
// The logger to use for credential acquisition debugging
Logger hclog.Logger
}
// GenerateCredentialChain uses the config to generate a credential chain
// suitable for creating AWS sessions and clients.
//
// Supported options: WithAccessKey, WithSecretKey, WithLogger, WithStsEndpoint,
// WithIamEndpoint, WithMaxRetries, WithRegion, WithHttpClient, WithRoleArn,
// WithRoleSessionName, WithRoleExternalId, WithRoleTags, WithWebIdentityTokenFile,
// WithWebIdentityToken.
func NewCredentialsConfig(opt ...Option) (*CredentialsConfig, error) {
opts, err := getOpts(opt...)
if err != nil {
return nil, fmt.Errorf("error reading options in NewCredentialsConfig: %w", err)
}
c := &CredentialsConfig{
AccessKey: opts.withAccessKey,
SecretKey: opts.withSecretKey,
Logger: opts.withLogger,
STSEndpoint: opts.withStsEndpoint,
IAMEndpoint: opts.withIamEndpoint,
MaxRetries: opts.withMaxRetries,
RoleExternalId: opts.withRoleExternalId,
RoleTags: opts.withRoleTags,
}
c.Region = opts.withRegion
if c.Region == "" {
c.Region = os.Getenv("AWS_REGION")
if c.Region == "" {
c.Region = os.Getenv("AWS_DEFAULT_REGION")
if c.Region == "" {
c.Region = "us-east-1"
}
}
}
c.RoleARN = opts.withRoleArn
if c.RoleARN == "" {
c.RoleARN = os.Getenv("AWS_ROLE_ARN")
}
c.RoleSessionName = opts.withRoleSessionName
if c.RoleSessionName == "" {
c.RoleSessionName = os.Getenv("AWS_ROLE_SESSION_NAME")
}
c.WebIdentityTokenFile = opts.withWebIdentityTokenFile
if c.WebIdentityTokenFile == "" {
c.WebIdentityTokenFile = os.Getenv("AWS_WEB_IDENTITY_TOKEN_FILE")
}
c.WebIdentityToken = opts.withWebIdentityToken
if c.RoleARN == "" {
if c.RoleSessionName != "" {
return nil, fmt.Errorf("role session name specified without role ARN")
}
if c.RoleExternalId != "" {
return nil, fmt.Errorf("role external ID specified without role ARN")
}
if len(c.RoleTags) > 0 {
return nil, fmt.Errorf("role tags specified without role ARN")
}
if c.WebIdentityTokenFile != "" {
return nil, fmt.Errorf("web identity token file specified without role ARN")
}
if len(c.WebIdentityToken) > 0 {
return nil, fmt.Errorf("web identity token specified without role ARN")
}
}
c.HTTPClient = opts.withHttpClient
if c.HTTPClient == nil {
c.HTTPClient = cleanhttp.DefaultClient()
}
return c, nil
}
// Make sure the logger isn't nil before logging
func (c *CredentialsConfig) log(level hclog.Level, msg string, args ...interface{}) {
if c.Logger != nil {
c.Logger.Log(level, msg, args...)
}
}
func (c *CredentialsConfig) generateAwsConfigOptions(opts options) []func(*config.LoadOptions) error {
var cfgOpts []func(*config.LoadOptions) error
if c.Region != "" {
cfgOpts = append(cfgOpts, config.WithRegion(c.Region))
}
if c.MaxRetries != nil {
cfgOpts = append(cfgOpts, config.WithRetryMaxAttempts(*c.MaxRetries))
}
if c.HTTPClient != nil {
cfgOpts = append(cfgOpts, config.WithHTTPClient(c.HTTPClient))
}
// Add the shared credentials
if opts.withSharedCredentials {
profile := os.Getenv("AWS_PROFILE")
if profile != "" {
c.Profile = profile
}
if c.Profile == "" {
c.Profile = "default"
}
cfgOpts = append(cfgOpts, config.WithSharedConfigProfile(c.Profile))
cfgOpts = append(cfgOpts, config.WithSharedCredentialsFiles([]string{c.Filename}))
c.log(hclog.Debug, "added shared profile credential provider")
}
// Add the static credential
if c.AccessKey != "" && c.SecretKey != "" {
staticCred := credentials.NewStaticCredentialsProvider(c.AccessKey, c.SecretKey, c.SessionToken)
cfgOpts = append(cfgOpts, config.WithCredentialsProvider(staticCred))
c.log(hclog.Debug, "added static credential provider", "AccessKey", c.AccessKey)
}
// Add the assume role provider
if c.RoleARN != "" {
if c.WebIdentityTokenFile != "" {
// this session is only created to create the WebIdentityRoleProvider, variables used to
// assume a role are pulled from values provided in options. If the option values are
// not set, then the provider will default to using the environment variables.
webIdentityRoleCred := config.WithWebIdentityRoleCredentialOptions(func(options *stscreds.WebIdentityRoleOptions) {
options.RoleARN = c.RoleARN
options.RoleSessionName = c.RoleSessionName
options.TokenRetriever = stscreds.IdentityTokenFile(c.WebIdentityTokenFile)
})
cfgOpts = append(cfgOpts, webIdentityRoleCred)
c.log(hclog.Debug, "added web identity provider", "roleARN", c.RoleARN)
} else if c.WebIdentityToken != "" {
webIdentityRoleCred := config.WithWebIdentityRoleCredentialOptions(func(options *stscreds.WebIdentityRoleOptions) {
options.RoleARN = c.RoleARN
options.RoleSessionName = c.RoleSessionName
options.TokenRetriever = FetchTokenContents(c.WebIdentityToken)
})
cfgOpts = append(cfgOpts, webIdentityRoleCred)
c.log(hclog.Debug, "added web identity provider with token", "roleARN", c.RoleARN)
} else {
// this session is only created to create the AssumeRoleProvider, variables used to
// assume a role are pulled from values provided in options. If the option values are
// not set, then the provider will default to using the environment variables.
assumeRoleCred := config.WithAssumeRoleCredentialOptions(func(options *stscreds.AssumeRoleOptions) {
options.RoleARN = c.RoleARN
options.RoleSessionName = c.RoleSessionName
options.ExternalID = aws.String(c.RoleExternalId)
for k, v := range c.RoleTags {
options.Tags = append(options.Tags, types.Tag{
Key: aws.String(k),
Value: aws.String(v),
})
}
})
cfgOpts = append(cfgOpts, assumeRoleCred)
c.log(hclog.Debug, "added ec2-instance role provider", "roleARN", c.RoleARN)
}
}
return cfgOpts
}
// GenerateCredentialChain uses the config to generate a credential chain
// suitable for creating AWS clients. This will by default load configuration
// values from environment variables and append additional configuration options
// provided to the CredentialsConfig.
//
// Supported options: WithSharedCredentials, WithCredentialsProvider
func (c *CredentialsConfig) GenerateCredentialChain(ctx context.Context, opt ...Option) (*aws.Config, error) {
opts, err := getOpts(opt...)
if err != nil {
return nil, fmt.Errorf("error reading options in GenerateCredentialChain: %w", err)
}
// Have one or the other but not both and not neither
if (c.AccessKey != "" && c.SecretKey == "") || (c.AccessKey == "" && c.SecretKey != "") {
return nil, fmt.Errorf("static AWS client credentials haven't been properly configured (the access key or secret key were provided but not both)")
}
awsConfig, err := config.LoadDefaultConfig(ctx, c.generateAwsConfigOptions(opts)...)
if err != nil {
return nil, fmt.Errorf("failed to load SDK's default configurations with given credential options")
}
if opts.withCredentialsProvider != nil {
awsConfig.Credentials = opts.withCredentialsProvider
}
return &awsConfig, nil
}
func RetrieveCreds(ctx context.Context, accessKey, secretKey, sessionToken string, logger hclog.Logger, opt ...Option) (*aws.Config, error) {
credConfig := CredentialsConfig{
AccessKey: accessKey,
SecretKey: secretKey,
SessionToken: sessionToken,
Logger: logger,
}
creds, err := credConfig.GenerateCredentialChain(ctx, opt...)
if err != nil {
return nil, err
}
if creds == nil {
return nil, fmt.Errorf("could not compile valid credential providers from static config, environment, shared, or instance metadata")
}
_, err = creds.Credentials.Retrieve(ctx)
if err != nil {
return nil, fmt.Errorf("failed to retrieve credentials from credential chain: %w", err)
}
return creds, nil
}
// FetchTokenContents allows the use of the content of a token in the
// WebIdentityProvider, instead of the path to a token. Useful with a
// serviceaccount token requested directly from the EKS/K8s API, for example.
type FetchTokenContents []byte
var _ stscreds.IdentityTokenRetriever = (*FetchTokenContents)(nil)
func (f FetchTokenContents) GetIdentityToken() ([]byte, error) {
return f, nil
}