Skip to content

Commit 178b9b5

Browse files
Add new processor vault_key
Retrieves Keys from Vault. Supports Role auth currently.
1 parent 6a8353e commit 178b9b5

File tree

6 files changed

+420
-0
lines changed

6 files changed

+420
-0
lines changed

go.mod

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ require (
6464
github.com/gofrs/uuid v4.4.0+incompatible
6565
github.com/golang-jwt/jwt/v5 v5.2.1
6666
github.com/gosimple/slug v1.14.0
67+
github.com/hashicorp/vault-client-go v0.4.3
6768
github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c
6869
github.com/jackc/pgx/v4 v4.18.3
6970
github.com/jhump/protoreflect v1.16.0
@@ -140,10 +141,16 @@ require (
140141
cloud.google.com/go/aiplatform v1.68.0 // indirect
141142
cloud.google.com/go/longrunning v0.5.9 // indirect
142143
github.com/hamba/avro/v2 v2.22.2-0.20240625062549-66aad10411d9 // indirect
144+
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
145+
github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
146+
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
147+
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
143148
github.com/json-iterator/go v1.1.12 // indirect
149+
github.com/mitchellh/go-homedir v1.1.0 // indirect
144150
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
145151
github.com/modern-go/reflect2 v1.0.2 // indirect
146152
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
153+
github.com/ryanuber/go-glob v1.0.0 // indirect
147154
)
148155

149156
require (

go.sum

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,11 @@ github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv
635635
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
636636
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
637637
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
638+
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
639+
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
640+
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
638641
github.com/hashicorp/go-hclog v0.9.1/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
642+
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
639643
github.com/hashicorp/go-hclog v1.1.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
640644
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
641645
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
@@ -650,6 +654,12 @@ github.com/hashicorp/go-msgpack/v2 v2.1.2/go.mod h1:upybraOAblm4S7rx0+jeNy+CWWhz
650654
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
651655
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
652656
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
657+
github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ=
658+
github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
659+
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
660+
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
661+
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
662+
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
653663
github.com/hashicorp/go-uuid v0.0.0-20180228145832-27454136f036/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
654664
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
655665
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
@@ -666,6 +676,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyf
666676
github.com/hashicorp/raft v1.3.9/go.mod h1:4Ak7FSPnuvmb0GV6vgIAJ4vYT4bek9bb6Q+7HVbyzqM=
667677
github.com/hashicorp/raft v1.6.1 h1:v/jm5fcYHvVkL0akByAp+IDdDSzCNCGhdO6VdB56HIM=
668678
github.com/hashicorp/raft v1.6.1/go.mod h1:N1sKh6Vn47mrWvEArQgILTyng8GoDRNYlgKyK7PMjs0=
679+
github.com/hashicorp/vault-client-go v0.4.3 h1:zG7STGVgn/VK6rnZc0k8PGbfv2x/sJExRKHSUg3ljWc=
680+
github.com/hashicorp/vault-client-go v0.4.3/go.mod h1:4tDw7Uhq5XOxS1fO+oMtotHL7j4sB9cp0T7U6m4FzDY=
669681
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
670682
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
671683
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@@ -851,6 +863,8 @@ github.com/microsoft/gocosmos v1.1.1 h1:zJUelhWCm9yvHxiHRuPSY+9loQcGi+tYS7gcOIt8
851863
github.com/microsoft/gocosmos v1.1.1/go.mod h1:M1dL6uI65ocCJYWvA8eKaTdy9URTYdpkaF+LPhjqd7I=
852864
github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
853865
github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
866+
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
867+
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
854868
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
855869
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
856870
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
@@ -1052,6 +1066,8 @@ github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThC
10521066
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
10531067
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
10541068
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
1069+
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
1070+
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
10551071
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
10561072
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
10571073
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=

internal/impl/vault/vault.go

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
package vault
2+
3+
import (
4+
"context"
5+
_ "embed"
6+
"encoding/json"
7+
"errors"
8+
"fmt"
9+
"net/url"
10+
"strings"
11+
12+
_ "github.com/redpanda-data/benthos/v4/public/components/pure"
13+
14+
"github.com/hashicorp/vault-client-go"
15+
"github.com/hashicorp/vault-client-go/schema"
16+
"github.com/redpanda-data/benthos/v4/public/bloblang"
17+
"github.com/redpanda-data/benthos/v4/public/service"
18+
)
19+
20+
var (
21+
_ service.Processor = (*processor)(nil)
22+
spec *service.ConfigSpec
23+
errLoginResponseEmpty = errors.New("login responded with unexpected empty response")
24+
errLoginResponseMissingAuth = errors.New("login responded with unexpected missing auth")
25+
errLoginResponseEmptyClientToken = errors.New("login responded with unexpected missing or empty client_token")
26+
)
27+
28+
func init() {
29+
spec = service.NewConfigSpec().
30+
Beta().
31+
Summary("Fetches a Value for a Key from Hashicorp Vault").
32+
Description(`
33+
The fields `+"`mount_path`"+`, `+"`path`"+` and `+"`version`"+` support
34+
xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions], allowing
35+
you to create a unique `+"`mount_path`"+`, `+"`path`"+` and/or `+"`version`"+` for each message.
36+
37+
`).
38+
Fields(
39+
service.NewStringField("url").
40+
Description("The base URL of the Vault server."),
41+
service.NewObjectField(
42+
"auth",
43+
service.NewStringField("mount_path").
44+
Optional(),
45+
service.NewObjectField(
46+
"app_role",
47+
service.NewStringField("role_id").
48+
Description("Unique identifier of the Role").
49+
Secret(),
50+
service.NewStringField("secret_id").
51+
Description("SecretID belong to the App role").
52+
Secret(),
53+
),
54+
),
55+
service.NewBloblangField("mount_path").
56+
Description(`Supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions].
57+
`).
58+
Optional(),
59+
service.NewBloblangField("path").
60+
Description(`The key path to fetch from Vault.
61+
Supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions].
62+
If root gets deleted no message gets produced.
63+
`),
64+
service.NewInterpolatedStringField("version").
65+
Description(`The specific key version to fetch from Vault.
66+
Supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions].
67+
`).
68+
Optional(),
69+
)
70+
err := service.RegisterProcessor("vault_key", spec, ctor)
71+
if err != nil {
72+
panic(err)
73+
}
74+
}
75+
76+
func ctor(conf *service.ParsedConfig, mgr *service.Resources) (service.Processor, error) {
77+
78+
url, err := conf.FieldString("url")
79+
if err != nil {
80+
return nil, fmt.Errorf("missing url for Vault: %w", err)
81+
}
82+
if strings.TrimSpace(url) == "" {
83+
return nil, fmt.Errorf("unexpected empty url for Vault AppRole login: %w", err)
84+
}
85+
86+
roleId, err := conf.FieldString("auth", "app_role", "role_id")
87+
if err != nil {
88+
return nil, fmt.Errorf("missing role_id for Vault AppRole login: %w", err)
89+
}
90+
if strings.TrimSpace(roleId) == "" {
91+
return nil, fmt.Errorf("unexpected empty role_id for Vault AppRole login: %w", err)
92+
}
93+
94+
secretId, err := conf.FieldString("auth", "app_role", "secret_id")
95+
if err != nil {
96+
return nil, fmt.Errorf("missing secret_id for Vault AppRole login: %w", err)
97+
}
98+
if strings.TrimSpace(secretId) == "" {
99+
return nil, fmt.Errorf("unexpected empty secret_id for Vault AppRole login: %w", err)
100+
}
101+
102+
client, err := vault.New(
103+
vault.WithAddress(url),
104+
)
105+
if err != nil {
106+
return nil, fmt.Errorf("failed creating Vault client: %w", err)
107+
}
108+
109+
var authOptions []vault.RequestOption
110+
authMountPath := ""
111+
if conf.Contains("auth", "mount_path") {
112+
authMountPath, err = conf.FieldString("auth", "mount_path")
113+
if err != nil {
114+
return nil, err
115+
}
116+
authOptions = append(authOptions, vault.WithMountPath(authMountPath))
117+
}
118+
119+
ctx := context.Background()
120+
resp, err := client.Auth.AppRoleLogin(ctx, schema.AppRoleLoginRequest{
121+
RoleId: roleId,
122+
SecretId: secretId,
123+
}, authOptions...)
124+
if err != nil {
125+
return nil, fmt.Errorf("failed to login via Vault client: %w", err)
126+
}
127+
if resp == nil {
128+
return nil, errLoginResponseEmpty
129+
}
130+
131+
var mountPath *service.InterpolatedString
132+
if conf.Contains("mount_path") {
133+
mountPath, err = conf.FieldInterpolatedString("mount_path")
134+
if err != nil {
135+
return nil, err
136+
}
137+
}
138+
139+
var path *bloblang.Executor
140+
path, err = conf.FieldBloblang("path")
141+
if err != nil {
142+
return nil, fmt.Errorf("missing key path for Vault fetch: %w", err)
143+
}
144+
145+
var version *service.InterpolatedString
146+
if conf.Contains("version") {
147+
version, err = conf.FieldInterpolatedString("version")
148+
if err != nil {
149+
return nil, err
150+
}
151+
}
152+
153+
if resp.Auth == nil {
154+
return nil, errLoginResponseMissingAuth
155+
}
156+
157+
if resp.Auth.ClientToken == "" {
158+
return nil, errLoginResponseEmptyClientToken
159+
}
160+
161+
clientToken := resp.Auth.ClientToken
162+
163+
return &processor{
164+
client: client,
165+
clientToken: clientToken,
166+
logger: mgr.Logger(),
167+
metrics: mgr.Metrics(),
168+
mountPath: mountPath,
169+
path: path,
170+
version: version,
171+
}, nil
172+
}
173+
174+
type processor struct {
175+
client *vault.Client
176+
clientToken string
177+
logger *service.Logger
178+
metrics *service.Metrics
179+
mountPath *service.InterpolatedString
180+
path *bloblang.Executor
181+
version *service.InterpolatedString
182+
}
183+
184+
func (p *processor) Process(ctx context.Context, message *service.Message) (service.MessageBatch, error) {
185+
186+
opts := []vault.RequestOption{
187+
vault.WithToken(p.clientToken),
188+
}
189+
190+
mountPath := ""
191+
if p.mountPath != nil {
192+
var err error
193+
mountPath, err = p.mountPath.TryString(message)
194+
if err != nil {
195+
return nil, err
196+
}
197+
if mountPath != "" {
198+
opts = append(opts, vault.WithMountPath(mountPath))
199+
}
200+
}
201+
202+
output, err := p.path.Query(message)
203+
if errors.Is(err, bloblang.ErrRootDeleted) {
204+
// Take this as an indicator to not produce a message
205+
return nil, nil
206+
}
207+
path := output.(string)
208+
if path == "" {
209+
return nil, fmt.Errorf("empty key path")
210+
}
211+
212+
version := ""
213+
if p.version != nil {
214+
version, err := p.version.TryString(message)
215+
if err != nil {
216+
return nil, err
217+
}
218+
if version != "" {
219+
opts = append(opts, vault.WithQueryParameters(url.Values{
220+
"version": []string{version},
221+
}))
222+
}
223+
}
224+
225+
p.logger.Tracef("Reading key value from Vault (mount_path: %s, path: %s, version: %s)", mountPath, path, version)
226+
kv, err := p.client.Secrets.KvV2Read(ctx, path, opts...)
227+
if err != nil {
228+
outMsg := message.Copy()
229+
outMsg.SetError(err)
230+
return service.MessageBatch{outMsg}, nil
231+
}
232+
233+
bs, err := json.Marshal(kv.Data.Data)
234+
if err != nil {
235+
return nil, fmt.Errorf("failed to marshal Vault response: %w", err)
236+
}
237+
238+
outMsg := message.Copy()
239+
outMsg.SetBytes(bs)
240+
for k, v := range kv.Data.Metadata {
241+
outMsg.MetaSetMut(k, v)
242+
}
243+
244+
return service.MessageBatch{
245+
outMsg,
246+
}, nil
247+
}
248+
249+
func (p *processor) Close(ctx context.Context) error {
250+
return nil
251+
}

0 commit comments

Comments
 (0)