Skip to content

Commit

Permalink
Create skeleton of HC Vault based server keymanager plugin (spiffe#5058)
Browse files Browse the repository at this point in the history
  • Loading branch information
InverseIntegral committed Aug 16, 2024
1 parent 033d8d6 commit 4a137ce
Show file tree
Hide file tree
Showing 4 changed files with 735 additions and 0 deletions.
2 changes: 2 additions & 0 deletions pkg/server/catalog/keymanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package catalog

import (
"github.com/spiffe/spire/pkg/common/catalog"
"github.com/spiffe/spire/pkg/server/plugin/keymanager/hashicorpvault"

"github.com/spiffe/spire/pkg/server/plugin/keymanager"
"github.com/spiffe/spire/pkg/server/plugin/keymanager/awskms"
Expand Down Expand Up @@ -33,6 +34,7 @@ func (repo *keyManagerRepository) BuiltIns() []catalog.BuiltIn {
disk.BuiltIn(),
gcpkms.BuiltIn(),
azurekeyvault.BuiltIn(),
hashicorpvault.BuiltIn(),
memory.BuiltIn(),
}
}
Expand Down
310 changes: 310 additions & 0 deletions pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
package hashicorpvault

import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/pem"
"errors"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/hcl"
keymanagerv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/plugin/server/keymanager/v1"
configv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/service/common/config/v1"
"github.com/spiffe/spire/pkg/common/catalog"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"os"
"sync"
)

const (
pluginName = "hashicorp_vault"
)

func BuiltIn() catalog.BuiltIn {
return builtin(New())
}

func builtin(p *Plugin) catalog.BuiltIn {
return catalog.MakeBuiltIn(pluginName,
keymanagerv1.KeyManagerPluginServer(p),
configv1.ConfigServiceServer(p),
)
}

type keyEntry struct {
PublicKey *keymanagerv1.PublicKey
}

type pluginHooks struct {
// Used for testing only.
scheduleDeleteSignal chan error
refreshKeysSignal chan error
disposeKeysSignal chan error

lookupEnv func(string) (string, bool)
}

// Config provides configuration context for the plugin.
type Config struct {
// A URL of Vault server. (e.g., https://vault.example.com:8443/)
VaultAddr string `hcl:"vault_addr" json:"vault_addr"`

// Configuration for the Token authentication method
TokenAuth *TokenAuthConfig `hcl:"token_auth" json:"token_auth,omitempty"`

// TODO: Support other auth methods
// TODO: Support client certificate and key
}

type TokenAuthConfig struct {
// Token string to set into "X-Vault-Token" header
Token string `hcl:"token" json:"token"`
}

// Plugin is the main representation of this keymanager plugin
type Plugin struct {
keymanagerv1.UnsafeKeyManagerServer
configv1.UnsafeConfigServer

logger hclog.Logger
mu sync.RWMutex
entries map[string]keyEntry

authMethod AuthMethod
cc *ClientConfig
vc *Client

hooks pluginHooks
}

// New returns an instantiated plugin.
func New() *Plugin {
return newPlugin()
}

// newPlugin returns a new plugin instance.
func newPlugin() *Plugin {
return &Plugin{
entries: make(map[string]keyEntry),
hooks: pluginHooks{
lookupEnv: os.LookupEnv,
},
}
}

// SetLogger sets a logger
func (p *Plugin) SetLogger(log hclog.Logger) {
p.logger = log
}

func (p *Plugin) Configure(_ context.Context, req *configv1.ConfigureRequest) (*configv1.ConfigureResponse, error) {
config := new(Config)

if err := hcl.Decode(&config, req.HclConfiguration); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "unable to decode configuration: %v", err)
}

p.mu.Lock()
defer p.mu.Unlock()

am, err := parseAuthMethod(config)
if err != nil {
return nil, err
}
cp, err := p.genClientParams(am, config)
if err != nil {
return nil, err
}
vcConfig, err := NewClientConfig(cp, p.logger)
if err != nil {
return nil, err
}

p.authMethod = am
p.cc = vcConfig

return &configv1.ConfigureResponse{}, nil
}

func parseAuthMethod(config *Config) (AuthMethod, error) {
var authMethod AuthMethod
if config.TokenAuth != nil {
authMethod = TOKEN
}

if authMethod != 0 {
return authMethod, nil
}

return 0, status.Error(codes.InvalidArgument, "must be configured one of these authentication method 'Token'")
}

func (p *Plugin) genClientParams(method AuthMethod, config *Config) (*ClientParams, error) {
cp := &ClientParams{
VaultAddr: p.getEnvOrDefault(envVaultAddr, config.VaultAddr),
}

switch method {
case TOKEN:
cp.Token = p.getEnvOrDefault(envVaultToken, config.TokenAuth.Token)
}

return cp, nil
}

func (p *Plugin) getEnvOrDefault(envKey, fallback string) string {
if value, ok := p.hooks.lookupEnv(envKey); ok {
return value
}
return fallback
}

func (p *Plugin) GenerateKey(ctx context.Context, req *keymanagerv1.GenerateKeyRequest) (*keymanagerv1.GenerateKeyResponse, error) {
if req.KeyId == "" {
return nil, status.Error(codes.InvalidArgument, "key id is required")
}
if req.KeyType == keymanagerv1.KeyType_UNSPECIFIED_KEY_TYPE {
return nil, status.Error(codes.InvalidArgument, "key type is required")
}

p.mu.Lock()
defer p.mu.Unlock()

spireKeyID := req.KeyId
newKeyEntry, err := p.createKey(ctx, spireKeyID, req.KeyType)
if err != nil {
return nil, err
}

p.entries[spireKeyID] = *newKeyEntry

return &keymanagerv1.GenerateKeyResponse{
PublicKey: newKeyEntry.PublicKey,
}, nil
}

func (p *Plugin) SignData(ctx context.Context, req *keymanagerv1.SignDataRequest) (*keymanagerv1.SignDataResponse, error) {
return nil, errors.New("sign data is not implemented")
}

func (p *Plugin) GetPublicKey(_ context.Context, req *keymanagerv1.GetPublicKeyRequest) (*keymanagerv1.GetPublicKeyResponse, error) {
if req.KeyId == "" {
return nil, status.Error(codes.InvalidArgument, "key id is required")
}

p.mu.RLock()
defer p.mu.RUnlock()

entry, ok := p.entries[req.KeyId]
if !ok {
return nil, status.Errorf(codes.NotFound, "key %q not found", req.KeyId)
}

return &keymanagerv1.GetPublicKeyResponse{
PublicKey: entry.PublicKey,
}, nil
}

func (p *Plugin) GetPublicKeys(context.Context, *keymanagerv1.GetPublicKeysRequest) (*keymanagerv1.GetPublicKeysResponse, error) {
var keys = make([]*keymanagerv1.PublicKey, len(p.entries), 0)

p.mu.RLock()
defer p.mu.RUnlock()

for _, key := range p.entries {
keys = append(keys, key.PublicKey)
}

p.logger.Debug("getting public keys")

return &keymanagerv1.GetPublicKeysResponse{PublicKeys: keys}, nil
}

func (p *Plugin) createKey(ctx context.Context, spireKeyID string, keyType keymanagerv1.KeyType) (*keyEntry, error) {
err := p.genVaultClient()
if err != nil {
return nil, err
}

kt, err := convertToTransitKeyType(keyType)
if err != nil {
return nil, err
}

s, err := p.vc.CreateKey(ctx, spireKeyID, *kt)
if err != nil {
return nil, err
}

s, err = p.vc.GetKey(ctx, spireKeyID)
if err != nil {
return nil, err
}

// TODO: Should we support multiple versions of the key?
keys := s.Data["keys"].(map[string]interface{})
last := keys["1"].(map[string]interface{})
encodedPub := []byte(last["public_key"].(string))

// TODO: Should I handle the rest somehow?
pemBlock, _ := pem.Decode(encodedPub)
if pemBlock == nil || pemBlock.Type != "PUBLIC KEY" {
return nil, status.Error(codes.Internal, "unable to decode PEM key")
}

return &keyEntry{
PublicKey: &keymanagerv1.PublicKey{
Id: spireKeyID,
Type: keyType,
PkixData: pemBlock.Bytes,
Fingerprint: makeFingerprint(pemBlock.Bytes),
},
}, nil
}

func convertToTransitKeyType(keyType keymanagerv1.KeyType) (*TransitKeyType, error) {
switch keyType {
case keymanagerv1.KeyType_EC_P256:
return to.Ptr(TransitKeyType_ECDSA_P256), nil
case keymanagerv1.KeyType_EC_P384:
return to.Ptr(TransitKeyType_ECDSA_P384), nil
case keymanagerv1.KeyType_RSA_2048:
return to.Ptr(TransitKeyType_RSA_2048), nil
case keymanagerv1.KeyType_RSA_4096:
return to.Ptr(TransitKeyType_RSA_4096), nil
default:
return nil, status.Errorf(codes.Internal, "unsupported key type: %v", keyType)
}
}

// TODO: Use context here (?)
// TODO: Should we really generate the client like this, relies on the fact that the mutex is already locked :(
func (p *Plugin) genVaultClient() error {
if p.vc == nil {
renewCh := make(chan struct{})
vc, err := p.cc.NewAuthenticatedClient(p.authMethod, renewCh)
if err != nil {
return status.Errorf(codes.Internal, "failed to prepare authenticated client: %v", err)
}
p.vc = vc

// if renewCh has been closed, the token can not be renewed and may expire,
// it needs to re-authenticate to the Vault.
go func() {
<-renewCh
p.mu.Lock()
defer p.mu.Unlock()
p.vc = nil
p.logger.Debug("Going to re-authenticate to the Vault during the next key manager operation")
}()
}

return nil
}

func makeFingerprint(pkixData []byte) string {
s := sha256.Sum256(pkixData)
return hex.EncodeToString(s[:])
}
50 changes: 50 additions & 0 deletions pkg/server/plugin/keymanager/hashicorpvault/renewer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package hashicorpvault

import (
"github.com/hashicorp/go-hclog"
vapi "github.com/hashicorp/vault/api"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

const (
defaultRenewBehavior = vapi.RenewBehaviorIgnoreErrors
)

type Renew struct {
logger hclog.Logger
watcher *vapi.LifetimeWatcher
}

func NewRenew(client *vapi.Client, secret *vapi.Secret, logger hclog.Logger) (*Renew, error) {
watcher, err := client.NewLifetimeWatcher(&vapi.LifetimeWatcherInput{
Secret: secret,
RenewBehavior: defaultRenewBehavior,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to initialize Renewer: %v", err)
}
return &Renew{
logger: logger,
watcher: watcher,
}, nil
}

func (r *Renew) Run() {
go r.watcher.Start()
defer r.watcher.Stop()

for {
select {
case err := <-r.watcher.DoneCh():
if err != nil {
r.logger.Error("Failed to renew auth token", "err", err)
return
}
r.logger.Error("Failed to renew auth token. Retries may have exceeded the lease time threshold")
return
case renewal := <-r.watcher.RenewCh():
r.logger.Debug("Successfully renew auth token", "request_id", renewal.Secret.RequestID, "lease_duration", renewal.Secret.Auth.LeaseDuration)
}
}
}
Loading

0 comments on commit 4a137ce

Please sign in to comment.