iam_aws.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. /*
  2. * MinIO Go Library for Amazon S3 Compatible Cloud Storage
  3. * Copyright 2017 MinIO, Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. package credentials
  18. import (
  19. "bufio"
  20. "errors"
  21. "fmt"
  22. "io/ioutil"
  23. "net"
  24. "net/http"
  25. "net/url"
  26. "os"
  27. "path"
  28. "strings"
  29. "time"
  30. jsoniter "github.com/json-iterator/go"
  31. )
  32. // DefaultExpiryWindow - Default expiry window.
  33. // ExpiryWindow will allow the credentials to trigger refreshing
  34. // prior to the credentials actually expiring. This is beneficial
  35. // so race conditions with expiring credentials do not cause
  36. // request to fail unexpectedly due to ExpiredTokenException exceptions.
  37. // DefaultExpiryWindow can be used as parameter to (*Expiry).SetExpiration.
  38. // When used the tokens refresh will be triggered when 80% of the elapsed
  39. // time until the actual expiration time is passed.
  40. const DefaultExpiryWindow = -1
  41. // A IAM retrieves credentials from the EC2 service, and keeps track if
  42. // those credentials are expired.
  43. type IAM struct {
  44. Expiry
  45. // Required http Client to use when connecting to IAM metadata service.
  46. Client *http.Client
  47. // Custom endpoint to fetch IAM role credentials.
  48. Endpoint string
  49. }
  50. // IAM Roles for Amazon EC2
  51. // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
  52. const (
  53. defaultIAMRoleEndpoint = "http://169.254.169.254"
  54. defaultECSRoleEndpoint = "http://169.254.170.2"
  55. defaultSTSRoleEndpoint = "https://sts.amazonaws.com"
  56. defaultIAMSecurityCredsPath = "/latest/meta-data/iam/security-credentials/"
  57. tokenRequestTTLHeader = "X-aws-ec2-metadata-token-ttl-seconds"
  58. tokenPath = "/latest/api/token"
  59. tokenTTL = "21600"
  60. tokenRequestHeader = "X-aws-ec2-metadata-token"
  61. )
  62. // NewIAM returns a pointer to a new Credentials object wrapping the IAM.
  63. func NewIAM(endpoint string) *Credentials {
  64. return New(&IAM{
  65. Client: &http.Client{
  66. Transport: http.DefaultTransport,
  67. },
  68. Endpoint: endpoint,
  69. })
  70. }
  71. // Retrieve retrieves credentials from the EC2 service.
  72. // Error will be returned if the request fails, or unable to extract
  73. // the desired
  74. func (m *IAM) Retrieve() (Value, error) {
  75. token := os.Getenv("AWS_CONTAINER_AUTHORIZATION_TOKEN")
  76. var roleCreds ec2RoleCredRespBody
  77. var err error
  78. endpoint := m.Endpoint
  79. switch {
  80. case len(os.Getenv("AWS_WEB_IDENTITY_TOKEN_FILE")) > 0:
  81. if len(endpoint) == 0 {
  82. if len(os.Getenv("AWS_REGION")) > 0 {
  83. if strings.HasPrefix(os.Getenv("AWS_REGION"), "cn-") {
  84. endpoint = "https://sts." + os.Getenv("AWS_REGION") + ".amazonaws.com.cn"
  85. } else {
  86. endpoint = "https://sts." + os.Getenv("AWS_REGION") + ".amazonaws.com"
  87. }
  88. } else {
  89. endpoint = defaultSTSRoleEndpoint
  90. }
  91. }
  92. creds := &STSWebIdentity{
  93. Client: m.Client,
  94. STSEndpoint: endpoint,
  95. GetWebIDTokenExpiry: func() (*WebIdentityToken, error) {
  96. token, err := ioutil.ReadFile(os.Getenv("AWS_WEB_IDENTITY_TOKEN_FILE"))
  97. if err != nil {
  98. return nil, err
  99. }
  100. return &WebIdentityToken{Token: string(token)}, nil
  101. },
  102. roleARN: os.Getenv("AWS_ROLE_ARN"),
  103. roleSessionName: os.Getenv("AWS_ROLE_SESSION_NAME"),
  104. }
  105. stsWebIdentityCreds, err := creds.Retrieve()
  106. if err == nil {
  107. m.SetExpiration(creds.Expiration(), DefaultExpiryWindow)
  108. }
  109. return stsWebIdentityCreds, err
  110. case len(os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")) > 0:
  111. if len(endpoint) == 0 {
  112. endpoint = fmt.Sprintf("%s%s", defaultECSRoleEndpoint,
  113. os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"))
  114. }
  115. roleCreds, err = getEcsTaskCredentials(m.Client, endpoint, token)
  116. case len(os.Getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI")) > 0:
  117. if len(endpoint) == 0 {
  118. endpoint = os.Getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI")
  119. var ok bool
  120. if ok, err = isLoopback(endpoint); !ok {
  121. if err == nil {
  122. err = fmt.Errorf("uri host is not a loopback address: %s", endpoint)
  123. }
  124. break
  125. }
  126. }
  127. roleCreds, err = getEcsTaskCredentials(m.Client, endpoint, token)
  128. default:
  129. roleCreds, err = getCredentials(m.Client, endpoint)
  130. }
  131. if err != nil {
  132. return Value{}, err
  133. }
  134. // Expiry window is set to 10secs.
  135. m.SetExpiration(roleCreds.Expiration, DefaultExpiryWindow)
  136. return Value{
  137. AccessKeyID: roleCreds.AccessKeyID,
  138. SecretAccessKey: roleCreds.SecretAccessKey,
  139. SessionToken: roleCreds.Token,
  140. SignerType: SignatureV4,
  141. }, nil
  142. }
  143. // A ec2RoleCredRespBody provides the shape for unmarshaling credential
  144. // request responses.
  145. type ec2RoleCredRespBody struct {
  146. // Success State
  147. Expiration time.Time
  148. AccessKeyID string
  149. SecretAccessKey string
  150. Token string
  151. // Error state
  152. Code string
  153. Message string
  154. // Unused params.
  155. LastUpdated time.Time
  156. Type string
  157. }
  158. // Get the final IAM role URL where the request will
  159. // be sent to fetch the rolling access credentials.
  160. // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
  161. func getIAMRoleURL(endpoint string) (*url.URL, error) {
  162. u, err := url.Parse(endpoint)
  163. if err != nil {
  164. return nil, err
  165. }
  166. u.Path = defaultIAMSecurityCredsPath
  167. return u, nil
  168. }
  169. // listRoleNames lists of credential role names associated
  170. // with the current EC2 service. If there are no credentials,
  171. // or there is an error making or receiving the request.
  172. // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
  173. func listRoleNames(client *http.Client, u *url.URL, token string) ([]string, error) {
  174. req, err := http.NewRequest(http.MethodGet, u.String(), nil)
  175. if err != nil {
  176. return nil, err
  177. }
  178. if token != "" {
  179. req.Header.Add(tokenRequestHeader, token)
  180. }
  181. resp, err := client.Do(req)
  182. if err != nil {
  183. return nil, err
  184. }
  185. defer resp.Body.Close()
  186. if resp.StatusCode != http.StatusOK {
  187. return nil, errors.New(resp.Status)
  188. }
  189. credsList := []string{}
  190. s := bufio.NewScanner(resp.Body)
  191. for s.Scan() {
  192. credsList = append(credsList, s.Text())
  193. }
  194. if err := s.Err(); err != nil {
  195. return nil, err
  196. }
  197. return credsList, nil
  198. }
  199. func getEcsTaskCredentials(client *http.Client, endpoint string, token string) (ec2RoleCredRespBody, error) {
  200. req, err := http.NewRequest(http.MethodGet, endpoint, nil)
  201. if err != nil {
  202. return ec2RoleCredRespBody{}, err
  203. }
  204. if token != "" {
  205. req.Header.Set("Authorization", token)
  206. }
  207. resp, err := client.Do(req)
  208. if err != nil {
  209. return ec2RoleCredRespBody{}, err
  210. }
  211. defer resp.Body.Close()
  212. if resp.StatusCode != http.StatusOK {
  213. return ec2RoleCredRespBody{}, errors.New(resp.Status)
  214. }
  215. respCreds := ec2RoleCredRespBody{}
  216. if err := jsoniter.NewDecoder(resp.Body).Decode(&respCreds); err != nil {
  217. return ec2RoleCredRespBody{}, err
  218. }
  219. return respCreds, nil
  220. }
  221. func fetchIMDSToken(client *http.Client, endpoint string) (string, error) {
  222. req, err := http.NewRequest(http.MethodPut, endpoint+tokenPath, nil)
  223. if err != nil {
  224. return "", err
  225. }
  226. req.Header.Add(tokenRequestTTLHeader, tokenTTL)
  227. resp, err := client.Do(req)
  228. if err != nil {
  229. return "", err
  230. }
  231. defer resp.Body.Close()
  232. data, err := ioutil.ReadAll(resp.Body)
  233. if err != nil {
  234. return "", err
  235. }
  236. if resp.StatusCode != http.StatusOK {
  237. return "", errors.New(resp.Status)
  238. }
  239. return string(data), nil
  240. }
  241. // getCredentials - obtains the credentials from the IAM role name associated with
  242. // the current EC2 service.
  243. //
  244. // If the credentials cannot be found, or there is an error
  245. // reading the response an error will be returned.
  246. func getCredentials(client *http.Client, endpoint string) (ec2RoleCredRespBody, error) {
  247. if endpoint == "" {
  248. endpoint = defaultIAMRoleEndpoint
  249. }
  250. // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
  251. token, _ := fetchIMDSToken(client, endpoint)
  252. // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
  253. u, err := getIAMRoleURL(endpoint)
  254. if err != nil {
  255. return ec2RoleCredRespBody{}, err
  256. }
  257. // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
  258. roleNames, err := listRoleNames(client, u, token)
  259. if err != nil {
  260. return ec2RoleCredRespBody{}, err
  261. }
  262. if len(roleNames) == 0 {
  263. return ec2RoleCredRespBody{}, errors.New("No IAM roles attached to this EC2 service")
  264. }
  265. // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
  266. // - An instance profile can contain only one IAM role. This limit cannot be increased.
  267. roleName := roleNames[0]
  268. // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
  269. // The following command retrieves the security credentials for an
  270. // IAM role named `s3access`.
  271. //
  272. // $ curl http://169.254.169.254/latest/meta-data/iam/security-credentials/s3access
  273. //
  274. u.Path = path.Join(u.Path, roleName)
  275. req, err := http.NewRequest(http.MethodGet, u.String(), nil)
  276. if err != nil {
  277. return ec2RoleCredRespBody{}, err
  278. }
  279. if token != "" {
  280. req.Header.Add(tokenRequestHeader, token)
  281. }
  282. resp, err := client.Do(req)
  283. if err != nil {
  284. return ec2RoleCredRespBody{}, err
  285. }
  286. defer resp.Body.Close()
  287. if resp.StatusCode != http.StatusOK {
  288. return ec2RoleCredRespBody{}, errors.New(resp.Status)
  289. }
  290. respCreds := ec2RoleCredRespBody{}
  291. if err := jsoniter.NewDecoder(resp.Body).Decode(&respCreds); err != nil {
  292. return ec2RoleCredRespBody{}, err
  293. }
  294. if respCreds.Code != "Success" {
  295. // If an error code was returned something failed requesting the role.
  296. return ec2RoleCredRespBody{}, errors.New(respCreds.Message)
  297. }
  298. return respCreds, nil
  299. }
  300. // isLoopback identifies if a uri's host is on a loopback address
  301. func isLoopback(uri string) (bool, error) {
  302. u, err := url.Parse(uri)
  303. if err != nil {
  304. return false, err
  305. }
  306. host := u.Hostname()
  307. if len(host) == 0 {
  308. return false, fmt.Errorf("can't parse host from uri: %s", uri)
  309. }
  310. ips, err := net.LookupHost(host)
  311. if err != nil {
  312. return false, err
  313. }
  314. for _, ip := range ips {
  315. if !net.ParseIP(ip).IsLoopback() {
  316. return false, nil
  317. }
  318. }
  319. return true, nil
  320. }