-
Notifications
You must be signed in to change notification settings - Fork 33
Expand file tree
/
Copy pathiam.go
More file actions
202 lines (166 loc) · 5.14 KB
/
iam.go
File metadata and controls
202 lines (166 loc) · 5.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
// LICENSE BSD-2-Clause-FreeBSD
// Copyright (c) 2018, Rohan Verma <hello@rohanverma.net>
package simples3
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
const (
imdsTokenHeader = "X-aws-ec2-metadata-token"
imdsTokenTtlHeader = "X-aws-ec2-metadata-token-ttl-seconds"
metadataBaseURL = "http://169.254.169.254/latest"
securityCredentialsURI = "/meta-data/iam/security-credentials/"
imdsTokenURI = "/api/token"
defaultIMDSTokenTTL = "60"
)
// IAMResponse is used by NewUsingIAM to auto
// detect the credentials.
type IAMResponse struct {
Code string `json:"Code"`
LastUpdated string `json:"LastUpdated"`
Type string `json:"Type"`
AccessKeyID string `json:"AccessKeyId"`
SecretAccessKey string `json:"SecretAccessKey"`
Token string `json:"Token"`
Expiration time.Time `json:"Expiration"`
}
// NewUsingIAM automatically generates an Instance of S3
// using instance metatdata.
func NewUsingIAM(region string) (*S3, error) {
return newUsingIAM(
&http.Client{
// Set a timeout of 3 seconds for AWS IAM Calls.
Timeout: time.Second * 3, //nolint:gomnd
}, metadataBaseURL, region)
}
// fetchIMDSToken retrieves an IMDSv2 token from the
// EC2 instance metadata service. It returns a token and boolean,
// only if IMDSv2 is enabled in the EC2 instance metadata
// configuration, otherwise returns an error.
func fetchIMDSToken(cl *http.Client, baseURL string) (string, bool, error) {
req, err := http.NewRequest(http.MethodPut, baseURL+imdsTokenURI, nil)
if err != nil {
return "", false, err
}
// Set the token TTL to 60 seconds.
req.Header.Set(imdsTokenTtlHeader, defaultIMDSTokenTTL)
resp, err := cl.Do(req)
if err != nil {
return "", false, err
}
defer func() {
resp.Body.Close()
io.Copy(io.Discard, resp.Body)
}()
if resp.StatusCode != http.StatusOK {
return "", false, fmt.Errorf("failed to request IMDSv2 token: %s", resp.Status)
}
token, err := io.ReadAll(resp.Body)
if err != nil {
return "", false, err
}
return string(token), true, nil
}
// fetchIAMData fetches the IAM data from the given URL.
// In case of a normal AWS setup, baseURL would be metadataBaseURL.
// You can use this method, to manually fetch IAM data from a custom
// endpoint and pass it to SetIAMData.
func fetchIAMData(cl *http.Client, baseURL string) (IAMResponse, error) {
token, useIMDSv2, err := fetchIMDSToken(cl, baseURL)
if err != nil {
return IAMResponse{}, fmt.Errorf("error fetching IMDSv2 token: %w", err)
}
url := baseURL + securityCredentialsURI
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return IAMResponse{}, fmt.Errorf("error creating imdsv2 token request: %w", err)
}
if useIMDSv2 {
req.Header.Set(imdsTokenHeader, token)
}
resp, err := cl.Do(req)
if err != nil {
return IAMResponse{}, err
}
if resp.StatusCode != http.StatusOK {
return IAMResponse{}, fmt.Errorf("error fetching IAM data: %s", resp.Status)
}
role, err := io.ReadAll(resp.Body)
if err != nil {
return IAMResponse{}, err
}
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
req, err = http.NewRequest(http.MethodGet, url+string(role), nil)
if err != nil {
return IAMResponse{}, fmt.Errorf("error creating role request: %w", err)
}
if useIMDSv2 {
req.Header.Set(imdsTokenHeader, token)
}
resp, err = cl.Do(req)
if err != nil {
return IAMResponse{}, fmt.Errorf("error fetching role data: %w", err)
}
defer func() {
// Drain and close the body to let the Transport reuse the connection
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
return IAMResponse{}, fmt.Errorf("error fetching role data, got non 200 code: %s", resp.Status)
}
var jResp IAMResponse
jsonString, err := io.ReadAll(resp.Body)
if err != nil {
return IAMResponse{}, fmt.Errorf("error reading role data: %w", err)
}
if err := json.Unmarshal(jsonString, &jResp); err != nil {
return IAMResponse{}, fmt.Errorf("error unmarshalling role data: %w (%s)", err, jsonString)
}
return jResp, nil
}
func newUsingIAM(cl *http.Client, baseURL, region string) (*S3, error) {
// Get the IAM role
iamResp, err := fetchIAMData(cl, baseURL)
if err != nil {
return nil, fmt.Errorf("error fetching IAM data: %w", err)
}
return &S3{
Region: region,
AccessKey: iamResp.AccessKeyID,
SecretKey: iamResp.SecretAccessKey,
Token: iamResp.Token,
URIFormat: "https://s3.%s.amazonaws.com/%s",
initMode: "iam",
expiry: iamResp.Expiration,
}, nil
}
// setIAMData sets the IAM data on the S3 instance.
func (s3 *S3) SetIAMData(iamResp IAMResponse) {
s3.AccessKey = iamResp.AccessKeyID
s3.SecretKey = iamResp.SecretAccessKey
s3.Token = iamResp.Token
}
func (s3 *S3) renewIAMToken() error {
if s3.initMode != "iam" {
return nil
}
if time.Since(s3.expiry) < 0 {
return nil
}
s3.mu.Lock()
defer s3.mu.Unlock()
iamResp, err := fetchIAMData(s3.getClient(), metadataBaseURL)
if err != nil {
return fmt.Errorf("error fetching IAM data: %w", err)
}
s3.expiry = iamResp.Expiration
s3.Token = iamResp.Token
s3.AccessKey = iamResp.AccessKeyID
s3.SecretKey = iamResp.SecretAccessKey
return nil
}