resolve_credentials.go (19777B)
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 ctx, err = resolveCredsFromProfile(ctx, cfg, envConfig, sharedConfig, other) 116 case envConfig.Credentials.HasKeys(): 117 ctx = addCredentialSource(ctx, aws.CredentialSourceEnvVars) 118 cfg.Credentials = credentials.StaticCredentialsProvider{Value: envConfig.Credentials, Source: getCredentialSources(ctx)} 119 case len(envConfig.WebIdentityTokenFilePath) > 0: 120 ctx = addCredentialSource(ctx, aws.CredentialSourceEnvVarsSTSWebIDToken) 121 err = assumeWebIdentity(ctx, cfg, envConfig.WebIdentityTokenFilePath, envConfig.RoleARN, envConfig.RoleSessionName, configs) 122 default: 123 ctx, err = resolveCredsFromProfile(ctx, cfg, envConfig, sharedConfig, other) 124 } 125 if err != nil { 126 return err 127 } 128 129 // Wrap the resolved provider in a cache so the SDK will cache credentials. 130 cfg.Credentials, err = wrapWithCredentialsCache(ctx, configs, cfg.Credentials) 131 if err != nil { 132 return err 133 } 134 135 return nil 136 } 137 138 func resolveCredsFromProfile(ctx context.Context, cfg *aws.Config, envConfig *EnvConfig, sharedConfig *SharedConfig, configs configs) (ctx2 context.Context, err error) { 139 switch { 140 case sharedConfig.Source != nil: 141 ctx = addCredentialSource(ctx, aws.CredentialSourceProfileSourceProfile) 142 // Assume IAM role with credentials source from a different profile. 143 ctx, err = resolveCredsFromProfile(ctx, cfg, envConfig, sharedConfig.Source, configs) 144 145 case sharedConfig.Credentials.HasKeys(): 146 // Static Credentials from Shared Config/Credentials file. 147 ctx = addCredentialSource(ctx, aws.CredentialSourceProfile) 148 cfg.Credentials = credentials.StaticCredentialsProvider{ 149 Value: sharedConfig.Credentials, 150 Source: getCredentialSources(ctx), 151 } 152 153 case len(sharedConfig.CredentialSource) != 0: 154 ctx = addCredentialSource(ctx, aws.CredentialSourceProfileNamedProvider) 155 ctx, err = resolveCredsFromSource(ctx, cfg, envConfig, sharedConfig, configs) 156 157 case len(sharedConfig.WebIdentityTokenFile) != 0: 158 // Credentials from Assume Web Identity token require an IAM Role, and 159 // that roll will be assumed. May be wrapped with another assume role 160 // via SourceProfile. 161 ctx = addCredentialSource(ctx, aws.CredentialSourceProfileSTSWebIDToken) 162 return ctx, assumeWebIdentity(ctx, cfg, sharedConfig.WebIdentityTokenFile, sharedConfig.RoleARN, sharedConfig.RoleSessionName, configs) 163 164 case sharedConfig.hasSSOConfiguration(): 165 if sharedConfig.hasLegacySSOConfiguration() { 166 ctx = addCredentialSource(ctx, aws.CredentialSourceProfileSSOLegacy) 167 ctx = addCredentialSource(ctx, aws.CredentialSourceSSOLegacy) 168 } else { 169 ctx = addCredentialSource(ctx, aws.CredentialSourceSSO) 170 } 171 if sharedConfig.SSOSession != nil { 172 ctx = addCredentialSource(ctx, aws.CredentialSourceProfileSSO) 173 } 174 err = resolveSSOCredentials(ctx, cfg, sharedConfig, configs) 175 176 case len(sharedConfig.CredentialProcess) != 0: 177 // Get credentials from CredentialProcess 178 ctx = addCredentialSource(ctx, aws.CredentialSourceProfileProcess) 179 ctx = addCredentialSource(ctx, aws.CredentialSourceProcess) 180 err = processCredentials(ctx, cfg, sharedConfig, configs) 181 182 case len(envConfig.ContainerCredentialsRelativePath) != 0: 183 ctx = addCredentialSource(ctx, aws.CredentialSourceHTTP) 184 err = resolveHTTPCredProvider(ctx, cfg, ecsContainerURI(envConfig.ContainerCredentialsRelativePath), envConfig.ContainerAuthorizationToken, configs) 185 186 case len(envConfig.ContainerCredentialsEndpoint) != 0: 187 ctx = addCredentialSource(ctx, aws.CredentialSourceHTTP) 188 err = resolveLocalHTTPCredProvider(ctx, cfg, envConfig.ContainerCredentialsEndpoint, envConfig.ContainerAuthorizationToken, configs) 189 190 default: 191 ctx = addCredentialSource(ctx, aws.CredentialSourceIMDS) 192 err = resolveEC2RoleCredentials(ctx, cfg, configs) 193 } 194 if err != nil { 195 return ctx, err 196 } 197 198 if len(sharedConfig.RoleARN) > 0 { 199 return ctx, credsFromAssumeRole(ctx, cfg, sharedConfig, configs) 200 } 201 202 return ctx, nil 203 } 204 205 func resolveSSOCredentials(ctx context.Context, cfg *aws.Config, sharedConfig *SharedConfig, configs configs) error { 206 if err := sharedConfig.validateSSOConfiguration(); err != nil { 207 return err 208 } 209 210 var options []func(*ssocreds.Options) 211 v, found, err := getSSOProviderOptions(ctx, configs) 212 if err != nil { 213 return err 214 } 215 if found { 216 options = append(options, v) 217 } 218 219 cfgCopy := cfg.Copy() 220 221 options = append(options, func(o *ssocreds.Options) { 222 o.CredentialSources = getCredentialSources(ctx) 223 }) 224 225 if sharedConfig.SSOSession != nil { 226 ssoTokenProviderOptionsFn, found, err := getSSOTokenProviderOptions(ctx, configs) 227 if err != nil { 228 return fmt.Errorf("failed to get SSOTokenProviderOptions from config sources, %w", err) 229 } 230 var optFns []func(*ssocreds.SSOTokenProviderOptions) 231 if found { 232 optFns = append(optFns, ssoTokenProviderOptionsFn) 233 } 234 cfgCopy.Region = sharedConfig.SSOSession.SSORegion 235 cachedPath, err := ssocreds.StandardCachedTokenFilepath(sharedConfig.SSOSession.Name) 236 if err != nil { 237 return err 238 } 239 oidcClient := ssooidc.NewFromConfig(cfgCopy) 240 tokenProvider := ssocreds.NewSSOTokenProvider(oidcClient, cachedPath, optFns...) 241 options = append(options, func(o *ssocreds.Options) { 242 o.SSOTokenProvider = tokenProvider 243 o.CachedTokenFilepath = cachedPath 244 }) 245 } else { 246 cfgCopy.Region = sharedConfig.SSORegion 247 } 248 249 cfg.Credentials = ssocreds.New(sso.NewFromConfig(cfgCopy), sharedConfig.SSOAccountID, sharedConfig.SSORoleName, sharedConfig.SSOStartURL, options...) 250 251 return nil 252 } 253 254 func ecsContainerURI(path string) string { 255 return fmt.Sprintf("%s%s", ecsContainerEndpoint, path) 256 } 257 258 func processCredentials(ctx context.Context, cfg *aws.Config, sharedConfig *SharedConfig, configs configs) error { 259 var opts []func(*processcreds.Options) 260 261 options, found, err := getProcessCredentialOptions(ctx, configs) 262 if err != nil { 263 return err 264 } 265 if found { 266 opts = append(opts, options) 267 } 268 269 opts = append(opts, func(o *processcreds.Options) { 270 o.CredentialSources = getCredentialSources(ctx) 271 }) 272 273 cfg.Credentials = processcreds.NewProvider(sharedConfig.CredentialProcess, opts...) 274 275 return nil 276 } 277 278 // isAllowedHost allows host to be loopback or known ECS/EKS container IPs 279 // 280 // host can either be an IP address OR an unresolved hostname - resolution will 281 // be automatically performed in the latter case 282 func isAllowedHost(host string) (bool, error) { 283 if ip := net.ParseIP(host); ip != nil { 284 return isIPAllowed(ip), nil 285 } 286 287 addrs, err := lookupHostFn(host) 288 if err != nil { 289 return false, err 290 } 291 292 for _, addr := range addrs { 293 if ip := net.ParseIP(addr); ip == nil || !isIPAllowed(ip) { 294 return false, nil 295 } 296 } 297 298 return true, nil 299 } 300 301 func isIPAllowed(ip net.IP) bool { 302 return ip.IsLoopback() || 303 ip.Equal(ecsContainerIPv4) || 304 ip.Equal(eksContainerIPv4) || 305 ip.Equal(eksContainerIPv6) 306 } 307 308 func resolveLocalHTTPCredProvider(ctx context.Context, cfg *aws.Config, endpointURL, authToken string, configs configs) error { 309 var resolveErr error 310 311 parsed, err := url.Parse(endpointURL) 312 if err != nil { 313 resolveErr = fmt.Errorf("invalid URL, %w", err) 314 } else { 315 host := parsed.Hostname() 316 if len(host) == 0 { 317 resolveErr = fmt.Errorf("unable to parse host from local HTTP cred provider URL") 318 } else if parsed.Scheme == "http" { 319 if isAllowedHost, allowHostErr := isAllowedHost(host); allowHostErr != nil { 320 resolveErr = fmt.Errorf("failed to resolve host %q, %v", host, allowHostErr) 321 } else if !isAllowedHost { 322 resolveErr = fmt.Errorf("invalid endpoint host, %q, only loopback/ecs/eks hosts are allowed", host) 323 } 324 } 325 } 326 327 if resolveErr != nil { 328 return resolveErr 329 } 330 331 return resolveHTTPCredProvider(ctx, cfg, endpointURL, authToken, configs) 332 } 333 334 func resolveHTTPCredProvider(ctx context.Context, cfg *aws.Config, url, authToken string, configs configs) error { 335 optFns := []func(*endpointcreds.Options){ 336 func(options *endpointcreds.Options) { 337 if len(authToken) != 0 { 338 options.AuthorizationToken = authToken 339 } 340 if authFilePath := os.Getenv(httpProviderAuthFileEnvVar); authFilePath != "" { 341 options.AuthorizationTokenProvider = endpointcreds.TokenProviderFunc(func() (string, error) { 342 var contents []byte 343 var err error 344 if contents, err = ioutil.ReadFile(authFilePath); err != nil { 345 return "", fmt.Errorf("failed to read authorization token from %v: %v", authFilePath, err) 346 } 347 return string(contents), nil 348 }) 349 } 350 options.APIOptions = cfg.APIOptions 351 if cfg.Retryer != nil { 352 options.Retryer = cfg.Retryer() 353 } 354 options.CredentialSources = getCredentialSources(ctx) 355 }, 356 } 357 358 optFn, found, err := getEndpointCredentialProviderOptions(ctx, configs) 359 if err != nil { 360 return err 361 } 362 if found { 363 optFns = append(optFns, optFn) 364 } 365 366 provider := endpointcreds.New(url, optFns...) 367 368 cfg.Credentials, err = wrapWithCredentialsCache(ctx, configs, provider, func(options *aws.CredentialsCacheOptions) { 369 options.ExpiryWindow = 5 * time.Minute 370 }) 371 if err != nil { 372 return err 373 } 374 375 return nil 376 } 377 378 func resolveCredsFromSource(ctx context.Context, cfg *aws.Config, envConfig *EnvConfig, sharedCfg *SharedConfig, configs configs) (context.Context, error) { 379 switch sharedCfg.CredentialSource { 380 case credSourceEc2Metadata: 381 ctx = addCredentialSource(ctx, aws.CredentialSourceIMDS) 382 return ctx, resolveEC2RoleCredentials(ctx, cfg, configs) 383 384 case credSourceEnvironment: 385 ctx = addCredentialSource(ctx, aws.CredentialSourceHTTP) 386 cfg.Credentials = credentials.StaticCredentialsProvider{Value: envConfig.Credentials, Source: getCredentialSources(ctx)} 387 388 case credSourceECSContainer: 389 ctx = addCredentialSource(ctx, aws.CredentialSourceHTTP) 390 if len(envConfig.ContainerCredentialsRelativePath) != 0 { 391 return ctx, resolveHTTPCredProvider(ctx, cfg, ecsContainerURI(envConfig.ContainerCredentialsRelativePath), envConfig.ContainerAuthorizationToken, configs) 392 } 393 if len(envConfig.ContainerCredentialsEndpoint) != 0 { 394 return ctx, resolveLocalHTTPCredProvider(ctx, cfg, envConfig.ContainerCredentialsEndpoint, envConfig.ContainerAuthorizationToken, configs) 395 } 396 return ctx, fmt.Errorf("EcsContainer was specified as the credential_source, but neither 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI' or AWS_CONTAINER_CREDENTIALS_FULL_URI' was set") 397 398 default: 399 return ctx, fmt.Errorf("credential_source values must be EcsContainer, Ec2InstanceMetadata, or Environment") 400 } 401 402 return ctx, nil 403 } 404 405 func resolveEC2RoleCredentials(ctx context.Context, cfg *aws.Config, configs configs) error { 406 optFns := make([]func(*ec2rolecreds.Options), 0, 2) 407 408 optFn, found, err := getEC2RoleCredentialProviderOptions(ctx, configs) 409 if err != nil { 410 return err 411 } 412 if found { 413 optFns = append(optFns, optFn) 414 } 415 416 optFns = append(optFns, func(o *ec2rolecreds.Options) { 417 // Only define a client from config if not already defined. 418 if o.Client == nil { 419 o.Client = imds.NewFromConfig(*cfg) 420 } 421 o.CredentialSources = getCredentialSources(ctx) 422 }) 423 424 provider := ec2rolecreds.New(optFns...) 425 426 cfg.Credentials, err = wrapWithCredentialsCache(ctx, configs, provider) 427 if err != nil { 428 return err 429 } 430 return nil 431 } 432 433 func getAWSConfigSources(cfgs configs) (*EnvConfig, *SharedConfig, configs) { 434 var ( 435 envConfig *EnvConfig 436 sharedConfig *SharedConfig 437 other configs 438 ) 439 440 for i := range cfgs { 441 switch c := cfgs[i].(type) { 442 case EnvConfig: 443 if envConfig == nil { 444 envConfig = &c 445 } 446 case *EnvConfig: 447 if envConfig == nil { 448 envConfig = c 449 } 450 case SharedConfig: 451 if sharedConfig == nil { 452 sharedConfig = &c 453 } 454 case *SharedConfig: 455 if envConfig == nil { 456 sharedConfig = c 457 } 458 default: 459 other = append(other, c) 460 } 461 } 462 463 if envConfig == nil { 464 envConfig = &EnvConfig{} 465 } 466 467 if sharedConfig == nil { 468 sharedConfig = &SharedConfig{} 469 } 470 471 return envConfig, sharedConfig, other 472 } 473 474 // AssumeRoleTokenProviderNotSetError is an error returned when creating a 475 // session when the MFAToken option is not set when shared config is configured 476 // load assume a role with an MFA token. 477 type AssumeRoleTokenProviderNotSetError struct{} 478 479 // Error is the error message 480 func (e AssumeRoleTokenProviderNotSetError) Error() string { 481 return fmt.Sprintf("assume role with MFA enabled, but AssumeRoleTokenProvider session option not set.") 482 } 483 484 func assumeWebIdentity(ctx context.Context, cfg *aws.Config, filepath string, roleARN, sessionName string, configs configs) error { 485 if len(filepath) == 0 { 486 return fmt.Errorf("token file path is not set") 487 } 488 489 optFns := []func(*stscreds.WebIdentityRoleOptions){ 490 func(options *stscreds.WebIdentityRoleOptions) { 491 options.RoleSessionName = sessionName 492 }, 493 } 494 495 optFn, found, err := getWebIdentityCredentialProviderOptions(ctx, configs) 496 if err != nil { 497 return err 498 } 499 500 if found { 501 optFns = append(optFns, optFn) 502 } 503 504 opts := stscreds.WebIdentityRoleOptions{ 505 RoleARN: roleARN, 506 } 507 508 optFns = append(optFns, func(options *stscreds.WebIdentityRoleOptions) { 509 options.CredentialSources = getCredentialSources(ctx) 510 }) 511 512 for _, fn := range optFns { 513 fn(&opts) 514 } 515 516 if len(opts.RoleARN) == 0 { 517 return fmt.Errorf("role ARN is not set") 518 } 519 520 client := opts.Client 521 if client == nil { 522 client = sts.NewFromConfig(*cfg) 523 } 524 525 provider := stscreds.NewWebIdentityRoleProvider(client, roleARN, stscreds.IdentityTokenFile(filepath), optFns...) 526 527 cfg.Credentials = provider 528 529 return nil 530 } 531 532 func credsFromAssumeRole(ctx context.Context, cfg *aws.Config, sharedCfg *SharedConfig, configs configs) (err error) { 533 // resolve credentials early 534 credentialSources := getCredentialSources(ctx) 535 optFns := []func(*stscreds.AssumeRoleOptions){ 536 func(options *stscreds.AssumeRoleOptions) { 537 options.RoleSessionName = sharedCfg.RoleSessionName 538 if sharedCfg.RoleDurationSeconds != nil { 539 if *sharedCfg.RoleDurationSeconds/time.Minute > 15 { 540 options.Duration = *sharedCfg.RoleDurationSeconds 541 } 542 } 543 // Assume role with external ID 544 if len(sharedCfg.ExternalID) > 0 { 545 options.ExternalID = aws.String(sharedCfg.ExternalID) 546 } 547 548 // Assume role with MFA 549 if len(sharedCfg.MFASerial) != 0 { 550 options.SerialNumber = aws.String(sharedCfg.MFASerial) 551 } 552 553 // add existing credential chain 554 options.CredentialSources = credentialSources 555 }, 556 } 557 558 optFn, found, err := getAssumeRoleCredentialProviderOptions(ctx, configs) 559 if err != nil { 560 return err 561 } 562 if found { 563 optFns = append(optFns, optFn) 564 } 565 566 { 567 // Synthesize options early to validate configuration errors sooner to ensure a token provider 568 // is present if the SerialNumber was set. 569 var o stscreds.AssumeRoleOptions 570 for _, fn := range optFns { 571 fn(&o) 572 } 573 if o.TokenProvider == nil && o.SerialNumber != nil { 574 return AssumeRoleTokenProviderNotSetError{} 575 } 576 } 577 cfg.Credentials = stscreds.NewAssumeRoleProvider(sts.NewFromConfig(*cfg), sharedCfg.RoleARN, optFns...) 578 579 return nil 580 } 581 582 // wrapWithCredentialsCache will wrap provider with an aws.CredentialsCache 583 // with the provided options if the provider is not already a 584 // aws.CredentialsCache. 585 func wrapWithCredentialsCache( 586 ctx context.Context, 587 cfgs configs, 588 provider aws.CredentialsProvider, 589 optFns ...func(options *aws.CredentialsCacheOptions), 590 ) (aws.CredentialsProvider, error) { 591 _, ok := provider.(*aws.CredentialsCache) 592 if ok { 593 return provider, nil 594 } 595 596 credCacheOptions, optionsFound, err := getCredentialsCacheOptionsProvider(ctx, cfgs) 597 if err != nil { 598 return nil, err 599 } 600 601 // force allocation of a new slice if the additional options are 602 // needed, to prevent overwriting the passed in slice of options. 603 optFns = optFns[:len(optFns):len(optFns)] 604 if optionsFound { 605 optFns = append(optFns, credCacheOptions) 606 } 607 608 return aws.NewCredentialsCache(provider, optFns...), nil 609 } 610 611 // credentialSource stores the chain of providers that was used to create an instance of 612 // a credentials provider on the context 613 type credentialSource struct{} 614 615 func addCredentialSource(ctx context.Context, source aws.CredentialSource) context.Context { 616 existing, ok := ctx.Value(credentialSource{}).([]aws.CredentialSource) 617 if !ok { 618 existing = []aws.CredentialSource{source} 619 } else { 620 existing = append(existing, source) 621 } 622 return context.WithValue(ctx, credentialSource{}, existing) 623 } 624 625 func getCredentialSources(ctx context.Context) []aws.CredentialSource { 626 return ctx.Value(credentialSource{}).([]aws.CredentialSource) 627 }