2022-10-01 16:12:07 +02:00
package config
import (
"context"
"fmt"
2023-11-14 22:45:07 +01:00
"io/ioutil"
"net"
2022-10-01 16:12:07 +02:00
"net/url"
2023-11-14 22:45:07 +01:00
"os"
2022-10-01 16:12:07 +02:00
"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"
2022-11-17 00:38:29 +01:00
"github.com/aws/aws-sdk-go-v2/service/ssooidc"
2022-10-01 16:12:07 +02:00
"github.com/aws/aws-sdk-go-v2/service/sts"
)
const (
// valid credential source values
2023-11-14 22:45:07 +01:00
credSourceEc2Metadata = "Ec2InstanceMetadata"
credSourceEnvironment = "Environment"
credSourceECSContainer = "EcsContainer"
httpProviderAuthFileEnvVar = "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE"
2022-10-01 16:12:07 +02:00
)
2023-11-14 22:45:07 +01:00
// 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 ,
}
2022-10-01 16:12:07 +02:00
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 . ContainerCredentialsRelativePath ) != 0 :
err = resolveHTTPCredProvider ( ctx , cfg , ecsContainerURI ( envConfig . ContainerCredentialsRelativePath ) , envConfig . ContainerAuthorizationToken , configs )
2024-09-26 22:33:05 +02:00
case len ( envConfig . ContainerCredentialsEndpoint ) != 0 :
err = resolveLocalHTTPCredProvider ( ctx , cfg , envConfig . ContainerCredentialsEndpoint , envConfig . ContainerAuthorizationToken , configs )
2022-10-01 16:12:07 +02:00
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 ( )
2022-11-17 00:38:29 +01:00
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
}
2022-10-01 16:12:07 +02:00
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
}
2023-11-14 22:45:07 +01:00
// 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 )
}
2022-10-01 16:12:07 +02:00
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" )
2023-11-14 22:45:07 +01:00
} 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 )
}
2022-10-01 16:12:07 +02:00
}
}
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
}
2023-11-14 22:45:07 +01:00
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
} )
}
2022-10-01 16:12:07 +02:00
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 :
2024-09-26 22:33:05 +02:00
if len ( envConfig . ContainerCredentialsRelativePath ) != 0 {
return resolveHTTPCredProvider ( ctx , cfg , ecsContainerURI ( envConfig . ContainerCredentialsRelativePath ) , envConfig . ContainerAuthorizationToken , configs )
}
if len ( envConfig . ContainerCredentialsEndpoint ) != 0 {
return resolveLocalHTTPCredProvider ( ctx , cfg , envConfig . ContainerCredentialsEndpoint , envConfig . ContainerAuthorizationToken , configs )
2022-10-01 16:12:07 +02:00
}
2024-09-26 22:33:05 +02:00
return fmt . Errorf ( "EcsContainer was specified as the credential_source, but neither 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI' or AWS_CONTAINER_CREDENTIALS_FULL_URI' was set" )
2022-10-01 16:12:07 +02:00
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
}
2023-03-25 02:08:06 +01:00
2022-10-01 16:12:07 +02:00
if found {
optFns = append ( optFns , optFn )
}
2023-03-25 02:08:06 +01:00
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 ... )
2022-10-01 16:12:07 +02:00
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
}