Skip to content

Commit 7f4dac1

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

File tree

6 files changed

+419
-0
lines changed

6 files changed

+419
-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: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
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+
if conf.Contains("auth", "mount_path") {
111+
authMountPath, err := conf.FieldString("auth", "mount_path")
112+
if err != nil {
113+
return nil, err
114+
}
115+
authOptions = append(authOptions, vault.WithMountPath(authMountPath))
116+
}
117+
118+
ctx := context.Background()
119+
resp, err := client.Auth.AppRoleLogin(ctx, schema.AppRoleLoginRequest{
120+
RoleId: roleID,
121+
SecretId: secretID,
122+
}, authOptions...)
123+
if err != nil {
124+
return nil, fmt.Errorf("failed to login via Vault client: %w", err)
125+
}
126+
if resp == nil {
127+
return nil, errLoginResponseEmpty
128+
}
129+
130+
var mountPath *service.InterpolatedString
131+
if conf.Contains("mount_path") {
132+
mountPath, err = conf.FieldInterpolatedString("mount_path")
133+
if err != nil {
134+
return nil, err
135+
}
136+
}
137+
138+
var path *bloblang.Executor
139+
path, err = conf.FieldBloblang("path")
140+
if err != nil {
141+
return nil, fmt.Errorf("missing key path for Vault fetch: %w", err)
142+
}
143+
144+
var version *service.InterpolatedString
145+
if conf.Contains("version") {
146+
version, err = conf.FieldInterpolatedString("version")
147+
if err != nil {
148+
return nil, err
149+
}
150+
}
151+
152+
if resp.Auth == nil {
153+
return nil, errLoginResponseMissingAuth
154+
}
155+
156+
if resp.Auth.ClientToken == "" {
157+
return nil, errLoginResponseEmptyClientToken
158+
}
159+
160+
clientToken := resp.Auth.ClientToken
161+
162+
return &processor{
163+
client: client,
164+
clientToken: clientToken,
165+
logger: mgr.Logger(),
166+
metrics: mgr.Metrics(),
167+
mountPath: mountPath,
168+
path: path,
169+
version: version,
170+
}, nil
171+
}
172+
173+
type processor struct {
174+
client *vault.Client
175+
clientToken string
176+
logger *service.Logger
177+
metrics *service.Metrics
178+
mountPath *service.InterpolatedString
179+
path *bloblang.Executor
180+
version *service.InterpolatedString
181+
}
182+
183+
func (p *processor) Process(ctx context.Context, message *service.Message) (service.MessageBatch, error) {
184+
185+
opts := []vault.RequestOption{
186+
vault.WithToken(p.clientToken),
187+
}
188+
189+
mountPath := ""
190+
if p.mountPath != nil {
191+
var err error
192+
mountPath, err = p.mountPath.TryString(message)
193+
if err != nil {
194+
return nil, err
195+
}
196+
if mountPath != "" {
197+
opts = append(opts, vault.WithMountPath(mountPath))
198+
}
199+
}
200+
201+
output, err := p.path.Query(message)
202+
if errors.Is(err, bloblang.ErrRootDeleted) {
203+
// Take this as an indicator to not produce a message
204+
return nil, nil
205+
}
206+
path := output.(string)
207+
if path == "" {
208+
return nil, errors.New("empty key path")
209+
}
210+
211+
version := ""
212+
if p.version != nil {
213+
version, err := p.version.TryString(message)
214+
if err != nil {
215+
return nil, err
216+
}
217+
if version != "" {
218+
opts = append(opts, vault.WithQueryParameters(url.Values{
219+
"version": []string{version},
220+
}))
221+
}
222+
}
223+
224+
p.logger.Tracef("Reading key value from Vault (mount_path: %s, path: %s, version: %s)", mountPath, path, version)
225+
kv, err := p.client.Secrets.KvV2Read(ctx, path, opts...)
226+
if err != nil {
227+
outMsg := message.Copy()
228+
outMsg.SetError(err)
229+
return service.MessageBatch{outMsg}, nil
230+
}
231+
232+
bs, err := json.Marshal(kv.Data.Data)
233+
if err != nil {
234+
return nil, fmt.Errorf("failed to marshal Vault response: %w", err)
235+
}
236+
237+
outMsg := message.Copy()
238+
outMsg.SetBytes(bs)
239+
for k, v := range kv.Data.Metadata {
240+
outMsg.MetaSetMut(k, v)
241+
}
242+
243+
return service.MessageBatch{
244+
outMsg,
245+
}, nil
246+
}
247+
248+
func (p *processor) Close(ctx context.Context) error {
249+
return nil
250+
}

0 commit comments

Comments
 (0)