master
go 344 lines 12.2 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package aws
4
5 import (
6 "context"
7 "crypto/hmac"
8 "crypto/sha256"
9 "encoding/hex"
10 "encoding/json"
11 "fmt"
12 "io"
13 "net/http"
14 "net/url"
15 "sort"
16 "strings"
17 "time"
18
19 "github.com/netdata/netdata/go/plugins/logger"
20 "github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore"
21 "github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore/internal/envx"
22 "github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore/internal/httpx"
23 )
24
25 func (s *publishedStore) Resolve(ctx context.Context, req secretstore.ResolveRequest) (string, error) {
26 return s.resolve(ctx, req)
27 }
28
29 func (s *publishedStore) resolve(ctx context.Context, req secretstore.ResolveRequest) (string, error) {
30 secretName, jsonKey, _ := strings.Cut(req.Operand, "#")
31 if secretName == "" {
32 return "", fmt.Errorf("resolving secret '%s': store '%s': secret name is empty", req.Original, req.StoreKey)
33 }
34
35 creds, err := s.credentials(ctx)
36 if err != nil {
37 return "", fmt.Errorf("resolving secret '%s': store '%s': %w", req.Original, req.StoreKey, err)
38 }
39
40 region, err := s.region()
41 if err != nil {
42 return "", fmt.Errorf("resolving secret '%s': store '%s': %w", req.Original, req.StoreKey, err)
43 }
44
45 secretString, err := s.secretValue(ctx, creds, region, secretName, req.Original)
46 if err != nil {
47 return "", fmt.Errorf("resolving secret '%s': store '%s': %w", req.Original, req.StoreKey, err)
48 }
49
50 if jsonKey == "" {
51 logResolvedRequest(ctx, req, secretName, "")
52 return secretString, nil
53 }
54
55 var parsed map[string]any
56 if err := json.Unmarshal([]byte(secretString), &parsed); err != nil {
57 return "", fmt.Errorf("resolving secret '%s': store '%s': parsing SecretString as JSON: %w", req.Original, req.StoreKey, err)
58 }
59 val, ok := parsed[jsonKey]
60 if !ok {
61 return "", fmt.Errorf("resolving secret '%s': store '%s': key '%s' not found in SecretString JSON", req.Original, req.StoreKey, jsonKey)
62 }
63 if value, ok := val.(string); ok {
64 logResolvedRequest(ctx, req, secretName, jsonKey)
65 return value, nil
66 }
67 b, err := json.Marshal(val)
68 if err != nil {
69 return "", fmt.Errorf("resolving secret '%s': store '%s': encoding value for key '%s': %w", req.Original, req.StoreKey, jsonKey, err)
70 }
71 logResolvedRequest(ctx, req, secretName, jsonKey)
72 return string(b), nil
73 }
74
75 func logResolvedRequest(ctx context.Context, req secretstore.ResolveRequest, secretName, jsonKey string) {
76 log, ok := logger.LoggerFromContext(ctx)
77 if !ok {
78 return
79 }
80 if jsonKey == "" {
81 log.Infof("resolved secret via aws-sm secretstore '%s' secret '%s'", req.StoreKey, secretName)
82 return
83 }
84 log.Infof("resolved secret via aws-sm secretstore '%s' secret '%s' key '%s'", req.StoreKey, secretName, jsonKey)
85 }
86
87 func (s *publishedStore) region() (string, error) {
88 if s.regionValue == "" {
89 return "", fmt.Errorf("region is required")
90 }
91 return s.regionValue, nil
92 }
93
94 func (s *publishedStore) credentials(ctx context.Context) (*credentials, error) {
95 switch s.mode {
96 case "env":
97 return envCredentials()
98 case "ecs":
99 uri, ok := envx.Lookup("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
100 if !ok || uri == "" {
101 return nil, fmt.Errorf("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI is not set")
102 }
103 return s.ecsCredentials(ctx, uri)
104 case "imds":
105 return s.imdsCredentials(ctx)
106 default:
107 return nil, fmt.Errorf("auth_mode '%s' is invalid for aws-sm", s.mode)
108 }
109 }
110
111 func envCredentials() (*credentials, error) {
112 ak, ok := envx.Lookup("AWS_ACCESS_KEY_ID")
113 if !ok || ak == "" {
114 return nil, fmt.Errorf("AWS_ACCESS_KEY_ID is not set")
115 }
116 sk, ok := envx.Lookup("AWS_SECRET_ACCESS_KEY")
117 if !ok || sk == "" {
118 return nil, fmt.Errorf("AWS_SECRET_ACCESS_KEY is not set")
119 }
120 token, _ := envx.Lookup("AWS_SESSION_TOKEN")
121 return &credentials{accessKeyID: ak, secretAccessKey: sk, sessionToken: token}, nil
122 }
123
124 func (s *publishedStore) ecsCredentials(ctx context.Context, relativeURI string) (*credentials, error) {
125 req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://169.254.170.2"+relativeURI, nil)
126 if err != nil {
127 return nil, fmt.Errorf("creating ECS credentials request: %w", err)
128 }
129 resp, err := s.runtime.imdsClient.Do(req)
130 if err != nil {
131 return nil, fmt.Errorf("ECS credentials request failed: %w", err)
132 }
133 defer resp.Body.Close()
134 body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
135 if err != nil {
136 return nil, fmt.Errorf("reading ECS credentials response: %w", err)
137 }
138 if resp.StatusCode != http.StatusOK {
139 return nil, fmt.Errorf("ECS credentials returned HTTP %d: %s", resp.StatusCode, httpx.TruncateBody(body))
140 }
141 var result struct {
142 AccessKeyID string `json:"AccessKeyId"`
143 SecretAccessKey string `json:"SecretAccessKey"`
144 Token string `json:"Token"`
145 }
146 if err := json.Unmarshal(body, &result); err != nil {
147 return nil, fmt.Errorf("parsing ECS credentials response: %w", err)
148 }
149 if result.AccessKeyID == "" || result.SecretAccessKey == "" {
150 return nil, fmt.Errorf("ECS credentials response missing required fields")
151 }
152 return &credentials{
153 accessKeyID: result.AccessKeyID,
154 secretAccessKey: result.SecretAccessKey,
155 sessionToken: result.Token,
156 }, nil
157 }
158
159 func (s *publishedStore) imdsCredentials(ctx context.Context) (*credentials, error) {
160 tokenReq, err := http.NewRequestWithContext(ctx, http.MethodPut, "http://169.254.169.254/latest/api/token", nil)
161 if err != nil {
162 return nil, fmt.Errorf("creating IMDS token request: %w", err)
163 }
164 tokenReq.Header.Set("X-aws-ec2-metadata-token-ttl-seconds", "21600")
165 tokenResp, err := s.runtime.imdsClient.Do(tokenReq)
166 if err != nil {
167 return nil, fmt.Errorf("IMDS token request failed: %w", err)
168 }
169 defer tokenResp.Body.Close()
170 tokenBody, err := io.ReadAll(io.LimitReader(tokenResp.Body, 1<<20))
171 if err != nil {
172 return nil, fmt.Errorf("reading IMDS token response: %w", err)
173 }
174 if tokenResp.StatusCode != http.StatusOK {
175 return nil, fmt.Errorf("IMDS token request returned HTTP %d", tokenResp.StatusCode)
176 }
177 imdsToken := strings.TrimSpace(string(tokenBody))
178
179 roleReq, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://169.254.169.254/latest/meta-data/iam/security-credentials/", nil)
180 if err != nil {
181 return nil, fmt.Errorf("creating IMDS role request: %w", err)
182 }
183 roleReq.Header.Set("X-aws-ec2-metadata-token", imdsToken)
184 roleResp, err := s.runtime.imdsClient.Do(roleReq)
185 if err != nil {
186 return nil, fmt.Errorf("IMDS role request failed: %w", err)
187 }
188 defer roleResp.Body.Close()
189 roleBody, err := io.ReadAll(io.LimitReader(roleResp.Body, 1<<20))
190 if err != nil {
191 return nil, fmt.Errorf("reading IMDS role response: %w", err)
192 }
193 if roleResp.StatusCode != http.StatusOK {
194 return nil, fmt.Errorf("IMDS role request returned HTTP %d", roleResp.StatusCode)
195 }
196 role := strings.TrimSpace(string(roleBody))
197 if role == "" {
198 return nil, fmt.Errorf("IMDS returned empty role name")
199 }
200
201 credReq, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://169.254.169.254/latest/meta-data/iam/security-credentials/"+role, nil)
202 if err != nil {
203 return nil, fmt.Errorf("creating IMDS credentials request: %w", err)
204 }
205 credReq.Header.Set("X-aws-ec2-metadata-token", imdsToken)
206 credResp, err := s.runtime.imdsClient.Do(credReq)
207 if err != nil {
208 return nil, fmt.Errorf("IMDS credentials request failed: %w", err)
209 }
210 defer credResp.Body.Close()
211 credBody, err := io.ReadAll(io.LimitReader(credResp.Body, 1<<20))
212 if err != nil {
213 return nil, fmt.Errorf("reading IMDS credentials response: %w", err)
214 }
215 if credResp.StatusCode != http.StatusOK {
216 return nil, fmt.Errorf("IMDS credentials request returned HTTP %d", credResp.StatusCode)
217 }
218 var result struct {
219 AccessKeyID string `json:"AccessKeyId"`
220 SecretAccessKey string `json:"SecretAccessKey"`
221 Token string `json:"Token"`
222 }
223 if err := json.Unmarshal(credBody, &result); err != nil {
224 return nil, fmt.Errorf("parsing IMDS credentials response: %w", err)
225 }
226 if result.AccessKeyID == "" || result.SecretAccessKey == "" {
227 return nil, fmt.Errorf("IMDS credentials response missing required fields")
228 }
229 return &credentials{accessKeyID: result.AccessKeyID, secretAccessKey: result.SecretAccessKey, sessionToken: result.Token}, nil
230 }
231
232 func (s *publishedStore) secretValue(ctx context.Context, creds *credentials, region, secretName, original string) (string, error) {
233 host := secretsManagerHost(region)
234 endpoint := (&url.URL{
235 Scheme: "https",
236 Host: host,
237 Path: "/",
238 }).String()
239 secretIDJSON, err := json.Marshal(secretName)
240 if err != nil {
241 return "", fmt.Errorf("resolving secret '%s': encoding secret name: %w", original, err)
242 }
243 payload := `{"SecretId":` + string(secretIDJSON) + `}`
244 now := time.Now().UTC()
245 timestamp := now.Format("20060102T150405Z")
246 datestamp := now.Format("20060102")
247 headers := map[string]string{
248 "host": host,
249 "x-amz-date": timestamp,
250 "x-amz-target": "secretsmanager.GetSecretValue",
251 "content-type": "application/x-amz-json-1.1",
252 }
253 if creds.sessionToken != "" {
254 headers["x-amz-security-token"] = creds.sessionToken
255 }
256 authHeader := sigV4Sign("POST", "/", "", headers, payload, creds, region, datestamp, timestamp)
257 httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(payload))
258 if err != nil {
259 return "", fmt.Errorf("resolving secret '%s': creating request: %w", original, err)
260 }
261 for k, v := range headers {
262 httpReq.Header.Set(k, v)
263 }
264 httpReq.Host = host
265 httpReq.Header.Set("Authorization", authHeader)
266 resp, err := s.runtime.apiClient.Do(httpReq)
267 if err != nil {
268 return "", fmt.Errorf("resolving secret '%s': request failed: %w", original, err)
269 }
270 defer resp.Body.Close()
271 body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
272 if err != nil {
273 return "", fmt.Errorf("resolving secret '%s': reading response: %w", original, err)
274 }
275 if resp.StatusCode != http.StatusOK {
276 return "", fmt.Errorf("resolving secret '%s': AWS Secrets Manager returned HTTP %d: %s", original, resp.StatusCode, httpx.TruncateBody(body))
277 }
278 var result struct {
279 SecretString *string `json:"SecretString"`
280 }
281 if err := json.Unmarshal(body, &result); err != nil {
282 return "", fmt.Errorf("resolving secret '%s': parsing response: %w", original, err)
283 }
284 if result.SecretString == nil {
285 return "", fmt.Errorf("resolving secret '%s': SecretString is empty (binary secrets are not supported)", original)
286 }
287 return *result.SecretString, nil
288 }
289
290 func secretsManagerHost(region string) string {
291 suffix := "amazonaws.com"
292 if strings.HasPrefix(region, "cn-") {
293 suffix = "amazonaws.com.cn"
294 }
295 return fmt.Sprintf("secretsmanager.%s.%s", region, suffix)
296 }
297
298 func sigV4Sign(method, uri, query string, headers map[string]string, payload string, creds *credentials, region, datestamp, timestamp string) string {
299 canonicalHeaders, signedHeaders := canonicalHeaders(headers)
300 payloadHash := sha256Hex([]byte(payload))
301 canonicalRequest := strings.Join([]string{method, uri, query, canonicalHeaders, signedHeaders, payloadHash}, "\n")
302 scope := datestamp + "/" + region + "/secretsmanager/aws4_request"
303 stringToSign := strings.Join([]string{"AWS4-HMAC-SHA256", timestamp, scope, sha256Hex([]byte(canonicalRequest))}, "\n")
304 signingKey := deriveSigningKey(creds.secretAccessKey, datestamp, region)
305 signature := hex.EncodeToString(hmacSHA256(signingKey, []byte(stringToSign)))
306 return fmt.Sprintf("AWS4-HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s", creds.accessKeyID, scope, signedHeaders, signature)
307 }
308
309 func canonicalHeaders(headers map[string]string) (string, string) {
310 norm := make(map[string]string, len(headers))
311 keys := make([]string, 0, len(headers))
312 for k, v := range headers {
313 lk := strings.ToLower(k)
314 norm[lk] = v
315 keys = append(keys, lk)
316 }
317 sort.Strings(keys)
318 var canonical strings.Builder
319 for _, k := range keys {
320 canonical.WriteString(k)
321 canonical.WriteByte(':')
322 canonical.WriteString(strings.TrimSpace(norm[k]))
323 canonical.WriteByte('\n')
324 }
325 return canonical.String(), strings.Join(keys, ";")
326 }
327
328 func deriveSigningKey(secretKey, datestamp, region string) []byte {
329 kDate := hmacSHA256([]byte("AWS4"+secretKey), []byte(datestamp))
330 kRegion := hmacSHA256(kDate, []byte(region))
331 kService := hmacSHA256(kRegion, []byte("secretsmanager"))
332 return hmacSHA256(kService, []byte("aws4_request"))
333 }
334
335 func hmacSHA256(key, data []byte) []byte {
336 h := hmac.New(sha256.New, key)
337 _, _ = h.Write(data)
338 return h.Sum(nil)
339 }
340
341 func sha256Hex(data []byte) string {
342 sum := sha256.Sum256(data)
343 return hex.EncodeToString(sum[:])
344 }