aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/github.com/minio/minio-go/v7/pkg/credentials/iam_aws.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/minio/minio-go/v7/pkg/credentials/iam_aws.go')
-rw-r--r--vendor/github.com/minio/minio-go/v7/pkg/credentials/iam_aws.go433
1 files changed, 433 insertions, 0 deletions
diff --git a/vendor/github.com/minio/minio-go/v7/pkg/credentials/iam_aws.go b/vendor/github.com/minio/minio-go/v7/pkg/credentials/iam_aws.go
new file mode 100644
index 0000000..c5153c4
--- /dev/null
+++ b/vendor/github.com/minio/minio-go/v7/pkg/credentials/iam_aws.go
@@ -0,0 +1,433 @@
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
18package credentials
19
20import (
21 "bufio"
22 "context"
23 "errors"
24 "fmt"
25 "io"
26 "net"
27 "net/http"
28 "net/url"
29 "os"
30 "path"
31 "strings"
32 "time"
33
34 jsoniter "github.com/json-iterator/go"
35)
36
37// DefaultExpiryWindow - Default expiry window.
38// ExpiryWindow will allow the credentials to trigger refreshing
39// prior to the credentials actually expiring. This is beneficial
40// so race conditions with expiring credentials do not cause
41// request to fail unexpectedly due to ExpiredTokenException exceptions.
42// DefaultExpiryWindow can be used as parameter to (*Expiry).SetExpiration.
43// When used the tokens refresh will be triggered when 80% of the elapsed
44// time until the actual expiration time is passed.
45const DefaultExpiryWindow = -1
46
47// A IAM retrieves credentials from the EC2 service, and keeps track if
48// those credentials are expired.
49type IAM struct {
50 Expiry
51
52 // Required http Client to use when connecting to IAM metadata service.
53 Client *http.Client
54
55 // Custom endpoint to fetch IAM role credentials.
56 Endpoint string
57
58 // Region configurable custom region for STS
59 Region string
60
61 // Support for container authorization token https://docs.aws.amazon.com/sdkref/latest/guide/feature-container-credentials.html
62 Container struct {
63 AuthorizationToken string
64 CredentialsFullURI string
65 CredentialsRelativeURI string
66 }
67
68 // EKS based k8s RBAC authorization - https://docs.aws.amazon.com/eks/latest/userguide/pod-configuration.html
69 EKSIdentity struct {
70 TokenFile string
71 RoleARN string
72 RoleSessionName string
73 }
74}
75
76// IAM Roles for Amazon EC2
77// http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
78const (
79 DefaultIAMRoleEndpoint = "http://169.254.169.254"
80 DefaultECSRoleEndpoint = "http://169.254.170.2"
81 DefaultSTSRoleEndpoint = "https://sts.amazonaws.com"
82 DefaultIAMSecurityCredsPath = "/latest/meta-data/iam/security-credentials/"
83 TokenRequestTTLHeader = "X-aws-ec2-metadata-token-ttl-seconds"
84 TokenPath = "/latest/api/token"
85 TokenTTL = "21600"
86 TokenRequestHeader = "X-aws-ec2-metadata-token"
87)
88
89// NewIAM returns a pointer to a new Credentials object wrapping the IAM.
90func NewIAM(endpoint string) *Credentials {
91 return New(&IAM{
92 Client: &http.Client{
93 Transport: http.DefaultTransport,
94 },
95 Endpoint: endpoint,
96 })
97}
98
99// Retrieve retrieves credentials from the EC2 service.
100// Error will be returned if the request fails, or unable to extract
101// the desired
102func (m *IAM) Retrieve() (Value, error) {
103 token := os.Getenv("AWS_CONTAINER_AUTHORIZATION_TOKEN")
104 if token == "" {
105 token = m.Container.AuthorizationToken
106 }
107
108 relativeURI := os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
109 if relativeURI == "" {
110 relativeURI = m.Container.CredentialsRelativeURI
111 }
112
113 fullURI := os.Getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI")
114 if fullURI == "" {
115 fullURI = m.Container.CredentialsFullURI
116 }
117
118 identityFile := os.Getenv("AWS_WEB_IDENTITY_TOKEN_FILE")
119 if identityFile == "" {
120 identityFile = m.EKSIdentity.TokenFile
121 }
122
123 roleArn := os.Getenv("AWS_ROLE_ARN")
124 if roleArn == "" {
125 roleArn = m.EKSIdentity.RoleARN
126 }
127
128 roleSessionName := os.Getenv("AWS_ROLE_SESSION_NAME")
129 if roleSessionName == "" {
130 roleSessionName = m.EKSIdentity.RoleSessionName
131 }
132
133 region := os.Getenv("AWS_REGION")
134 if region == "" {
135 region = m.Region
136 }
137
138 var roleCreds ec2RoleCredRespBody
139 var err error
140
141 endpoint := m.Endpoint
142 switch {
143 case identityFile != "":
144 if len(endpoint) == 0 {
145 if region != "" {
146 if strings.HasPrefix(region, "cn-") {
147 endpoint = "https://sts." + region + ".amazonaws.com.cn"
148 } else {
149 endpoint = "https://sts." + region + ".amazonaws.com"
150 }
151 } else {
152 endpoint = DefaultSTSRoleEndpoint
153 }
154 }
155
156 creds := &STSWebIdentity{
157 Client: m.Client,
158 STSEndpoint: endpoint,
159 GetWebIDTokenExpiry: func() (*WebIdentityToken, error) {
160 token, err := os.ReadFile(identityFile)
161 if err != nil {
162 return nil, err
163 }
164
165 return &WebIdentityToken{Token: string(token)}, nil
166 },
167 RoleARN: roleArn,
168 roleSessionName: roleSessionName,
169 }
170
171 stsWebIdentityCreds, err := creds.Retrieve()
172 if err == nil {
173 m.SetExpiration(creds.Expiration(), DefaultExpiryWindow)
174 }
175 return stsWebIdentityCreds, err
176
177 case relativeURI != "":
178 if len(endpoint) == 0 {
179 endpoint = fmt.Sprintf("%s%s", DefaultECSRoleEndpoint, relativeURI)
180 }
181
182 roleCreds, err = getEcsTaskCredentials(m.Client, endpoint, token)
183
184 case fullURI != "":
185 if len(endpoint) == 0 {
186 endpoint = fullURI
187 var ok bool
188 if ok, err = isLoopback(endpoint); !ok {
189 if err == nil {
190 err = fmt.Errorf("uri host is not a loopback address: %s", endpoint)
191 }
192 break
193 }
194 }
195
196 roleCreds, err = getEcsTaskCredentials(m.Client, endpoint, token)
197
198 default:
199 roleCreds, err = getCredentials(m.Client, endpoint)
200 }
201
202 if err != nil {
203 return Value{}, err
204 }
205 // Expiry window is set to 10secs.
206 m.SetExpiration(roleCreds.Expiration, DefaultExpiryWindow)
207
208 return Value{
209 AccessKeyID: roleCreds.AccessKeyID,
210 SecretAccessKey: roleCreds.SecretAccessKey,
211 SessionToken: roleCreds.Token,
212 SignerType: SignatureV4,
213 }, nil
214}
215
216// A ec2RoleCredRespBody provides the shape for unmarshaling credential
217// request responses.
218type ec2RoleCredRespBody struct {
219 // Success State
220 Expiration time.Time
221 AccessKeyID string
222 SecretAccessKey string
223 Token string
224
225 // Error state
226 Code string
227 Message string
228
229 // Unused params.
230 LastUpdated time.Time
231 Type string
232}
233
234// Get the final IAM role URL where the request will
235// be sent to fetch the rolling access credentials.
236// http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
237func getIAMRoleURL(endpoint string) (*url.URL, error) {
238 u, err := url.Parse(endpoint)
239 if err != nil {
240 return nil, err
241 }
242 u.Path = DefaultIAMSecurityCredsPath
243 return u, nil
244}
245
246// listRoleNames lists of credential role names associated
247// with the current EC2 service. If there are no credentials,
248// or there is an error making or receiving the request.
249// http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
250func listRoleNames(client *http.Client, u *url.URL, token string) ([]string, error) {
251 req, err := http.NewRequest(http.MethodGet, u.String(), nil)
252 if err != nil {
253 return nil, err
254 }
255 if token != "" {
256 req.Header.Add(TokenRequestHeader, token)
257 }
258 resp, err := client.Do(req)
259 if err != nil {
260 return nil, err
261 }
262 defer resp.Body.Close()
263 if resp.StatusCode != http.StatusOK {
264 return nil, errors.New(resp.Status)
265 }
266
267 credsList := []string{}
268 s := bufio.NewScanner(resp.Body)
269 for s.Scan() {
270 credsList = append(credsList, s.Text())
271 }
272
273 if err := s.Err(); err != nil {
274 return nil, err
275 }
276
277 return credsList, nil
278}
279
280func getEcsTaskCredentials(client *http.Client, endpoint, token string) (ec2RoleCredRespBody, error) {
281 req, err := http.NewRequest(http.MethodGet, endpoint, nil)
282 if err != nil {
283 return ec2RoleCredRespBody{}, err
284 }
285
286 if token != "" {
287 req.Header.Set("Authorization", token)
288 }
289
290 resp, err := client.Do(req)
291 if err != nil {
292 return ec2RoleCredRespBody{}, err
293 }
294 defer resp.Body.Close()
295 if resp.StatusCode != http.StatusOK {
296 return ec2RoleCredRespBody{}, errors.New(resp.Status)
297 }
298
299 respCreds := ec2RoleCredRespBody{}
300 if err := jsoniter.NewDecoder(resp.Body).Decode(&respCreds); err != nil {
301 return ec2RoleCredRespBody{}, err
302 }
303
304 return respCreds, nil
305}
306
307func fetchIMDSToken(client *http.Client, endpoint string) (string, error) {
308 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
309 defer cancel()
310
311 req, err := http.NewRequestWithContext(ctx, http.MethodPut, endpoint+TokenPath, nil)
312 if err != nil {
313 return "", err
314 }
315 req.Header.Add(TokenRequestTTLHeader, TokenTTL)
316 resp, err := client.Do(req)
317 if err != nil {
318 return "", err
319 }
320 defer resp.Body.Close()
321 data, err := io.ReadAll(resp.Body)
322 if err != nil {
323 return "", err
324 }
325 if resp.StatusCode != http.StatusOK {
326 return "", errors.New(resp.Status)
327 }
328 return string(data), nil
329}
330
331// getCredentials - obtains the credentials from the IAM role name associated with
332// the current EC2 service.
333//
334// If the credentials cannot be found, or there is an error
335// reading the response an error will be returned.
336func getCredentials(client *http.Client, endpoint string) (ec2RoleCredRespBody, error) {
337 if endpoint == "" {
338 endpoint = DefaultIAMRoleEndpoint
339 }
340
341 // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
342 token, err := fetchIMDSToken(client, endpoint)
343 if err != nil {
344 // Return only errors for valid situations, if the IMDSv2 is not enabled
345 // we will not be able to get the token, in such a situation we have
346 // to rely on IMDSv1 behavior as a fallback, this check ensures that.
347 // Refer https://github.com/minio/minio-go/issues/1866
348 if !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, context.Canceled) {
349 return ec2RoleCredRespBody{}, err
350 }
351 }
352
353 // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
354 u, err := getIAMRoleURL(endpoint)
355 if err != nil {
356 return ec2RoleCredRespBody{}, err
357 }
358
359 // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
360 roleNames, err := listRoleNames(client, u, token)
361 if err != nil {
362 return ec2RoleCredRespBody{}, err
363 }
364
365 if len(roleNames) == 0 {
366 return ec2RoleCredRespBody{}, errors.New("No IAM roles attached to this EC2 service")
367 }
368
369 // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
370 // - An instance profile can contain only one IAM role. This limit cannot be increased.
371 roleName := roleNames[0]
372
373 // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
374 // The following command retrieves the security credentials for an
375 // IAM role named `s3access`.
376 //
377 // $ curl http://169.254.169.254/latest/meta-data/iam/security-credentials/s3access
378 //
379 u.Path = path.Join(u.Path, roleName)
380 req, err := http.NewRequest(http.MethodGet, u.String(), nil)
381 if err != nil {
382 return ec2RoleCredRespBody{}, err
383 }
384 if token != "" {
385 req.Header.Add(TokenRequestHeader, token)
386 }
387
388 resp, err := client.Do(req)
389 if err != nil {
390 return ec2RoleCredRespBody{}, err
391 }
392 defer resp.Body.Close()
393 if resp.StatusCode != http.StatusOK {
394 return ec2RoleCredRespBody{}, errors.New(resp.Status)
395 }
396
397 respCreds := ec2RoleCredRespBody{}
398 if err := jsoniter.NewDecoder(resp.Body).Decode(&respCreds); err != nil {
399 return ec2RoleCredRespBody{}, err
400 }
401
402 if respCreds.Code != "Success" {
403 // If an error code was returned something failed requesting the role.
404 return ec2RoleCredRespBody{}, errors.New(respCreds.Message)
405 }
406
407 return respCreds, nil
408}
409
410// isLoopback identifies if a uri's host is on a loopback address
411func isLoopback(uri string) (bool, error) {
412 u, err := url.Parse(uri)
413 if err != nil {
414 return false, err
415 }
416
417 host := u.Hostname()
418 if len(host) == 0 {
419 return false, fmt.Errorf("can't parse host from uri: %s", uri)
420 }
421
422 ips, err := net.LookupHost(host)
423 if err != nil {
424 return false, err
425 }
426 for _, ip := range ips {
427 if !net.ParseIP(ip).IsLoopback() {
428 return false, nil
429 }
430 }
431
432 return true, nil
433}