forked from spiffe/spire
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create skeleton of HC Vault based server keymanager plugin (spiffe#5058)
- Loading branch information
1 parent
033d8d6
commit 4a137ce
Showing
4 changed files
with
735 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
310 changes: 310 additions & 0 deletions
310
pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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[:]) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
Oops, something went wrong.