package config import ( "context" "fmt" "io/ioutil" "net" "net/url" "os" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds" "github.com/aws/aws-sdk-go-v2/credentials/endpointcreds" "github.com/aws/aws-sdk-go-v2/credentials/processcreds" "github.com/aws/aws-sdk-go-v2/credentials/ssocreds" "github.com/aws/aws-sdk-go-v2/credentials/stscreds" "github.com/aws/aws-sdk-go-v2/feature/ec2/imds" "github.com/aws/aws-sdk-go-v2/service/sso" "github.com/aws/aws-sdk-go-v2/service/ssooidc" "github.com/aws/aws-sdk-go-v2/service/sts" ) const ( // valid credential source values credSourceEc2Metadata = "Ec2InstanceMetadata" credSourceEnvironment = "Environment" credSourceECSContainer = "EcsContainer" httpProviderAuthFileEnvVar = "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE" ) // direct representation of the IPv4 address for the ECS container // "169.254.170.2" var ecsContainerIPv4 net.IP = []byte{ 169, 254, 170, 2, } // direct representation of the IPv4 address for the EKS container // "169.254.170.23" var eksContainerIPv4 net.IP = []byte{ 169, 254, 170, 23, } // direct representation of the IPv6 address for the EKS container // "fd00:ec2::23" var eksContainerIPv6 net.IP = []byte{ 0xFD, 0, 0xE, 0xC2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x23, } var ( ecsContainerEndpoint = "http://169.254.170.2" // not constant to allow for swapping during unit-testing ) // resolveCredentials extracts a credential provider from slice of config // sources. // // If an explicit credential provider is not found the resolver will fallback // to resolving credentials by extracting a credential provider from EnvConfig // and SharedConfig. func resolveCredentials(ctx context.Context, cfg *aws.Config, configs configs) error { found, err := resolveCredentialProvider(ctx, cfg, configs) if found || err != nil { return err } return resolveCredentialChain(ctx, cfg, configs) } // resolveCredentialProvider extracts the first instance of Credentials from the // config slices. // // The resolved CredentialProvider will be wrapped in a cache to ensure the // credentials are only refreshed when needed. This also protects the // credential provider to be used concurrently. // // Config providers used: // * credentialsProviderProvider func resolveCredentialProvider(ctx context.Context, cfg *aws.Config, configs configs) (bool, error) { credProvider, found, err := getCredentialsProvider(ctx, configs) if !found || err != nil { return false, err } cfg.Credentials, err = wrapWithCredentialsCache(ctx, configs, credProvider) if err != nil { return false, err } return true, nil } // resolveCredentialChain resolves a credential provider chain using EnvConfig // and SharedConfig if present in the slice of provided configs. // // The resolved CredentialProvider will be wrapped in a cache to ensure the // credentials are only refreshed when needed. This also protects the // credential provider to be used concurrently. func resolveCredentialChain(ctx context.Context, cfg *aws.Config, configs configs) (err error) { envConfig, sharedConfig, other := getAWSConfigSources(configs) // When checking if a profile was specified programmatically we should only consider the "other" // configuration sources that have been provided. This ensures we correctly honor the expected credential // hierarchy. _, sharedProfileSet, err := getSharedConfigProfile(ctx, other) if err != nil { return err } switch { case sharedProfileSet: err = resolveCredsFromProfile(ctx, cfg, envConfig, sharedConfig, other) case envConfig.Credentials.HasKeys(): cfg.Credentials = credentials.StaticCredentialsProvider{Value: envConfig.Credentials} case len(envConfig.WebIdentityTokenFilePath) > 0: err = assumeWebIdentity(ctx, cfg, envConfig.WebIdentityTokenFilePath, envConfig.RoleARN, envConfig.RoleSessionName, configs) default: err = resolveCredsFromProfile(ctx, cfg, envConfig, sharedConfig, other) } if err != nil { return err } // Wrap the resolved provider in a cache so the SDK will cache credentials. cfg.Credentials, err = wrapWithCredentialsCache(ctx, configs, cfg.Credentials) if err != nil { return err } return nil } func resolveCredsFromProfile(ctx context.Context, cfg *aws.Config, envConfig *EnvConfig, sharedConfig *SharedConfig, configs configs) (err error) { switch { case sharedConfig.Source != nil: // Assume IAM role with credentials source from a different profile. err = resolveCredsFromProfile(ctx, cfg, envConfig, sharedConfig.Source, configs) case sharedConfig.Credentials.HasKeys(): // Static Credentials from Shared Config/Credentials file. cfg.Credentials = credentials.StaticCredentialsProvider{ Value: sharedConfig.Credentials, } case len(sharedConfig.CredentialSource) != 0: err = resolveCredsFromSource(ctx, cfg, envConfig, sharedConfig, configs) case len(sharedConfig.WebIdentityTokenFile) != 0: // Credentials from Assume Web Identity token require an IAM Role, and // that roll will be assumed. May be wrapped with another assume role // via SourceProfile. return assumeWebIdentity(ctx, cfg, sharedConfig.WebIdentityTokenFile, sharedConfig.RoleARN, sharedConfig.RoleSessionName, configs) case sharedConfig.hasSSOConfiguration(): err = resolveSSOCredentials(ctx, cfg, sharedConfig, configs) case len(sharedConfig.CredentialProcess) != 0: // Get credentials from CredentialProcess err = processCredentials(ctx, cfg, sharedConfig, configs) case len(envConfig.ContainerCredentialsEndpoint) != 0: err = resolveLocalHTTPCredProvider(ctx, cfg, envConfig.ContainerCredentialsEndpoint, envConfig.ContainerAuthorizationToken, configs) case len(envConfig.ContainerCredentialsRelativePath) != 0: err = resolveHTTPCredProvider(ctx, cfg, ecsContainerURI(envConfig.ContainerCredentialsRelativePath), envConfig.ContainerAuthorizationToken, configs) default: err = resolveEC2RoleCredentials(ctx, cfg, configs) } if err != nil { return err } if len(sharedConfig.RoleARN) > 0 { return credsFromAssumeRole(ctx, cfg, sharedConfig, configs) } return nil } func resolveSSOCredentials(ctx context.Context, cfg *aws.Config, sharedConfig *SharedConfig, configs configs) error { if err := sharedConfig.validateSSOConfiguration(); err != nil { return err } var options []func(*ssocreds.Options) v, found, err := getSSOProviderOptions(ctx, configs) if err != nil { return err } if found { options = append(options, v) } cfgCopy := cfg.Copy() if sharedConfig.SSOSession != nil { ssoTokenProviderOptionsFn, found, err := getSSOTokenProviderOptions(ctx, configs) if err != nil { return fmt.Errorf("failed to get SSOTokenProviderOptions from config sources, %w", err) } var optFns []func(*ssocreds.SSOTokenProviderOptions) if found { optFns = append(optFns, ssoTokenProviderOptionsFn) } cfgCopy.Region = sharedConfig.SSOSession.SSORegion cachedPath, err := ssocreds.StandardCachedTokenFilepath(sharedConfig.SSOSession.Name) if err != nil { return err } oidcClient := ssooidc.NewFromConfig(cfgCopy) tokenProvider := ssocreds.NewSSOTokenProvider(oidcClient, cachedPath, optFns...) options = append(options, func(o *ssocreds.Options) { o.SSOTokenProvider = tokenProvider o.CachedTokenFilepath = cachedPath }) } else { cfgCopy.Region = sharedConfig.SSORegion } cfg.Credentials = ssocreds.New(sso.NewFromConfig(cfgCopy), sharedConfig.SSOAccountID, sharedConfig.SSORoleName, sharedConfig.SSOStartURL, options...) return nil } func ecsContainerURI(path string) string { return fmt.Sprintf("%s%s", ecsContainerEndpoint, path) } func processCredentials(ctx context.Context, cfg *aws.Config, sharedConfig *SharedConfig, configs configs) error { var opts []func(*processcreds.Options) options, found, err := getProcessCredentialOptions(ctx, configs) if err != nil { return err } if found { opts = append(opts, options) } cfg.Credentials = processcreds.NewProvider(sharedConfig.CredentialProcess, opts...) return nil } // isAllowedHost allows host to be loopback or known ECS/EKS container IPs // // host can either be an IP address OR an unresolved hostname - resolution will // be automatically performed in the latter case func isAllowedHost(host string) (bool, error) { if ip := net.ParseIP(host); ip != nil { return isIPAllowed(ip), nil } addrs, err := lookupHostFn(host) if err != nil { return false, err } for _, addr := range addrs { if ip := net.ParseIP(addr); ip == nil || !isIPAllowed(ip) { return false, nil } } return true, nil } func isIPAllowed(ip net.IP) bool { return ip.IsLoopback() || ip.Equal(ecsContainerIPv4) || ip.Equal(eksContainerIPv4) || ip.Equal(eksContainerIPv6) } func resolveLocalHTTPCredProvider(ctx context.Context, cfg *aws.Config, endpointURL, authToken string, configs configs) error { var resolveErr error parsed, err := url.Parse(endpointURL) if err != nil { resolveErr = fmt.Errorf("invalid URL, %w", err) } else { host := parsed.Hostname() if len(host) == 0 { resolveErr = fmt.Errorf("unable to parse host from local HTTP cred provider URL") } else if parsed.Scheme == "http" { if isAllowedHost, allowHostErr := isAllowedHost(host); allowHostErr != nil { resolveErr = fmt.Errorf("failed to resolve host %q, %v", host, allowHostErr) } else if !isAllowedHost { resolveErr = fmt.Errorf("invalid endpoint host, %q, only loopback/ecs/eks hosts are allowed", host) } } } if resolveErr != nil { return resolveErr } return resolveHTTPCredProvider(ctx, cfg, endpointURL, authToken, configs) } func resolveHTTPCredProvider(ctx context.Context, cfg *aws.Config, url, authToken string, configs configs) error { optFns := []func(*endpointcreds.Options){ func(options *endpointcreds.Options) { if len(authToken) != 0 { options.AuthorizationToken = authToken } if authFilePath := os.Getenv(httpProviderAuthFileEnvVar); authFilePath != "" { options.AuthorizationTokenProvider = endpointcreds.TokenProviderFunc(func() (string, error) { var contents []byte var err error if contents, err = ioutil.ReadFile(authFilePath); err != nil { return "", fmt.Errorf("failed to read authorization token from %v: %v", authFilePath, err) } return string(contents), nil }) } options.APIOptions = cfg.APIOptions if cfg.Retryer != nil { options.Retryer = cfg.Retryer() } }, } optFn, found, err := getEndpointCredentialProviderOptions(ctx, configs) if err != nil { return err } if found { optFns = append(optFns, optFn) } provider := endpointcreds.New(url, optFns...) cfg.Credentials, err = wrapWithCredentialsCache(ctx, configs, provider, func(options *aws.CredentialsCacheOptions) { options.ExpiryWindow = 5 * time.Minute }) if err != nil { return err } return nil } func resolveCredsFromSource(ctx context.Context, cfg *aws.Config, envConfig *EnvConfig, sharedCfg *SharedConfig, configs configs) (err error) { switch sharedCfg.CredentialSource { case credSourceEc2Metadata: return resolveEC2RoleCredentials(ctx, cfg, configs) case credSourceEnvironment: cfg.Credentials = credentials.StaticCredentialsProvider{Value: envConfig.Credentials} case credSourceECSContainer: if len(envConfig.ContainerCredentialsRelativePath) == 0 { return fmt.Errorf("EcsContainer was specified as the credential_source, but 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI' was not set") } return resolveHTTPCredProvider(ctx, cfg, ecsContainerURI(envConfig.ContainerCredentialsRelativePath), envConfig.ContainerAuthorizationToken, configs) default: return fmt.Errorf("credential_source values must be EcsContainer, Ec2InstanceMetadata, or Environment") } return nil } func resolveEC2RoleCredentials(ctx context.Context, cfg *aws.Config, configs configs) error { optFns := make([]func(*ec2rolecreds.Options), 0, 2) optFn, found, err := getEC2RoleCredentialProviderOptions(ctx, configs) if err != nil { return err } if found { optFns = append(optFns, optFn) } optFns = append(optFns, func(o *ec2rolecreds.Options) { // Only define a client from config if not already defined. if o.Client == nil { o.Client = imds.NewFromConfig(*cfg) } }) provider := ec2rolecreds.New(optFns...) cfg.Credentials, err = wrapWithCredentialsCache(ctx, configs, provider) if err != nil { return err } return nil } func getAWSConfigSources(cfgs configs) (*EnvConfig, *SharedConfig, configs) { var ( envConfig *EnvConfig sharedConfig *SharedConfig other configs ) for i := range cfgs { switch c := cfgs[i].(type) { case EnvConfig: if envConfig == nil { envConfig = &c } case *EnvConfig: if envConfig == nil { envConfig = c } case SharedConfig: if sharedConfig == nil { sharedConfig = &c } case *SharedConfig: if envConfig == nil { sharedConfig = c } default: other = append(other, c) } } if envConfig == nil { envConfig = &EnvConfig{} } if sharedConfig == nil { sharedConfig = &SharedConfig{} } return envConfig, sharedConfig, other } // AssumeRoleTokenProviderNotSetError is an error returned when creating a // session when the MFAToken option is not set when shared config is configured // load assume a role with an MFA token. type AssumeRoleTokenProviderNotSetError struct{} // Error is the error message func (e AssumeRoleTokenProviderNotSetError) Error() string { return fmt.Sprintf("assume role with MFA enabled, but AssumeRoleTokenProvider session option not set.") } func assumeWebIdentity(ctx context.Context, cfg *aws.Config, filepath string, roleARN, sessionName string, configs configs) error { if len(filepath) == 0 { return fmt.Errorf("token file path is not set") } optFns := []func(*stscreds.WebIdentityRoleOptions){ func(options *stscreds.WebIdentityRoleOptions) { options.RoleSessionName = sessionName }, } optFn, found, err := getWebIdentityCredentialProviderOptions(ctx, configs) if err != nil { return err } if found { optFns = append(optFns, optFn) } opts := stscreds.WebIdentityRoleOptions{ RoleARN: roleARN, } for _, fn := range optFns { fn(&opts) } if len(opts.RoleARN) == 0 { return fmt.Errorf("role ARN is not set") } client := opts.Client if client == nil { client = sts.NewFromConfig(*cfg) } provider := stscreds.NewWebIdentityRoleProvider(client, roleARN, stscreds.IdentityTokenFile(filepath), optFns...) cfg.Credentials = provider return nil } func credsFromAssumeRole(ctx context.Context, cfg *aws.Config, sharedCfg *SharedConfig, configs configs) (err error) { optFns := []func(*stscreds.AssumeRoleOptions){ func(options *stscreds.AssumeRoleOptions) { options.RoleSessionName = sharedCfg.RoleSessionName if sharedCfg.RoleDurationSeconds != nil { if *sharedCfg.RoleDurationSeconds/time.Minute > 15 { options.Duration = *sharedCfg.RoleDurationSeconds } } // Assume role with external ID if len(sharedCfg.ExternalID) > 0 { options.ExternalID = aws.String(sharedCfg.ExternalID) } // Assume role with MFA if len(sharedCfg.MFASerial) != 0 { options.SerialNumber = aws.String(sharedCfg.MFASerial) } }, } optFn, found, err := getAssumeRoleCredentialProviderOptions(ctx, configs) if err != nil { return err } if found { optFns = append(optFns, optFn) } { // Synthesize options early to validate configuration errors sooner to ensure a token provider // is present if the SerialNumber was set. var o stscreds.AssumeRoleOptions for _, fn := range optFns { fn(&o) } if o.TokenProvider == nil && o.SerialNumber != nil { return AssumeRoleTokenProviderNotSetError{} } } cfg.Credentials = stscreds.NewAssumeRoleProvider(sts.NewFromConfig(*cfg), sharedCfg.RoleARN, optFns...) return nil } // wrapWithCredentialsCache will wrap provider with an aws.CredentialsCache // with the provided options if the provider is not already a // aws.CredentialsCache. func wrapWithCredentialsCache( ctx context.Context, cfgs configs, provider aws.CredentialsProvider, optFns ...func(options *aws.CredentialsCacheOptions), ) (aws.CredentialsProvider, error) { _, ok := provider.(*aws.CredentialsCache) if ok { return provider, nil } credCacheOptions, optionsFound, err := getCredentialsCacheOptionsProvider(ctx, cfgs) if err != nil { return nil, err } // force allocation of a new slice if the additional options are // needed, to prevent overwriting the passed in slice of options. optFns = optFns[:len(optFns):len(optFns)] if optionsFound { optFns = append(optFns, credCacheOptions) } return aws.NewCredentialsCache(provider, optFns...), nil }