Skip to content

Commit

Permalink
Add new processor vault_key
Browse files Browse the repository at this point in the history
Retrieves Keys from Vault.
Supports Role auth currently.
  • Loading branch information
AndreasBergmeier6176 committed Aug 26, 2024
1 parent 6a8353e commit 178b9b5
Show file tree
Hide file tree
Showing 6 changed files with 420 additions and 0 deletions.
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ require (
github.com/gofrs/uuid v4.4.0+incompatible
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/gosimple/slug v1.14.0
github.com/hashicorp/vault-client-go v0.4.3
github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c
github.com/jackc/pgx/v4 v4.18.3
github.com/jhump/protoreflect v1.16.0
Expand Down Expand Up @@ -140,10 +141,16 @@ require (
cloud.google.com/go/aiplatform v1.68.0 // indirect
cloud.google.com/go/longrunning v0.5.9 // indirect
github.com/hamba/avro/v2 v2.22.2-0.20240625062549-66aad10411d9 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
)

require (
Expand Down
16 changes: 16 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,11 @@ github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.1/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v1.1.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
Expand All @@ -650,6 +654,12 @@ github.com/hashicorp/go-msgpack/v2 v2.1.2/go.mod h1:upybraOAblm4S7rx0+jeNy+CWWhz
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ=
github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
github.com/hashicorp/go-uuid v0.0.0-20180228145832-27454136f036/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
Expand All @@ -666,6 +676,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyf
github.com/hashicorp/raft v1.3.9/go.mod h1:4Ak7FSPnuvmb0GV6vgIAJ4vYT4bek9bb6Q+7HVbyzqM=
github.com/hashicorp/raft v1.6.1 h1:v/jm5fcYHvVkL0akByAp+IDdDSzCNCGhdO6VdB56HIM=
github.com/hashicorp/raft v1.6.1/go.mod h1:N1sKh6Vn47mrWvEArQgILTyng8GoDRNYlgKyK7PMjs0=
github.com/hashicorp/vault-client-go v0.4.3 h1:zG7STGVgn/VK6rnZc0k8PGbfv2x/sJExRKHSUg3ljWc=
github.com/hashicorp/vault-client-go v0.4.3/go.mod h1:4tDw7Uhq5XOxS1fO+oMtotHL7j4sB9cp0T7U6m4FzDY=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
Expand Down Expand Up @@ -851,6 +863,8 @@ github.com/microsoft/gocosmos v1.1.1 h1:zJUelhWCm9yvHxiHRuPSY+9loQcGi+tYS7gcOIt8
github.com/microsoft/gocosmos v1.1.1/go.mod h1:M1dL6uI65ocCJYWvA8eKaTdy9URTYdpkaF+LPhjqd7I=
github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
Expand Down Expand Up @@ -1052,6 +1066,8 @@ github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThC
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
Expand Down
251 changes: 251 additions & 0 deletions internal/impl/vault/vault.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
package vault

import (
"context"
_ "embed"
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"

_ "github.com/redpanda-data/benthos/v4/public/components/pure"

"github.com/hashicorp/vault-client-go"
"github.com/hashicorp/vault-client-go/schema"
"github.com/redpanda-data/benthos/v4/public/bloblang"
"github.com/redpanda-data/benthos/v4/public/service"
)

var (
_ service.Processor = (*processor)(nil)
spec *service.ConfigSpec
errLoginResponseEmpty = errors.New("login responded with unexpected empty response")
errLoginResponseMissingAuth = errors.New("login responded with unexpected missing auth")
errLoginResponseEmptyClientToken = errors.New("login responded with unexpected missing or empty client_token")
)

func init() {
spec = service.NewConfigSpec().
Beta().
Summary("Fetches a Value for a Key from Hashicorp Vault").
Description(`
The fields `+"`mount_path`"+`, `+"`path`"+` and `+"`version`"+` support
xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions], allowing
you to create a unique `+"`mount_path`"+`, `+"`path`"+` and/or `+"`version`"+` for each message.
`).
Fields(
service.NewStringField("url").
Description("The base URL of the Vault server."),
service.NewObjectField(
"auth",
service.NewStringField("mount_path").
Optional(),
service.NewObjectField(
"app_role",
service.NewStringField("role_id").
Description("Unique identifier of the Role").
Secret(),
service.NewStringField("secret_id").
Description("SecretID belong to the App role").
Secret(),
),
),
service.NewBloblangField("mount_path").
Description(`Supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions].
`).
Optional(),
service.NewBloblangField("path").
Description(`The key path to fetch from Vault.
Supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions].
If root gets deleted no message gets produced.
`),
service.NewInterpolatedStringField("version").
Description(`The specific key version to fetch from Vault.
Supports xref:configuration:interpolation.adoc#bloblang-queries[interpolation functions].
`).
Optional(),
)
err := service.RegisterProcessor("vault_key", spec, ctor)
if err != nil {
panic(err)
}
}

func ctor(conf *service.ParsedConfig, mgr *service.Resources) (service.Processor, error) {

url, err := conf.FieldString("url")
if err != nil {
return nil, fmt.Errorf("missing url for Vault: %w", err)
}
if strings.TrimSpace(url) == "" {
return nil, fmt.Errorf("unexpected empty url for Vault AppRole login: %w", err)
}

roleId, err := conf.FieldString("auth", "app_role", "role_id")
if err != nil {
return nil, fmt.Errorf("missing role_id for Vault AppRole login: %w", err)
}
if strings.TrimSpace(roleId) == "" {
return nil, fmt.Errorf("unexpected empty role_id for Vault AppRole login: %w", err)
}

secretId, err := conf.FieldString("auth", "app_role", "secret_id")
if err != nil {
return nil, fmt.Errorf("missing secret_id for Vault AppRole login: %w", err)
}
if strings.TrimSpace(secretId) == "" {
return nil, fmt.Errorf("unexpected empty secret_id for Vault AppRole login: %w", err)
}

client, err := vault.New(
vault.WithAddress(url),
)
if err != nil {
return nil, fmt.Errorf("failed creating Vault client: %w", err)
}

var authOptions []vault.RequestOption
authMountPath := ""
if conf.Contains("auth", "mount_path") {
authMountPath, err = conf.FieldString("auth", "mount_path")
if err != nil {
return nil, err
}
authOptions = append(authOptions, vault.WithMountPath(authMountPath))
}

ctx := context.Background()
resp, err := client.Auth.AppRoleLogin(ctx, schema.AppRoleLoginRequest{
RoleId: roleId,
SecretId: secretId,
}, authOptions...)
if err != nil {
return nil, fmt.Errorf("failed to login via Vault client: %w", err)
}
if resp == nil {
return nil, errLoginResponseEmpty
}

var mountPath *service.InterpolatedString
if conf.Contains("mount_path") {
mountPath, err = conf.FieldInterpolatedString("mount_path")
if err != nil {
return nil, err
}
}

var path *bloblang.Executor
path, err = conf.FieldBloblang("path")
if err != nil {
return nil, fmt.Errorf("missing key path for Vault fetch: %w", err)
}

var version *service.InterpolatedString
if conf.Contains("version") {
version, err = conf.FieldInterpolatedString("version")
if err != nil {
return nil, err
}
}

if resp.Auth == nil {
return nil, errLoginResponseMissingAuth
}

if resp.Auth.ClientToken == "" {
return nil, errLoginResponseEmptyClientToken
}

clientToken := resp.Auth.ClientToken

return &processor{
client: client,
clientToken: clientToken,
logger: mgr.Logger(),
metrics: mgr.Metrics(),
mountPath: mountPath,
path: path,
version: version,
}, nil
}

type processor struct {
client *vault.Client
clientToken string
logger *service.Logger
metrics *service.Metrics
mountPath *service.InterpolatedString
path *bloblang.Executor
version *service.InterpolatedString
}

func (p *processor) Process(ctx context.Context, message *service.Message) (service.MessageBatch, error) {

opts := []vault.RequestOption{
vault.WithToken(p.clientToken),
}

mountPath := ""
if p.mountPath != nil {
var err error
mountPath, err = p.mountPath.TryString(message)
if err != nil {
return nil, err
}
if mountPath != "" {
opts = append(opts, vault.WithMountPath(mountPath))
}
}

output, err := p.path.Query(message)
if errors.Is(err, bloblang.ErrRootDeleted) {
// Take this as an indicator to not produce a message
return nil, nil
}
path := output.(string)
if path == "" {
return nil, fmt.Errorf("empty key path")
}

version := ""
if p.version != nil {
version, err := p.version.TryString(message)
if err != nil {
return nil, err
}
if version != "" {
opts = append(opts, vault.WithQueryParameters(url.Values{
"version": []string{version},
}))
}
}

p.logger.Tracef("Reading key value from Vault (mount_path: %s, path: %s, version: %s)", mountPath, path, version)
kv, err := p.client.Secrets.KvV2Read(ctx, path, opts...)
if err != nil {
outMsg := message.Copy()
outMsg.SetError(err)
return service.MessageBatch{outMsg}, nil
}

bs, err := json.Marshal(kv.Data.Data)
if err != nil {
return nil, fmt.Errorf("failed to marshal Vault response: %w", err)
}

outMsg := message.Copy()
outMsg.SetBytes(bs)
for k, v := range kv.Data.Metadata {
outMsg.MetaSetMut(k, v)
}

return service.MessageBatch{
outMsg,
}, nil
}

func (p *processor) Close(ctx context.Context) error {
return nil
}
Loading

0 comments on commit 178b9b5

Please sign in to comment.