master
go 207 lines 6.42 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package vault
4
5 import (
6 "context"
7 "encoding/json"
8 "fmt"
9 "io"
10 "net/http"
11 "os"
12 "strings"
13
14 "github.com/netdata/netdata/go/plugins/logger"
15 "github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore"
16 "github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore/internal/httpx"
17 )
18
19 func (s *publishedStore) Resolve(ctx context.Context, req secretstore.ResolveRequest) (string, error) {
20 return s.resolve(ctx, req)
21 }
22
23 func (s *publishedStore) resolve(ctx context.Context, req secretstore.ResolveRequest) (string, error) {
24 path, key, ok := strings.Cut(req.Operand, "#")
25 if !ok || key == "" {
26 return "", fmt.Errorf("resolving secret '%s': store '%s': operand must be in format 'path#key'", req.Original, req.StoreKey)
27 }
28 if path == "" {
29 return "", fmt.Errorf("resolving secret '%s': store '%s': vault path is empty", req.Original, req.StoreKey)
30 }
31 if strings.Contains(path, "..") || strings.ContainsAny(path, "?#") {
32 return "", fmt.Errorf("resolving secret '%s': store '%s': vault path contains invalid characters", req.Original, req.StoreKey)
33 }
34
35 addr, err := s.address()
36 if err != nil {
37 return "", fmt.Errorf("resolving secret '%s': store '%s': %w", req.Original, req.StoreKey, err)
38 }
39
40 token, err := s.token()
41 if err != nil {
42 return "", fmt.Errorf("resolving secret '%s': store '%s': %w", req.Original, req.StoreKey, err)
43 }
44
45 httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(addr, "/")+"/v1/"+path, nil)
46 if err != nil {
47 return "", fmt.Errorf("resolving secret '%s': store '%s': %w", req.Original, req.StoreKey, err)
48 }
49 httpReq.Header.Set("X-Vault-Token", token)
50 if ns, ok := s.namespace(); ok {
51 httpReq.Header.Set("X-Vault-Namespace", ns)
52 }
53
54 client := s.runtime.httpClient
55 if s.skipVerify() {
56 client = s.runtime.httpClientInsecure
57 }
58
59 resp, err := client.Do(httpReq)
60 if err != nil {
61 return "", fmt.Errorf("resolving secret '%s': store '%s': vault request failed: %w", req.Original, req.StoreKey, err)
62 }
63 defer resp.Body.Close()
64
65 body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
66 if err != nil {
67 return "", fmt.Errorf("resolving secret '%s': store '%s': reading vault response: %w", req.Original, req.StoreKey, err)
68 }
69 if resp.StatusCode != http.StatusOK {
70 return "", fmt.Errorf("resolving secret '%s': store '%s': vault returned HTTP %d: %s", req.Original, req.StoreKey, resp.StatusCode, httpx.TruncateBody(body))
71 }
72 value, err := parseResponse(body, key, req)
73 if err != nil {
74 return "", err
75 }
76 logResolvedRequest(ctx, req, path, key)
77 return value, nil
78 }
79
80 func logResolvedRequest(ctx context.Context, req secretstore.ResolveRequest, path, key string) {
81 if log, ok := logger.LoggerFromContext(ctx); ok {
82 log.Infof("resolved secret via vault secretstore '%s' path '%s' key '%s'", req.StoreKey, path, key)
83 }
84 }
85
86 func (s *publishedStore) address() (string, error) {
87 if s.addr == "" {
88 return "", fmt.Errorf("addr is required")
89 }
90 return s.addr, nil
91 }
92
93 func (s *publishedStore) namespace() (string, bool) {
94 if s.namespaceValue == "" {
95 return "", false
96 }
97 return s.namespaceValue, true
98 }
99
100 func (s *publishedStore) skipVerify() bool {
101 return s.tlsSkipVerify
102 }
103
104 func (s *publishedStore) token() (string, error) {
105 switch s.mode {
106 case "token":
107 if s.tokenValue == "" {
108 return "", fmt.Errorf("mode_token.token is required")
109 }
110 return s.tokenValue, nil
111 case "token_file":
112 path := s.tokenFilePath
113 if path == "" {
114 return "", fmt.Errorf("mode_token_file.path is required")
115 }
116 data, err := os.ReadFile(path)
117 if err != nil {
118 return "", fmt.Errorf("cannot read token file '%s': %w", path, err)
119 }
120 token := strings.TrimSpace(string(data))
121 if token == "" {
122 return "", fmt.Errorf("token file '%s' is empty", path)
123 }
124 return token, nil
125 default:
126 return "", fmt.Errorf("mode '%s' is invalid for vault", s.mode)
127 }
128 }
129
130 func parseResponse(body []byte, key string, req secretstore.ResolveRequest) (string, error) {
131 var resp vaultReadResponse
132 if err := json.Unmarshal(body, &resp); err != nil {
133 return "", fmt.Errorf("resolving secret '%s': store '%s': parsing vault response: %w", req.Original, req.StoreKey, err)
134 }
135
136 var kvV2 vaultKV2ReadPayload
137 if err := json.Unmarshal(resp.Data, &kvV2); err != nil {
138 return "", fmt.Errorf("resolving secret '%s': store '%s': parsing vault response data: %w", req.Original, req.StoreKey, err)
139 }
140 if kvV2.isDetected() {
141 data, err := unmarshalVaultDataPayload(kvV2.Data)
142 if err != nil {
143 return "", fmt.Errorf("resolving secret '%s': store '%s': parsing vault response data: %w", req.Original, req.StoreKey, err)
144 }
145 if val, ok := data[key]; ok {
146 return valueToString(val)
147 }
148 return "", fmt.Errorf("resolving secret '%s': store '%s': key '%s' not found in vault response", req.Original, req.StoreKey, key)
149 }
150
151 payload, err := unmarshalVaultDataPayload(resp.Data)
152 if err != nil {
153 return "", fmt.Errorf("resolving secret '%s': store '%s': parsing vault response data: %w", req.Original, req.StoreKey, err)
154 }
155 if val, ok := payload[key]; ok {
156 return valueToString(val)
157 }
158
159 return "", fmt.Errorf("resolving secret '%s': store '%s': key '%s' not found in vault response", req.Original, req.StoreKey, key)
160 }
161
162 type vaultReadResponse struct {
163 Data json.RawMessage `json:"data"`
164 }
165
166 type vaultKV2ReadPayload struct {
167 Data json.RawMessage `json:"data"`
168 Metadata *vaultKV2Metadata `json:"metadata"`
169 }
170
171 type vaultKV2Metadata struct {
172 CreatedTime *string `json:"created_time"`
173 DeletionTime *string `json:"deletion_time"`
174 Destroyed *bool `json:"destroyed"`
175 Version *int `json:"version"`
176 }
177
178 func (p vaultKV2ReadPayload) isDetected() bool {
179 // KV v2 read responses expose secret values under data.data and include
180 // version metadata alongside it. Requiring the standard metadata fields
181 // avoids misclassifying KV v1 secrets that happen to have top-level
182 // keys named "data" and "metadata".
183 return len(p.Data) != 0 && p.Metadata != nil &&
184 p.Metadata.CreatedTime != nil &&
185 p.Metadata.DeletionTime != nil &&
186 p.Metadata.Destroyed != nil &&
187 p.Metadata.Version != nil
188 }
189
190 func unmarshalVaultDataPayload(data json.RawMessage) (map[string]any, error) {
191 var payload map[string]any
192 if err := json.Unmarshal(data, &payload); err != nil {
193 return nil, err
194 }
195 return payload, nil
196 }
197
198 func valueToString(val any) (string, error) {
199 if s, ok := val.(string); ok {
200 return s, nil
201 }
202 b, err := json.Marshal(val)
203 if err != nil {
204 return "", fmt.Errorf("encoding vault value: %w", err)
205 }
206 return string(b), nil
207 }