Skip to content

Commit

Permalink
remote.vault: initial commit of new component
Browse files Browse the repository at this point in the history
The remote.vault component retrieves secrets from Vault.
  • Loading branch information
rfratto committed Apr 3, 2023
1 parent 962f741 commit 7aa20ae
Show file tree
Hide file tree
Showing 8 changed files with 1,015 additions and 16 deletions.
339 changes: 339 additions & 0 deletions component/remote/vault/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
package vault

import (
"context"
"fmt"

"github.com/grafana/agent/pkg/flow/rivertypes"
vault "github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/api/auth/approle"
"github.com/hashicorp/vault/api/auth/aws"
"github.com/hashicorp/vault/api/auth/azure"
"github.com/hashicorp/vault/api/auth/gcp"
"github.com/hashicorp/vault/api/auth/kubernetes"
"github.com/hashicorp/vault/api/auth/ldap"
"github.com/hashicorp/vault/api/auth/userpass"
)

// An authMethod can configure a Vault client to be authenticated using a
// specific authentication method.
//
// The vaultAuthenticate method will be called each time a new token is needed
// (e.g., if renewal failed). vaultAuthenticate method may return a nil secret
// if the authentication method does not generate a secret.
type authMethod interface {
vaultAuthenticate(context.Context, *vault.Client) (*vault.Secret, error)
}

// AuthArguments defines a single authentication type in a remote.vault
// component instance. These are embedded as an enum field so only one may be
// set per AuthArguments.
type AuthArguments struct {
AuthToken *AuthToken `river:"token,block,optional"`
AuthAppRole *AuthAppRole `river:"approle,block,optional"`
AuthAWS *AuthAWS `river:"aws,block,optional"`
AuthAzure *AuthAzure `river:"azure,block,optional"`
AuthGCP *AuthGCP `river:"gcp,block,optional"`
AuthKubernetes *AuthKubernetes `river:"kubernetes,block,optional"`
AuthLDAP *AuthLDAP `river:"ldap,block,optional"`
AuthUserPass *AuthUserPass `river:"userpass,block,optional"`
AuthCustom *AuthCustom `river:"auth.custom,block,optional"`
}

func (a *AuthArguments) authMethod() authMethod {
switch {
case a.AuthToken != nil:
return a.AuthToken
case a.AuthAppRole != nil:
return a.AuthAppRole
case a.AuthAWS != nil:
return a.AuthAWS
case a.AuthAzure != nil:
return a.AuthAzure
case a.AuthGCP != nil:
return a.AuthGCP
case a.AuthKubernetes != nil:
return a.AuthKubernetes
case a.AuthLDAP != nil:
return a.AuthLDAP
case a.AuthUserPass != nil:
return a.AuthUserPass
case a.AuthCustom != nil:
return a.AuthCustom
}

panic("remote.vault: unreachable")
}

// AuthToken authenticates against Vault with a token.
type AuthToken struct {
Token rivertypes.Secret `river:"token,attr"`
}

func (a *AuthToken) vaultAuthenticate(ctx context.Context, cli *vault.Client) (*vault.Secret, error) {
cli.SetToken(string(a.Token))
return nil, nil
}

// AuthAppRole authenticates against Vault with AppRole.
type AuthAppRole struct {
RoleID string `river:"role_id,attr"`
Secret rivertypes.Secret `river:"secret,attr"`
WrappingToken bool `river:"wrapping_token,attr,optional"`
MountPath string `river:"mouth_path,attr,optional"`
}

func (a *AuthAppRole) vaultAuthenticate(ctx context.Context, cli *vault.Client) (*vault.Secret, error) {
secret := &approle.SecretID{FromString: string(a.Secret)}

var opts []approle.LoginOption
if a.WrappingToken {
opts = append(opts, approle.WithWrappingToken())
}
if a.MountPath != "" {
opts = append(opts, approle.WithMountPath(a.MountPath))
}

auth, err := approle.NewAppRoleAuth(a.RoleID, secret, opts...)
if err != nil {
return nil, fmt.Errorf("auth.approle: %w", err)
}
s, err := cli.Auth().Login(ctx, auth)
if err != nil {
return nil, fmt.Errorf("auth.approle: %w", err)
}
return s, nil
}

// AuthAWS authenticates against Vault with AWS.
type AuthAWS struct {
// Type specifies the mechanism used to authenticate with AWS. Should be
// either ec2 or iam.
Type string `river:"type,attr"`
Region string `river:"region,attr,optional"`
Role string `river:"role,attr,optional"`
IAMServerIDHeader string `river:"iam_server_id_header,attr,optional"`
// EC2SignatureType specifies the signature to use against EC2. Only used
// when Type is ec2. Valid options are identity and pkcs7 (default).
EC2SignatureType string `river:"ec2_signature_type,attr,optional"`
MountPath string `river:"mouth_path,attr,optional"`
}

func (a *AuthAWS) vaultAuthenticate(ctx context.Context, cli *vault.Client) (*vault.Secret, error) {
var opts []aws.LoginOption

switch a.Type {
case "":
return nil, fmt.Errorf("auth.aws: role must not be empty")
case "ec2":
opts = append(opts, aws.WithEC2Auth())
case "iam":
opts = append(opts, aws.WithIAMAuth())
default:
return nil, fmt.Errorf("auth.aws: unrecognized type %q, expected one of ec2,iam", a.Type)
}
if a.Region != "" {
opts = append(opts, aws.WithRegion(a.Region))
}
if a.Role != "" {
opts = append(opts, aws.WithRegion(a.Role))
}
if a.IAMServerIDHeader != "" {
opts = append(opts, aws.WithIAMServerIDHeader(a.IAMServerIDHeader))
}
switch a.EC2SignatureType {
case "", "pkcs7":
opts = append(opts, aws.WithPKCS7Signature())
case "identity":
opts = append(opts, aws.WithIdentitySignature())
default:
return nil, fmt.Errorf("auth.aws: unrecognized ec2_signature_type %q, expected one of pkcs7,identity", a.Type)
}
if a.MountPath != "" {
opts = append(opts, aws.WithMountPath(a.MountPath))
}

auth, err := aws.NewAWSAuth(opts...)
if err != nil {
return nil, fmt.Errorf("auth.aws: %w", err)
}
s, err := cli.Auth().Login(ctx, auth)
if err != nil {
return nil, fmt.Errorf("auth.aws: %w", err)
}
return s, nil
}

// AuthAzure authenticates against Vault with Azure.
type AuthAzure struct {
Role string `river:"role,attr"`
ResourceURL string `river:"resource_url,attr,optional"`
MountPath string `river:"mouth_path,attr,optional"`
}

func (a *AuthAzure) vaultAuthenticate(ctx context.Context, cli *vault.Client) (*vault.Secret, error) {
var opts []azure.LoginOption

if a.ResourceURL != "" {
opts = append(opts, azure.WithResource(a.ResourceURL))
}
if a.MountPath != "" {
opts = append(opts, azure.WithMountPath(a.MountPath))
}

auth, err := azure.NewAzureAuth(a.Role, opts...)
if err != nil {
return nil, fmt.Errorf("auth.azure: %w", err)
}
s, err := cli.Auth().Login(ctx, auth)
if err != nil {
return nil, fmt.Errorf("auth.azure: %w", err)
}
return s, nil
}

// AuthAzure authenticates against Vault with GCP.
type AuthGCP struct {
Role string `river:"role,attr"`
// Type specifies the mechanism used to authenticate with GCS. Should be
// either gce or iam.
Type string `river:"type,attr"`
IAMServiceAccount string `river:"iam_service_account,attr,optional"`
MountPath string `river:"mouth_path,attr,optional"`
}

func (a *AuthGCP) vaultAuthenticate(ctx context.Context, cli *vault.Client) (*vault.Secret, error) {
var opts []gcp.LoginOption

switch a.Type {
case "gce":
opts = append(opts, gcp.WithGCEAuth())
case "iam":
if a.IAMServiceAccount == "" {
return nil, fmt.Errorf("auth.gcp: iam_service_account must be provided when type is iam")
}
opts = append(opts, gcp.WithIAMAuth(a.IAMServiceAccount))
default:
return nil, fmt.Errorf("auth.gcp: unrecognized type %q, expected one of gce,iam", a.Type)
}

if a.MountPath != "" {
opts = append(opts, gcp.WithMountPath(a.MountPath))
}

auth, err := gcp.NewGCPAuth(a.Role, opts...)
if err != nil {
return nil, fmt.Errorf("auth.gcp: %w", err)
}
s, err := cli.Auth().Login(ctx, auth)
if err != nil {
return nil, fmt.Errorf("auth.gcp: %w", err)
}
return s, nil
}

// AuthAzure authenticates against Vault with Kubernetes.
type AuthKubernetes struct {
Role string `river:"role,attr"`
ServiceAccountTokenFile string `river:"service_account_file,attr,optional"`
MountPath string `river:"mouth_path,attr,optional"`
}

func (a *AuthKubernetes) vaultAuthenticate(ctx context.Context, cli *vault.Client) (*vault.Secret, error) {
var opts []kubernetes.LoginOption

if a.ServiceAccountTokenFile != "" {
opts = append(opts, kubernetes.WithServiceAccountTokenPath(a.ServiceAccountTokenFile))
}
if a.MountPath != "" {
opts = append(opts, kubernetes.WithMountPath(a.MountPath))
}

auth, err := kubernetes.NewKubernetesAuth(a.Role, opts...)
if err != nil {
return nil, fmt.Errorf("auth.kubernetes: %w", err)
}
s, err := cli.Auth().Login(ctx, auth)
if err != nil {
return nil, fmt.Errorf("auth.kubernetes: %w", err)
}
return s, nil
}

// AuthLDAP authenticates against Vault with LDAP.
type AuthLDAP struct {
Username string `river:"username,attr"`
Password rivertypes.Secret `river:"password,attr"`
MountPath string `river:"mouth_path,attr,optional"`
}

func (a *AuthLDAP) vaultAuthenticate(ctx context.Context, cli *vault.Client) (*vault.Secret, error) {
secret := &ldap.Password{FromString: string(a.Password)}

var opts []ldap.LoginOption

if a.MountPath != "" {
opts = append(opts, ldap.WithMountPath(a.MountPath))
}

auth, err := ldap.NewLDAPAuth(a.Username, secret, opts...)
if err != nil {
return nil, fmt.Errorf("auth.ldap: %w", err)
}
s, err := cli.Auth().Login(ctx, auth)
if err != nil {
return nil, fmt.Errorf("auth.ldap: %w", err)
}
return s, nil
}

// AuthUserPass authenticates against Vault with a username and password.
type AuthUserPass struct {
Username string `river:"username,attr"`
Password rivertypes.Secret `river:"password,attr"`
MountPath string `river:"mouth_path,attr,optional"`
}

func (a *AuthUserPass) vaultAuthenticate(ctx context.Context, cli *vault.Client) (*vault.Secret, error) {
secret := &userpass.Password{FromString: string(a.Password)}

var opts []userpass.LoginOption

if a.MountPath != "" {
opts = append(opts, userpass.WithMountPath(a.MountPath))
}

auth, err := userpass.NewUserpassAuth(a.Username, secret, opts...)
if err != nil {
return nil, fmt.Errorf("auth.userpass: %w", err)
}
s, err := cli.Auth().Login(ctx, auth)
if err != nil {
return nil, fmt.Errorf("auth.userpass: %w", err)
}
return s, nil
}

// AuthCustom provides a custom authentication method.
type AuthCustom struct {
// Path to use for logging in (e.g., auth/kubernetes/login, etc.)
Path string `river:"path,attr"`
Data map[string]rivertypes.Secret `river:"data,attr"`
}

// Login implements vault.AuthMethod.
func (a *AuthCustom) Login(ctx context.Context, client *vault.Client) (*vault.Secret, error) {
data := make(map[string]interface{}, len(a.Data))
for k, v := range a.Data {
data[k] = string(v)
}
return client.Logical().WriteWithContext(ctx, a.Path, data)
}

func (a *AuthCustom) vaultAuthenticate(ctx context.Context, cli *vault.Client) (*vault.Secret, error) {
s, err := cli.Auth().Login(ctx, a)
if err != nil {
return nil, fmt.Errorf("auth.custom: %w", err)
}
return s, nil
}
40 changes: 40 additions & 0 deletions component/remote/vault/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package vault

import (
"context"
"fmt"
"strings"

vault "github.com/hashicorp/vault/api"
)

// secretStore abstracts away the details for how a secret is retrieved from a
// vault.Client.
type secretStore interface {
Read(ctx context.Context, args *Arguments) (*vault.Secret, error)

// TODO(rfratto): does this also need to support the LifetimeWatcher stuff?
}

// TODO(rfratto): support logical stores.

type kvStore struct{ c *vault.Client }

func (ks *kvStore) Read(ctx context.Context, args *Arguments) (*vault.Secret, error) {
// Split the path so we know which kv mount we want to use.
pathParts := strings.SplitN(args.Path, "/", 2)
if len(pathParts) != 2 {
return nil, fmt.Errorf("missing mount path in %q", args.Path)
}

kv := ks.c.KVv2(pathParts[0])
kvSecret, err := kv.Get(ctx, pathParts[1])
if err != nil {
return nil, err
}

// kvSecret.Data contains unwrapped data. Let's assign that to the raw secret
// and return it. This is a bit of a hack, but should work just fine.
kvSecret.Raw.Data = kvSecret.Data
return kvSecret.Raw, nil
}
Loading

0 comments on commit 7aa20ae

Please sign in to comment.