src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

resolve_credentials.go (16792B)


      1 package config
      2 
      3 import (
      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 
     25 const (
     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"
     35 var 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"
     41 var 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"
     47 var 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 
     54 var (
     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.
     64 func 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
     82 func 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.
    102 func 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 
    136 func 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 
    185 func 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 
    230 func ecsContainerURI(path string) string {
    231 	return fmt.Sprintf("%s%s", ecsContainerEndpoint, path)
    232 }
    233 
    234 func 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
    254 func 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 
    273 func isIPAllowed(ip net.IP) bool {
    274 	return ip.IsLoopback() ||
    275 		ip.Equal(ecsContainerIPv4) ||
    276 		ip.Equal(eksContainerIPv4) ||
    277 		ip.Equal(eksContainerIPv6)
    278 }
    279 
    280 func 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 
    306 func 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 
    349 func 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 
    370 func 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 
    398 func 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.
    442 type AssumeRoleTokenProviderNotSetError struct{}
    443 
    444 // Error is the error message
    445 func (e AssumeRoleTokenProviderNotSetError) Error() string {
    446 	return fmt.Sprintf("assume role with MFA enabled, but AssumeRoleTokenProvider session option not set.")
    447 }
    448 
    449 func 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 
    493 func 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.
    542 func 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 }