resolve_credentials.go

  1package config
  2
  3import (
  4	"context"
  5	"fmt"
  6	"io/ioutil"
  7	"net"
  8	"net/url"
  9	"os"
 10	"time"
 11
 12	"github.com/aws/aws-sdk-go-v2/aws"
 13	"github.com/aws/aws-sdk-go-v2/credentials"
 14	"github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds"
 15	"github.com/aws/aws-sdk-go-v2/credentials/endpointcreds"
 16	"github.com/aws/aws-sdk-go-v2/credentials/processcreds"
 17	"github.com/aws/aws-sdk-go-v2/credentials/ssocreds"
 18	"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
 19	"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
 20	"github.com/aws/aws-sdk-go-v2/service/sso"
 21	"github.com/aws/aws-sdk-go-v2/service/ssooidc"
 22	"github.com/aws/aws-sdk-go-v2/service/sts"
 23)
 24
 25const (
 26	// valid credential source values
 27	credSourceEc2Metadata      = "Ec2InstanceMetadata"
 28	credSourceEnvironment      = "Environment"
 29	credSourceECSContainer     = "EcsContainer"
 30	httpProviderAuthFileEnvVar = "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE"
 31)
 32
 33// direct representation of the IPv4 address for the ECS container
 34// "169.254.170.2"
 35var ecsContainerIPv4 net.IP = []byte{
 36	169, 254, 170, 2,
 37}
 38
 39// direct representation of the IPv4 address for the EKS container
 40// "169.254.170.23"
 41var eksContainerIPv4 net.IP = []byte{
 42	169, 254, 170, 23,
 43}
 44
 45// direct representation of the IPv6 address for the EKS container
 46// "fd00:ec2::23"
 47var eksContainerIPv6 net.IP = []byte{
 48	0xFD, 0, 0xE, 0xC2,
 49	0, 0, 0, 0,
 50	0, 0, 0, 0,
 51	0, 0, 0, 0x23,
 52}
 53
 54var (
 55	ecsContainerEndpoint = "http://169.254.170.2" // not constant to allow for swapping during unit-testing
 56)
 57
 58// resolveCredentials extracts a credential provider from slice of config
 59// sources.
 60//
 61// If an explicit credential provider is not found the resolver will fallback
 62// to resolving credentials by extracting a credential provider from EnvConfig
 63// and SharedConfig.
 64func resolveCredentials(ctx context.Context, cfg *aws.Config, configs configs) error {
 65	found, err := resolveCredentialProvider(ctx, cfg, configs)
 66	if found || err != nil {
 67		return err
 68	}
 69
 70	return resolveCredentialChain(ctx, cfg, configs)
 71}
 72
 73// resolveCredentialProvider extracts the first instance of Credentials from the
 74// config slices.
 75//
 76// The resolved CredentialProvider will be wrapped in a cache to ensure the
 77// credentials are only refreshed when needed. This also protects the
 78// credential provider to be used concurrently.
 79//
 80// Config providers used:
 81// * credentialsProviderProvider
 82func resolveCredentialProvider(ctx context.Context, cfg *aws.Config, configs configs) (bool, error) {
 83	credProvider, found, err := getCredentialsProvider(ctx, configs)
 84	if !found || err != nil {
 85		return false, err
 86	}
 87
 88	cfg.Credentials, err = wrapWithCredentialsCache(ctx, configs, credProvider)
 89	if err != nil {
 90		return false, err
 91	}
 92
 93	return true, nil
 94}
 95
 96// resolveCredentialChain resolves a credential provider chain using EnvConfig
 97// and SharedConfig if present in the slice of provided configs.
 98//
 99// The resolved CredentialProvider will be wrapped in a cache to ensure the
100// credentials are only refreshed when needed. This also protects the
101// credential provider to be used concurrently.
102func resolveCredentialChain(ctx context.Context, cfg *aws.Config, configs configs) (err error) {
103	envConfig, sharedConfig, other := getAWSConfigSources(configs)
104
105	// When checking if a profile was specified programmatically we should only consider the "other"
106	// configuration sources that have been provided. This ensures we correctly honor the expected credential
107	// hierarchy.
108	_, sharedProfileSet, err := getSharedConfigProfile(ctx, other)
109	if err != nil {
110		return err
111	}
112
113	switch {
114	case sharedProfileSet:
115		err = resolveCredsFromProfile(ctx, cfg, envConfig, sharedConfig, other)
116	case envConfig.Credentials.HasKeys():
117		cfg.Credentials = credentials.StaticCredentialsProvider{Value: envConfig.Credentials}
118	case len(envConfig.WebIdentityTokenFilePath) > 0:
119		err = assumeWebIdentity(ctx, cfg, envConfig.WebIdentityTokenFilePath, envConfig.RoleARN, envConfig.RoleSessionName, configs)
120	default:
121		err = resolveCredsFromProfile(ctx, cfg, envConfig, sharedConfig, other)
122	}
123	if err != nil {
124		return err
125	}
126
127	// Wrap the resolved provider in a cache so the SDK will cache credentials.
128	cfg.Credentials, err = wrapWithCredentialsCache(ctx, configs, cfg.Credentials)
129	if err != nil {
130		return err
131	}
132
133	return nil
134}
135
136func resolveCredsFromProfile(ctx context.Context, cfg *aws.Config, envConfig *EnvConfig, sharedConfig *SharedConfig, configs configs) (err error) {
137
138	switch {
139	case sharedConfig.Source != nil:
140		// Assume IAM role with credentials source from a different profile.
141		err = resolveCredsFromProfile(ctx, cfg, envConfig, sharedConfig.Source, configs)
142
143	case sharedConfig.Credentials.HasKeys():
144		// Static Credentials from Shared Config/Credentials file.
145		cfg.Credentials = credentials.StaticCredentialsProvider{
146			Value: sharedConfig.Credentials,
147		}
148
149	case len(sharedConfig.CredentialSource) != 0:
150		err = resolveCredsFromSource(ctx, cfg, envConfig, sharedConfig, configs)
151
152	case len(sharedConfig.WebIdentityTokenFile) != 0:
153		// Credentials from Assume Web Identity token require an IAM Role, and
154		// that roll will be assumed. May be wrapped with another assume role
155		// via SourceProfile.
156		return assumeWebIdentity(ctx, cfg, sharedConfig.WebIdentityTokenFile, sharedConfig.RoleARN, sharedConfig.RoleSessionName, configs)
157
158	case sharedConfig.hasSSOConfiguration():
159		err = resolveSSOCredentials(ctx, cfg, sharedConfig, configs)
160
161	case len(sharedConfig.CredentialProcess) != 0:
162		// Get credentials from CredentialProcess
163		err = processCredentials(ctx, cfg, sharedConfig, configs)
164
165	case len(envConfig.ContainerCredentialsEndpoint) != 0:
166		err = resolveLocalHTTPCredProvider(ctx, cfg, envConfig.ContainerCredentialsEndpoint, envConfig.ContainerAuthorizationToken, configs)
167
168	case len(envConfig.ContainerCredentialsRelativePath) != 0:
169		err = resolveHTTPCredProvider(ctx, cfg, ecsContainerURI(envConfig.ContainerCredentialsRelativePath), envConfig.ContainerAuthorizationToken, configs)
170
171	default:
172		err = resolveEC2RoleCredentials(ctx, cfg, configs)
173	}
174	if err != nil {
175		return err
176	}
177
178	if len(sharedConfig.RoleARN) > 0 {
179		return credsFromAssumeRole(ctx, cfg, sharedConfig, configs)
180	}
181
182	return nil
183}
184
185func resolveSSOCredentials(ctx context.Context, cfg *aws.Config, sharedConfig *SharedConfig, configs configs) error {
186	if err := sharedConfig.validateSSOConfiguration(); err != nil {
187		return err
188	}
189
190	var options []func(*ssocreds.Options)
191	v, found, err := getSSOProviderOptions(ctx, configs)
192	if err != nil {
193		return err
194	}
195	if found {
196		options = append(options, v)
197	}
198
199	cfgCopy := cfg.Copy()
200
201	if sharedConfig.SSOSession != nil {
202		ssoTokenProviderOptionsFn, found, err := getSSOTokenProviderOptions(ctx, configs)
203		if err != nil {
204			return fmt.Errorf("failed to get SSOTokenProviderOptions from config sources, %w", err)
205		}
206		var optFns []func(*ssocreds.SSOTokenProviderOptions)
207		if found {
208			optFns = append(optFns, ssoTokenProviderOptionsFn)
209		}
210		cfgCopy.Region = sharedConfig.SSOSession.SSORegion
211		cachedPath, err := ssocreds.StandardCachedTokenFilepath(sharedConfig.SSOSession.Name)
212		if err != nil {
213			return err
214		}
215		oidcClient := ssooidc.NewFromConfig(cfgCopy)
216		tokenProvider := ssocreds.NewSSOTokenProvider(oidcClient, cachedPath, optFns...)
217		options = append(options, func(o *ssocreds.Options) {
218			o.SSOTokenProvider = tokenProvider
219			o.CachedTokenFilepath = cachedPath
220		})
221	} else {
222		cfgCopy.Region = sharedConfig.SSORegion
223	}
224
225	cfg.Credentials = ssocreds.New(sso.NewFromConfig(cfgCopy), sharedConfig.SSOAccountID, sharedConfig.SSORoleName, sharedConfig.SSOStartURL, options...)
226
227	return nil
228}
229
230func ecsContainerURI(path string) string {
231	return fmt.Sprintf("%s%s", ecsContainerEndpoint, path)
232}
233
234func processCredentials(ctx context.Context, cfg *aws.Config, sharedConfig *SharedConfig, configs configs) error {
235	var opts []func(*processcreds.Options)
236
237	options, found, err := getProcessCredentialOptions(ctx, configs)
238	if err != nil {
239		return err
240	}
241	if found {
242		opts = append(opts, options)
243	}
244
245	cfg.Credentials = processcreds.NewProvider(sharedConfig.CredentialProcess, opts...)
246
247	return nil
248}
249
250// isAllowedHost allows host to be loopback or known ECS/EKS container IPs
251//
252// host can either be an IP address OR an unresolved hostname - resolution will
253// be automatically performed in the latter case
254func isAllowedHost(host string) (bool, error) {
255	if ip := net.ParseIP(host); ip != nil {
256		return isIPAllowed(ip), nil
257	}
258
259	addrs, err := lookupHostFn(host)
260	if err != nil {
261		return false, err
262	}
263
264	for _, addr := range addrs {
265		if ip := net.ParseIP(addr); ip == nil || !isIPAllowed(ip) {
266			return false, nil
267		}
268	}
269
270	return true, nil
271}
272
273func isIPAllowed(ip net.IP) bool {
274	return ip.IsLoopback() ||
275		ip.Equal(ecsContainerIPv4) ||
276		ip.Equal(eksContainerIPv4) ||
277		ip.Equal(eksContainerIPv6)
278}
279
280func resolveLocalHTTPCredProvider(ctx context.Context, cfg *aws.Config, endpointURL, authToken string, configs configs) error {
281	var resolveErr error
282
283	parsed, err := url.Parse(endpointURL)
284	if err != nil {
285		resolveErr = fmt.Errorf("invalid URL, %w", err)
286	} else {
287		host := parsed.Hostname()
288		if len(host) == 0 {
289			resolveErr = fmt.Errorf("unable to parse host from local HTTP cred provider URL")
290		} else if parsed.Scheme == "http" {
291			if isAllowedHost, allowHostErr := isAllowedHost(host); allowHostErr != nil {
292				resolveErr = fmt.Errorf("failed to resolve host %q, %v", host, allowHostErr)
293			} else if !isAllowedHost {
294				resolveErr = fmt.Errorf("invalid endpoint host, %q, only loopback/ecs/eks hosts are allowed", host)
295			}
296		}
297	}
298
299	if resolveErr != nil {
300		return resolveErr
301	}
302
303	return resolveHTTPCredProvider(ctx, cfg, endpointURL, authToken, configs)
304}
305
306func resolveHTTPCredProvider(ctx context.Context, cfg *aws.Config, url, authToken string, configs configs) error {
307	optFns := []func(*endpointcreds.Options){
308		func(options *endpointcreds.Options) {
309			if len(authToken) != 0 {
310				options.AuthorizationToken = authToken
311			}
312			if authFilePath := os.Getenv(httpProviderAuthFileEnvVar); authFilePath != "" {
313				options.AuthorizationTokenProvider = endpointcreds.TokenProviderFunc(func() (string, error) {
314					var contents []byte
315					var err error
316					if contents, err = ioutil.ReadFile(authFilePath); err != nil {
317						return "", fmt.Errorf("failed to read authorization token from %v: %v", authFilePath, err)
318					}
319					return string(contents), nil
320				})
321			}
322			options.APIOptions = cfg.APIOptions
323			if cfg.Retryer != nil {
324				options.Retryer = cfg.Retryer()
325			}
326		},
327	}
328
329	optFn, found, err := getEndpointCredentialProviderOptions(ctx, configs)
330	if err != nil {
331		return err
332	}
333	if found {
334		optFns = append(optFns, optFn)
335	}
336
337	provider := endpointcreds.New(url, optFns...)
338
339	cfg.Credentials, err = wrapWithCredentialsCache(ctx, configs, provider, func(options *aws.CredentialsCacheOptions) {
340		options.ExpiryWindow = 5 * time.Minute
341	})
342	if err != nil {
343		return err
344	}
345
346	return nil
347}
348
349func resolveCredsFromSource(ctx context.Context, cfg *aws.Config, envConfig *EnvConfig, sharedCfg *SharedConfig, configs configs) (err error) {
350	switch sharedCfg.CredentialSource {
351	case credSourceEc2Metadata:
352		return resolveEC2RoleCredentials(ctx, cfg, configs)
353
354	case credSourceEnvironment:
355		cfg.Credentials = credentials.StaticCredentialsProvider{Value: envConfig.Credentials}
356
357	case credSourceECSContainer:
358		if len(envConfig.ContainerCredentialsRelativePath) == 0 {
359			return fmt.Errorf("EcsContainer was specified as the credential_source, but 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI' was not set")
360		}
361		return resolveHTTPCredProvider(ctx, cfg, ecsContainerURI(envConfig.ContainerCredentialsRelativePath), envConfig.ContainerAuthorizationToken, configs)
362
363	default:
364		return fmt.Errorf("credential_source values must be EcsContainer, Ec2InstanceMetadata, or Environment")
365	}
366
367	return nil
368}
369
370func resolveEC2RoleCredentials(ctx context.Context, cfg *aws.Config, configs configs) error {
371	optFns := make([]func(*ec2rolecreds.Options), 0, 2)
372
373	optFn, found, err := getEC2RoleCredentialProviderOptions(ctx, configs)
374	if err != nil {
375		return err
376	}
377	if found {
378		optFns = append(optFns, optFn)
379	}
380
381	optFns = append(optFns, func(o *ec2rolecreds.Options) {
382		// Only define a client from config if not already defined.
383		if o.Client == nil {
384			o.Client = imds.NewFromConfig(*cfg)
385		}
386	})
387
388	provider := ec2rolecreds.New(optFns...)
389
390	cfg.Credentials, err = wrapWithCredentialsCache(ctx, configs, provider)
391	if err != nil {
392		return err
393	}
394
395	return nil
396}
397
398func getAWSConfigSources(cfgs configs) (*EnvConfig, *SharedConfig, configs) {
399	var (
400		envConfig    *EnvConfig
401		sharedConfig *SharedConfig
402		other        configs
403	)
404
405	for i := range cfgs {
406		switch c := cfgs[i].(type) {
407		case EnvConfig:
408			if envConfig == nil {
409				envConfig = &c
410			}
411		case *EnvConfig:
412			if envConfig == nil {
413				envConfig = c
414			}
415		case SharedConfig:
416			if sharedConfig == nil {
417				sharedConfig = &c
418			}
419		case *SharedConfig:
420			if envConfig == nil {
421				sharedConfig = c
422			}
423		default:
424			other = append(other, c)
425		}
426	}
427
428	if envConfig == nil {
429		envConfig = &EnvConfig{}
430	}
431
432	if sharedConfig == nil {
433		sharedConfig = &SharedConfig{}
434	}
435
436	return envConfig, sharedConfig, other
437}
438
439// AssumeRoleTokenProviderNotSetError is an error returned when creating a
440// session when the MFAToken option is not set when shared config is configured
441// load assume a role with an MFA token.
442type AssumeRoleTokenProviderNotSetError struct{}
443
444// Error is the error message
445func (e AssumeRoleTokenProviderNotSetError) Error() string {
446	return fmt.Sprintf("assume role with MFA enabled, but AssumeRoleTokenProvider session option not set.")
447}
448
449func assumeWebIdentity(ctx context.Context, cfg *aws.Config, filepath string, roleARN, sessionName string, configs configs) error {
450	if len(filepath) == 0 {
451		return fmt.Errorf("token file path is not set")
452	}
453
454	optFns := []func(*stscreds.WebIdentityRoleOptions){
455		func(options *stscreds.WebIdentityRoleOptions) {
456			options.RoleSessionName = sessionName
457		},
458	}
459
460	optFn, found, err := getWebIdentityCredentialProviderOptions(ctx, configs)
461	if err != nil {
462		return err
463	}
464
465	if found {
466		optFns = append(optFns, optFn)
467	}
468
469	opts := stscreds.WebIdentityRoleOptions{
470		RoleARN: roleARN,
471	}
472
473	for _, fn := range optFns {
474		fn(&opts)
475	}
476
477	if len(opts.RoleARN) == 0 {
478		return fmt.Errorf("role ARN is not set")
479	}
480
481	client := opts.Client
482	if client == nil {
483		client = sts.NewFromConfig(*cfg)
484	}
485
486	provider := stscreds.NewWebIdentityRoleProvider(client, roleARN, stscreds.IdentityTokenFile(filepath), optFns...)
487
488	cfg.Credentials = provider
489
490	return nil
491}
492
493func credsFromAssumeRole(ctx context.Context, cfg *aws.Config, sharedCfg *SharedConfig, configs configs) (err error) {
494	optFns := []func(*stscreds.AssumeRoleOptions){
495		func(options *stscreds.AssumeRoleOptions) {
496			options.RoleSessionName = sharedCfg.RoleSessionName
497			if sharedCfg.RoleDurationSeconds != nil {
498				if *sharedCfg.RoleDurationSeconds/time.Minute > 15 {
499					options.Duration = *sharedCfg.RoleDurationSeconds
500				}
501			}
502			// Assume role with external ID
503			if len(sharedCfg.ExternalID) > 0 {
504				options.ExternalID = aws.String(sharedCfg.ExternalID)
505			}
506
507			// Assume role with MFA
508			if len(sharedCfg.MFASerial) != 0 {
509				options.SerialNumber = aws.String(sharedCfg.MFASerial)
510			}
511		},
512	}
513
514	optFn, found, err := getAssumeRoleCredentialProviderOptions(ctx, configs)
515	if err != nil {
516		return err
517	}
518	if found {
519		optFns = append(optFns, optFn)
520	}
521
522	{
523		// Synthesize options early to validate configuration errors sooner to ensure a token provider
524		// is present if the SerialNumber was set.
525		var o stscreds.AssumeRoleOptions
526		for _, fn := range optFns {
527			fn(&o)
528		}
529		if o.TokenProvider == nil && o.SerialNumber != nil {
530			return AssumeRoleTokenProviderNotSetError{}
531		}
532	}
533
534	cfg.Credentials = stscreds.NewAssumeRoleProvider(sts.NewFromConfig(*cfg), sharedCfg.RoleARN, optFns...)
535
536	return nil
537}
538
539// wrapWithCredentialsCache will wrap provider with an aws.CredentialsCache
540// with the provided options if the provider is not already a
541// aws.CredentialsCache.
542func wrapWithCredentialsCache(
543	ctx context.Context,
544	cfgs configs,
545	provider aws.CredentialsProvider,
546	optFns ...func(options *aws.CredentialsCacheOptions),
547) (aws.CredentialsProvider, error) {
548	_, ok := provider.(*aws.CredentialsCache)
549	if ok {
550		return provider, nil
551	}
552
553	credCacheOptions, optionsFound, err := getCredentialsCacheOptionsProvider(ctx, cfgs)
554	if err != nil {
555		return nil, err
556	}
557
558	// force allocation of a new slice if the additional options are
559	// needed, to prevent overwriting the passed in slice of options.
560	optFns = optFns[:len(optFns):len(optFns)]
561	if optionsFound {
562		optFns = append(optFns, credCacheOptions)
563	}
564
565	return aws.NewCredentialsCache(provider, optFns...), nil
566}