resolve_credentials.go 16 KB

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