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 }