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