From 46239494c2e78836a22073aec6bf5d31ccefa1bb Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:22 +0200 Subject: [PATCH 01/29] Create skeleton of HC Vault based server keymanager plugin (#5058) Signed-off-by: Matteo Kamm --- pkg/server/catalog/keymanager.go | 2 + .../hashicorpvault/hashicorp_vault.go | 310 +++++++++++++++ .../keymanager/hashicorpvault/renewer.go | 50 +++ .../keymanager/hashicorpvault/vault_client.go | 373 ++++++++++++++++++ 4 files changed, 735 insertions(+) create mode 100644 pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go create mode 100644 pkg/server/plugin/keymanager/hashicorpvault/renewer.go create mode 100644 pkg/server/plugin/keymanager/hashicorpvault/vault_client.go diff --git a/pkg/server/catalog/keymanager.go b/pkg/server/catalog/keymanager.go index 645570c34d..13338bc96d 100644 --- a/pkg/server/catalog/keymanager.go +++ b/pkg/server/catalog/keymanager.go @@ -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" @@ -33,6 +34,7 @@ func (repo *keyManagerRepository) BuiltIns() []catalog.BuiltIn { disk.BuiltIn(), gcpkms.BuiltIn(), azurekeyvault.BuiltIn(), + hashicorpvault.BuiltIn(), memory.BuiltIn(), } } diff --git a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go new file mode 100644 index 0000000000..2a207a7cfa --- /dev/null +++ b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go @@ -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[:]) +} diff --git a/pkg/server/plugin/keymanager/hashicorpvault/renewer.go b/pkg/server/plugin/keymanager/hashicorpvault/renewer.go new file mode 100644 index 0000000000..8f14ab5ab3 --- /dev/null +++ b/pkg/server/plugin/keymanager/hashicorpvault/renewer.go @@ -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) + } + } +} diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go new file mode 100644 index 0000000000..951665ad76 --- /dev/null +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go @@ -0,0 +1,373 @@ +package hashicorpvault + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "github.com/hashicorp/go-hclog" + vapi "github.com/hashicorp/vault/api" + "github.com/imdario/mergo" + "golang.org/x/net/context" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "net/http" + "os" + + "github.com/spiffe/spire/pkg/common/pemutil" +) + +// TODO: Delete everything that is unused in here + +const ( + envVaultAddr = "VAULT_ADDR" + envVaultToken = "VAULT_TOKEN" + envVaultClientCert = "VAULT_CLIENT_CERT" + envVaultClientKey = "VAULT_CLIENT_KEY" + envVaultCACert = "VAULT_CACERT" + envVaultAppRoleID = "VAULT_APPROLE_ID" + envVaultAppRoleSecretID = "VAULT_APPROLE_SECRET_ID" // #nosec G101 + envVaultNamespace = "VAULT_NAMESPACE" + + defaultCertMountPoint = "cert" + defaultPKIMountPoint = "pki" + defaultAppRoleMountPoint = "approle" + defaultK8sMountPoint = "kubernetes" +) + +type AuthMethod int + +const ( + _ AuthMethod = iota + CERT + TOKEN + APPROLE + K8S +) + +// ClientConfig represents configuration parameters for vault client +type ClientConfig struct { + Logger hclog.Logger + // vault client parameters + clientParams *ClientParams +} + +type ClientParams struct { + // A URL of Vault server. (e.g., https://vault.example.com:8443/) + VaultAddr string + // Name of mount point where PKI secret engine is mounted. (e.e., //ca/pem ) + PKIMountPoint string + // token string to use when auth method is 'token' + Token string + // Name of mount point where TLS Cert auth method is mounted. (e.g., /auth//login ) + CertAuthMountPoint string + // Name of the Vault role. + // If given, the plugin authenticates against only the named role + CertAuthRoleName string + // Path to a client certificate file to be used when auth method is 'cert' + ClientCertPath string + // Path to a client private key file to be used when auth method is 'cert' + ClientKeyPath string + // Path to a CA certificate file to be used when client verifies a server certificate + CACertPath string + // Name of mount point where AppRole auth method is mounted. (e.g., /auth//login ) + AppRoleAuthMountPoint string + // An identifier of AppRole + AppRoleID string + // A credential set of AppRole + AppRoleSecretID string + // Name of the mount point where Kubernetes auth method is mounted. (e.g., /auth//login) + K8sAuthMountPoint string + // Name of the Vault role. + // The plugin authenticates against the named role. + K8sAuthRoleName string + // Path to a K8s Service Account Token to be used when auth method is 'k8s' + K8sAuthTokenPath string + // If true, client accepts any certificates. + // It should be used only test environment so on. + TLSSKipVerify bool + // MaxRetries controls the number of times to retry to connect + // Set to 0 to disable retrying. + // If the value is nil, to use the default in hashicorp/vault/api. + MaxRetries *int + // Name of the Vault namespace + Namespace string +} + +type Client struct { + vaultClient *vapi.Client + clientParams *ClientParams +} + +// SignCSRResponse includes certificates which are generates by Vault +type SignCSRResponse struct { + // A certificate requested to sign + CACertPEM string + // A certificate of CA(Vault) + UpstreamCACertPEM string + // Set of Upstream CA certificates + UpstreamCACertChainPEM []string +} + +// NewClientConfig returns a new *ClientConfig with default parameters. +func NewClientConfig(cp *ClientParams, logger hclog.Logger) (*ClientConfig, error) { + cc := &ClientConfig{ + Logger: logger, + } + defaultParams := &ClientParams{ + CertAuthMountPoint: defaultCertMountPoint, + AppRoleAuthMountPoint: defaultAppRoleMountPoint, + K8sAuthMountPoint: defaultK8sMountPoint, + PKIMountPoint: defaultPKIMountPoint, + } + if err := mergo.Merge(cp, defaultParams); err != nil { + return nil, status.Errorf(codes.Internal, "unable to merge client params: %v", err) + } + cc.clientParams = cp + return cc, nil +} + +// NewAuthenticatedClient returns a new authenticated vault client with given authentication method +func (c *ClientConfig) NewAuthenticatedClient(method AuthMethod, renewCh chan struct{}) (client *Client, err error) { + config := vapi.DefaultConfig() + config.Address = c.clientParams.VaultAddr + if c.clientParams.MaxRetries != nil { + config.MaxRetries = *c.clientParams.MaxRetries + } + + if err := c.configureTLS(config); err != nil { + return nil, err + } + vc, err := vapi.NewClient(config) + if err != nil { + return nil, status.Errorf(codes.Internal, "unable to create Vault client: %v", err) + } + + if c.clientParams.Namespace != "" { + vc.SetNamespace(c.clientParams.Namespace) + } + + client = &Client{ + vaultClient: vc, + clientParams: c.clientParams, + } + + var sec *vapi.Secret + switch method { + case TOKEN: + sec, err = client.LookupSelf(c.clientParams.Token) + if err != nil { + return nil, err + } + if sec == nil { + return nil, status.Error(codes.Internal, "lookup self response is nil") + } + case CERT: + path := fmt.Sprintf("auth/%v/login", c.clientParams.CertAuthMountPoint) + sec, err = client.Auth(path, map[string]any{ + "name": c.clientParams.CertAuthRoleName, + }) + if err != nil { + return nil, err + } + if sec == nil { + return nil, status.Error(codes.Internal, "tls cert authentication response is nil") + } + case APPROLE: + path := fmt.Sprintf("auth/%v/login", c.clientParams.AppRoleAuthMountPoint) + body := map[string]any{ + "role_id": c.clientParams.AppRoleID, + "secret_id": c.clientParams.AppRoleSecretID, + } + sec, err = client.Auth(path, body) + if err != nil { + return nil, err + } + if sec == nil { + return nil, status.Error(codes.Internal, "approle authentication response is nil") + } + case K8S: + b, err := os.ReadFile(c.clientParams.K8sAuthTokenPath) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to read k8s service account token: %v", err) + } + path := fmt.Sprintf("auth/%s/login", c.clientParams.K8sAuthMountPoint) + body := map[string]any{ + "role": c.clientParams.K8sAuthRoleName, + "jwt": string(b), + } + sec, err = client.Auth(path, body) + if err != nil { + return nil, err + } + if sec == nil { + return nil, status.Error(codes.Internal, "k8s authentication response is nil") + } + } + + err = handleRenewToken(vc, sec, renewCh, c.Logger) + if err != nil { + return nil, err + } + + return client, nil +} + +// handleRenewToken handles renewing the vault token. +// if the token is non-renewable or renew failed, renewCh will be closed. +func handleRenewToken(vc *vapi.Client, sec *vapi.Secret, renewCh chan struct{}, logger hclog.Logger) error { + if sec == nil || sec.Auth == nil { + return status.Error(codes.InvalidArgument, "secret is nil") + } + + if sec.Auth.LeaseDuration == 0 { + logger.Debug("Token will never expire") + return nil + } + if !sec.Auth.Renewable { + logger.Debug("Token is not renewable") + close(renewCh) + return nil + } + renew, err := NewRenew(vc, sec, logger) + if err != nil { + logger.Error("unable to create renew", err) + return err + } + + go func() { + defer close(renewCh) + renew.Run() + }() + + logger.Debug("Token will be renewed") + + return nil +} + +// ConfigureTLS Configures TLS for Vault Client +func (c *ClientConfig) configureTLS(vc *vapi.Config) error { + if vc.HttpClient == nil { + vc.HttpClient = vapi.DefaultConfig().HttpClient + } + clientTLSConfig := vc.HttpClient.Transport.(*http.Transport).TLSClientConfig + + var clientCert tls.Certificate + foundClientCert := false + + switch { + case c.clientParams.ClientCertPath != "" && c.clientParams.ClientKeyPath != "": + c, err := tls.LoadX509KeyPair(c.clientParams.ClientCertPath, c.clientParams.ClientKeyPath) + if err != nil { + return status.Errorf(codes.InvalidArgument, "failed to parse client cert and private-key: %v", err) + } + clientCert = c + foundClientCert = true + case c.clientParams.ClientCertPath != "" || c.clientParams.ClientKeyPath != "": + return status.Error(codes.InvalidArgument, "both client cert and client key are required") + } + + if c.clientParams.CACertPath != "" { + certs, err := pemutil.LoadCertificates(c.clientParams.CACertPath) + if err != nil { + return status.Errorf(codes.InvalidArgument, "failed to load CA certificate: %v", err) + } + pool := x509.NewCertPool() + for _, cert := range certs { + pool.AddCert(cert) + } + clientTLSConfig.RootCAs = pool + } + + if c.clientParams.TLSSKipVerify { + clientTLSConfig.InsecureSkipVerify = true + } + + if foundClientCert { + clientTLSConfig.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { + return &clientCert, nil + } + } + + return nil +} + +// SetToken wraps vapi.Client.SetToken() +func (c *Client) SetToken(v string) { + c.vaultClient.SetToken(v) +} + +// Auth authenticates to vault server with TLS certificate method +func (c *Client) Auth(path string, body map[string]any) (*vapi.Secret, error) { + c.vaultClient.ClearToken() + secret, err := c.vaultClient.Logical().Write(path, body) + if err != nil { + return nil, status.Errorf(codes.Unauthenticated, "authentication failed %v: %v", path, err) + } + + tokenID, err := secret.TokenID() + if err != nil { + return nil, status.Errorf(codes.Internal, "authentication is successful, but could not get token: %v", err) + } + c.vaultClient.SetToken(tokenID) + return secret, nil +} + +func (c *Client) LookupSelf(token string) (*vapi.Secret, error) { + if token == "" { + return nil, status.Error(codes.InvalidArgument, "token is empty") + } + c.SetToken(token) + + secret, err := c.vaultClient.Logical().Read("auth/token/lookup-self") + if err != nil { + return nil, status.Errorf(codes.Internal, "token lookup failed: %v", err) + } + + id, err := secret.TokenID() + if err != nil { + return nil, status.Errorf(codes.Internal, "unable to get TokenID: %v", err) + } + renewable, err := secret.TokenIsRenewable() + if err != nil { + return nil, status.Errorf(codes.Internal, "unable to determine if token is renewable: %v", err) + } + ttl, err := secret.TokenTTL() + if err != nil { + return nil, status.Errorf(codes.Internal, "unable to get token ttl: %v", err) + } + secret.Auth = &vapi.SecretAuth{ + ClientToken: id, + Renewable: renewable, + LeaseDuration: int(ttl.Seconds()), + // don't care any parameters + } + return secret, nil +} + +type TransitKeyType string + +const ( + TransitKeyType_RSA_2048 TransitKeyType = "rsa-2048" + TransitKeyType_RSA_4096 TransitKeyType = "rsa-4096" + TransitKeyType_ECDSA_P256 TransitKeyType = "ecdsa-p256" + TransitKeyType_ECDSA_P384 TransitKeyType = "ecdsa-p384" +) + +// CreateKey creates a new key in the specified transit secret engine +// See: https://developer.hashicorp.com/vault/api-docs/secret/transit#create-key +func (c *Client) CreateKey(ctx context.Context, spireKeyID string, keyType TransitKeyType) (*vapi.Secret, error) { + arguments := map[string]interface{}{ + "type": keyType, + "exportable": "false", // TODO: Maybe make this configurable + } + + // TODO: Handle errors here such as key already exists + // TODO: Make the transit engine path configurable + return c.vaultClient.Logical().WriteWithContext(ctx, fmt.Sprintf("/transit/keys/%s", spireKeyID), arguments) +} + +func (c *Client) GetKey(ctx context.Context, spireKeyID string) (*vapi.Secret, error) { + // TODO: Handle errors here + // TODO: Make the transit engine path configurable + return c.vaultClient.Logical().ReadWithContext(ctx, fmt.Sprintf("/transit/keys/%s", spireKeyID)) +} From e0dbff77ec50ac46502bb696e3f4d87f44b57f01 Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:22 +0200 Subject: [PATCH 02/29] Start implementing signing function for HC vault (#5058) Signed-off-by: Matteo Kamm --- .../hashicorpvault/hashicorp_vault.go | 84 ++++++++++++++++++- .../keymanager/hashicorpvault/vault_client.go | 28 +++++++ 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go index 2a207a7cfa..0cedb1d403 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "encoding/pem" "errors" + "fmt" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/hashicorp/go-hclog" "github.com/hashicorp/hcl" @@ -186,7 +187,41 @@ func (p *Plugin) GenerateKey(ctx context.Context, req *keymanagerv1.GenerateKeyR } func (p *Plugin) SignData(ctx context.Context, req *keymanagerv1.SignDataRequest) (*keymanagerv1.SignDataResponse, error) { - return nil, errors.New("sign data is not implemented") + if req.KeyId == "" { + return nil, status.Error(codes.InvalidArgument, "key id is required") + } + if req.SignerOpts == nil { + return nil, status.Error(codes.InvalidArgument, "signer opts is required") + } + + p.mu.RLock() + defer p.mu.RUnlock() + + keyEntry, hasKey := p.entries[req.KeyId] + if !hasKey { + return nil, status.Errorf(codes.NotFound, "key %q not found", req.KeyId) + } + + hashAlgo, signingAlgo, err := algosForKMS(keyEntry.PublicKey.Type, req.SignerOpts) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + err = p.genVaultClient() + if err != nil { + return nil, err + } + + signResp, err := p.vc.SignData(ctx, req.KeyId, req.Data, hashAlgo, signingAlgo) + + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to sign: %v", err) + } + + return &keymanagerv1.SignDataResponse{ + Signature: signResp.Data["signature"].([]byte), + KeyFingerprint: keyEntry.PublicKey.Fingerprint, + }, nil } func (p *Plugin) GetPublicKey(_ context.Context, req *keymanagerv1.GetPublicKeyRequest) (*keymanagerv1.GetPublicKeyResponse, error) { @@ -222,6 +257,53 @@ func (p *Plugin) GetPublicKeys(context.Context, *keymanagerv1.GetPublicKeysReque return &keymanagerv1.GetPublicKeysResponse{PublicKeys: keys}, nil } +func algosForKMS(keyType keymanagerv1.KeyType, signerOpts any) (TransitHashAlgorithm, TransitSignatureAlgorithm, error) { + var ( + hashAlgo keymanagerv1.HashAlgorithm + isPSS bool + ) + + switch opts := signerOpts.(type) { + case *keymanagerv1.SignDataRequest_HashAlgorithm: + hashAlgo = opts.HashAlgorithm + isPSS = false + case *keymanagerv1.SignDataRequest_PssOptions: + if opts.PssOptions == nil { + return "", "", errors.New("PSS options are required") + } + hashAlgo = opts.PssOptions.HashAlgorithm + isPSS = true + // opts.PssOptions.SaltLength is handled by Vault. The salt length matches the bits of the hashing algorithm. + default: + return "", "", fmt.Errorf("unsupported signer opts type %T", opts) + } + + isRSA := keyType == keymanagerv1.KeyType_RSA_2048 || keyType == keymanagerv1.KeyType_RSA_4096 + + switch { + case hashAlgo == keymanagerv1.HashAlgorithm_UNSPECIFIED_HASH_ALGORITHM: + return "", "", errors.New("hash algorithm is required") + case keyType == keymanagerv1.KeyType_EC_P256 && hashAlgo == keymanagerv1.HashAlgorithm_SHA256: + return TransitHashAlgorithmSHA256, TransitSignatureSignatureAlgorithmPKCS1v15, nil + case keyType == keymanagerv1.KeyType_EC_P384 && hashAlgo == keymanagerv1.HashAlgorithm_SHA384: + return TransitHashAlgorithmSHA384, TransitSignatureSignatureAlgorithmPKCS1v15, nil + case isRSA && !isPSS && hashAlgo == keymanagerv1.HashAlgorithm_SHA256: + return TransitHashAlgorithmSHA256, TransitSignatureSignatureAlgorithmPKCS1v15, nil + case isRSA && !isPSS && hashAlgo == keymanagerv1.HashAlgorithm_SHA384: + return TransitHashAlgorithmSHA384, TransitSignatureSignatureAlgorithmPKCS1v15, nil + case isRSA && !isPSS && hashAlgo == keymanagerv1.HashAlgorithm_SHA512: + return TransitHashAlgorithmSHA512, TransitSignatureSignatureAlgorithmPKCS1v15, nil + case isRSA && isPSS && hashAlgo == keymanagerv1.HashAlgorithm_SHA256: + return TransitHashAlgorithmSHA256, TransitSignatureSignatureAlgorithmPSS, nil + case isRSA && isPSS && hashAlgo == keymanagerv1.HashAlgorithm_SHA384: + return TransitHashAlgorithmSHA384, TransitSignatureSignatureAlgorithmPSS, nil + case isRSA && isPSS && hashAlgo == keymanagerv1.HashAlgorithm_SHA512: + return TransitHashAlgorithmSHA512, TransitSignatureSignatureAlgorithmPSS, nil + default: + return "", "", fmt.Errorf("unsupported combination of keytype: %v and hashing algorithm: %v", keyType, hashAlgo) + } +} + func (p *Plugin) createKey(ctx context.Context, spireKeyID string, keyType keymanagerv1.KeyType) (*keyEntry, error) { err := p.genVaultClient() if err != nil { diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go index 951665ad76..7387fc1532 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go @@ -353,6 +353,21 @@ const ( TransitKeyType_ECDSA_P384 TransitKeyType = "ecdsa-p384" ) +type TransitHashAlgorithm string + +const ( + TransitHashAlgorithmSHA256 TransitHashAlgorithm = "sha2-256" + TransitHashAlgorithmSHA384 TransitHashAlgorithm = "sha2-384" + TransitHashAlgorithmSHA512 TransitHashAlgorithm = "sha2-512" +) + +type TransitSignatureAlgorithm string + +const ( + TransitSignatureSignatureAlgorithmPSS TransitSignatureAlgorithm = "pss" + TransitSignatureSignatureAlgorithmPKCS1v15 TransitSignatureAlgorithm = "pkcs1v15" +) + // CreateKey creates a new key in the specified transit secret engine // See: https://developer.hashicorp.com/vault/api-docs/secret/transit#create-key func (c *Client) CreateKey(ctx context.Context, spireKeyID string, keyType TransitKeyType) (*vapi.Secret, error) { @@ -371,3 +386,16 @@ func (c *Client) GetKey(ctx context.Context, spireKeyID string) (*vapi.Secret, e // TODO: Make the transit engine path configurable return c.vaultClient.Logical().ReadWithContext(ctx, fmt.Sprintf("/transit/keys/%s", spireKeyID)) } + +func (c *Client) SignData(ctx context.Context, spireKeyID string, data []byte, hashAlgo TransitHashAlgorithm, signatureAlgo TransitSignatureAlgorithm) (*vapi.Secret, error) { + // TODO: Handle errors here + // TODO: Make the transit engine path configurable + return c.vaultClient.Logical().WriteWithContext(ctx, fmt.Sprintf("/transit/sign/%s", spireKeyID), map[string]interface{}{ + "key_version": "0", + "hash_algorithm": hashAlgo, + "input": data, + "signature_algorithm": signatureAlgo, + "marshalling_algorithm": "asn1", // TODO: Should this be jwt? + "salt_length": "hash", // TODO: Should this be auto or should we let it be configured? + }) +} From 8784293173a2f839170e94cec2b200e0e10e6036 Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:22 +0200 Subject: [PATCH 03/29] Make SignData work with HC Vault keymanager plugin (#5058) Signed-off-by: Matteo Kamm --- .../hashicorpvault/hashicorp_vault.go | 29 ++++++++++++++----- .../keymanager/hashicorpvault/vault_client.go | 18 +++++++----- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go index 0cedb1d403..a8dc39a9d7 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go @@ -3,6 +3,7 @@ package hashicorpvault import ( "context" "crypto/sha256" + "encoding/base64" "encoding/hex" "encoding/pem" "errors" @@ -16,6 +17,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "os" + "strings" "sync" ) @@ -212,14 +214,29 @@ func (p *Plugin) SignData(ctx context.Context, req *keymanagerv1.SignDataRequest return nil, err } - signResp, err := p.vc.SignData(ctx, req.KeyId, req.Data, hashAlgo, signingAlgo) + // TODO: Should this be done in SignData? + encodedData := base64.StdEncoding.EncodeToString(req.Data) + signResp, err := p.vc.SignData(ctx, req.KeyId, encodedData, hashAlgo, signingAlgo) if err != nil { return nil, status.Errorf(codes.Internal, "failed to sign: %v", err) } + // TODO: Should this be done in SignData? + sig := signResp.Data["signature"].(string) + cutSig, ok := strings.CutPrefix(sig, "vault:v1:") + + if !ok { + return nil, status.Errorf(codes.Internal, "response should contain vault prefix: %v", err) + } + + data, err := base64.StdEncoding.DecodeString(cutSig) + if err != nil { + return nil, status.Errorf(codes.Internal, "unable to base64 decode signature: %v", err) + } + return &keymanagerv1.SignDataResponse{ - Signature: signResp.Data["signature"].([]byte), + Signature: data, KeyFingerprint: keyEntry.PublicKey.Fingerprint, }, nil } @@ -252,8 +269,6 @@ func (p *Plugin) GetPublicKeys(context.Context, *keymanagerv1.GetPublicKeysReque keys = append(keys, key.PublicKey) } - p.logger.Debug("getting public keys") - return &keymanagerv1.GetPublicKeysResponse{PublicKeys: keys}, nil } @@ -283,10 +298,8 @@ func algosForKMS(keyType keymanagerv1.KeyType, signerOpts any) (TransitHashAlgor switch { case hashAlgo == keymanagerv1.HashAlgorithm_UNSPECIFIED_HASH_ALGORITHM: return "", "", errors.New("hash algorithm is required") - case keyType == keymanagerv1.KeyType_EC_P256 && hashAlgo == keymanagerv1.HashAlgorithm_SHA256: - return TransitHashAlgorithmSHA256, TransitSignatureSignatureAlgorithmPKCS1v15, nil - case keyType == keymanagerv1.KeyType_EC_P384 && hashAlgo == keymanagerv1.HashAlgorithm_SHA384: - return TransitHashAlgorithmSHA384, TransitSignatureSignatureAlgorithmPKCS1v15, nil + case keyType == keymanagerv1.KeyType_EC_P256 || keyType == keymanagerv1.KeyType_EC_P384: + return TransitHashAlgorithmNone, TransitSignatureSignatureAlgorithmPKCS1v15, nil case isRSA && !isPSS && hashAlgo == keymanagerv1.HashAlgorithm_SHA256: return TransitHashAlgorithmSHA256, TransitSignatureSignatureAlgorithmPKCS1v15, nil case isRSA && !isPSS && hashAlgo == keymanagerv1.HashAlgorithm_SHA384: diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go index 7387fc1532..cfb9422259 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go @@ -359,6 +359,7 @@ const ( TransitHashAlgorithmSHA256 TransitHashAlgorithm = "sha2-256" TransitHashAlgorithmSHA384 TransitHashAlgorithm = "sha2-384" TransitHashAlgorithmSHA512 TransitHashAlgorithm = "sha2-512" + TransitHashAlgorithmNone TransitHashAlgorithm = "none" ) type TransitSignatureAlgorithm string @@ -387,15 +388,16 @@ func (c *Client) GetKey(ctx context.Context, spireKeyID string) (*vapi.Secret, e return c.vaultClient.Logical().ReadWithContext(ctx, fmt.Sprintf("/transit/keys/%s", spireKeyID)) } -func (c *Client) SignData(ctx context.Context, spireKeyID string, data []byte, hashAlgo TransitHashAlgorithm, signatureAlgo TransitSignatureAlgorithm) (*vapi.Secret, error) { - // TODO: Handle errors here - // TODO: Make the transit engine path configurable - return c.vaultClient.Logical().WriteWithContext(ctx, fmt.Sprintf("/transit/sign/%s", spireKeyID), map[string]interface{}{ +func (c *Client) SignData(ctx context.Context, spireKeyID string, data string, hashAlgo TransitHashAlgorithm, signatureAlgo TransitSignatureAlgorithm) (*vapi.Secret, error) { + body := map[string]interface{}{ "key_version": "0", - "hash_algorithm": hashAlgo, "input": data, "signature_algorithm": signatureAlgo, - "marshalling_algorithm": "asn1", // TODO: Should this be jwt? - "salt_length": "hash", // TODO: Should this be auto or should we let it be configured? - }) + "marshalling_algorithm": "asn1", + "prehashed": "true", + } + + // TODO: Handle errors here + // TODO: Make the transit engine path configurable + return c.vaultClient.Logical().WriteWithContext(ctx, fmt.Sprintf("/transit/sign/%s/%s", spireKeyID, hashAlgo), body) } From beabe569728658845479f9d91b084d9aa52ce24c Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:22 +0200 Subject: [PATCH 04/29] Rename transit key type constants (#5058) Signed-off-by: Matteo Kamm --- .../keymanager/hashicorpvault/hashicorp_vault.go | 8 ++++---- .../plugin/keymanager/hashicorpvault/vault_client.go | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go index a8dc39a9d7..fd82921010 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go @@ -362,13 +362,13 @@ func (p *Plugin) createKey(ctx context.Context, spireKeyID string, keyType keyma func convertToTransitKeyType(keyType keymanagerv1.KeyType) (*TransitKeyType, error) { switch keyType { case keymanagerv1.KeyType_EC_P256: - return to.Ptr(TransitKeyType_ECDSA_P256), nil + return to.Ptr(TransitKeyTypeECDSAP256), nil case keymanagerv1.KeyType_EC_P384: - return to.Ptr(TransitKeyType_ECDSA_P384), nil + return to.Ptr(TransitKeyTypeECDSAP384), nil case keymanagerv1.KeyType_RSA_2048: - return to.Ptr(TransitKeyType_RSA_2048), nil + return to.Ptr(TransitKeyTypeRSA2048), nil case keymanagerv1.KeyType_RSA_4096: - return to.Ptr(TransitKeyType_RSA_4096), nil + return to.Ptr(TransitKeyTypeRSA4096), nil default: return nil, status.Errorf(codes.Internal, "unsupported key type: %v", keyType) } diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go index cfb9422259..6345fa9aa8 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go @@ -347,10 +347,10 @@ func (c *Client) LookupSelf(token string) (*vapi.Secret, error) { type TransitKeyType string const ( - TransitKeyType_RSA_2048 TransitKeyType = "rsa-2048" - TransitKeyType_RSA_4096 TransitKeyType = "rsa-4096" - TransitKeyType_ECDSA_P256 TransitKeyType = "ecdsa-p256" - TransitKeyType_ECDSA_P384 TransitKeyType = "ecdsa-p384" + TransitKeyTypeRSA2048 TransitKeyType = "rsa-2048" + TransitKeyTypeRSA4096 TransitKeyType = "rsa-4096" + TransitKeyTypeECDSAP256 TransitKeyType = "ecdsa-p256" + TransitKeyTypeECDSAP384 TransitKeyType = "ecdsa-p384" ) type TransitHashAlgorithm string @@ -390,7 +390,7 @@ func (c *Client) GetKey(ctx context.Context, spireKeyID string) (*vapi.Secret, e func (c *Client) SignData(ctx context.Context, spireKeyID string, data string, hashAlgo TransitHashAlgorithm, signatureAlgo TransitSignatureAlgorithm) (*vapi.Secret, error) { body := map[string]interface{}{ - "key_version": "0", + "key_version": "0", // always use tha latest version "input": data, "signature_algorithm": signatureAlgo, "marshalling_algorithm": "asn1", From 39e4803655cf3d656088a0d65d58dfd28f5383c7 Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:22 +0200 Subject: [PATCH 05/29] Make vault client more robust by handling invariant violations (#5058) Signed-off-by: Matteo Kamm --- .../hashicorpvault/hashicorp_vault.go | 22 +++---------- .../keymanager/hashicorpvault/vault_client.go | 31 +++++++++++++++++-- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go index fd82921010..5af9f821c2 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go @@ -17,7 +17,6 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "os" - "strings" "sync" ) @@ -214,29 +213,16 @@ func (p *Plugin) SignData(ctx context.Context, req *keymanagerv1.SignDataRequest return nil, err } - // TODO: Should this be done in SignData? + // TODO: Should the encoding be done in SignData? encodedData := base64.StdEncoding.EncodeToString(req.Data) - signResp, err := p.vc.SignData(ctx, req.KeyId, encodedData, hashAlgo, signingAlgo) + signature, err := p.vc.SignData(ctx, req.KeyId, encodedData, hashAlgo, signingAlgo) if err != nil { - return nil, status.Errorf(codes.Internal, "failed to sign: %v", err) - } - - // TODO: Should this be done in SignData? - sig := signResp.Data["signature"].(string) - cutSig, ok := strings.CutPrefix(sig, "vault:v1:") - - if !ok { - return nil, status.Errorf(codes.Internal, "response should contain vault prefix: %v", err) - } - - data, err := base64.StdEncoding.DecodeString(cutSig) - if err != nil { - return nil, status.Errorf(codes.Internal, "unable to base64 decode signature: %v", err) + return nil, err } return &keymanagerv1.SignDataResponse{ - Signature: data, + Signature: signature, KeyFingerprint: keyEntry.PublicKey.Fingerprint, }, nil } diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go index 6345fa9aa8..e7c31ae628 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go @@ -3,6 +3,7 @@ package hashicorpvault import ( "crypto/tls" "crypto/x509" + "encoding/base64" "fmt" "github.com/hashicorp/go-hclog" vapi "github.com/hashicorp/vault/api" @@ -12,6 +13,7 @@ import ( "google.golang.org/grpc/status" "net/http" "os" + "strings" "github.com/spiffe/spire/pkg/common/pemutil" ) @@ -388,7 +390,7 @@ func (c *Client) GetKey(ctx context.Context, spireKeyID string) (*vapi.Secret, e return c.vaultClient.Logical().ReadWithContext(ctx, fmt.Sprintf("/transit/keys/%s", spireKeyID)) } -func (c *Client) SignData(ctx context.Context, spireKeyID string, data string, hashAlgo TransitHashAlgorithm, signatureAlgo TransitSignatureAlgorithm) (*vapi.Secret, error) { +func (c *Client) SignData(ctx context.Context, spireKeyID string, data string, hashAlgo TransitHashAlgorithm, signatureAlgo TransitSignatureAlgorithm) ([]byte, error) { body := map[string]interface{}{ "key_version": "0", // always use tha latest version "input": data, @@ -399,5 +401,30 @@ func (c *Client) SignData(ctx context.Context, spireKeyID string, data string, h // TODO: Handle errors here // TODO: Make the transit engine path configurable - return c.vaultClient.Logical().WriteWithContext(ctx, fmt.Sprintf("/transit/sign/%s/%s", spireKeyID, hashAlgo), body) + sigResp, err := c.vaultClient.Logical().WriteWithContext(ctx, fmt.Sprintf("/transit/sign/%s/%s", spireKeyID, hashAlgo), body) + if err != nil { + return nil, status.Errorf(codes.Internal, "transit engine sign call failed: %v", err) + } + + sig, ok := sigResp.Data["signature"] + if !ok { + return nil, status.Errorf(codes.Internal, "transit engine sign call was successful but signature is missing: %v", err) + } + + sigStr, ok := sig.(string) + if !ok { + return nil, status.Errorf(codes.Internal, "expected signature data type %T but got %T", sigStr, sig) + } + + cutSig, ok := strings.CutPrefix(sigStr, "vault:v1:") + if !ok { + return nil, status.Errorf(codes.Internal, "signature is missing vault prefix: %v", err) + } + + sigData, err := base64.StdEncoding.DecodeString(cutSig) + if err != nil { + return nil, status.Errorf(codes.Internal, "unable to base64 decode signature: %v", err) + } + + return sigData, nil } From 8d60d506cad5b212fed389122ceea980f77e8f8b Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:22 +0200 Subject: [PATCH 06/29] Move logic from plugin to vault client (#5058) Signed-off-by: Matteo Kamm --- .../hashicorpvault/hashicorp_vault.go | 24 ++++--- .../keymanager/hashicorpvault/vault_client.go | 63 ++++++++++++++----- 2 files changed, 59 insertions(+), 28 deletions(-) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go index 5af9f821c2..2806d563ea 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go @@ -45,6 +45,7 @@ type pluginHooks struct { refreshKeysSignal chan error disposeKeysSignal chan error + newClient func(*ClientConfig, AuthMethod, chan struct{}) (client cloudKeyManagementService, err error) lookupEnv func(string) (string, bool) } @@ -76,22 +77,25 @@ type Plugin struct { authMethod AuthMethod cc *ClientConfig - vc *Client + vc cloudKeyManagementService hooks pluginHooks } // New returns an instantiated plugin. func New() *Plugin { - return newPlugin() + return newPlugin(func(config *ClientConfig, method AuthMethod, renewCh chan struct{}) (client cloudKeyManagementService, err error) { + return config.NewAuthenticatedClient(method, renewCh) + }) } // newPlugin returns a new plugin instance. -func newPlugin() *Plugin { +func newPlugin(newClient func(*ClientConfig, AuthMethod, chan struct{}) (client cloudKeyManagementService, err error)) *Plugin { return &Plugin{ entries: make(map[string]keyEntry), hooks: pluginHooks{ lookupEnv: os.LookupEnv, + newClient: newClient, }, } } @@ -314,23 +318,17 @@ func (p *Plugin) createKey(ctx context.Context, spireKeyID string, keyType keyma return nil, err } - s, err := p.vc.CreateKey(ctx, spireKeyID, *kt) + err = p.vc.CreateKey(ctx, spireKeyID, *kt) if err != nil { return nil, err } - s, err = p.vc.GetKey(ctx, spireKeyID) + pk, 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) + pemBlock, _ := pem.Decode([]byte(pk)) if pemBlock == nil || pemBlock.Type != "PUBLIC KEY" { return nil, status.Error(codes.Internal, "unable to decode PEM key") } @@ -365,7 +363,7 @@ func convertToTransitKeyType(keyType keymanagerv1.KeyType) (*TransitKeyType, err func (p *Plugin) genVaultClient() error { if p.vc == nil { renewCh := make(chan struct{}) - vc, err := p.cc.NewAuthenticatedClient(p.authMethod, renewCh) + vc, err := p.hooks.newClient(p.cc, p.authMethod, renewCh) if err != nil { return status.Errorf(codes.Internal, "failed to prepare authenticated client: %v", err) } diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go index e7c31ae628..366f0c0d52 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go @@ -36,6 +36,12 @@ const ( defaultK8sMountPoint = "kubernetes" ) +type cloudKeyManagementService interface { + CreateKey(ctx context.Context, spireKeyID string, keyType TransitKeyType) error + GetKey(ctx context.Context, spireKeyID string) (string, error) + SignData(ctx context.Context, spireKeyID string, data string, hashAlgo TransitHashAlgorithm, signatureAlgo TransitSignatureAlgorithm) ([]byte, error) +} + type AuthMethod int const ( @@ -100,16 +106,6 @@ type Client struct { clientParams *ClientParams } -// SignCSRResponse includes certificates which are generates by Vault -type SignCSRResponse struct { - // A certificate requested to sign - CACertPEM string - // A certificate of CA(Vault) - UpstreamCACertPEM string - // Set of Upstream CA certificates - UpstreamCACertChainPEM []string -} - // NewClientConfig returns a new *ClientConfig with default parameters. func NewClientConfig(cp *ClientParams, logger hclog.Logger) (*ClientConfig, error) { cc := &ClientConfig{ @@ -373,7 +369,7 @@ const ( // CreateKey creates a new key in the specified transit secret engine // See: https://developer.hashicorp.com/vault/api-docs/secret/transit#create-key -func (c *Client) CreateKey(ctx context.Context, spireKeyID string, keyType TransitKeyType) (*vapi.Secret, error) { +func (c *Client) CreateKey(ctx context.Context, spireKeyID string, keyType TransitKeyType) error { arguments := map[string]interface{}{ "type": keyType, "exportable": "false", // TODO: Maybe make this configurable @@ -381,13 +377,50 @@ func (c *Client) CreateKey(ctx context.Context, spireKeyID string, keyType Trans // TODO: Handle errors here such as key already exists // TODO: Make the transit engine path configurable - return c.vaultClient.Logical().WriteWithContext(ctx, fmt.Sprintf("/transit/keys/%s", spireKeyID), arguments) + _, err := c.vaultClient.Logical().WriteWithContext(ctx, fmt.Sprintf("/transit/keys/%s", spireKeyID), arguments) + return err } -func (c *Client) GetKey(ctx context.Context, spireKeyID string) (*vapi.Secret, error) { +func (c *Client) GetKey(ctx context.Context, spireKeyID string) (string, error) { // TODO: Handle errors here // TODO: Make the transit engine path configurable - return c.vaultClient.Logical().ReadWithContext(ctx, fmt.Sprintf("/transit/keys/%s", spireKeyID)) + res, err := c.vaultClient.Logical().ReadWithContext(ctx, fmt.Sprintf("/transit/keys/%s", spireKeyID)) + if err != nil { + return "", err + } + + keys, ok := res.Data["keys"] + if !ok { + return "", status.Errorf(codes.Internal, "transit engine get key call was successful but keys are missing") + } + + keyMap, ok := keys.(map[string]interface{}) + if !ok { + return "", status.Errorf(codes.Internal, "expected key map data type %T but got %T", keyMap, keys) + } + + // TODO: Should we support multiple versions of the key? + currentKey, ok := keyMap["1"] + if !ok { + return "", status.Errorf(codes.Internal, "unable to find key with version 1 in %v", keyMap) + } + + currentKeyMap, ok := currentKey.(map[string]interface{}) + if !ok { + return "", status.Errorf(codes.Internal, "expected key data type %T but got %T", currentKeyMap, currentKey) + } + + pk, ok := currentKeyMap["public_key"] + if !ok { + return "", status.Errorf(codes.Internal, "expected public key to be present") + } + + pkStr, ok := pk.(string) + if !ok { + return "", status.Errorf(codes.Internal, "expected public key data type %T but got %T", pkStr, pk) + } + + return pkStr, nil } func (c *Client) SignData(ctx context.Context, spireKeyID string, data string, hashAlgo TransitHashAlgorithm, signatureAlgo TransitSignatureAlgorithm) ([]byte, error) { @@ -408,7 +441,7 @@ func (c *Client) SignData(ctx context.Context, spireKeyID string, data string, h sig, ok := sigResp.Data["signature"] if !ok { - return nil, status.Errorf(codes.Internal, "transit engine sign call was successful but signature is missing: %v", err) + return nil, status.Errorf(codes.Internal, "transit engine sign call was successful but signature is missing") } sigStr, ok := sig.(string) From 90f68fd88e343ac10fef57b1411f88f52c72f86b Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:22 +0200 Subject: [PATCH 07/29] Refactor logic to generate vault client (#5058) Signed-off-by: Matteo Kamm --- .../hashicorpvault/hashicorp_vault.go | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go index 2806d563ea..ae7b618ef9 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go @@ -212,9 +212,11 @@ func (p *Plugin) SignData(ctx context.Context, req *keymanagerv1.SignDataRequest return nil, status.Error(codes.InvalidArgument, err.Error()) } - err = p.genVaultClient() - if err != nil { - return nil, err + if p.vc == nil { + err := p.genVaultClient() + if err != nil { + return nil, err + } } // TODO: Should the encoding be done in SignData? @@ -308,9 +310,11 @@ func algosForKMS(keyType keymanagerv1.KeyType, signerOpts any) (TransitHashAlgor } func (p *Plugin) createKey(ctx context.Context, spireKeyID string, keyType keymanagerv1.KeyType) (*keyEntry, error) { - err := p.genVaultClient() - if err != nil { - return nil, err + if p.vc == nil { + err := p.genVaultClient() + if err != nil { + return nil, err + } } kt, err := convertToTransitKeyType(keyType) @@ -358,27 +362,23 @@ func convertToTransitKeyType(keyType keymanagerv1.KeyType) (*TransitKeyType, err } } -// 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.hooks.newClient(p.cc, 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") - }() + renewCh := make(chan struct{}) + vc, err := p.hooks.newClient(p.cc, 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 } From c6332a0c2f39c1f95f3ac1ca45f7b861286e1828 Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:22 +0200 Subject: [PATCH 08/29] Use latest key version to sign data (#5058) Signed-off-by: Matteo Kamm --- pkg/server/plugin/keymanager/hashicorpvault/vault_client.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go index 366f0c0d52..1f32ba9e61 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go @@ -425,7 +425,6 @@ func (c *Client) GetKey(ctx context.Context, spireKeyID string) (string, error) func (c *Client) SignData(ctx context.Context, spireKeyID string, data string, hashAlgo TransitHashAlgorithm, signatureAlgo TransitSignatureAlgorithm) ([]byte, error) { body := map[string]interface{}{ - "key_version": "0", // always use tha latest version "input": data, "signature_algorithm": signatureAlgo, "marshalling_algorithm": "asn1", From 80428771ee80519c9d3d02f3cc4ceb210da0d60f Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:22 +0200 Subject: [PATCH 09/29] Move data encoding to vault client (#5058) Signed-off-by: Matteo Kamm --- .../plugin/keymanager/hashicorpvault/hashicorp_vault.go | 6 +----- .../plugin/keymanager/hashicorpvault/vault_client.go | 8 +++++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go index ae7b618ef9..e710a4010b 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go @@ -3,7 +3,6 @@ package hashicorpvault import ( "context" "crypto/sha256" - "encoding/base64" "encoding/hex" "encoding/pem" "errors" @@ -219,10 +218,7 @@ func (p *Plugin) SignData(ctx context.Context, req *keymanagerv1.SignDataRequest } } - // TODO: Should the encoding be done in SignData? - encodedData := base64.StdEncoding.EncodeToString(req.Data) - - signature, err := p.vc.SignData(ctx, req.KeyId, encodedData, hashAlgo, signingAlgo) + signature, err := p.vc.SignData(ctx, req.KeyId, req.Data, hashAlgo, signingAlgo) if err != nil { return nil, err } diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go index 1f32ba9e61..2b0d1a6d7a 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go @@ -39,7 +39,7 @@ const ( type cloudKeyManagementService interface { CreateKey(ctx context.Context, spireKeyID string, keyType TransitKeyType) error GetKey(ctx context.Context, spireKeyID string) (string, error) - SignData(ctx context.Context, spireKeyID string, data string, hashAlgo TransitHashAlgorithm, signatureAlgo TransitSignatureAlgorithm) ([]byte, error) + SignData(ctx context.Context, spireKeyID string, data []byte, hashAlgo TransitHashAlgorithm, signatureAlgo TransitSignatureAlgorithm) ([]byte, error) } type AuthMethod int @@ -423,9 +423,11 @@ func (c *Client) GetKey(ctx context.Context, spireKeyID string) (string, error) return pkStr, nil } -func (c *Client) SignData(ctx context.Context, spireKeyID string, data string, hashAlgo TransitHashAlgorithm, signatureAlgo TransitSignatureAlgorithm) ([]byte, error) { +func (c *Client) SignData(ctx context.Context, spireKeyID string, data []byte, hashAlgo TransitHashAlgorithm, signatureAlgo TransitSignatureAlgorithm) ([]byte, error) { + encodedData := base64.StdEncoding.EncodeToString(data) + body := map[string]interface{}{ - "input": data, + "input": encodedData, "signature_algorithm": signatureAlgo, "marshalling_algorithm": "asn1", "prehashed": "true", From 702193af4bacbb940011639e21564f22138c3129 Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:22 +0200 Subject: [PATCH 10/29] Add simple vault client auth test (#5058) Signed-off-by: Matteo Kamm --- .../hashicorpvault/hashicorp_vault.go | 12 +- .../hashicorpvault/testdata/root-cert.pem | 9 + .../hashicorpvault/testdata/server-cert.pem | 9 + .../hashicorpvault/testdata/server-key.pem | 5 + .../keymanager/hashicorpvault/vault_client.go | 6 - .../hashicorpvault/vault_client_test.go | 185 +++++++++++++ .../hashicorpvault/vault_fake_test.go | 249 ++++++++++++++++++ 7 files changed, 461 insertions(+), 14 deletions(-) create mode 100644 pkg/server/plugin/keymanager/hashicorpvault/testdata/root-cert.pem create mode 100644 pkg/server/plugin/keymanager/hashicorpvault/testdata/server-cert.pem create mode 100644 pkg/server/plugin/keymanager/hashicorpvault/testdata/server-key.pem create mode 100644 pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go create mode 100644 pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go diff --git a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go index e710a4010b..dc403dd75e 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go @@ -44,7 +44,6 @@ type pluginHooks struct { refreshKeysSignal chan error disposeKeysSignal chan error - newClient func(*ClientConfig, AuthMethod, chan struct{}) (client cloudKeyManagementService, err error) lookupEnv func(string) (string, bool) } @@ -76,25 +75,22 @@ type Plugin struct { authMethod AuthMethod cc *ClientConfig - vc cloudKeyManagementService + vc *Client hooks pluginHooks } // New returns an instantiated plugin. func New() *Plugin { - return newPlugin(func(config *ClientConfig, method AuthMethod, renewCh chan struct{}) (client cloudKeyManagementService, err error) { - return config.NewAuthenticatedClient(method, renewCh) - }) + return newPlugin() } // newPlugin returns a new plugin instance. -func newPlugin(newClient func(*ClientConfig, AuthMethod, chan struct{}) (client cloudKeyManagementService, err error)) *Plugin { +func newPlugin() *Plugin { return &Plugin{ entries: make(map[string]keyEntry), hooks: pluginHooks{ lookupEnv: os.LookupEnv, - newClient: newClient, }, } } @@ -360,7 +356,7 @@ func convertToTransitKeyType(keyType keymanagerv1.KeyType) (*TransitKeyType, err func (p *Plugin) genVaultClient() error { renewCh := make(chan struct{}) - vc, err := p.hooks.newClient(p.cc, p.authMethod, renewCh) + vc, err := p.cc.NewAuthenticatedClient(p.authMethod, renewCh) if err != nil { return status.Errorf(codes.Internal, "failed to prepare authenticated client: %v", err) } diff --git a/pkg/server/plugin/keymanager/hashicorpvault/testdata/root-cert.pem b/pkg/server/plugin/keymanager/hashicorpvault/testdata/root-cert.pem new file mode 100644 index 0000000000..0c02782af5 --- /dev/null +++ b/pkg/server/plugin/keymanager/hashicorpvault/testdata/root-cert.pem @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE----- +MIIBMjCB2aADAgECAgEBMAoGCCqGSM49BAMCMAAwIBgPMDAwMTAxMDEwMDAwMDBa +Fw0zMjA0MTIxNjA4NDRaMAAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQaWBAL +TN4YPe4yQgMhDp9DZOPXaglEchzUo++feITLXN9XuUICLNWO9YEtAsaRsajul8Bc +GL9Rmbv2f6J2Lnueo0IwQDAOBgNVHQ8BAf8EBAMCAgQwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUmEs2MBzULBomV0lWA7OfcN/lGDcwCgYIKoZIzj0EAwIDSAAw +RQIhAP86wRV1PHg6rFkjl1Nx6He+Y2LSdOoEGnGlVM0ztzlUAiBpPhSMqonlFLZa +nLW9psyWrQMHai7KZLJjLfw+UMl0sQ== +-----END CERTIFICATE----- diff --git a/pkg/server/plugin/keymanager/hashicorpvault/testdata/server-cert.pem b/pkg/server/plugin/keymanager/hashicorpvault/testdata/server-cert.pem new file mode 100644 index 0000000000..d12aa83669 --- /dev/null +++ b/pkg/server/plugin/keymanager/hashicorpvault/testdata/server-cert.pem @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE----- +MIIBPTCB46ADAgECAgECMAoGCCqGSM49BAMCMAAwIBgPMDAwMTAxMDEwMDAwMDBa +Fw0zMjA0MTIxNjA4NDRaMAAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAS6v/nm +XmVkQGMfqDpEq6aiV/AnwcGAJBGTL/ixbDqCPD5crgrXaycLdbZqy8jYVA5uWfHh +Ps+5/8acn3cSSAc2o0wwSjATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAW +gBSYSzYwHNQsGiZXSVYDs59w3+UYNzASBgNVHREBAf8ECDAGhwR/AAABMAoGCCqG +SM49BAMCA0kAMEYCIQDkCDZP2InFWBBazaVJZlIwMz/o2cm3K7xaPbVucHPuswIh +AJstcTQ/RjJKhfZQo7mOIHO+l5U0TeInMCYg9XEPcNJt +-----END CERTIFICATE----- diff --git a/pkg/server/plugin/keymanager/hashicorpvault/testdata/server-key.pem b/pkg/server/plugin/keymanager/hashicorpvault/testdata/server-key.pem new file mode 100644 index 0000000000..dc98bde505 --- /dev/null +++ b/pkg/server/plugin/keymanager/hashicorpvault/testdata/server-key.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgjHE1FFYDxseFqNrC +jjh72BLj5tHTh5vIMcdn0w3W1PKhRANCAAS6v/nmXmVkQGMfqDpEq6aiV/AnwcGA +JBGTL/ixbDqCPD5crgrXaycLdbZqy8jYVA5uWfHhPs+5/8acn3cSSAc2 +-----END PRIVATE KEY----- diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go index 2b0d1a6d7a..561674f0ad 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go @@ -36,12 +36,6 @@ const ( defaultK8sMountPoint = "kubernetes" ) -type cloudKeyManagementService interface { - CreateKey(ctx context.Context, spireKeyID string, keyType TransitKeyType) error - GetKey(ctx context.Context, spireKeyID string) (string, error) - SignData(ctx context.Context, spireKeyID string, data []byte, hashAlgo TransitHashAlgorithm, signatureAlgo TransitSignatureAlgorithm) ([]byte, error) -} - type AuthMethod int const ( diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go new file mode 100644 index 0000000000..f311c2586c --- /dev/null +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go @@ -0,0 +1,185 @@ +package hashicorpvault + +import ( + "fmt" + "testing" + "time" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/vault/sdk/helper/consts" + "github.com/spiffe/spire/test/spiretest" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" +) + +const ( + testRootCert = "testdata/root-cert.pem" + testServerCert = "testdata/server-cert.pem" + testServerKey = "testdata/server-key.pem" +) + +func TestNewClientConfigWithDefaultValues(t *testing.T) { + p := &ClientParams{ + VaultAddr: "http://example.org:8200/", + PKIMountPoint: "", // Expect the default value to be used. + Token: "test-token", + CertAuthMountPoint: "", // Expect the default value to be used. + AppRoleAuthMountPoint: "", // Expect the default value to be used. + K8sAuthMountPoint: "", // Expect the default value to be used. + } + + cc, err := NewClientConfig(p, hclog.Default()) + require.NoError(t, err) + require.Equal(t, defaultPKIMountPoint, cc.clientParams.PKIMountPoint) + require.Equal(t, defaultCertMountPoint, cc.clientParams.CertAuthMountPoint) + require.Equal(t, defaultAppRoleMountPoint, cc.clientParams.AppRoleAuthMountPoint) + require.Equal(t, defaultK8sMountPoint, cc.clientParams.K8sAuthMountPoint) +} + +func TestNewClientConfigWithGivenValuesInsteadOfDefaults(t *testing.T) { + p := &ClientParams{ + VaultAddr: "http://example.org:8200/", + PKIMountPoint: "test-pki", + Token: "test-token", + CertAuthMountPoint: "test-tls-cert", + AppRoleAuthMountPoint: "test-approle", + K8sAuthMountPoint: "test-k8s", + } + + cc, err := NewClientConfig(p, hclog.Default()) + require.NoError(t, err) + require.Equal(t, "test-pki", cc.clientParams.PKIMountPoint) + require.Equal(t, "test-tls-cert", cc.clientParams.CertAuthMountPoint) + require.Equal(t, "test-approle", cc.clientParams.AppRoleAuthMountPoint) + require.Equal(t, "test-k8s", cc.clientParams.K8sAuthMountPoint) +} + +func TestNewAuthenticatedClientTokenAuth(t *testing.T) { + fakeVaultServer := newFakeVaultServer() + fakeVaultServer.LookupSelfResponseCode = 200 + for _, tt := range []struct { + name string + token string + response []byte + renew bool + namespace string + expectCode codes.Code + expectMsgPrefix string + }{ + { + name: "Token Authentication success / Token never expire", + token: "test-token", + response: []byte(testLookupSelfResponseNeverExpire), + renew: true, + }, + { + name: "Token Authentication success / Token is renewable", + token: "test-token", + response: []byte(testLookupSelfResponse), + renew: true, + }, + { + name: "Token Authentication success / Token is not renewable", + token: "test-token", + response: []byte(testLookupSelfResponseNotRenewable), + }, + { + name: "Token Authentication success / Token is renewable / Namespace is given", + token: "test-token", + response: []byte(testCertAuthResponse), + renew: true, + namespace: "test-ns", + }, + { + name: "Token Authentication error / Token is empty", + token: "", + response: []byte(testCertAuthResponse), + renew: true, + namespace: "test-ns", + expectCode: codes.InvalidArgument, + expectMsgPrefix: "token is empty", + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + fakeVaultServer.LookupSelfResponse = tt.response + + s, addr, err := fakeVaultServer.NewTLSServer() + require.NoError(t, err) + + s.Start() + defer s.Close() + + cp := &ClientParams{ + VaultAddr: fmt.Sprintf("https://%v/", addr), + Namespace: tt.namespace, + CACertPath: testRootCert, + Token: tt.token, + } + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + renewCh := make(chan struct{}) + client, err := cc.NewAuthenticatedClient(TOKEN, renewCh) + if tt.expectMsgPrefix != "" { + spiretest.RequireGRPCStatusHasPrefix(t, err, tt.expectCode, tt.expectMsgPrefix) + return + } + + require.NoError(t, err) + + select { + case <-renewCh: + require.Equal(t, false, tt.renew) + default: + require.Equal(t, true, tt.renew) + } + + if cp.Namespace != "" { + headers := client.vaultClient.Headers() + require.Equal(t, cp.Namespace, headers.Get(consts.NamespaceHeaderName)) + } + }) + } +} + +func TestRenewTokenFailed(t *testing.T) { + fakeVaultServer := newFakeVaultServer() + fakeVaultServer.LookupSelfResponse = []byte(testLookupSelfResponseShortTTL) + fakeVaultServer.LookupSelfResponseCode = 200 + fakeVaultServer.RenewResponse = []byte("fake renew error") + fakeVaultServer.RenewResponseCode = 500 + + s, addr, err := fakeVaultServer.NewTLSServer() + require.NoError(t, err) + + s.Start() + defer s.Close() + + retry := 0 + cp := &ClientParams{ + MaxRetries: &retry, + VaultAddr: fmt.Sprintf("https://%v/", addr), + CACertPath: testRootCert, + Token: "test-token", + } + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + renewCh := make(chan struct{}) + _, err = cc.NewAuthenticatedClient(TOKEN, renewCh) + require.NoError(t, err) + + select { + case <-renewCh: + case <-time.After(1 * time.Second): + t.Error("renewChan did not close in the expected time") + } +} + +func newFakeVaultServer() *FakeVaultServerConfig { + fakeVaultServer := NewFakeVaultServerConfig() + fakeVaultServer.RenewResponseCode = 200 + fakeVaultServer.RenewResponse = []byte(testRenewResponse) + return fakeVaultServer +} diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go new file mode 100644 index 0000000000..df87f71f1d --- /dev/null +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go @@ -0,0 +1,249 @@ +package hashicorpvault + +import ( + "crypto/tls" + "fmt" + "net/http" + "net/http/httptest" +) + +const ( + defaultTLSAuthEndpoint = "/v1/auth/cert/login" + defaultAppRoleAuthEndpoint = "/v1/auth/approle/login" + defaultK8sAuthEndpoint = "/v1/auth/kubernetes/login" + defaultSignIntermediateEndpoint = "/v1/pki/root/sign-intermediate" + defaultRenewEndpoint = "/v1/auth/token/renew-self" + defaultLookupSelfEndpoint = "/v1/auth/token/lookup-self" + + listenAddr = "127.0.0.1:0" +) + +var ( + testCertAuthResponse = `{ + "auth": { + "client_token": "cf95f87d-f95b-47ff-b1f5-ba7bff850425", + "policies": [ + "web", + "stage" + ], + "lease_duration": 3600, + "renewable": true + } +}` + + testRenewResponse = `{ + "auth": { + "client_token": "test-client-token", + "policies": ["app", "test"], + "metadata": { + "user": "test" + }, + "lease_duration": 3600, + "renewable": true + } +}` + + testLookupSelfResponseNeverExpire = `{ + "request_id": "90e4b86a-5c61-1aeb-0fc7-50a05056c3b3", + "lease_id": "", + "lease_duration": 0, + "renewable": false, + "data": { + "accessor": "rQuZeGOEdH4IazavJWqwTCRk", + "creation_time": 1605502335, + "creation_ttl": 0, + "display_name": "root", + "entity_id": "", + "expire_time": null, + "explicit_max_ttl": 0, + "id": "test-token", + "meta": null, + "num_uses": 0, + "orphan": true, + "path": "auth/token/root", + "policies": [ + "root" + ], + "ttl": 0, + "type": "service" + }, + "warnings": null +}` + + testLookupSelfResponse = `{ + "request_id": "8dc10d02-797d-1c23-f9f3-c7f07be89150", + "lease_id": "", + "lease_duration": 0, + "renewable": false, + "data": { + "accessor": "sB3mNrjoIr2JscfNsAUM1k0A", + "creation_time": 1605502988, + "creation_ttl": 2764800, + "display_name": "approle", + "entity_id": "0bee5a2d-efe5-6fd3-9c5a-972266ecccf4", + "expire_time": "2020-12-18T05:03:08.5694729Z", + "explicit_max_ttl": 0, + "id": "test-token", + "issue_time": "2020-11-16T05:03:08.5694807Z", + "meta": { + "role_name": "test" + }, + "num_uses": 0, + "orphan": true, + "path": "auth/approle/login", + "policies": [ + "default" + ], + "renewable": true, + "ttl": 3600, + "type": "service" + }, + "warnings": null +}` + + testLookupSelfResponseShortTTL = `{ + "request_id": "8dc10d02-797d-1c23-f9f3-c7f07be89150", + "lease_id": "", + "lease_duration": 0, + "renewable": false, + "data": { + "accessor": "sB3mNrjoIr2JscfNsAUM1k0A", + "creation_time": 1605502988, + "creation_ttl": 2764800, + "display_name": "approle", + "entity_id": "0bee5a2d-efe5-6fd3-9c5a-972266ecccf4", + "expire_time": "2020-12-18T05:03:08.5694729Z", + "explicit_max_ttl": 0, + "id": "test-token", + "issue_time": "2020-11-16T05:03:08.5694807Z", + "meta": { + "role_name": "test" + }, + "num_uses": 0, + "orphan": true, + "path": "auth/approle/login", + "policies": [ + "default" + ], + "renewable": true, + "ttl": 1, + "type": "service" + }, + "warnings": null +}` + + testLookupSelfResponseNotRenewable = `{ + "request_id": "ac39fad7-02d7-48df-2f8a-7a1872c41a4b", + "lease_id": "", + "lease_duration": 0, + "renewable": false, + "data": { + "accessor": "", + "creation_time": 1605506361, + "creation_ttl": 3600, + "display_name": "approle", + "entity_id": "0bee5a2d-efe5-6fd3-9c5a-972266ecccf4", + "expire_time": "2020-11-16T06:59:21Z", + "explicit_max_ttl": 0, + "id": "test-token", + "issue_time": "2020-11-16T05:59:21Z", + "meta": { + "role_name": "test" + }, + "num_uses": 0, + "orphan": true, + "path": "auth/approle/login", + "policies": [ + "default" + ], + "renewable": false, + "ttl": 3517, + "type": "batch" + }, + "warnings": null +}` +) + +type FakeVaultServerConfig struct { + ListenAddr string + ServerCertificatePemPath string + ServerKeyPemPath string + CertAuthReqEndpoint string + CertAuthReqHandler func(code int, resp []byte) func(http.ResponseWriter, *http.Request) + CertAuthResponseCode int + CertAuthResponse []byte + AppRoleAuthReqEndpoint string + AppRoleAuthReqHandler func(code int, resp []byte) func(w http.ResponseWriter, r *http.Request) + AppRoleAuthResponseCode int + AppRoleAuthResponse []byte + K8sAuthReqEndpoint string + K8sAuthReqHandler func(code int, resp []byte) func(w http.ResponseWriter, r *http.Request) + K8sAuthResponseCode int + K8sAuthResponse []byte + SignIntermediateReqEndpoint string + SignIntermediateReqHandler func(code int, resp []byte) func(http.ResponseWriter, *http.Request) + SignIntermediateResponseCode int + SignIntermediateResponse []byte + RenewReqEndpoint string + RenewReqHandler func(code int, resp []byte) func(http.ResponseWriter, *http.Request) + RenewResponseCode int + RenewResponse []byte + LookupSelfReqEndpoint string + LookupSelfReqHandler func(code int, resp []byte) func(w http.ResponseWriter, r *http.Request) + LookupSelfResponseCode int + LookupSelfResponse []byte +} + +// NewFakeVaultServerConfig returns VaultServerConfig with default values +func NewFakeVaultServerConfig() *FakeVaultServerConfig { + return &FakeVaultServerConfig{ + ListenAddr: listenAddr, + CertAuthReqEndpoint: defaultTLSAuthEndpoint, + CertAuthReqHandler: defaultReqHandler, + AppRoleAuthReqEndpoint: defaultAppRoleAuthEndpoint, + AppRoleAuthReqHandler: defaultReqHandler, + K8sAuthReqEndpoint: defaultK8sAuthEndpoint, + K8sAuthReqHandler: defaultReqHandler, + SignIntermediateReqEndpoint: defaultSignIntermediateEndpoint, + SignIntermediateReqHandler: defaultReqHandler, + RenewReqEndpoint: defaultRenewEndpoint, + RenewReqHandler: defaultReqHandler, + LookupSelfReqEndpoint: defaultLookupSelfEndpoint, + LookupSelfReqHandler: defaultReqHandler, + } +} + +func defaultReqHandler(code int, resp []byte) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(code) + _, _ = w.Write(resp) + } +} + +func (v *FakeVaultServerConfig) NewTLSServer() (srv *httptest.Server, addr string, err error) { + cert, err := tls.LoadX509KeyPair(testServerCert, testServerKey) + if err != nil { + return nil, "", fmt.Errorf("failed to load key-pair: %w", err) + } + config := &tls.Config{ + Certificates: []tls.Certificate{cert}, + MinVersion: tls.VersionTLS12, + } + + l, err := tls.Listen("tcp", v.ListenAddr, config) + if err != nil { + return nil, "", fmt.Errorf("failed to listen test server: %w", err) + } + + mux := http.NewServeMux() + mux.HandleFunc(v.CertAuthReqEndpoint, v.CertAuthReqHandler(v.CertAuthResponseCode, v.CertAuthResponse)) + mux.HandleFunc(v.AppRoleAuthReqEndpoint, v.AppRoleAuthReqHandler(v.AppRoleAuthResponseCode, v.AppRoleAuthResponse)) + mux.HandleFunc(v.K8sAuthReqEndpoint, v.AppRoleAuthReqHandler(v.K8sAuthResponseCode, v.K8sAuthResponse)) + mux.HandleFunc(v.SignIntermediateReqEndpoint, v.SignIntermediateReqHandler(v.SignIntermediateResponseCode, v.SignIntermediateResponse)) + mux.HandleFunc(v.RenewReqEndpoint, v.RenewReqHandler(v.RenewResponseCode, v.RenewResponse)) + mux.HandleFunc(v.LookupSelfReqEndpoint, v.LookupSelfReqHandler(v.LookupSelfResponseCode, v.LookupSelfResponse)) + + srv = httptest.NewUnstartedServer(mux) + srv.Listener = l + return srv, l.Addr().String(), nil +} From 0d78ffaa798305b57a651a2ae3c9d769dcc43015 Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:22 +0200 Subject: [PATCH 11/29] Remove unused test code (#5058) Signed-off-by: Matteo Kamm --- .../hashicorpvault/vault_fake_test.go | 86 +++++++++---------- 1 file changed, 39 insertions(+), 47 deletions(-) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go index df87f71f1d..8d9f9d5fc5 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go @@ -8,12 +8,11 @@ import ( ) const ( - defaultTLSAuthEndpoint = "/v1/auth/cert/login" - defaultAppRoleAuthEndpoint = "/v1/auth/approle/login" - defaultK8sAuthEndpoint = "/v1/auth/kubernetes/login" - defaultSignIntermediateEndpoint = "/v1/pki/root/sign-intermediate" - defaultRenewEndpoint = "/v1/auth/token/renew-self" - defaultLookupSelfEndpoint = "/v1/auth/token/lookup-self" + defaultTLSAuthEndpoint = "/v1/auth/cert/login" + defaultAppRoleAuthEndpoint = "/v1/auth/approle/login" + defaultK8sAuthEndpoint = "/v1/auth/kubernetes/login" + defaultRenewEndpoint = "/v1/auth/token/renew-self" + defaultLookupSelfEndpoint = "/v1/auth/token/lookup-self" listenAddr = "127.0.0.1:0" ) @@ -165,51 +164,45 @@ var ( ) type FakeVaultServerConfig struct { - ListenAddr string - ServerCertificatePemPath string - ServerKeyPemPath string - CertAuthReqEndpoint string - CertAuthReqHandler func(code int, resp []byte) func(http.ResponseWriter, *http.Request) - CertAuthResponseCode int - CertAuthResponse []byte - AppRoleAuthReqEndpoint string - AppRoleAuthReqHandler func(code int, resp []byte) func(w http.ResponseWriter, r *http.Request) - AppRoleAuthResponseCode int - AppRoleAuthResponse []byte - K8sAuthReqEndpoint string - K8sAuthReqHandler func(code int, resp []byte) func(w http.ResponseWriter, r *http.Request) - K8sAuthResponseCode int - K8sAuthResponse []byte - SignIntermediateReqEndpoint string - SignIntermediateReqHandler func(code int, resp []byte) func(http.ResponseWriter, *http.Request) - SignIntermediateResponseCode int - SignIntermediateResponse []byte - RenewReqEndpoint string - RenewReqHandler func(code int, resp []byte) func(http.ResponseWriter, *http.Request) - RenewResponseCode int - RenewResponse []byte - LookupSelfReqEndpoint string - LookupSelfReqHandler func(code int, resp []byte) func(w http.ResponseWriter, r *http.Request) - LookupSelfResponseCode int - LookupSelfResponse []byte + ListenAddr string + ServerCertificatePemPath string + ServerKeyPemPath string + CertAuthReqEndpoint string + CertAuthReqHandler func(code int, resp []byte) func(http.ResponseWriter, *http.Request) + CertAuthResponseCode int + CertAuthResponse []byte + AppRoleAuthReqEndpoint string + AppRoleAuthReqHandler func(code int, resp []byte) func(w http.ResponseWriter, r *http.Request) + AppRoleAuthResponseCode int + AppRoleAuthResponse []byte + K8sAuthReqEndpoint string + K8sAuthReqHandler func(code int, resp []byte) func(w http.ResponseWriter, r *http.Request) + K8sAuthResponseCode int + K8sAuthResponse []byte + RenewReqEndpoint string + RenewReqHandler func(code int, resp []byte) func(http.ResponseWriter, *http.Request) + RenewResponseCode int + RenewResponse []byte + LookupSelfReqEndpoint string + LookupSelfReqHandler func(code int, resp []byte) func(w http.ResponseWriter, r *http.Request) + LookupSelfResponseCode int + LookupSelfResponse []byte } // NewFakeVaultServerConfig returns VaultServerConfig with default values func NewFakeVaultServerConfig() *FakeVaultServerConfig { return &FakeVaultServerConfig{ - ListenAddr: listenAddr, - CertAuthReqEndpoint: defaultTLSAuthEndpoint, - CertAuthReqHandler: defaultReqHandler, - AppRoleAuthReqEndpoint: defaultAppRoleAuthEndpoint, - AppRoleAuthReqHandler: defaultReqHandler, - K8sAuthReqEndpoint: defaultK8sAuthEndpoint, - K8sAuthReqHandler: defaultReqHandler, - SignIntermediateReqEndpoint: defaultSignIntermediateEndpoint, - SignIntermediateReqHandler: defaultReqHandler, - RenewReqEndpoint: defaultRenewEndpoint, - RenewReqHandler: defaultReqHandler, - LookupSelfReqEndpoint: defaultLookupSelfEndpoint, - LookupSelfReqHandler: defaultReqHandler, + ListenAddr: listenAddr, + CertAuthReqEndpoint: defaultTLSAuthEndpoint, + CertAuthReqHandler: defaultReqHandler, + AppRoleAuthReqEndpoint: defaultAppRoleAuthEndpoint, + AppRoleAuthReqHandler: defaultReqHandler, + K8sAuthReqEndpoint: defaultK8sAuthEndpoint, + K8sAuthReqHandler: defaultReqHandler, + RenewReqEndpoint: defaultRenewEndpoint, + RenewReqHandler: defaultReqHandler, + LookupSelfReqEndpoint: defaultLookupSelfEndpoint, + LookupSelfReqHandler: defaultReqHandler, } } @@ -239,7 +232,6 @@ func (v *FakeVaultServerConfig) NewTLSServer() (srv *httptest.Server, addr strin mux.HandleFunc(v.CertAuthReqEndpoint, v.CertAuthReqHandler(v.CertAuthResponseCode, v.CertAuthResponse)) mux.HandleFunc(v.AppRoleAuthReqEndpoint, v.AppRoleAuthReqHandler(v.AppRoleAuthResponseCode, v.AppRoleAuthResponse)) mux.HandleFunc(v.K8sAuthReqEndpoint, v.AppRoleAuthReqHandler(v.K8sAuthResponseCode, v.K8sAuthResponse)) - mux.HandleFunc(v.SignIntermediateReqEndpoint, v.SignIntermediateReqHandler(v.SignIntermediateResponseCode, v.SignIntermediateResponse)) mux.HandleFunc(v.RenewReqEndpoint, v.RenewReqHandler(v.RenewResponseCode, v.RenewResponse)) mux.HandleFunc(v.LookupSelfReqEndpoint, v.LookupSelfReqHandler(v.LookupSelfResponseCode, v.LookupSelfResponse)) From 41b91e576c4b573ceac14b423f36e919ed19f7dd Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:22 +0200 Subject: [PATCH 12/29] Support configuring vault namespace (#5058) Signed-off-by: Matteo Kamm --- .../plugin/keymanager/hashicorpvault/hashicorp_vault.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go index dc403dd75e..7af54a8b4b 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go @@ -55,6 +55,9 @@ type Config struct { // Configuration for the Token authentication method TokenAuth *TokenAuthConfig `hcl:"token_auth" json:"token_auth,omitempty"` + // Name of the Vault namespace + Namespace string `hcl:"namespace" json:"namespace"` + // TODO: Support other auth methods // TODO: Support client certificate and key } @@ -145,6 +148,7 @@ func parseAuthMethod(config *Config) (AuthMethod, error) { func (p *Plugin) genClientParams(method AuthMethod, config *Config) (*ClientParams, error) { cp := &ClientParams{ VaultAddr: p.getEnvOrDefault(envVaultAddr, config.VaultAddr), + Namespace: p.getEnvOrDefault(envVaultNamespace, config.Namespace), } switch method { From c8e9b0b5d404e6585c9545da2c2833bfcd7d97d4 Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:22 +0200 Subject: [PATCH 13/29] Support AppRole authentication (#5058) Signed-off-by: Matteo Kamm --- .../hashicorpvault/hashicorp_vault.go | 39 +++++++++-- .../hashicorpvault/vault_client_test.go | 64 +++++++++++++++++++ .../hashicorpvault/vault_fake_test.go | 38 +++++++++++ 3 files changed, 137 insertions(+), 4 deletions(-) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go index 7af54a8b4b..3f2e73dbe2 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go @@ -51,22 +51,35 @@ type pluginHooks struct { type Config struct { // A URL of Vault server. (e.g., https://vault.example.com:8443/) VaultAddr string `hcl:"vault_addr" json:"vault_addr"` + // Name of the Vault namespace + Namespace string `hcl:"namespace" json:"namespace"` // Configuration for the Token authentication method TokenAuth *TokenAuthConfig `hcl:"token_auth" json:"token_auth,omitempty"` - - // Name of the Vault namespace - Namespace string `hcl:"namespace" json:"namespace"` + // Configuration for the AppRole authentication method + AppRoleAuth *AppRoleAuthConfig `hcl:"approle_auth" json:"approle_auth,omitempty"` // TODO: Support other auth methods // TODO: Support client certificate and key } +// TokenAuthConfig represents parameters for token auth method type TokenAuthConfig struct { // Token string to set into "X-Vault-Token" header Token string `hcl:"token" json:"token"` } +// AppRoleAuthConfig represents parameters for AppRole auth method. +type AppRoleAuthConfig struct { + // Name of the mount point where AppRole auth method is mounted. (e.g., /auth//login) + // If the value is empty, use default mount point (/auth/approle) + AppRoleMountPoint string `hcl:"approle_auth_mount_point" json:"approle_auth_mount_point"` + // An identifier that selects the AppRole + RoleID string `hcl:"approle_id" json:"approle_id"` + // A credential that is required for login. + SecretID string `hcl:"approle_secret_id" json:"approle_secret_id"` +} + // Plugin is the main representation of this keymanager plugin type Plugin struct { keymanagerv1.UnsafeKeyManagerServer @@ -138,11 +151,25 @@ func parseAuthMethod(config *Config) (AuthMethod, error) { authMethod = TOKEN } + if config.AppRoleAuth != nil { + if err := checkForAuthMethodConfigured(authMethod); err != nil { + return 0, err + } + authMethod = APPROLE + } + if authMethod != 0 { return authMethod, nil } - return 0, status.Error(codes.InvalidArgument, "must be configured one of these authentication method 'Token'") + return 0, status.Error(codes.InvalidArgument, "one of the available authentication methods must be configured: 'Token, AppRole'") +} + +func checkForAuthMethodConfigured(authMethod AuthMethod) error { + if authMethod != 0 { + return status.Error(codes.InvalidArgument, "only one authentication method can be configured") + } + return nil } func (p *Plugin) genClientParams(method AuthMethod, config *Config) (*ClientParams, error) { @@ -154,6 +181,10 @@ func (p *Plugin) genClientParams(method AuthMethod, config *Config) (*ClientPara switch method { case TOKEN: cp.Token = p.getEnvOrDefault(envVaultToken, config.TokenAuth.Token) + case APPROLE: + cp.AppRoleAuthMountPoint = config.AppRoleAuth.AppRoleMountPoint + cp.AppRoleID = p.getEnvOrDefault(envVaultAppRoleID, config.AppRoleAuth.RoleID) + cp.AppRoleSecretID = p.getEnvOrDefault(envVaultAppRoleSecretID, config.AppRoleAuth.SecretID) } return cp, nil diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go index f311c2586c..25880e7277 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go @@ -143,6 +143,70 @@ func TestNewAuthenticatedClientTokenAuth(t *testing.T) { } } +func TestNewAuthenticatedClientAppRoleAuth(t *testing.T) { + fakeVaultServer := newFakeVaultServer() + fakeVaultServer.AppRoleAuthResponseCode = 200 + for _, tt := range []struct { + name string + response []byte + renew bool + namespace string + }{ + { + name: "AppRole Authentication success / Token is renewable", + response: []byte(testAppRoleAuthResponse), + renew: true, + }, + { + name: "AppRole Authentication success / Token is not renewable", + response: []byte(testAppRoleAuthResponseNotRenewable), + }, + { + name: "AppRole Authentication success / Token is renewable / Namespace is given", + response: []byte(testAppRoleAuthResponse), + renew: true, + namespace: "test-ns", + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + fakeVaultServer.AppRoleAuthResponse = tt.response + + s, addr, err := fakeVaultServer.NewTLSServer() + require.NoError(t, err) + + s.Start() + defer s.Close() + + cp := &ClientParams{ + VaultAddr: fmt.Sprintf("https://%v/", addr), + Namespace: tt.namespace, + CACertPath: testRootCert, + AppRoleID: "test-approle-id", + AppRoleSecretID: "test-approle-secret-id", + } + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + renewCh := make(chan struct{}) + client, err := cc.NewAuthenticatedClient(APPROLE, renewCh) + require.NoError(t, err) + + select { + case <-renewCh: + require.Equal(t, false, tt.renew) + default: + require.Equal(t, true, tt.renew) + } + + if cp.Namespace != "" { + headers := client.vaultClient.Headers() + require.Equal(t, cp.Namespace, headers.Get(consts.NamespaceHeaderName)) + } + }) + } +} + func TestRenewTokenFailed(t *testing.T) { fakeVaultServer := newFakeVaultServer() fakeVaultServer.LookupSelfResponse = []byte(testLookupSelfResponseShortTTL) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go index 8d9f9d5fc5..f709aab4e8 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go @@ -30,6 +30,44 @@ var ( } }` + testAppRoleAuthResponse = `{ + "auth": { + "renewable": true, + "lease_duration": 1200, + "metadata": null, + "token_policies": [ + "default" + ], + "accessor": "fd6c9a00-d2dc-3b11-0be5-af7ae0e1d374", + "client_token": "5b1a0318-679c-9c45-e5c6-d1b9a9035d49" + }, + "warnings": null, + "wrap_info": null, + "data": null, + "lease_duration": 0, + "renewable": false, + "lease_id": "" +}` + + testAppRoleAuthResponseNotRenewable = `{ + "auth": { + "renewable": false, + "lease_duration": 3600, + "metadata": null, + "token_policies": [ + "default" + ], + "accessor": "fd6c9a00-d2dc-3b11-0be5-af7ae0e1d374", + "client_token": "5b1a0318-679c-9c45-e5c6-d1b9a9035d49" + }, + "warnings": null, + "wrap_info": null, + "data": null, + "lease_duration": 0, + "renewable": false, + "lease_id": "" +}` + testRenewResponse = `{ "auth": { "client_token": "test-client-token", From 573cca6bae62d0ceebb9032c5d988fb680d6e1a7 Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:22 +0200 Subject: [PATCH 14/29] Make transit engine path configurable (#5058) Signed-off-by: Matteo Kamm --- .../hashicorpvault/hashicorp_vault.go | 7 +++-- .../keymanager/hashicorpvault/vault_client.go | 30 ++++++++++--------- .../hashicorpvault/vault_client_test.go | 4 +++ 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go index 3f2e73dbe2..f6485625ef 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go @@ -53,6 +53,8 @@ type Config struct { VaultAddr string `hcl:"vault_addr" json:"vault_addr"` // Name of the Vault namespace Namespace string `hcl:"namespace" json:"namespace"` + // TransitEnginePath specifies the path to the transit engine to perform key operations. + TransitEnginePath string `hcl:"transit_engine_path" json:"transit_engine_path"` // Configuration for the Token authentication method TokenAuth *TokenAuthConfig `hcl:"token_auth" json:"token_auth,omitempty"` @@ -174,8 +176,9 @@ func checkForAuthMethodConfigured(authMethod AuthMethod) error { func (p *Plugin) genClientParams(method AuthMethod, config *Config) (*ClientParams, error) { cp := &ClientParams{ - VaultAddr: p.getEnvOrDefault(envVaultAddr, config.VaultAddr), - Namespace: p.getEnvOrDefault(envVaultNamespace, config.Namespace), + VaultAddr: p.getEnvOrDefault(envVaultAddr, config.VaultAddr), + Namespace: p.getEnvOrDefault(envVaultNamespace, config.Namespace), + TransitEnginePath: p.getEnvOrDefault(envVaultTransitEnginePath, config.TransitEnginePath), } switch method { diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go index 561674f0ad..f2a147fc1f 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go @@ -21,17 +21,19 @@ import ( // TODO: Delete everything that is unused in here const ( - envVaultAddr = "VAULT_ADDR" - envVaultToken = "VAULT_TOKEN" - envVaultClientCert = "VAULT_CLIENT_CERT" - envVaultClientKey = "VAULT_CLIENT_KEY" - envVaultCACert = "VAULT_CACERT" - envVaultAppRoleID = "VAULT_APPROLE_ID" - envVaultAppRoleSecretID = "VAULT_APPROLE_SECRET_ID" // #nosec G101 - envVaultNamespace = "VAULT_NAMESPACE" + envVaultAddr = "VAULT_ADDR" + envVaultToken = "VAULT_TOKEN" + envVaultClientCert = "VAULT_CLIENT_CERT" + envVaultClientKey = "VAULT_CLIENT_KEY" + envVaultCACert = "VAULT_CACERT" + envVaultAppRoleID = "VAULT_APPROLE_ID" + envVaultAppRoleSecretID = "VAULT_APPROLE_SECRET_ID" // #nosec G101 + envVaultNamespace = "VAULT_NAMESPACE" + envVaultTransitEnginePath = "VAULT_TRANSIT_ENGINE_PATH" defaultCertMountPoint = "cert" defaultPKIMountPoint = "pki" + defaultTransitEnginePath = "transit" defaultAppRoleMountPoint = "approle" defaultK8sMountPoint = "kubernetes" ) @@ -93,6 +95,8 @@ type ClientParams struct { MaxRetries *int // Name of the Vault namespace Namespace string + // TransitEnginePath specifies the path to the transit engine to perform key operations. + TransitEnginePath string } type Client struct { @@ -110,6 +114,7 @@ func NewClientConfig(cp *ClientParams, logger hclog.Logger) (*ClientConfig, erro AppRoleAuthMountPoint: defaultAppRoleMountPoint, K8sAuthMountPoint: defaultK8sMountPoint, PKIMountPoint: defaultPKIMountPoint, + TransitEnginePath: defaultTransitEnginePath, } if err := mergo.Merge(cp, defaultParams); err != nil { return nil, status.Errorf(codes.Internal, "unable to merge client params: %v", err) @@ -370,15 +375,13 @@ func (c *Client) CreateKey(ctx context.Context, spireKeyID string, keyType Trans } // TODO: Handle errors here such as key already exists - // TODO: Make the transit engine path configurable - _, err := c.vaultClient.Logical().WriteWithContext(ctx, fmt.Sprintf("/transit/keys/%s", spireKeyID), arguments) + _, err := c.vaultClient.Logical().WriteWithContext(ctx, fmt.Sprintf("/%s/keys/%s", c.clientParams.TransitEnginePath, spireKeyID), arguments) return err } func (c *Client) GetKey(ctx context.Context, spireKeyID string) (string, error) { // TODO: Handle errors here - // TODO: Make the transit engine path configurable - res, err := c.vaultClient.Logical().ReadWithContext(ctx, fmt.Sprintf("/transit/keys/%s", spireKeyID)) + res, err := c.vaultClient.Logical().ReadWithContext(ctx, fmt.Sprintf("/%s/keys/%s", c.clientParams.TransitEnginePath, spireKeyID)) if err != nil { return "", err } @@ -428,8 +431,7 @@ func (c *Client) SignData(ctx context.Context, spireKeyID string, data []byte, h } // TODO: Handle errors here - // TODO: Make the transit engine path configurable - sigResp, err := c.vaultClient.Logical().WriteWithContext(ctx, fmt.Sprintf("/transit/sign/%s/%s", spireKeyID, hashAlgo), body) + sigResp, err := c.vaultClient.Logical().WriteWithContext(ctx, fmt.Sprintf("/%s/sign/%s/%s", c.clientParams.TransitEnginePath, spireKeyID, hashAlgo), body) if err != nil { return nil, status.Errorf(codes.Internal, "transit engine sign call failed: %v", err) } diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go index 25880e7277..0f7801650b 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go @@ -26,6 +26,7 @@ func TestNewClientConfigWithDefaultValues(t *testing.T) { CertAuthMountPoint: "", // Expect the default value to be used. AppRoleAuthMountPoint: "", // Expect the default value to be used. K8sAuthMountPoint: "", // Expect the default value to be used. + TransitEnginePath: "", // Expect the default value to be used. } cc, err := NewClientConfig(p, hclog.Default()) @@ -34,6 +35,7 @@ func TestNewClientConfigWithDefaultValues(t *testing.T) { require.Equal(t, defaultCertMountPoint, cc.clientParams.CertAuthMountPoint) require.Equal(t, defaultAppRoleMountPoint, cc.clientParams.AppRoleAuthMountPoint) require.Equal(t, defaultK8sMountPoint, cc.clientParams.K8sAuthMountPoint) + require.Equal(t, defaultTransitEnginePath, cc.clientParams.TransitEnginePath) } func TestNewClientConfigWithGivenValuesInsteadOfDefaults(t *testing.T) { @@ -44,6 +46,7 @@ func TestNewClientConfigWithGivenValuesInsteadOfDefaults(t *testing.T) { CertAuthMountPoint: "test-tls-cert", AppRoleAuthMountPoint: "test-approle", K8sAuthMountPoint: "test-k8s", + TransitEnginePath: "test-transit", } cc, err := NewClientConfig(p, hclog.Default()) @@ -52,6 +55,7 @@ func TestNewClientConfigWithGivenValuesInsteadOfDefaults(t *testing.T) { require.Equal(t, "test-tls-cert", cc.clientParams.CertAuthMountPoint) require.Equal(t, "test-approle", cc.clientParams.AppRoleAuthMountPoint) require.Equal(t, "test-k8s", cc.clientParams.K8sAuthMountPoint) + require.Equal(t, "test-transit", cc.clientParams.TransitEnginePath) } func TestNewAuthenticatedClientTokenAuth(t *testing.T) { From 7d3d9f710c5d2f0d1ee9c9bbe4776ba8ae273092 Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:22 +0200 Subject: [PATCH 15/29] Add comments to exported functions (#5058) Signed-off-by: Matteo Kamm --- pkg/server/plugin/keymanager/hashicorpvault/vault_client.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go index f2a147fc1f..a1ecfde3c4 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go @@ -379,6 +379,8 @@ func (c *Client) CreateKey(ctx context.Context, spireKeyID string, keyType Trans return err } +// GetKey gets the transit engine key with the specified spire key id. +// See: https://developer.hashicorp.com/vault/api-docs/secret/transit#read-key func (c *Client) GetKey(ctx context.Context, spireKeyID string) (string, error) { // TODO: Handle errors here res, err := c.vaultClient.Logical().ReadWithContext(ctx, fmt.Sprintf("/%s/keys/%s", c.clientParams.TransitEnginePath, spireKeyID)) @@ -420,6 +422,8 @@ func (c *Client) GetKey(ctx context.Context, spireKeyID string) (string, error) return pkStr, nil } +// SignData signs the data using the transit engine key with the provided spire key id. +// See: https://developer.hashicorp.com/vault/api-docs/secret/transit#sign-data func (c *Client) SignData(ctx context.Context, spireKeyID string, data []byte, hashAlgo TransitHashAlgorithm, signatureAlgo TransitSignatureAlgorithm) ([]byte, error) { encodedData := base64.StdEncoding.EncodeToString(data) @@ -446,6 +450,7 @@ func (c *Client) SignData(ctx context.Context, spireKeyID string, data []byte, h return nil, status.Errorf(codes.Internal, "expected signature data type %T but got %T", sigStr, sig) } + // Vault adds an application specific prefix that we need to remove cutSig, ok := strings.CutPrefix(sigStr, "vault:v1:") if !ok { return nil, status.Errorf(codes.Internal, "signature is missing vault prefix: %v", err) From 53f5709ba3857a81f3d531850e83368fe6f312e3 Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:22 +0200 Subject: [PATCH 16/29] Support certificate authentication (#5058) Signed-off-by: Matteo Kamm --- .../hashicorpvault/hashicorp_vault.go | 34 +++++++ .../hashicorpvault/testdata/client-cert.pem | 9 ++ .../hashicorpvault/testdata/client-key.pem | 5 + .../hashicorpvault/vault_client_test.go | 93 +++++++++++++++++++ .../hashicorpvault/vault_fake_test.go | 12 +++ 5 files changed, 153 insertions(+) create mode 100644 pkg/server/plugin/keymanager/hashicorpvault/testdata/client-cert.pem create mode 100644 pkg/server/plugin/keymanager/hashicorpvault/testdata/client-key.pem diff --git a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go index f6485625ef..a4862b3da2 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go @@ -55,11 +55,16 @@ type Config struct { Namespace string `hcl:"namespace" json:"namespace"` // TransitEnginePath specifies the path to the transit engine to perform key operations. TransitEnginePath string `hcl:"transit_engine_path" json:"transit_engine_path"` + // If true, vault client accepts any server certificates. + // It should be used only test environment so on. + InsecureSkipVerify bool `hcl:"insecure_skip_verify" json:"insecure_skip_verify"` // Configuration for the Token authentication method TokenAuth *TokenAuthConfig `hcl:"token_auth" json:"token_auth,omitempty"` // Configuration for the AppRole authentication method AppRoleAuth *AppRoleAuthConfig `hcl:"approle_auth" json:"approle_auth,omitempty"` + // Configuration for the Client Certificate authentication method + CertAuth *CertAuthConfig `hcl:"cert_auth" json:"cert_auth,omitempty"` // TODO: Support other auth methods // TODO: Support client certificate and key @@ -82,6 +87,22 @@ type AppRoleAuthConfig struct { SecretID string `hcl:"approle_secret_id" json:"approle_secret_id"` } +// CertAuthConfig represents parameters for cert auth method +type CertAuthConfig struct { + // Name of the mount point where Client Certificate Auth method is mounted. (e.g., /auth//login) + // If the value is empty, use default mount point (/auth/cert) + CertAuthMountPoint string `hcl:"cert_auth_mount_point" json:"cert_auth_mount_point"` + // Name of the Vault role. + // If given, the plugin authenticates against only the named role. + CertAuthRoleName string `hcl:"cert_auth_role_name" json:"cert_auth_role_name"` + // Path to a client certificate file. + // Only PEM format is supported. + ClientCertPath string `hcl:"client_cert_path" json:"client_cert_path"` + // Path to a client private key file. + // Only PEM format is supported. + ClientKeyPath string `hcl:"client_key_path" json:"client_key_path"` +} + // Plugin is the main representation of this keymanager plugin type Plugin struct { keymanagerv1.UnsafeKeyManagerServer @@ -160,6 +181,13 @@ func parseAuthMethod(config *Config) (AuthMethod, error) { authMethod = APPROLE } + if config.CertAuth != nil { + if err := checkForAuthMethodConfigured(authMethod); err != nil { + return 0, err + } + authMethod = CERT + } + if authMethod != 0 { return authMethod, nil } @@ -179,6 +207,7 @@ func (p *Plugin) genClientParams(method AuthMethod, config *Config) (*ClientPara VaultAddr: p.getEnvOrDefault(envVaultAddr, config.VaultAddr), Namespace: p.getEnvOrDefault(envVaultNamespace, config.Namespace), TransitEnginePath: p.getEnvOrDefault(envVaultTransitEnginePath, config.TransitEnginePath), + TLSSKipVerify: config.InsecureSkipVerify, } switch method { @@ -188,6 +217,11 @@ func (p *Plugin) genClientParams(method AuthMethod, config *Config) (*ClientPara cp.AppRoleAuthMountPoint = config.AppRoleAuth.AppRoleMountPoint cp.AppRoleID = p.getEnvOrDefault(envVaultAppRoleID, config.AppRoleAuth.RoleID) cp.AppRoleSecretID = p.getEnvOrDefault(envVaultAppRoleSecretID, config.AppRoleAuth.SecretID) + case CERT: + cp.CertAuthMountPoint = config.CertAuth.CertAuthMountPoint + cp.CertAuthRoleName = config.CertAuth.CertAuthRoleName + cp.ClientCertPath = p.getEnvOrDefault(envVaultClientCert, config.CertAuth.ClientCertPath) + cp.ClientKeyPath = p.getEnvOrDefault(envVaultClientKey, config.CertAuth.ClientKeyPath) } return cp, nil diff --git a/pkg/server/plugin/keymanager/hashicorpvault/testdata/client-cert.pem b/pkg/server/plugin/keymanager/hashicorpvault/testdata/client-cert.pem new file mode 100644 index 0000000000..ab411834a0 --- /dev/null +++ b/pkg/server/plugin/keymanager/hashicorpvault/testdata/client-cert.pem @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE----- +MIIBKDCBz6ADAgECAgEDMAoGCCqGSM49BAMCMAAwIBgPMDAwMTAxMDEwMDAwMDBa +Fw0zMjA0MTIxNjA4NDRaMAAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQymtYU +je8Cue4bRUr76kUGb5F2iyM/Isxt8khYmCRi3TsW21NrOGHmFpIWQ6OVya7UHR0v +QbutQJAflrR12cqeozgwNjATBgNVHSUEDDAKBggrBgEFBQcDAjAfBgNVHSMEGDAW +gBSYSzYwHNQsGiZXSVYDs59w3+UYNzAKBggqhkjOPQQDAgNIADBFAiEAzcRL2tVT +GpPtq6sJKN9quQcX8xxHq7NAxQ8u10C6UegCIECAEW+D8mNP2nM5J+6eSE7DGQ5d +FQZvf0i+L7y0UQQ3 +-----END CERTIFICATE----- diff --git a/pkg/server/plugin/keymanager/hashicorpvault/testdata/client-key.pem b/pkg/server/plugin/keymanager/hashicorpvault/testdata/client-key.pem new file mode 100644 index 0000000000..c9fcac5019 --- /dev/null +++ b/pkg/server/plugin/keymanager/hashicorpvault/testdata/client-key.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgC3sFQg3WCrosxeWb +pT67H8HE/lOcPq+zc6BMss947J6hRANCAAQymtYUje8Cue4bRUr76kUGb5F2iyM/ +Isxt8khYmCRi3TsW21NrOGHmFpIWQ6OVya7UHR0vQbutQJAflrR12cqe +-----END PRIVATE KEY----- diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go index 0f7801650b..25c561bd52 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go @@ -16,6 +16,8 @@ const ( testRootCert = "testdata/root-cert.pem" testServerCert = "testdata/server-cert.pem" testServerKey = "testdata/server-key.pem" + testClientCert = "testdata/client-cert.pem" + testClientKey = "testdata/client-key.pem" ) func TestNewClientConfigWithDefaultValues(t *testing.T) { @@ -211,6 +213,97 @@ func TestNewAuthenticatedClientAppRoleAuth(t *testing.T) { } } +func TestNewAuthenticatedClientCertAuth(t *testing.T) { + fakeVaultServer := newFakeVaultServer() + fakeVaultServer.CertAuthResponseCode = 200 + for _, tt := range []struct { + name string + response []byte + renew bool + namespace string + }{ + { + name: "Cert Authentication success / Token is renewable", + response: []byte(testCertAuthResponse), + renew: true, + }, + { + name: "Cert Authentication success / Token is not renewable", + response: []byte(testCertAuthResponseNotRenewable), + }, + { + name: "Cert Authentication success / Token is renewable / Namespace is given", + response: []byte(testCertAuthResponse), + renew: true, + namespace: "test-ns", + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + fakeVaultServer.CertAuthResponse = tt.response + + s, addr, err := fakeVaultServer.NewTLSServer() + require.NoError(t, err) + + s.Start() + defer s.Close() + + cp := &ClientParams{ + VaultAddr: fmt.Sprintf("https://%v/", addr), + Namespace: tt.namespace, + CACertPath: testRootCert, + ClientCertPath: testClientCert, + ClientKeyPath: testClientKey, + } + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + renewCh := make(chan struct{}) + client, err := cc.NewAuthenticatedClient(CERT, renewCh) + require.NoError(t, err) + + select { + case <-renewCh: + require.Equal(t, false, tt.renew) + default: + require.Equal(t, true, tt.renew) + } + + if cp.Namespace != "" { + headers := client.vaultClient.Headers() + require.Equal(t, cp.Namespace, headers.Get(consts.NamespaceHeaderName)) + } + }) + } +} + +func TestNewAuthenticatedClientCertAuthFailed(t *testing.T) { + fakeVaultServer := newFakeVaultServer() + fakeVaultServer.CertAuthResponseCode = 500 + + s, addr, err := fakeVaultServer.NewTLSServer() + require.NoError(t, err) + + s.Start() + defer s.Close() + + retry := 0 // Disable retry + cp := &ClientParams{ + MaxRetries: &retry, + VaultAddr: fmt.Sprintf("https://%v/", addr), + CACertPath: testRootCert, + ClientCertPath: testClientCert, + ClientKeyPath: testClientKey, + } + + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + renewCh := make(chan struct{}) + _, err = cc.NewAuthenticatedClient(CERT, renewCh) + spiretest.RequireGRPCStatusHasPrefix(t, err, codes.Unauthenticated, "authentication failed auth/cert/login: Error making API request.") +} + func TestRenewTokenFailed(t *testing.T) { fakeVaultServer := newFakeVaultServer() fakeVaultServer.LookupSelfResponse = []byte(testLookupSelfResponseShortTTL) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go index f709aab4e8..fa1aa14648 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go @@ -199,6 +199,18 @@ var ( }, "warnings": null }` + + testCertAuthResponseNotRenewable = `{ + "auth": { + "client_token": "cf95f87d-f95b-47ff-b1f5-ba7bff850425", + "policies": [ + "web", + "stage" + ], + "lease_duration": 3600, + "renewable": false + } +}` ) type FakeVaultServerConfig struct { From 9d6ea3e613fca65079739510c88c89f4e4ee785e Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:22 +0200 Subject: [PATCH 17/29] Add missing app role auth test case (#5058) Signed-off-by: Matteo Kamm --- .../hashicorpvault/vault_client_test.go | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go index 25c561bd52..a747b86043 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go @@ -213,6 +213,32 @@ func TestNewAuthenticatedClientAppRoleAuth(t *testing.T) { } } +func TestNewAuthenticatedClientAppRoleAuthFailed(t *testing.T) { + fakeVaultServer := newFakeVaultServer() + fakeVaultServer.AppRoleAuthResponseCode = 500 + + s, addr, err := fakeVaultServer.NewTLSServer() + require.NoError(t, err) + + s.Start() + defer s.Close() + + retry := 0 // Disable retry + cp := &ClientParams{ + MaxRetries: &retry, + VaultAddr: fmt.Sprintf("https://%v/", addr), + CACertPath: testRootCert, + AppRoleID: "test-approle-id", + AppRoleSecretID: "test-approle-secret-id", + } + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + renewCh := make(chan struct{}) + _, err = cc.NewAuthenticatedClient(APPROLE, renewCh) + spiretest.RequireGRPCStatusHasPrefix(t, err, codes.Unauthenticated, "authentication failed auth/approle/login: Error making API request.") +} + func TestNewAuthenticatedClientCertAuth(t *testing.T) { fakeVaultServer := newFakeVaultServer() fakeVaultServer.CertAuthResponseCode = 200 From 82f4be61d9f2fbea98037262e2aaea7eccc8d0bc Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:22 +0200 Subject: [PATCH 18/29] Support K8s auth (#5058) Signed-off-by: Matteo Kamm --- .../hashicorpvault/hashicorp_vault.go | 37 +++++- .../testdata/k8s/signing-key.pem | 27 +++++ .../hashicorpvault/testdata/k8s/token | 1 + .../hashicorpvault/vault_client_test.go | 106 ++++++++++++++++++ .../hashicorpvault/vault_fake_test.go | 55 +++++++++ 5 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 pkg/server/plugin/keymanager/hashicorpvault/testdata/k8s/signing-key.pem create mode 100644 pkg/server/plugin/keymanager/hashicorpvault/testdata/k8s/token diff --git a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go index a4862b3da2..ddf9c76b8e 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go @@ -55,19 +55,21 @@ type Config struct { Namespace string `hcl:"namespace" json:"namespace"` // TransitEnginePath specifies the path to the transit engine to perform key operations. TransitEnginePath string `hcl:"transit_engine_path" json:"transit_engine_path"` + // If true, vault client accepts any server certificates. // It should be used only test environment so on. InsecureSkipVerify bool `hcl:"insecure_skip_verify" json:"insecure_skip_verify"` + // TODO: Support CA certificate path here instead of insecure skip verify + // Configuration for the Token authentication method TokenAuth *TokenAuthConfig `hcl:"token_auth" json:"token_auth,omitempty"` // Configuration for the AppRole authentication method AppRoleAuth *AppRoleAuthConfig `hcl:"approle_auth" json:"approle_auth,omitempty"` // Configuration for the Client Certificate authentication method CertAuth *CertAuthConfig `hcl:"cert_auth" json:"cert_auth,omitempty"` - - // TODO: Support other auth methods - // TODO: Support client certificate and key + // Configuration for the Kubernetes authentication method + K8sAuth *K8sAuthConfig `hcl:"k8s_auth" json:"k8s_auth,omitempty"` } // TokenAuthConfig represents parameters for token auth method @@ -103,6 +105,18 @@ type CertAuthConfig struct { ClientKeyPath string `hcl:"client_key_path" json:"client_key_path"` } +// K8sAuthConfig represents parameters for Kubernetes auth method. +type K8sAuthConfig struct { + // Name of the mount point where Kubernetes auth method is mounted. (e.g., /auth//login) + // If the value is empty, use default mount point (/auth/kubernetes) + K8sAuthMountPoint string `hcl:"k8s_auth_mount_point" json:"k8s_auth_mount_point"` + // Name of the Vault role. + // The plugin authenticates against the named role. + K8sAuthRoleName string `hcl:"k8s_auth_role_name" json:"k8s_auth_role_name"` + // Path to the Kubernetes Service Account Token to use authentication with the Vault. + TokenPath string `hcl:"token_path" json:"token_path"` +} + // Plugin is the main representation of this keymanager plugin type Plugin struct { keymanagerv1.UnsafeKeyManagerServer @@ -188,6 +202,13 @@ func parseAuthMethod(config *Config) (AuthMethod, error) { authMethod = CERT } + if config.K8sAuth != nil { + if err := checkForAuthMethodConfigured(authMethod); err != nil { + return 0, err + } + authMethod = K8S + } + if authMethod != 0 { return authMethod, nil } @@ -222,6 +243,16 @@ func (p *Plugin) genClientParams(method AuthMethod, config *Config) (*ClientPara cp.CertAuthRoleName = config.CertAuth.CertAuthRoleName cp.ClientCertPath = p.getEnvOrDefault(envVaultClientCert, config.CertAuth.ClientCertPath) cp.ClientKeyPath = p.getEnvOrDefault(envVaultClientKey, config.CertAuth.ClientKeyPath) + case K8S: + if config.K8sAuth.K8sAuthRoleName == "" { + return nil, status.Error(codes.InvalidArgument, "k8s_auth_role_name is required") + } + if config.K8sAuth.TokenPath == "" { + return nil, status.Error(codes.InvalidArgument, "token_path is required") + } + cp.K8sAuthMountPoint = config.K8sAuth.K8sAuthMountPoint + cp.K8sAuthRoleName = config.K8sAuth.K8sAuthRoleName + cp.K8sAuthTokenPath = config.K8sAuth.TokenPath } return cp, nil diff --git a/pkg/server/plugin/keymanager/hashicorpvault/testdata/k8s/signing-key.pem b/pkg/server/plugin/keymanager/hashicorpvault/testdata/k8s/signing-key.pem new file mode 100644 index 0000000000..c6bb5eb4a4 --- /dev/null +++ b/pkg/server/plugin/keymanager/hashicorpvault/testdata/k8s/signing-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEnwIBAAKCAQAwrCHZ8ldBNltOjJTUMWopdAuHGcxuPUsTjdaoZL71q6YC8TbD +cD5aFX152g17tfSHbukr53YD+0TfrDcL/vdSt7Acs5FUHK1ULcuzGvhXx2rUiosW +Zk8Nc99gjwHXOV3DoUBVk04edXo7SMmVKPiYemwm0XvSoBhU3NpnBGJ/DQq7TG+W +wFIaxbURpVxpUP2oWZRebUuQgund8Pjh6kxUkX6XcFH+0y4+wMDV3YdLTuFTYwEc +q/XqdUIEasc1lPT7CwwAlxR+jQTKGnDji6KQerSiktwOUjBpQVb/j/m2+53suhju +XHLUcId2x9yfe73kTTMcYsQ4woEHt9xGRniJAgMBAAECggEAKC5y49LFZfjR+E7m +ryb8VayPt8D8nCXNzR7Tj8FcRMSoENXCOCZ50zTambYCW5cjgIt3w98Z9r+BZIZw +C186Hve2VHuKBr6F+XC1Me+aBh2DfGPD34Im0RxP1Q86ncumNNLyobMyUsL5XegB +QzrHwFmQ35shdgjlDWomg9WC2w/Y2P6zLpbua/lZNBBo3ISXdU1EZNdCl6cJct5N +Q9bbr6PJrL1JdQIC8fA0c1MXiN5XCAaVqSuxlLqiTVrNcTPweJb4iHbvgNf7pvwT +kPEH/10dQirdtjFPR8+WmihES8lIWBcqqemB/dpDoLpjyTo3ZcLODKQiE4o0gLyu +Mw+C4QKBgHOXDRwH/7rxif4S+ngxcJS1OKigfHbf0JLdEVX6X6y5QkGZAd9c3hgP +FbroBx0XXohrLaXcaDVAx8DpylPum2NuibsTg/HUToc9FxH5PXYYp15Thjai2KJ0 +zMjV/Z3DuzJ8465Cv0QL7kuCalgilEU01F03zVaIgnm1ZBjZyAtfAoGAa8u/x6C4 +9VNdYSgIhDzPPmTaxWy6jWFZV5mcmRckHqYGQFw8c9VFCFA07endetbn+3SniDi5 +ujnNV+HStLTHq5uv1QkqCWFXc8B06vKcfLbwsHCzPOcRz7NHGfICQpHKo64R+/un +RWJv1KO1u0gvMy/4/OJXDYFn2YsZ5CFKbRcCgYBXXIav9Ou24u8kdDuRs+weuIjG +CeWIAsik9ygvDzhYVvxYj8f2hT3meSA3Tz5xIkR0Xmz1uouYFAnlJ82fees/T0AR +gEJs98USOX3CO9nT8/YrOH1rtdB9mEFeWT2Bi3lkQzfhcNkWGN5Ve4/cZOYjGDaY +7Z/oEuxqCEpK7e5fiQKBgAqQ9kODJZ4mhci4O916eHYNPMSNW9vv5uoHTKpU8l1u +uL4mTGauSQ3/jrCjc+pOln63eJSJuureL5qlsBm2frv7jsi7FTvGJuRZwRwmm+A9 +rmodIfSeUciiMh4A8ufDkrFopqqkiEjs1Tlqsq2g7b9+vFFNfmr8fEl+sRMDkGAR +AoGACfGod8qGIMX8gxRiLGPVK98wJAJhlLxeVztoSP3pQbpyf+GU7r+Nf8DyzhE5 +xomJ1aF25lBMSGSo74QZgIpFxcNKbR+9zcYpzsSJfq9vIktvgLYPn9g7GDr1rWMi +r8G7GT0udgiJIODc7JFGuBDid4iwlHQZdCFmot3gfBbpsJk= +-----END RSA PRIVATE KEY----- diff --git a/pkg/server/plugin/keymanager/hashicorpvault/testdata/k8s/token b/pkg/server/plugin/keymanager/hashicorpvault/testdata/k8s/token new file mode 100644 index 0000000000..a9a0a94d73 --- /dev/null +++ b/pkg/server/plugin/keymanager/hashicorpvault/testdata/k8s/token @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsImtpZCI6Ik1ZWTNhcmZRaWVTRzlrR3Y0NE5JSEpmWHB6aUswVFRibFBQQ3ZjN1ZFX0UifQ.eyJhdWQiOlsiYXBpIiwic3BpcmUtc2VydmVyIl0sImV4cCI6NDgxMDQxMzg1MywiaWF0IjoxNjIzMjA0MjUzLCJpc3MiOiJodHRwczovL2t1YmVybmV0ZXMiLCJrdWJlcm5ldGVzLmlvIjp7Im5hbWVzcGFjZSI6InNwaXJlIiwicG9kIjp7Im5hbWUiOiJzcGlyZS1zZXJ2ZXItMCIsInVpZCI6ImY5MDIzNzAyLWY0ZWQtNGVkOS1hYjQwLWJjNjkxNDJhYTlhNiJ9LCJzZXJ2aWNlYWNjb3VudCI6eyJuYW1lIjoic3BpcmUtc2VydmVyIiwidWlkIjoiNjgwOGI0YzctMGI1My00NWY0LTgzZjctZTg5Mzc3NTZlZWFlIn19LCJuYmYiOjE2MjMyMDQyNTMsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpzcGlyZTpzcGlyZS1zZXJ2ZXIifQ.G_dFjt8NzCFq-_QRm8Kbvq4Lt2iJN7Eos57k82aj2dS4TEMkefc2D07MLG4Sur3f2TYZ0xt51Cp3tCKaH8trUyS7sM07_gPO1GLtj-sAKgiRSjrbLPh2Du_J7Rapb42CN77Nb9EhZcc-B1zSg-J56Ypnl54M4UDotbYxIdHEHNvVWQf4KPP2X2IX47b_7Osm1p1jE3p086F6xSA3iDTIIpa6c1Ch3EzjXPK7XgdEDaVpI0TyrO2r2wBeVDTXSO0E8GWzSnaMnAPzypmdSK7jhD0bpF1SClLTC7PCbkqF6K9C-dQM0F-QWoM1hPMTJGG5bQy_xtQS6PT_b-uPUYNpzA diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go index a747b86043..d626167cf6 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go @@ -330,6 +330,112 @@ func TestNewAuthenticatedClientCertAuthFailed(t *testing.T) { spiretest.RequireGRPCStatusHasPrefix(t, err, codes.Unauthenticated, "authentication failed auth/cert/login: Error making API request.") } +func TestNewAuthenticatedClientK8sAuth(t *testing.T) { + fakeVaultServer := newFakeVaultServer() + fakeVaultServer.K8sAuthResponseCode = 200 + for _, tt := range []struct { + name string + response []byte + renew bool + namespace string + }{ + { + name: "K8s Authentication success / Token is renewable", + response: []byte(testK8sAuthResponse), + renew: true, + }, + { + name: "K8s Authentication success / Token is not renewable", + response: []byte(testK8sAuthResponseNotRenewable), + }, + { + name: "K8s Authentication success / Token is renewable / Namespace is given", + response: []byte(testK8sAuthResponse), + renew: true, + namespace: "test-ns", + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + fakeVaultServer.K8sAuthResponse = tt.response + + s, addr, err := fakeVaultServer.NewTLSServer() + require.NoError(t, err) + + s.Start() + defer s.Close() + + cp := &ClientParams{ + VaultAddr: fmt.Sprintf("https://%v/", addr), + Namespace: tt.namespace, + CACertPath: testRootCert, + K8sAuthRoleName: "my-role", + K8sAuthTokenPath: "testdata/k8s/token", + } + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + renewCh := make(chan struct{}) + client, err := cc.NewAuthenticatedClient(K8S, renewCh) + require.NoError(t, err) + + select { + case <-renewCh: + require.Equal(t, false, tt.renew) + default: + require.Equal(t, true, tt.renew) + } + + if cp.Namespace != "" { + headers := client.vaultClient.Headers() + require.Equal(t, cp.Namespace, headers.Get(consts.NamespaceHeaderName)) + } + }) + } +} + +func TestNewAuthenticatedClientK8sAuthFailed(t *testing.T) { + fakeVaultServer := newFakeVaultServer() + fakeVaultServer.K8sAuthResponseCode = 500 + + s, addr, err := fakeVaultServer.NewTLSServer() + require.NoError(t, err) + + s.Start() + defer s.Close() + + retry := 0 // Disable retry + cp := &ClientParams{ + MaxRetries: &retry, + VaultAddr: fmt.Sprintf("https://%v/", addr), + CACertPath: testRootCert, + K8sAuthRoleName: "my-role", + K8sAuthTokenPath: "testdata/k8s/token", + } + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + renewCh := make(chan struct{}) + _, err = cc.NewAuthenticatedClient(K8S, renewCh) + spiretest.RequireGRPCStatusHasPrefix(t, err, codes.Unauthenticated, "authentication failed auth/kubernetes/login: Error making API request.") +} + +func TestNewAuthenticatedClientK8sAuthInvalidPath(t *testing.T) { + retry := 0 // Disable retry + cp := &ClientParams{ + MaxRetries: &retry, + VaultAddr: "https://example.org:8200", + CACertPath: testRootCert, + K8sAuthTokenPath: "invalid/k8s/token", + } + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + renewCh := make(chan struct{}) + _, err = cc.NewAuthenticatedClient(K8S, renewCh) + spiretest.RequireGRPCStatusHasPrefix(t, err, codes.Internal, "failed to read k8s service account token:") +} + func TestRenewTokenFailed(t *testing.T) { fakeVaultServer := newFakeVaultServer() fakeVaultServer.LookupSelfResponse = []byte(testLookupSelfResponseShortTTL) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go index fa1aa14648..4c178df431 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go @@ -211,6 +211,61 @@ var ( "renewable": false } }` + + testK8sAuthResponse = `{ + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": null, + "wrap_info": null, + "warnings": null, + "auth": { + "client_token": "s.scngmDktKCWVRhkggMiyV7E7", + "accessor": "", + "policies": ["default"], + "token_policies": ["default"], + "metadata": { + "role": "my-role", + "service_account_name": "spire-server", + "service_account_namespace": "spire", + "service_account_secret_name": "", + "service_account_uid": "6808b4c7-0b53-45f4-83f7-e8937756eeae" + }, + "lease_duration": 3600, + "renewable": true, + "entity_id": "c69a6e0e-3f2c-98a0-39f9-e4d3d7cc294f", + "token_type": "service", + "orphan": true + } +} +` + + testK8sAuthResponseNotRenewable = `{ + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": null, + "wrap_info": null, + "warnings": null, + "auth": { + "client_token": "b.AAAAAQIUprvfquccAKnvL....", + "accessor": "", + "policies": ["default"], + "token_policies": ["default"], + "metadata": { + "role": "my-role", + "service_account_name": "spire-server", + "service_account_namespace": "spire", + "service_account_secret_name": "", + "service_account_uid": "6808b4c7-0b53-45f4-83f7-e8937756eeae" + }, + "lease_duration": 3600, + "renewable": false, + "entity_id": "c69a6e0e-3f2c-98a0-39f9-e4d3d7cc294f", + "token_type": "batch", + "orphan": true + } +}` ) type FakeVaultServerConfig struct { From 12fe8c4889961d71f13b615a0b66543cf2b74313 Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:22 +0200 Subject: [PATCH 19/29] Support verifying server certificate via CA (#5058) Signed-off-by: Matteo Kamm --- .../hashicorpvault/hashicorp_vault.go | 6 +- .../testdata/invalid-client-cert.pem | 1 + .../testdata/invalid-client-key.pem | 1 + .../testdata/invalid-root-cert.pem | 1 + .../keymanager/hashicorpvault/vault_client.go | 2 - .../hashicorpvault/vault_client_test.go | 177 +++++++++++++++++- 6 files changed, 179 insertions(+), 9 deletions(-) create mode 100644 pkg/server/plugin/keymanager/hashicorpvault/testdata/invalid-client-cert.pem create mode 100644 pkg/server/plugin/keymanager/hashicorpvault/testdata/invalid-client-key.pem create mode 100644 pkg/server/plugin/keymanager/hashicorpvault/testdata/invalid-root-cert.pem diff --git a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go index ddf9c76b8e..61c22b4cda 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go @@ -59,8 +59,9 @@ type Config struct { // If true, vault client accepts any server certificates. // It should be used only test environment so on. InsecureSkipVerify bool `hcl:"insecure_skip_verify" json:"insecure_skip_verify"` - - // TODO: Support CA certificate path here instead of insecure skip verify + // Path to a CA certificate file that the client verifies the server certificate. + // Only PEM format is supported. + CACertPath string `hcl:"ca_cert_path" json:"ca_cert_path"` // Configuration for the Token authentication method TokenAuth *TokenAuthConfig `hcl:"token_auth" json:"token_auth,omitempty"` @@ -228,6 +229,7 @@ func (p *Plugin) genClientParams(method AuthMethod, config *Config) (*ClientPara VaultAddr: p.getEnvOrDefault(envVaultAddr, config.VaultAddr), Namespace: p.getEnvOrDefault(envVaultNamespace, config.Namespace), TransitEnginePath: p.getEnvOrDefault(envVaultTransitEnginePath, config.TransitEnginePath), + CACertPath: p.getEnvOrDefault(envVaultCACert, config.CACertPath), TLSSKipVerify: config.InsecureSkipVerify, } diff --git a/pkg/server/plugin/keymanager/hashicorpvault/testdata/invalid-client-cert.pem b/pkg/server/plugin/keymanager/hashicorpvault/testdata/invalid-client-cert.pem new file mode 100644 index 0000000000..7ec3efa757 --- /dev/null +++ b/pkg/server/plugin/keymanager/hashicorpvault/testdata/invalid-client-cert.pem @@ -0,0 +1 @@ +"invalid-client-cert-file" diff --git a/pkg/server/plugin/keymanager/hashicorpvault/testdata/invalid-client-key.pem b/pkg/server/plugin/keymanager/hashicorpvault/testdata/invalid-client-key.pem new file mode 100644 index 0000000000..2ce22f3da6 --- /dev/null +++ b/pkg/server/plugin/keymanager/hashicorpvault/testdata/invalid-client-key.pem @@ -0,0 +1 @@ +"invalid-client-key-file" diff --git a/pkg/server/plugin/keymanager/hashicorpvault/testdata/invalid-root-cert.pem b/pkg/server/plugin/keymanager/hashicorpvault/testdata/invalid-root-cert.pem new file mode 100644 index 0000000000..c224998f83 --- /dev/null +++ b/pkg/server/plugin/keymanager/hashicorpvault/testdata/invalid-root-cert.pem @@ -0,0 +1 @@ +"invalid-root-cert-file" diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go index a1ecfde3c4..bb60233a10 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go @@ -18,8 +18,6 @@ import ( "github.com/spiffe/spire/pkg/common/pemutil" ) -// TODO: Delete everything that is unused in here - const ( envVaultAddr = "VAULT_ADDR" envVaultToken = "VAULT_TOKEN" diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go index d626167cf6..1b24361756 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go @@ -1,7 +1,12 @@ package hashicorpvault import ( + "crypto/tls" + "crypto/x509" "fmt" + vapi "github.com/hashicorp/vault/api" + "net/http" + "os" "testing" "time" @@ -13,11 +18,14 @@ import ( ) const ( - testRootCert = "testdata/root-cert.pem" - testServerCert = "testdata/server-cert.pem" - testServerKey = "testdata/server-key.pem" - testClientCert = "testdata/client-cert.pem" - testClientKey = "testdata/client-key.pem" + testRootCert = "testdata/root-cert.pem" + testInvalidRootCert = "testdata/invalid-root-cert.pem" + testServerCert = "testdata/server-cert.pem" + testServerKey = "testdata/server-key.pem" + testClientCert = "testdata/client-cert.pem" + testInvalidClientCert = "testdata/invalid-client-cert.pem" + testClientKey = "testdata/client-key.pem" + testInvalidClientKey = "testdata/invalid-client-key.pem" ) func TestNewClientConfigWithDefaultValues(t *testing.T) { @@ -470,9 +478,168 @@ func TestRenewTokenFailed(t *testing.T) { } } +func TestConfigureTLSWithCertAuth(t *testing.T) { + cp := &ClientParams{ + VaultAddr: "http://example.org:8200", + ClientCertPath: testClientCert, + ClientKeyPath: testClientKey, + CACertPath: testRootCert, + } + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + vc := vapi.DefaultConfig() + err = cc.configureTLS(vc) + require.NoError(t, err) + + tcc := vc.HttpClient.Transport.(*http.Transport).TLSClientConfig + cert, err := tcc.GetClientCertificate(&tls.CertificateRequestInfo{}) + require.NoError(t, err) + + testCert, err := testClientCertificatePair() + require.NoError(t, err) + require.Equal(t, testCert.Certificate, cert.Certificate) + + testPool, err := testRootCAs() + require.NoError(t, err) + require.True(t, testPool.Equal(tcc.RootCAs)) +} + +func TestConfigureTLSWithTokenAuth(t *testing.T) { + cp := &ClientParams{ + VaultAddr: "http://example.org:8200", + CACertPath: testRootCert, + Token: "test-token", + } + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + vc := vapi.DefaultConfig() + err = cc.configureTLS(vc) + require.NoError(t, err) + + tcc := vc.HttpClient.Transport.(*http.Transport).TLSClientConfig + require.Nil(t, tcc.GetClientCertificate) + + testPool, err := testRootCAs() + require.NoError(t, err) + require.Equal(t, testPool.Subjects(), tcc.RootCAs.Subjects()) //nolint:staticcheck // these pools are not system pools so the use of Subjects() is ok for now +} + +func TestConfigureTLSWithAppRoleAuth(t *testing.T) { + cp := &ClientParams{ + VaultAddr: "http://example.org:8200", + CACertPath: testRootCert, + AppRoleID: "test-approle-id", + AppRoleSecretID: "test-approle-secret", + } + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + vc := vapi.DefaultConfig() + err = cc.configureTLS(vc) + require.NoError(t, err) + + tcc := vc.HttpClient.Transport.(*http.Transport).TLSClientConfig + require.Nil(t, tcc.GetClientCertificate) + + testPool, err := testRootCAs() + require.NoError(t, err) + require.Equal(t, testPool.Subjects(), tcc.RootCAs.Subjects()) //nolint:staticcheck // these pools are not system pools so the use of Subjects() is ok for now +} + +func TestConfigureTLSInvalidCACert(t *testing.T) { + cp := &ClientParams{ + VaultAddr: "http://example.org:8200", + ClientCertPath: testClientCert, + ClientKeyPath: testClientKey, + CACertPath: testInvalidRootCert, + } + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + vc := vapi.DefaultConfig() + err = cc.configureTLS(vc) + spiretest.RequireGRPCStatusHasPrefix(t, err, codes.InvalidArgument, "failed to load CA certificate: no PEM blocks") +} + +func TestConfigureTLSInvalidClientKey(t *testing.T) { + cp := &ClientParams{ + VaultAddr: "http://example.org:8200", + ClientCertPath: testClientCert, + ClientKeyPath: testInvalidClientKey, + CACertPath: testRootCert, + } + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + vc := vapi.DefaultConfig() + err = cc.configureTLS(vc) + spiretest.RequireGRPCStatusHasPrefix(t, err, codes.InvalidArgument, "failed to parse client cert and private-key: tls: failed to find any PEM data in key input") +} + +func TestConfigureTLSInvalidClientCert(t *testing.T) { + cp := &ClientParams{ + VaultAddr: "http://example.org:8200", + ClientCertPath: testInvalidClientCert, + ClientKeyPath: testClientKey, + CACertPath: testRootCert, + } + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + vc := vapi.DefaultConfig() + err = cc.configureTLS(vc) + spiretest.RequireGRPCStatusHasPrefix(t, err, codes.InvalidArgument, "failed to parse client cert and private-key: tls: failed to find any PEM data in certificate input") +} + +func TestConfigureTLSRequireClientCertAndKey(t *testing.T) { + cp := &ClientParams{ + VaultAddr: "http://example.org:8200", + ClientCertPath: testClientCert, + CACertPath: testRootCert, + } + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + vc := vapi.DefaultConfig() + err = cc.configureTLS(vc) + spiretest.RequireGRPCStatus(t, err, codes.InvalidArgument, "both client cert and client key are required") +} + +// TODO: Test CreateKey +// TODO: Test GetKey +// TODO: Test SignData + func newFakeVaultServer() *FakeVaultServerConfig { fakeVaultServer := NewFakeVaultServerConfig() fakeVaultServer.RenewResponseCode = 200 fakeVaultServer.RenewResponse = []byte(testRenewResponse) return fakeVaultServer } + +func testClientCertificatePair() (tls.Certificate, error) { + cert, err := os.ReadFile(testClientCert) + if err != nil { + return tls.Certificate{}, err + } + key, err := os.ReadFile(testClientKey) + if err != nil { + return tls.Certificate{}, err + } + + return tls.X509KeyPair(cert, key) +} + +func testRootCAs() (*x509.CertPool, error) { + pool := x509.NewCertPool() + pem, err := os.ReadFile(testRootCert) + if err != nil { + return nil, err + } + ok := pool.AppendCertsFromPEM(pem) + if !ok { + return nil, err + } + return pool, nil +} From d15d8ff71a6e731aabeb12573b2e2b1ac1385f6b Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:22 +0200 Subject: [PATCH 20/29] Test vault client create key function (#5058) Signed-off-by: Matteo Kamm --- .../keymanager/hashicorpvault/vault_client.go | 7 +- .../hashicorpvault/vault_client_test.go | 65 ++++++++++++++++++- .../hashicorpvault/vault_fake_test.go | 8 +++ 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go index bb60233a10..6bcf7a0cc9 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go @@ -372,9 +372,12 @@ func (c *Client) CreateKey(ctx context.Context, spireKeyID string, keyType Trans "exportable": "false", // TODO: Maybe make this configurable } - // TODO: Handle errors here such as key already exists _, err := c.vaultClient.Logical().WriteWithContext(ctx, fmt.Sprintf("/%s/keys/%s", c.clientParams.TransitEnginePath, spireKeyID), arguments) - return err + if err != nil { + return status.Errorf(codes.Internal, "failed to create transit engine key: %v", err) + } + + return nil } // GetKey gets the transit engine key with the specified spire key id. diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go index 1b24361756..b4d64e6d6d 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go @@ -1,6 +1,7 @@ package hashicorpvault import ( + "context" "crypto/tls" "crypto/x509" "fmt" @@ -607,7 +608,69 @@ func TestConfigureTLSRequireClientCertAndKey(t *testing.T) { spiretest.RequireGRPCStatus(t, err, codes.InvalidArgument, "both client cert and client key are required") } -// TODO: Test CreateKey +func TestCreateKey(t *testing.T) { + fakeVaultServer := newFakeVaultServer() + fakeVaultServer.CertAuthResponseCode = 200 + fakeVaultServer.CertAuthResponse = []byte(testCertAuthResponse) + fakeVaultServer.CreateKeyResponseCode = 204 + + s, addr, err := fakeVaultServer.NewTLSServer() + require.NoError(t, err) + + s.Start() + defer s.Close() + + cp := &ClientParams{ + VaultAddr: fmt.Sprintf("https://%v/", addr), + CACertPath: testRootCert, + ClientCertPath: testClientCert, + ClientKeyPath: testClientKey, + } + + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + renewCh := make(chan struct{}) + client, err := cc.NewAuthenticatedClient(CERT, renewCh) + require.NoError(t, err) + + err = client.CreateKey(context.Background(), "x509-CA-A", TransitKeyTypeRSA2048) + require.NoError(t, err) +} + +func TestCreateKeyErrorFromEndpoint(t *testing.T) { + fakeVaultServer := newFakeVaultServer() + fakeVaultServer.CertAuthResponseCode = 200 + fakeVaultServer.CertAuthResponse = []byte(testCertAuthResponse) + fakeVaultServer.CreateKeyResponseCode = 500 + fakeVaultServer.CreateKeyResponse = []byte("test error") + + s, addr, err := fakeVaultServer.NewTLSServer() + require.NoError(t, err) + + s.Start() + defer s.Close() + + retry := 0 // Disable retry + cp := &ClientParams{ + MaxRetries: &retry, + VaultAddr: fmt.Sprintf("https://%v/", addr), + CACertPath: testRootCert, + ClientCertPath: testClientCert, + ClientKeyPath: testClientKey, + } + + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + renewCh := make(chan struct{}) + client, err := cc.NewAuthenticatedClient(CERT, renewCh) + require.NoError(t, err) + + err = client.CreateKey(context.Background(), "x509-CA-A", TransitKeyTypeRSA2048) + spiretest.RequireGRPCStatusHasPrefix(t, err, codes.Internal, "failed to create transit engine key: Error making API request.") +} + // TODO: Test GetKey // TODO: Test SignData diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go index 4c178df431..804a7a2002 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go @@ -13,6 +13,7 @@ const ( defaultK8sAuthEndpoint = "/v1/auth/kubernetes/login" defaultRenewEndpoint = "/v1/auth/token/renew-self" defaultLookupSelfEndpoint = "/v1/auth/token/lookup-self" + defaultCreateKeyEndpoint = "/v1/transit/keys/x509-CA-A" listenAddr = "127.0.0.1:0" ) @@ -292,6 +293,10 @@ type FakeVaultServerConfig struct { LookupSelfReqHandler func(code int, resp []byte) func(w http.ResponseWriter, r *http.Request) LookupSelfResponseCode int LookupSelfResponse []byte + CreateKeyReqEndpoint string + CreateKeyReqHandler func(code int, resp []byte) func(http.ResponseWriter, *http.Request) + CreateKeyResponseCode int + CreateKeyResponse []byte } // NewFakeVaultServerConfig returns VaultServerConfig with default values @@ -308,6 +313,8 @@ func NewFakeVaultServerConfig() *FakeVaultServerConfig { RenewReqHandler: defaultReqHandler, LookupSelfReqEndpoint: defaultLookupSelfEndpoint, LookupSelfReqHandler: defaultReqHandler, + CreateKeyReqEndpoint: defaultCreateKeyEndpoint, + CreateKeyReqHandler: defaultReqHandler, } } @@ -339,6 +346,7 @@ func (v *FakeVaultServerConfig) NewTLSServer() (srv *httptest.Server, addr strin mux.HandleFunc(v.K8sAuthReqEndpoint, v.AppRoleAuthReqHandler(v.K8sAuthResponseCode, v.K8sAuthResponse)) mux.HandleFunc(v.RenewReqEndpoint, v.RenewReqHandler(v.RenewResponseCode, v.RenewResponse)) mux.HandleFunc(v.LookupSelfReqEndpoint, v.LookupSelfReqHandler(v.LookupSelfResponseCode, v.LookupSelfResponse)) + mux.HandleFunc(v.CreateKeyReqEndpoint, v.CreateKeyReqHandler(v.CreateKeyResponseCode, v.CreateKeyResponse)) srv = httptest.NewUnstartedServer(mux) srv.Listener = l From 5c33e058a0b043997997dd44736cc439604b7d9e Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:22 +0200 Subject: [PATCH 21/29] Test vault client get key function (#5058) Signed-off-by: Matteo Kamm --- .../keymanager/hashicorpvault/vault_client.go | 3 +- .../hashicorpvault/vault_client_test.go | 68 ++++++++++++++++++- .../hashicorpvault/vault_fake_test.go | 55 +++++++++++++-- 3 files changed, 117 insertions(+), 9 deletions(-) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go index 6bcf7a0cc9..06039ebb18 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go @@ -383,10 +383,9 @@ func (c *Client) CreateKey(ctx context.Context, spireKeyID string, keyType Trans // GetKey gets the transit engine key with the specified spire key id. // See: https://developer.hashicorp.com/vault/api-docs/secret/transit#read-key func (c *Client) GetKey(ctx context.Context, spireKeyID string) (string, error) { - // TODO: Handle errors here res, err := c.vaultClient.Logical().ReadWithContext(ctx, fmt.Sprintf("/%s/keys/%s", c.clientParams.TransitEnginePath, spireKeyID)) if err != nil { - return "", err + return "", status.Errorf(codes.Internal, "failed to get transit engine key: %v", err) } keys, ok := res.Data["keys"] diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go index b4d64e6d6d..c1d83ad169 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go @@ -671,7 +671,73 @@ func TestCreateKeyErrorFromEndpoint(t *testing.T) { spiretest.RequireGRPCStatusHasPrefix(t, err, codes.Internal, "failed to create transit engine key: Error making API request.") } -// TODO: Test GetKey +func TestGetKey(t *testing.T) { + fakeVaultServer := newFakeVaultServer() + fakeVaultServer.CertAuthResponseCode = 200 + fakeVaultServer.CertAuthResponse = []byte(testCertAuthResponse) + fakeVaultServer.GetKeyResponseCode = 200 + fakeVaultServer.GetKeyResponse = []byte(testGetKeyResponse) + + s, addr, err := fakeVaultServer.NewTLSServer() + require.NoError(t, err) + + s.Start() + defer s.Close() + + cp := &ClientParams{ + VaultAddr: fmt.Sprintf("https://%v/", addr), + CACertPath: testRootCert, + ClientCertPath: testClientCert, + ClientKeyPath: testClientKey, + } + + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + renewCh := make(chan struct{}) + client, err := cc.NewAuthenticatedClient(CERT, renewCh) + require.NoError(t, err) + + resp, err := client.GetKey(context.Background(), "x509-CA-A") + require.NoError(t, err) + + require.Equal(t, "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV57LFbIQZzyZ2YcKZfB9mGWkUhJv\niRzIZOqV4wRHoUOZjMuhBMR2WviEsy65TYpcBjreAc6pbneiyhlTwPvgmw==\n-----END PUBLIC KEY-----\n", resp) +} + +func TestGetKeyErrorFromEndpoint(t *testing.T) { + fakeVaultServer := newFakeVaultServer() + fakeVaultServer.CertAuthResponseCode = 200 + fakeVaultServer.CertAuthResponse = []byte(testCertAuthResponse) + fakeVaultServer.GetKeyResponseCode = 500 + fakeVaultServer.GetKeyResponse = []byte("test error") + + s, addr, err := fakeVaultServer.NewTLSServer() + require.NoError(t, err) + + s.Start() + defer s.Close() + + retry := 0 // Disable retry + cp := &ClientParams{ + MaxRetries: &retry, + VaultAddr: fmt.Sprintf("https://%v/", addr), + CACertPath: testRootCert, + ClientCertPath: testClientCert, + ClientKeyPath: testClientKey, + } + + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + renewCh := make(chan struct{}) + client, err := cc.NewAuthenticatedClient(CERT, renewCh) + require.NoError(t, err) + + resp, err := client.GetKey(context.Background(), "x509-CA-A") + spiretest.RequireGRPCStatusHasPrefix(t, err, codes.Internal, "failed to get transit engine key: Error making API request.") + require.Empty(t, resp) +} + // TODO: Test SignData func newFakeVaultServer() *FakeVaultServerConfig { diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go index 804a7a2002..990cc71e56 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go @@ -8,12 +8,13 @@ import ( ) const ( - defaultTLSAuthEndpoint = "/v1/auth/cert/login" - defaultAppRoleAuthEndpoint = "/v1/auth/approle/login" - defaultK8sAuthEndpoint = "/v1/auth/kubernetes/login" - defaultRenewEndpoint = "/v1/auth/token/renew-self" - defaultLookupSelfEndpoint = "/v1/auth/token/lookup-self" - defaultCreateKeyEndpoint = "/v1/transit/keys/x509-CA-A" + defaultTLSAuthEndpoint = "PUT /v1/auth/cert/login" + defaultAppRoleAuthEndpoint = "PUT /v1/auth/approle/login" + defaultK8sAuthEndpoint = "PUT /v1/auth/kubernetes/login" + defaultRenewEndpoint = "POST /v1/auth/token/renew-self" + defaultLookupSelfEndpoint = "GET /v1/auth/token/lookup-self" + defaultCreateKeyEndpoint = "PUT /v1/transit/keys/{id}" + defaultGetKeyEndpoint = "GET /v1/transit/keys/{id}" listenAddr = "127.0.0.1:0" ) @@ -267,6 +268,41 @@ var ( "orphan": true } }` + + testGetKeyResponse = `{ + "request_id": "646eddbd-83fd-0cc1-387b-f1a17fa88c3d", + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": { + "allow_plaintext_backup": false, + "auto_rotate_period": 0, + "deletion_allowed": false, + "derived": false, + "exportable": false, + "imported_key": false, + "keys": { + "1": { + "creation_time": "2024-09-16T18:18:54.284635756Z", + "name": "P-256", + "public_key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV57LFbIQZzyZ2YcKZfB9mGWkUhJv\niRzIZOqV4wRHoUOZjMuhBMR2WviEsy65TYpcBjreAc6pbneiyhlTwPvgmw==\n-----END PUBLIC KEY-----\n" + } + }, + "latest_version": 1, + "min_available_version": 0, + "min_decryption_version": 1, + "min_encryption_version": 0, + "name": "x509-CA-A", + "supports_decryption": false, + "supports_derivation": false, + "supports_encryption": false, + "supports_signing": true, + "type": "ecdsa-p256" + }, + "wrap_info": null, + "warnings": null, + "auth": null +}` ) type FakeVaultServerConfig struct { @@ -297,6 +333,10 @@ type FakeVaultServerConfig struct { CreateKeyReqHandler func(code int, resp []byte) func(http.ResponseWriter, *http.Request) CreateKeyResponseCode int CreateKeyResponse []byte + GetKeyReqEndpoint string + GetKeyReqHandler func(code int, resp []byte) func(http.ResponseWriter, *http.Request) + GetKeyResponseCode int + GetKeyResponse []byte } // NewFakeVaultServerConfig returns VaultServerConfig with default values @@ -315,6 +355,8 @@ func NewFakeVaultServerConfig() *FakeVaultServerConfig { LookupSelfReqHandler: defaultReqHandler, CreateKeyReqEndpoint: defaultCreateKeyEndpoint, CreateKeyReqHandler: defaultReqHandler, + GetKeyReqEndpoint: defaultGetKeyEndpoint, + GetKeyReqHandler: defaultReqHandler, } } @@ -347,6 +389,7 @@ func (v *FakeVaultServerConfig) NewTLSServer() (srv *httptest.Server, addr strin mux.HandleFunc(v.RenewReqEndpoint, v.RenewReqHandler(v.RenewResponseCode, v.RenewResponse)) mux.HandleFunc(v.LookupSelfReqEndpoint, v.LookupSelfReqHandler(v.LookupSelfResponseCode, v.LookupSelfResponse)) mux.HandleFunc(v.CreateKeyReqEndpoint, v.CreateKeyReqHandler(v.CreateKeyResponseCode, v.CreateKeyResponse)) + mux.HandleFunc(v.GetKeyReqEndpoint, v.GetKeyReqHandler(v.GetKeyResponseCode, v.GetKeyResponse)) srv = httptest.NewUnstartedServer(mux) srv.Listener = l From 1438bc4a58e16aa4bf9bb4fd64177b3ec52ffbb4 Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:22 +0200 Subject: [PATCH 22/29] Test vault client sign data function (#5058) Signed-off-by: Matteo Kamm --- .../keymanager/hashicorpvault/vault_client.go | 1 - .../hashicorpvault/vault_client_test.go | 70 ++++++++++++++++++- .../hashicorpvault/vault_fake_test.go | 22 ++++++ 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go index 06039ebb18..4047777801 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go @@ -434,7 +434,6 @@ func (c *Client) SignData(ctx context.Context, spireKeyID string, data []byte, h "prehashed": "true", } - // TODO: Handle errors here sigResp, err := c.vaultClient.Logical().WriteWithContext(ctx, fmt.Sprintf("/%s/sign/%s/%s", c.clientParams.TransitEnginePath, spireKeyID, hashAlgo), body) if err != nil { return nil, status.Errorf(codes.Internal, "transit engine sign call failed: %v", err) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go index c1d83ad169..baaf90a4eb 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "crypto/x509" + "encoding/base64" "fmt" vapi "github.com/hashicorp/vault/api" "net/http" @@ -738,7 +739,74 @@ func TestGetKeyErrorFromEndpoint(t *testing.T) { require.Empty(t, resp) } -// TODO: Test SignData +func TestSignData(t *testing.T) { + fakeVaultServer := newFakeVaultServer() + fakeVaultServer.CertAuthResponseCode = 200 + fakeVaultServer.CertAuthResponse = []byte(testCertAuthResponse) + fakeVaultServer.SignDataResponseCode = 200 + fakeVaultServer.SignDataResponse = []byte(testSignDataResponse) + + s, addr, err := fakeVaultServer.NewTLSServer() + require.NoError(t, err) + + s.Start() + defer s.Close() + + cp := &ClientParams{ + VaultAddr: fmt.Sprintf("https://%v/", addr), + CACertPath: testRootCert, + ClientCertPath: testClientCert, + ClientKeyPath: testClientKey, + } + + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + renewCh := make(chan struct{}) + client, err := cc.NewAuthenticatedClient(CERT, renewCh) + require.NoError(t, err) + + resp, err := client.SignData(context.Background(), "x509-CA-A", []byte("foo"), TransitHashAlgorithmSHA256, TransitSignatureSignatureAlgorithmPKCS1v15) + require.NoError(t, err) + + expected, err := base64.StdEncoding.DecodeString("MEQCIHw3maFgxsmzAUsUXnw2ahUgPcomjF8+XxflwH4CsouhAiAYL3RhWx8dP2ymm7hjSUvc9EQ8GPXmLrvgacqkEKQPGw==") + require.NoError(t, err) + require.Equal(t, expected, resp) +} + +func TestSignDataErrorFromEndpoint(t *testing.T) { + fakeVaultServer := newFakeVaultServer() + fakeVaultServer.CertAuthResponseCode = 200 + fakeVaultServer.CertAuthResponse = []byte(testCertAuthResponse) + fakeVaultServer.SignDataResponseCode = 500 + fakeVaultServer.SignDataResponse = []byte("test error") + + s, addr, err := fakeVaultServer.NewTLSServer() + require.NoError(t, err) + + s.Start() + defer s.Close() + + retry := 0 // Disable retry + cp := &ClientParams{ + MaxRetries: &retry, + VaultAddr: fmt.Sprintf("https://%v/", addr), + CACertPath: testRootCert, + ClientCertPath: testClientCert, + ClientKeyPath: testClientKey, + } + + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + renewCh := make(chan struct{}) + client, err := cc.NewAuthenticatedClient(CERT, renewCh) + require.NoError(t, err) + + resp, err := client.SignData(context.Background(), "x509-CA-A", []byte("foo"), TransitHashAlgorithmSHA256, TransitSignatureSignatureAlgorithmPKCS1v15) + spiretest.RequireGRPCStatusHasPrefix(t, err, codes.Internal, "transit engine sign call failed: Error making API request.") + require.Empty(t, resp) +} func newFakeVaultServer() *FakeVaultServerConfig { fakeVaultServer := NewFakeVaultServerConfig() diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go index 990cc71e56..23c3ba6086 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go @@ -15,6 +15,7 @@ const ( defaultLookupSelfEndpoint = "GET /v1/auth/token/lookup-self" defaultCreateKeyEndpoint = "PUT /v1/transit/keys/{id}" defaultGetKeyEndpoint = "GET /v1/transit/keys/{id}" + defaultSignDataEndpoint = "PUT /v1/transit/sign/{id}/{algo}" listenAddr = "127.0.0.1:0" ) @@ -303,6 +304,20 @@ var ( "warnings": null, "auth": null }` + testSignDataResponse = `{ + "request_id": "51bb98fa-8da3-8678-64e7-7220bc8b94a6", + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": { + "key_version": 1, + "signature": "vault:v1:MEQCIHw3maFgxsmzAUsUXnw2ahUgPcomjF8+XxflwH4CsouhAiAYL3RhWx8dP2ymm7hjSUvc9EQ8GPXmLrvgacqkEKQPGw==" + }, + "wrap_info": null, + "warnings": null, + "auth": null +} +` ) type FakeVaultServerConfig struct { @@ -337,6 +352,10 @@ type FakeVaultServerConfig struct { GetKeyReqHandler func(code int, resp []byte) func(http.ResponseWriter, *http.Request) GetKeyResponseCode int GetKeyResponse []byte + SignDataReqEndpoint string + SignDataReqHandler func(code int, resp []byte) func(http.ResponseWriter, *http.Request) + SignDataResponseCode int + SignDataResponse []byte } // NewFakeVaultServerConfig returns VaultServerConfig with default values @@ -357,6 +376,8 @@ func NewFakeVaultServerConfig() *FakeVaultServerConfig { CreateKeyReqHandler: defaultReqHandler, GetKeyReqEndpoint: defaultGetKeyEndpoint, GetKeyReqHandler: defaultReqHandler, + SignDataReqEndpoint: defaultSignDataEndpoint, + SignDataReqHandler: defaultReqHandler, } } @@ -390,6 +411,7 @@ func (v *FakeVaultServerConfig) NewTLSServer() (srv *httptest.Server, addr strin mux.HandleFunc(v.LookupSelfReqEndpoint, v.LookupSelfReqHandler(v.LookupSelfResponseCode, v.LookupSelfResponse)) mux.HandleFunc(v.CreateKeyReqEndpoint, v.CreateKeyReqHandler(v.CreateKeyResponseCode, v.CreateKeyResponse)) mux.HandleFunc(v.GetKeyReqEndpoint, v.GetKeyReqHandler(v.GetKeyResponseCode, v.GetKeyResponse)) + mux.HandleFunc(v.SignDataReqEndpoint, v.SignDataReqHandler(v.SignDataResponseCode, v.SignDataResponse)) srv = httptest.NewUnstartedServer(mux) srv.Listener = l From c2879e27609ddb8fc5a97b2b209269d9ca7315fc Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:22 +0200 Subject: [PATCH 23/29] Test vault key manager configure function (#5058) Signed-off-by: Matteo Kamm --- .../hashicorpvault/hashicorp_vault_test.go | 274 ++++++++++++++++++ .../hashicorpvault/vault_fake_test.go | 117 ++++++++ 2 files changed, 391 insertions(+) create mode 100644 pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault_test.go diff --git a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault_test.go b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault_test.go new file mode 100644 index 0000000000..a4ed7a6ad5 --- /dev/null +++ b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault_test.go @@ -0,0 +1,274 @@ +package hashicorpvault + +import ( + "bytes" + "fmt" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "testing" + "text/template" + + "github.com/spiffe/go-spiffe/v2/spiffeid" + "github.com/spiffe/spire/pkg/common/catalog" + "github.com/spiffe/spire/test/plugintest" + "github.com/spiffe/spire/test/spiretest" +) + +func TestConfigure(t *testing.T) { + fakeVaultServer := setupFakeVaultServer() + fakeVaultServer.CertAuthResponseCode = 200 + fakeVaultServer.CertAuthResponse = []byte(testCertAuthResponse) + fakeVaultServer.CertAuthReqEndpoint = "/v1/auth/test-cert-auth/login" + fakeVaultServer.AppRoleAuthResponseCode = 200 + fakeVaultServer.AppRoleAuthResponse = []byte(testAppRoleAuthResponse) + fakeVaultServer.AppRoleAuthReqEndpoint = "/v1/auth/test-approle-auth/login" + + s, addr, err := fakeVaultServer.NewTLSServer() + require.NoError(t, err) + + s.Start() + defer s.Close() + + for _, tt := range []struct { + name string + configTmpl string + plainConfig string + expectMsgPrefix string + expectCode codes.Code + wantAuth AuthMethod + expectNamespace string + envKeyVal map[string]string + expectToken string + expectCertAuthMountPoint string + expectClientCertPath string + expectClientKeyPath string + appRoleAuthMountPoint string + appRoleID string + appRoleSecretID string + expectK8sAuthMountPoint string + expectK8sAuthRoleName string + expectK8sAuthTokenPath string + expectTransitEnginePath string + }{ + { + name: "Configure plugin with Client Certificate authentication params given in config file", + configTmpl: testTokenAuthConfigTpl, + wantAuth: TOKEN, + expectToken: "test-token", + expectTransitEnginePath: "transit", + }, + { + name: "Configure plugin with Token authentication params given as environment variables", + configTmpl: testTokenAuthConfigWithEnvTpl, + envKeyVal: map[string]string{ + envVaultToken: "test-token", + }, + wantAuth: TOKEN, + expectToken: "test-token", + expectTransitEnginePath: "transit", + }, + { + name: "Configure plugin with Client Certificate authentication params given in config file", + configTmpl: testCertAuthConfigTpl, + wantAuth: CERT, + expectCertAuthMountPoint: "test-cert-auth", + expectClientCertPath: "testdata/client-cert.pem", + expectClientKeyPath: "testdata/client-key.pem", + expectTransitEnginePath: "transit", + }, + { + name: "Configure plugin with Client Certificate authentication params given as environment variables", + configTmpl: testCertAuthConfigWithEnvTpl, + envKeyVal: map[string]string{ + envVaultClientCert: "testdata/client-cert.pem", + envVaultClientKey: testClientKey, + }, + wantAuth: CERT, + expectCertAuthMountPoint: "test-cert-auth", + expectClientCertPath: testClientCert, + expectClientKeyPath: testClientKey, + expectTransitEnginePath: "transit", + }, + { + name: "Configure plugin with AppRole authenticate params given in config file", + configTmpl: testAppRoleAuthConfigTpl, + wantAuth: APPROLE, + appRoleAuthMountPoint: "test-approle-auth", + appRoleID: "test-approle-id", + appRoleSecretID: "test-approle-secret-id", + expectTransitEnginePath: "transit", + }, + { + name: "Configure plugin with AppRole authentication params given as environment variables", + configTmpl: testAppRoleAuthConfigWithEnvTpl, + envKeyVal: map[string]string{ + envVaultAppRoleID: "test-approle-id", + envVaultAppRoleSecretID: "test-approle-secret-id", + }, + wantAuth: APPROLE, + appRoleAuthMountPoint: "test-approle-auth", + appRoleID: "test-approle-id", + appRoleSecretID: "test-approle-secret-id", + expectTransitEnginePath: "transit", + }, + { + name: "Configure plugin with Kubernetes authentication params given in config file", + configTmpl: testK8sAuthConfigTpl, + wantAuth: K8S, + expectK8sAuthMountPoint: "test-k8s-auth", + expectK8sAuthTokenPath: "testdata/k8s/token", + expectK8sAuthRoleName: "my-role", + expectTransitEnginePath: "transit", + }, + { + name: "Multiple authentication methods configured", + configTmpl: testMultipleAuthConfigsTpl, + expectCode: codes.InvalidArgument, + expectMsgPrefix: "only one authentication method can be configured", + }, + { + name: "Pass VaultAddr via the environment variable", + configTmpl: testConfigWithVaultAddrEnvTpl, + envKeyVal: map[string]string{ + envVaultAddr: fmt.Sprintf("https://%v/", addr), + }, + wantAuth: TOKEN, + expectToken: "test-token", + expectTransitEnginePath: "transit", + }, + { + name: "Configure plugin with transit engine path given in config file", + configTmpl: testConfigWithTransitEnginePathTpl, + wantAuth: TOKEN, + expectToken: "test-token", + expectTransitEnginePath: "test-path", + }, + { + name: "Configure plugin with transit engine path given as environment variables", + configTmpl: testConfigWithTransitEnginePathEnvTpl, + envKeyVal: map[string]string{ + envVaultTransitEnginePath: "test-path", + }, + wantAuth: TOKEN, + expectToken: "test-token", + expectTransitEnginePath: "test-path", + }, + { + name: "Configure plugin with namespace given in config file", + configTmpl: testNamespaceConfigTpl, + wantAuth: TOKEN, + expectNamespace: "test-ns", + expectTransitEnginePath: "transit", + expectToken: "test-token", + }, + { + name: "Configure plugin with given namespace given as environment variable", + configTmpl: testNamespaceEnvTpl, + wantAuth: TOKEN, + envKeyVal: map[string]string{ + envVaultNamespace: "test-ns", + }, + expectNamespace: "test-ns", + expectTransitEnginePath: "transit", + expectToken: "test-token", + }, + { + name: "Malformed configuration", + plainConfig: "invalid-config", + expectCode: codes.InvalidArgument, + expectMsgPrefix: "unable to decode configuration:", + }, + { + name: "Required parameters are not given / k8s_auth_role_name", + configTmpl: testK8sAuthNoRoleNameTpl, + wantAuth: K8S, + expectCode: codes.InvalidArgument, + expectMsgPrefix: "k8s_auth_role_name is required", + }, + { + name: "Required parameters are not given / token_path", + configTmpl: testK8sAuthNoTokenPathTpl, + wantAuth: K8S, + expectCode: codes.InvalidArgument, + expectMsgPrefix: "token_path is required", + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + var err error + + p := New() + p.hooks.lookupEnv = func(s string) (string, bool) { + if len(tt.envKeyVal) == 0 { + return "", false + } + v, ok := tt.envKeyVal[s] + return v, ok + } + + plainConfig := "" + if tt.plainConfig != "" { + plainConfig = tt.plainConfig + } else { + plainConfig = getTestConfigureRequest(t, fmt.Sprintf("https://%v/", addr), tt.configTmpl) + } + plugintest.Load(t, builtin(p), nil, + plugintest.CaptureConfigureError(&err), + plugintest.Configure(plainConfig), + plugintest.CoreConfig(catalog.CoreConfig{ + TrustDomain: spiffeid.RequireTrustDomainFromString("localhost"), + }), + ) + + spiretest.RequireGRPCStatusHasPrefix(t, err, tt.expectCode, tt.expectMsgPrefix) + if tt.expectCode != codes.OK { + return + } + + require.NotNil(t, p.cc) + require.NotNil(t, p.cc.clientParams) + + switch tt.wantAuth { + case TOKEN: + require.Equal(t, tt.expectToken, p.cc.clientParams.Token) + case CERT: + require.Equal(t, tt.expectCertAuthMountPoint, p.cc.clientParams.CertAuthMountPoint) + require.Equal(t, tt.expectClientCertPath, p.cc.clientParams.ClientCertPath) + require.Equal(t, tt.expectClientKeyPath, p.cc.clientParams.ClientKeyPath) + case APPROLE: + require.NotNil(t, p.cc.clientParams.AppRoleAuthMountPoint) + require.NotNil(t, p.cc.clientParams.AppRoleID) + require.NotNil(t, p.cc.clientParams.AppRoleSecretID) + case K8S: + require.Equal(t, tt.expectK8sAuthMountPoint, p.cc.clientParams.K8sAuthMountPoint) + require.Equal(t, tt.expectK8sAuthRoleName, p.cc.clientParams.K8sAuthRoleName) + require.Equal(t, tt.expectK8sAuthTokenPath, p.cc.clientParams.K8sAuthTokenPath) + } + + require.Equal(t, tt.expectTransitEnginePath, p.cc.clientParams.TransitEnginePath) + require.Equal(t, tt.expectNamespace, p.cc.clientParams.Namespace) + }) + } +} + +func getTestConfigureRequest(t *testing.T, addr string, tpl string) string { + templ, err := template.New("plugin config").Parse(tpl) + require.NoError(t, err) + + cp := &struct{ Addr string }{Addr: addr} + + var c bytes.Buffer + err = templ.Execute(&c, cp) + require.NoError(t, err) + + return c.String() +} + +func setupFakeVaultServer() *FakeVaultServerConfig { + fakeVaultServer := NewFakeVaultServerConfig() + fakeVaultServer.ServerCertificatePemPath = testServerCert + fakeVaultServer.ServerKeyPemPath = testServerKey + fakeVaultServer.RenewResponseCode = 200 + fakeVaultServer.RenewResponse = []byte(testRenewResponse) + return fakeVaultServer +} diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go index 23c3ba6086..2e910d154a 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go @@ -21,6 +21,123 @@ const ( ) var ( + testTokenAuthConfigTpl = ` +vault_addr = "{{ .Addr }}" +ca_cert_path = "testdata/root-cert.pem" +token_auth { + token = "test-token" +}` + + testTokenAuthConfigWithEnvTpl = ` +vault_addr = "{{ .Addr }}" +ca_cert_path = "testdata/root-cert.pem" +token_auth {}` + + testCertAuthConfigTpl = ` +vault_addr = "{{ .Addr }}" +ca_cert_path = "testdata/root-cert.pem" +cert_auth { + cert_auth_mount_point = "test-cert-auth" + cert_auth_role_name = "test" + client_cert_path = "testdata/client-cert.pem" + client_key_path = "testdata/client-key.pem" +}` + + testCertAuthConfigWithEnvTpl = ` +vault_addr = "{{ .Addr }}" +ca_cert_path = "testdata/root-cert.pem" +cert_auth { + cert_auth_mount_point = "test-cert-auth" +}` + + testAppRoleAuthConfigTpl = ` +vault_addr = "{{ .Addr }}" +ca_cert_path = "testdata/root-cert.pem" +approle_auth { + approle_auth_mount_point = "test-approle-auth" + approle_id = "test-approle-id" + approle_secret_id = "test-approle-secret-id" +}` + + testAppRoleAuthConfigWithEnvTpl = ` +vault_addr = "{{ .Addr }}" +ca_cert_path = "testdata/root-cert.pem" +approle_auth { + approle_auth_mount_point = "test-approle-auth" +}` + + testK8sAuthConfigTpl = ` +vault_addr = "{{ .Addr }}" +ca_cert_path = "testdata/root-cert.pem" +k8s_auth { + k8s_auth_mount_point = "test-k8s-auth" + k8s_auth_role_name = "my-role" + token_path = "testdata/k8s/token" +}` + + testMultipleAuthConfigsTpl = ` +vault_addr = "{{ .Addr }}" +ca_cert_path = "testdata/root-cert.pem" +cert_auth {} +token_auth {} +approle_auth { + approle_auth_mount_point = "test-approle-auth" + approle_id = "test-approle-id" + approle_secret_id = "test-approle-secret-id" +}` + + testConfigWithVaultAddrEnvTpl = ` +ca_cert_path = "testdata/root-cert.pem" +token_auth { + token = "test-token" +}` + + testConfigWithTransitEnginePathTpl = ` +transit_engine_path = "test-path" +vault_addr = "{{ .Addr }}" +ca_cert_path = "testdata/root-cert.pem" +token_auth { + token = "test-token" +}` + + testConfigWithTransitEnginePathEnvTpl = ` +vault_addr = "{{ .Addr }}" +ca_cert_path = "testdata/root-cert.pem" +token_auth { + token = "test-token" +}` + + testNamespaceConfigTpl = ` +namespace = "test-ns" +vault_addr = "{{ .Addr }}" +ca_cert_path = "testdata/root-cert.pem" +token_auth { + token = "test-token" +}` + + testNamespaceEnvTpl = ` +vault_addr = "{{ .Addr }}" +ca_cert_path = "testdata/root-cert.pem" +token_auth { + token = "test-token" +}` + + testK8sAuthNoRoleNameTpl = ` +vault_addr = "{{ .Addr }}" +ca_cert_path = "testdata/root-cert.pem" +k8s_auth { + k8s_auth_mount_point = "test-k8s-auth" + token_path = "testdata/k8s/token" +}` + + testK8sAuthNoTokenPathTpl = ` +vault_addr = "{{ .Addr }}" +ca_cert_path = "testdata/root-cert.pem" +k8s_auth { + k8s_auth_mount_point = "test-k8s-auth" + k8s_auth_role_name = "my-role" +}` + testCertAuthResponse = `{ "auth": { "client_token": "cf95f87d-f95b-47ff-b1f5-ba7bff850425", From 2a074d9678179ba2e0aa59bb131c8703a4e1b333 Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:22 +0200 Subject: [PATCH 24/29] Test vault key manager generate key function (#5058) Signed-off-by: Matteo Kamm --- .../hashicorpvault/hashicorp_vault_test.go | 309 ++++++++++++++++++ .../hashicorpvault/vault_client_test.go | 2 +- .../hashicorpvault/vault_fake_test.go | 143 +++++++- 3 files changed, 452 insertions(+), 2 deletions(-) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault_test.go b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault_test.go index a4ed7a6ad5..0ed3fd03cd 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault_test.go @@ -2,7 +2,10 @@ package hashicorpvault import ( "bytes" + "context" "fmt" + "github.com/hashicorp/vault/sdk/helper/consts" + "github.com/spiffe/spire/pkg/server/plugin/keymanager" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "testing" @@ -251,6 +254,283 @@ func TestConfigure(t *testing.T) { } } +func TestGenerateKey(t *testing.T) { + successfulConfig := &Config{ + TransitEnginePath: "test-transit", + CACertPath: "testdata/root-cert.pem", + TokenAuth: &TokenAuthConfig{ + Token: "test-token", + }, + } + + for _, tt := range []struct { + name string + csr []byte + config *Config + authMethod AuthMethod + expectCode codes.Code + expectMsgPrefix string + id string + keyType keymanager.KeyType + + fakeServer func() *FakeVaultServerConfig + }{ + { + name: "Generate EC P-256 key with token auth", + id: "x509-CA-A", + keyType: keymanager.ECP256, + config: successfulConfig, + authMethod: TOKEN, + fakeServer: func() *FakeVaultServerConfig { + fakeServer := setupSuccessFakeVaultServer() + fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) + fakeServer.CertAuthResponse = []byte{} + fakeServer.AppRoleAuthResponse = []byte{} + + return fakeServer + }, + }, + { + name: "Generate P-384 key with token auth", + id: "x509-CA-A", + keyType: keymanager.ECP384, + config: successfulConfig, + authMethod: TOKEN, + fakeServer: func() *FakeVaultServerConfig { + fakeServer := setupSuccessFakeVaultServer() + fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) + fakeServer.CertAuthResponse = []byte{} + fakeServer.AppRoleAuthResponse = []byte{} + fakeServer.GetKeyResponse = []byte(testGetKeyResponseP384) + + return fakeServer + }, + }, + { + name: "Generate RSA 2048 key with token auth", + id: "x509-CA-A", + keyType: keymanager.RSA2048, + config: successfulConfig, + authMethod: TOKEN, + fakeServer: func() *FakeVaultServerConfig { + fakeServer := setupSuccessFakeVaultServer() + fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) + fakeServer.CertAuthResponse = []byte{} + fakeServer.AppRoleAuthResponse = []byte{} + fakeServer.GetKeyResponse = []byte(testGetKeyResponseRSA2048) + + return fakeServer + }, + }, + { + name: "Generate RSA 4096 key with token auth", + id: "x509-CA-A", + keyType: keymanager.RSA4096, + config: successfulConfig, + authMethod: TOKEN, + fakeServer: func() *FakeVaultServerConfig { + fakeServer := setupSuccessFakeVaultServer() + fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) + fakeServer.CertAuthResponse = []byte{} + fakeServer.AppRoleAuthResponse = []byte{} + fakeServer.GetKeyResponse = []byte(testGetKeyResponseRSA4096) + + return fakeServer + }, + }, + { + name: "Generate key with missing id", + keyType: keymanager.RSA2048, + config: successfulConfig, + authMethod: TOKEN, + fakeServer: func() *FakeVaultServerConfig { + fakeServer := setupSuccessFakeVaultServer() + fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) + fakeServer.CertAuthResponse = []byte{} + fakeServer.AppRoleAuthResponse = []byte{} + fakeServer.GetKeyResponse = []byte(testGetKeyResponseRSA2048) + + return fakeServer + }, + expectCode: codes.InvalidArgument, + expectMsgPrefix: "keymanager(hashicorp_vault): key id is required", + }, + { + name: "Generate key with missing key type", + id: "x509-CA-A", + config: successfulConfig, + authMethod: TOKEN, + fakeServer: func() *FakeVaultServerConfig { + fakeServer := setupSuccessFakeVaultServer() + fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) + fakeServer.CertAuthResponse = []byte{} + fakeServer.AppRoleAuthResponse = []byte{} + fakeServer.GetKeyResponse = []byte(testGetKeyResponseRSA2048) + + return fakeServer + }, + expectCode: codes.InvalidArgument, + expectMsgPrefix: "keymanager(hashicorp_vault): key type is required", + }, + { + name: "Generate key with unsupported key type", + id: "x509-CA-A", + keyType: 100, + config: successfulConfig, + authMethod: TOKEN, + fakeServer: func() *FakeVaultServerConfig { + fakeServer := setupSuccessFakeVaultServer() + fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) + fakeServer.CertAuthResponse = []byte{} + fakeServer.AppRoleAuthResponse = []byte{} + fakeServer.GetKeyResponse = []byte(testGetKeyResponseRSA2048) + + return fakeServer + }, + expectCode: codes.Internal, + expectMsgPrefix: "keymanager(hashicorp_vault): facade does not support key type \"UNKNOWN(100)\"", + }, + { + name: "Malformed get key response", + id: "x509-CA-A", + keyType: keymanager.RSA2048, + config: successfulConfig, + authMethod: TOKEN, + fakeServer: func() *FakeVaultServerConfig { + fakeServer := setupSuccessFakeVaultServer() + fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) + fakeServer.CertAuthResponse = []byte{} + fakeServer.AppRoleAuthResponse = []byte{} + fakeServer.GetKeyResponse = []byte("error") + + return fakeServer + }, + expectCode: codes.Internal, + expectMsgPrefix: "keymanager(hashicorp_vault): failed to get transit engine key: invalid character", + }, + { + name: "Malformed create key response", + id: "x509-CA-A", + keyType: keymanager.RSA2048, + config: successfulConfig, + authMethod: TOKEN, + fakeServer: func() *FakeVaultServerConfig { + fakeServer := setupSuccessFakeVaultServer() + fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) + fakeServer.CertAuthResponse = []byte{} + fakeServer.AppRoleAuthResponse = []byte{} + fakeServer.CreateKeyResponse = []byte("error") + + return fakeServer + }, + expectCode: codes.Internal, + expectMsgPrefix: "keymanager(hashicorp_vault): failed to create transit engine key: invalid character", + }, + { + name: "Bad get key response code", + id: "x509-CA-A", + keyType: keymanager.RSA2048, + config: successfulConfig, + authMethod: TOKEN, + fakeServer: func() *FakeVaultServerConfig { + fakeServer := setupSuccessFakeVaultServer() + fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) + fakeServer.CertAuthResponse = []byte{} + fakeServer.AppRoleAuthResponse = []byte{} + fakeServer.GetKeyResponseCode = 500 + + return fakeServer + }, + expectCode: codes.Internal, + expectMsgPrefix: "keymanager(hashicorp_vault): failed to get transit engine key: Error making API request.", + }, + { + name: "Bad create key response code", + id: "x509-CA-A", + keyType: keymanager.RSA2048, + config: successfulConfig, + authMethod: TOKEN, + fakeServer: func() *FakeVaultServerConfig { + fakeServer := setupSuccessFakeVaultServer() + fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) + fakeServer.CertAuthResponse = []byte{} + fakeServer.AppRoleAuthResponse = []byte{} + fakeServer.CreateKeyResponseCode = 500 + + return fakeServer + }, + expectCode: codes.Internal, + expectMsgPrefix: "keymanager(hashicorp_vault): failed to create transit engine key: Error making API request.", + }, + { + name: "Malformed key", + id: "x509-CA-A", + keyType: keymanager.RSA2048, + config: successfulConfig, + authMethod: TOKEN, + fakeServer: func() *FakeVaultServerConfig { + fakeServer := setupSuccessFakeVaultServer() + fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) + fakeServer.CertAuthResponse = []byte{} + fakeServer.AppRoleAuthResponse = []byte{} + fakeServer.GetKeyResponse = []byte(testGetKeyResponseMalformed) + + return fakeServer + }, + expectCode: codes.Internal, + expectMsgPrefix: "keymanager(hashicorp_vault): unable to decode PEM key", + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + fakeVaultServer := tt.fakeServer() + + s, addr, err := fakeVaultServer.NewTLSServer() + require.NoError(t, err) + + s.Start() + defer s.Close() + + p := New() + options := []plugintest.Option{ + plugintest.CaptureConfigureError(&err), + plugintest.CoreConfig(catalog.CoreConfig{TrustDomain: spiffeid.RequireTrustDomainFromString("example.org")}), + } + if tt.config != nil { + tt.config.VaultAddr = fmt.Sprintf("https://%s", addr) + cp, err := p.genClientParams(tt.authMethod, tt.config) + require.NoError(t, err) + cc, err := NewClientConfig(cp, p.logger) + require.NoError(t, err) + p.cc = cc + options = append(options, plugintest.ConfigureJSON(tt.config)) + } + p.authMethod = tt.authMethod + + v1 := new(keymanager.V1) + plugintest.Load(t, builtin(p), v1, + options..., + ) + + key, err := v1.GenerateKey(context.Background(), tt.id, tt.keyType) + + spiretest.RequireGRPCStatusHasPrefix(t, err, tt.expectCode, tt.expectMsgPrefix) + if tt.expectCode != codes.OK { + require.Nil(t, key) + return + } + + require.NotNil(t, key) + require.Equal(t, tt.id, key.ID()) + + if p.cc.clientParams.Namespace != "" { + headers := p.vc.vaultClient.Headers() + require.Equal(t, p.cc.clientParams.Namespace, headers.Get(consts.NamespaceHeaderName)) + } + }) + } +} + func getTestConfigureRequest(t *testing.T, addr string, tpl string) string { templ, err := template.New("plugin config").Parse(tpl) require.NoError(t, err) @@ -264,6 +544,35 @@ func getTestConfigureRequest(t *testing.T, addr string, tpl string) string { return c.String() } +func setupSuccessFakeVaultServer() *FakeVaultServerConfig { + fakeVaultServer := setupFakeVaultServer() + + fakeVaultServer.CertAuthResponseCode = 200 + fakeVaultServer.CertAuthResponse = []byte(testCertAuthResponse) + fakeVaultServer.CertAuthReqEndpoint = "/v1/auth/test-cert-auth/login" + + fakeVaultServer.AppRoleAuthResponseCode = 200 + fakeVaultServer.AppRoleAuthResponse = []byte(testAppRoleAuthResponse) + fakeVaultServer.AppRoleAuthReqEndpoint = "/v1/auth/test-approle-auth/login" + + fakeVaultServer.K8sAuthResponseCode = 200 + fakeVaultServer.K8sAuthReqEndpoint = "/v1/auth/test-k8s-auth/login" + fakeVaultServer.K8sAuthResponse = []byte(testK8sAuthResponse) + + fakeVaultServer.LookupSelfResponse = []byte(testLookupSelfResponse) + fakeVaultServer.LookupSelfReqEndpoint = "GET /v1/auth/token/lookup-self" + fakeVaultServer.LookupSelfResponseCode = 200 + + fakeVaultServer.CreateKeyResponseCode = 200 + fakeVaultServer.CreateKeyReqEndpoint = "PUT /v1/test-transit/keys/{id}" + + fakeVaultServer.GetKeyResponseCode = 200 + fakeVaultServer.GetKeyReqEndpoint = "GET /v1/test-transit/keys/{id}" + fakeVaultServer.GetKeyResponse = []byte(testGetKeyResponseP256) + + return fakeVaultServer +} + func setupFakeVaultServer() *FakeVaultServerConfig { fakeVaultServer := NewFakeVaultServerConfig() fakeVaultServer.ServerCertificatePemPath = testServerCert diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go index baaf90a4eb..5c0f3551a7 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go @@ -677,7 +677,7 @@ func TestGetKey(t *testing.T) { fakeVaultServer.CertAuthResponseCode = 200 fakeVaultServer.CertAuthResponse = []byte(testCertAuthResponse) fakeVaultServer.GetKeyResponseCode = 200 - fakeVaultServer.GetKeyResponse = []byte(testGetKeyResponse) + fakeVaultServer.GetKeyResponse = []byte(testGetKeyResponseP256) s, addr, err := fakeVaultServer.NewTLSServer() require.NoError(t, err) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go index 2e910d154a..4e129e95d2 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go @@ -387,7 +387,7 @@ k8s_auth { } }` - testGetKeyResponse = `{ + testGetKeyResponseP256 = `{ "request_id": "646eddbd-83fd-0cc1-387b-f1a17fa88c3d", "lease_id": "", "renewable": false, @@ -421,6 +421,147 @@ k8s_auth { "warnings": null, "auth": null }` + + testGetKeyResponseP384 = `{ + "request_id": "a97c3069-1369-dcbb-c687-a431f8d7f324", + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": { + "allow_plaintext_backup": false, + "auto_rotate_period": 0, + "deletion_allowed": false, + "derived": false, + "exportable": false, + "imported_key": false, + "keys": { + "1": { + "creation_time": "2024-09-17T18:27:19.664989473Z", + "name": "P-384", + "public_key": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEXpDQLh6ct/CJuMV2UIXnm/GilDNgy6Qy\ngzGhGsRaGrlYtM8g3sSHoGBIR+wT2hIF0ryY4mqYPtzw39WiHSdK3J985iX/bMXD\npr5xe142+1uHbJdKfSD5LrycBBtIsoEH\n-----END PUBLIC KEY-----\n" + } + }, + "latest_version": 1, + "min_available_version": 0, + "min_decryption_version": 1, + "min_encryption_version": 0, + "name": "x509-CA-A", + "supports_decryption": false, + "supports_derivation": false, + "supports_encryption": false, + "supports_signing": true, + "type": "ecdsa-p384" + }, + "wrap_info": null, + "warnings": null, + "auth": null +}` + + testGetKeyResponseRSA2048 = `{ + "request_id": "7a74d33f-2e4b-8f34-48ba-80ff1c0a447c", + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": { + "allow_plaintext_backup": false, + "auto_rotate_period": 0, + "deletion_allowed": false, + "derived": false, + "exportable": false, + "imported_key": false, + "keys": { + "1": { + "creation_time": "2024-09-17T18:30:26.427076525Z", + "name": "rsa-2048", + "public_key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnV4uS61DWBvfbpzuHzIQ\nRbPZfLbe5wolynACSBNB4DxskuAZOg27e9wKUVwg82gOFPM4t1mVMHYee2OqEspZ\n5zL6y5bfwK//F+H8B6egitPKcHIv6WtErCrl3NM7V8jv4JIxmSeLRFNLpsGPp2dc\nZ/Q/SwprFhMfBiskCmOf+FlOrLZXe7a6Wsfe2yTJIwC5zGn+jNPVBmscHqjzttME\n4/xoZxCg13uZa1rskIOW526RT7ccfIMo8qGoZ0KVjnAJGuTwhFvJ+D/jwhHDylsP\n1ngHgJlBnDo23GouQD13TRaRUamTb4sliRAFdrWwK3j9YaOgtJnBYikkG1T/SSsm\nMQIDAQAB\n-----END PUBLIC KEY-----\n" + } + }, + "latest_version": 1, + "min_available_version": 0, + "min_decryption_version": 1, + "min_encryption_version": 0, + "name": "x509-CA-A", + "supports_decryption": true, + "supports_derivation": false, + "supports_encryption": true, + "supports_signing": true, + "type": "rsa-2048" + }, + "wrap_info": null, + "warnings": null, + "auth": null +}` + + testGetKeyResponseRSA4096 = `{ + "request_id": "486c49b6-149a-7886-52c6-5d082d4329fc", + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": { + "allow_plaintext_backup": false, + "auto_rotate_period": 0, + "deletion_allowed": false, + "derived": false, + "exportable": false, + "imported_key": false, + "keys": { + "1": { + "creation_time": "2024-09-17T18:34:21.286589438Z", + "name": "rsa-4096", + "public_key": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsmp4dJSfPGDGhmWoBD7G\nYPBQ/KGCR8/huy7/bjNRprKKpnhDl+4y5OaQVUqvFnoJZYfQvowcaGrARwBrsvPw\nkwPe6dB33XZBCWWDIvMORAQhgGeQF0MRjKibxDxlwPLZLARnHF8674gDdbL7Tg/G\nxQqThWNqVk6/GiHnAjkBntyw3V5XI5RtmpdSLDcZOUdqh/Bwi6fGOwtW1kU2NVSG\nalhdQu1O2Pr72sVZ/9+LwMYv1ZI0lFULwr7ZaIo86+vei4BIk+Pd/kkOjn9KKJD1\n84eL1QnN03XPc9ENCt7rF/R+IT7YkoqCDBZawW6VpexrA6QxtxUO0DcAffIFJ61Z\n9N7p3VULjZZIJmpOaMTEu3wFritcTBZweI3gikisg3YMqRDzC97+WqKUGpWUfGcF\ngENRvqIlE05snmmwziGB4Rey3yAqZBHSXRWFWKdDX/X7gMEJ4Av7hAumMxgR34If\ndzEShW6ushnOEtlXQR0/DE814GBWI0+oa+w9m20XkzL60bUIZevP9mOhbSNxuN8m\naCDOjIa7qeX3yg1l4+dnAZ/S8O+K3GEWkqWwq/FXH1EfCGeztp2b0pN8n0r0Tr3S\nHkHMNNEXovlQevgEFEc01Kg8PXBDd1hP31dfMfZ6v+BXygGHg95zR4AFpcRIYJWu\n9dmMkmMWQN5rZeyDO7ZfDQ0CAwEAAQ==\n-----END PUBLIC KEY-----\n" + } + }, + "latest_version": 1, + "min_available_version": 0, + "min_decryption_version": 1, + "min_encryption_version": 0, + "name": "x509-CA-A", + "supports_decryption": true, + "supports_derivation": false, + "supports_encryption": true, + "supports_signing": true, + "type": "rsa-4096" + }, + "wrap_info": null, + "warnings": null, + "auth": null +}` + + testGetKeyResponseMalformed = `{ + "request_id": "486c49b6-149a-7886-52c6-5d082d4329fc", + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": { + "allow_plaintext_backup": false, + "auto_rotate_period": 0, + "deletion_allowed": false, + "derived": false, + "exportable": false, + "imported_key": false, + "keys": { + "1": { + "creation_time": "2024-09-17T18:34:21.286589438Z", + "name": "rsa-4096", + "public_key": "-----BEGIN MALFORMED KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsmp4dJSfPGDGhmWoBD7G\nYPBQ/KGCR8/huy7/bjNRprKKpnhDl+4y5OaQVUqvFnoJZYfQvowcaGrARwBrsvPw\nkwPe6dB33XZBCWWDIvMORAQhgGeQF0MRjKibxDxlwPLZLARnHF8674gDdbL7Tg/G\nxQqThWNqVk6/GiHnAjkBntyw3V5XI5RtmpdSLDcZOUdqh/Bwi6fGOwtW1kU2NVSG\nalhdQu1O2Pr72sVZ/9+LwMYv1ZI0lFULwr7ZaIo86+vei4BIk+Pd/kkOjn9KKJD1\n84eL1QnN03XPc9ENCt7rF/R+IT7YkoqCDBZawW6VpexrA6QxtxUO0DcAffIFJ61Z\n9N7p3VULjZZIJmpOaMTEu3wFritcTBZweI3gikisg3YMqRDzC97+WqKUGpWUfGcF\ngENRvqIlE05snmmwziGB4Rey3yAqZBHSXRWFWKdDX/X7gMEJ4Av7hAumMxgR34If\ndzEShW6ushnOEtlXQR0/DE814GBWI0+oa+w9m20XkzL60bUIZevP9mOhbSNxuN8m\naCDOjIa7qeX3yg1l4+dnAZ/S8O+K3GEWkqWwq/FXH1EfCGeztp2b0pN8n0r0Tr3S\nHkHMNNEXovlQevgEFEc01Kg8PXBDd1hP31dfMfZ6v+BXygGHg95zR4AFpcRIYJWu\n9dmMkmMWQN5rZeyDO7ZfDQ0CAwEAAQ==\n-----END MALFORMED KEY-----\n" + } + }, + "latest_version": 1, + "min_available_version": 0, + "min_decryption_version": 1, + "min_encryption_version": 0, + "name": "x509-CA-A", + "supports_decryption": true, + "supports_derivation": false, + "supports_encryption": true, + "supports_signing": true, + "type": "rsa-4096" + }, + "wrap_info": null, + "warnings": null, + "auth": null +}` + testSignDataResponse = `{ "request_id": "51bb98fa-8da3-8678-64e7-7220bc8b94a6", "lease_id": "", From d739d8e5e98b7cef48cf8c90c617d625cac57832 Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:22 +0200 Subject: [PATCH 25/29] Load keys from Vault on configure (#5058) Signed-off-by: Matteo Kamm --- .../hashicorpvault/hashicorp_vault.go | 50 +-- .../hashicorpvault/hashicorp_vault_test.go | 297 ++++++++++++++---- .../keymanager/hashicorpvault/vault_client.go | 176 ++++++++--- .../hashicorpvault/vault_client_test.go | 10 +- .../hashicorpvault/vault_fake_test.go | 36 +++ 5 files changed, 443 insertions(+), 126 deletions(-) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go index 61c22b4cda..2c91eddb1d 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go @@ -4,7 +4,6 @@ import ( "context" "crypto/sha256" "encoding/hex" - "encoding/pem" "errors" "fmt" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" @@ -154,7 +153,7 @@ func (p *Plugin) SetLogger(log hclog.Logger) { p.logger = log } -func (p *Plugin) Configure(_ context.Context, req *configv1.ConfigureRequest) (*configv1.ConfigureResponse, error) { +func (p *Plugin) Configure(ctx context.Context, req *configv1.ConfigureRequest) (*configv1.ConfigureResponse, error) { config := new(Config) if err := hcl.Decode(&config, req.HclConfiguration); err != nil { @@ -180,6 +179,21 @@ func (p *Plugin) Configure(_ context.Context, req *configv1.ConfigureRequest) (* p.authMethod = am p.cc = vcConfig + if p.vc == nil { + err := p.genVaultClient() + if err != nil { + return nil, err + } + } + + p.logger.Debug("Fetching keys from Vault") + keyEntries, err := p.vc.GetKeys(ctx) + if err != nil { + return nil, err + } + + p.setCache(keyEntries) + return &configv1.ConfigureResponse{}, nil } @@ -349,7 +363,7 @@ func (p *Plugin) GetPublicKey(_ context.Context, req *keymanagerv1.GetPublicKeyR } func (p *Plugin) GetPublicKeys(context.Context, *keymanagerv1.GetPublicKeysRequest) (*keymanagerv1.GetPublicKeysResponse, error) { - var keys = make([]*keymanagerv1.PublicKey, len(p.entries), 0) + var keys = make([]*keymanagerv1.PublicKey, 0, len(p.entries)) p.mu.RLock() defer p.mu.RUnlock() @@ -424,24 +438,7 @@ func (p *Plugin) createKey(ctx context.Context, spireKeyID string, keyType keyma return nil, err } - pk, err := p.vc.GetKey(ctx, spireKeyID) - if err != nil { - return nil, err - } - - pemBlock, _ := pem.Decode([]byte(pk)) - 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 + return p.vc.getKeyEntry(ctx, spireKeyID) } func convertToTransitKeyType(keyType keymanagerv1.KeyType) (*TransitKeyType, error) { @@ -484,3 +481,14 @@ func makeFingerprint(pkixData []byte) string { s := sha256.Sum256(pkixData) return hex.EncodeToString(s[:]) } + +func (p *Plugin) setCache(keyEntries []*keyEntry) { + // clean previous cache + p.entries = make(map[string]keyEntry) + + // add results to cache + for _, e := range keyEntries { + p.entries[e.PublicKey.Id] = *e + p.logger.Debug("Key loaded", "key_id", e.PublicKey.Id, "key_type", e.PublicKey.Type) + } +} diff --git a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault_test.go b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault_test.go index 0ed3fd03cd..5260ac47c5 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault_test.go @@ -17,21 +17,7 @@ import ( "github.com/spiffe/spire/test/spiretest" ) -func TestConfigure(t *testing.T) { - fakeVaultServer := setupFakeVaultServer() - fakeVaultServer.CertAuthResponseCode = 200 - fakeVaultServer.CertAuthResponse = []byte(testCertAuthResponse) - fakeVaultServer.CertAuthReqEndpoint = "/v1/auth/test-cert-auth/login" - fakeVaultServer.AppRoleAuthResponseCode = 200 - fakeVaultServer.AppRoleAuthResponse = []byte(testAppRoleAuthResponse) - fakeVaultServer.AppRoleAuthReqEndpoint = "/v1/auth/test-approle-auth/login" - - s, addr, err := fakeVaultServer.NewTLSServer() - require.NoError(t, err) - - s.Start() - defer s.Close() - +func TestPluginConfigure(t *testing.T) { for _, tt := range []struct { name string configTmpl string @@ -124,19 +110,10 @@ func TestConfigure(t *testing.T) { expectTransitEnginePath: "transit", }, { - name: "Multiple authentication methods configured", - configTmpl: testMultipleAuthConfigsTpl, - expectCode: codes.InvalidArgument, - expectMsgPrefix: "only one authentication method can be configured", - }, - { - name: "Pass VaultAddr via the environment variable", - configTmpl: testConfigWithVaultAddrEnvTpl, - envKeyVal: map[string]string{ - envVaultAddr: fmt.Sprintf("https://%v/", addr), - }, - wantAuth: TOKEN, - expectToken: "test-token", + name: "Multiple authentication methods configured", + configTmpl: testMultipleAuthConfigsTpl, + expectCode: codes.InvalidArgument, + expectMsgPrefix: "only one authentication method can be configured", expectTransitEnginePath: "transit", }, { @@ -176,29 +153,37 @@ func TestConfigure(t *testing.T) { expectToken: "test-token", }, { - name: "Malformed configuration", - plainConfig: "invalid-config", - expectCode: codes.InvalidArgument, - expectMsgPrefix: "unable to decode configuration:", + name: "Malformed configuration", + plainConfig: "invalid-config", + expectCode: codes.InvalidArgument, + expectMsgPrefix: "unable to decode configuration:", + expectTransitEnginePath: "transit", }, { - name: "Required parameters are not given / k8s_auth_role_name", - configTmpl: testK8sAuthNoRoleNameTpl, - wantAuth: K8S, - expectCode: codes.InvalidArgument, - expectMsgPrefix: "k8s_auth_role_name is required", + name: "Required parameters are not given / k8s_auth_role_name", + configTmpl: testK8sAuthNoRoleNameTpl, + wantAuth: K8S, + expectCode: codes.InvalidArgument, + expectMsgPrefix: "k8s_auth_role_name is required", + expectTransitEnginePath: "transit", }, { - name: "Required parameters are not given / token_path", - configTmpl: testK8sAuthNoTokenPathTpl, - wantAuth: K8S, - expectCode: codes.InvalidArgument, - expectMsgPrefix: "token_path is required", + name: "Required parameters are not given / token_path", + configTmpl: testK8sAuthNoTokenPathTpl, + wantAuth: K8S, + expectCode: codes.InvalidArgument, + expectMsgPrefix: "token_path is required", + expectTransitEnginePath: "transit", }, } { tt := tt t.Run(tt.name, func(t *testing.T) { - var err error + fakeVaultServer := setupSuccessFakeVaultServer(tt.expectTransitEnginePath) + s, addr, err := fakeVaultServer.NewTLSServer() + require.NoError(t, err) + + s.Start() + defer s.Close() p := New() p.hooks.lookupEnv = func(s string) (string, bool) { @@ -254,7 +239,7 @@ func TestConfigure(t *testing.T) { } } -func TestGenerateKey(t *testing.T) { +func TestPluginGenerateKey(t *testing.T) { successfulConfig := &Config{ TransitEnginePath: "test-transit", CACertPath: "testdata/root-cert.pem", @@ -265,7 +250,6 @@ func TestGenerateKey(t *testing.T) { for _, tt := range []struct { name string - csr []byte config *Config authMethod AuthMethod expectCode codes.Code @@ -282,7 +266,7 @@ func TestGenerateKey(t *testing.T) { config: successfulConfig, authMethod: TOKEN, fakeServer: func() *FakeVaultServerConfig { - fakeServer := setupSuccessFakeVaultServer() + fakeServer := setupSuccessFakeVaultServer("test-transit") fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) fakeServer.CertAuthResponse = []byte{} fakeServer.AppRoleAuthResponse = []byte{} @@ -297,7 +281,7 @@ func TestGenerateKey(t *testing.T) { config: successfulConfig, authMethod: TOKEN, fakeServer: func() *FakeVaultServerConfig { - fakeServer := setupSuccessFakeVaultServer() + fakeServer := setupSuccessFakeVaultServer("test-transit") fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) fakeServer.CertAuthResponse = []byte{} fakeServer.AppRoleAuthResponse = []byte{} @@ -313,7 +297,7 @@ func TestGenerateKey(t *testing.T) { config: successfulConfig, authMethod: TOKEN, fakeServer: func() *FakeVaultServerConfig { - fakeServer := setupSuccessFakeVaultServer() + fakeServer := setupSuccessFakeVaultServer("test-transit") fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) fakeServer.CertAuthResponse = []byte{} fakeServer.AppRoleAuthResponse = []byte{} @@ -329,7 +313,7 @@ func TestGenerateKey(t *testing.T) { config: successfulConfig, authMethod: TOKEN, fakeServer: func() *FakeVaultServerConfig { - fakeServer := setupSuccessFakeVaultServer() + fakeServer := setupSuccessFakeVaultServer("test-transit") fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) fakeServer.CertAuthResponse = []byte{} fakeServer.AppRoleAuthResponse = []byte{} @@ -344,7 +328,7 @@ func TestGenerateKey(t *testing.T) { config: successfulConfig, authMethod: TOKEN, fakeServer: func() *FakeVaultServerConfig { - fakeServer := setupSuccessFakeVaultServer() + fakeServer := setupSuccessFakeVaultServer("test-transit") fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) fakeServer.CertAuthResponse = []byte{} fakeServer.AppRoleAuthResponse = []byte{} @@ -361,7 +345,7 @@ func TestGenerateKey(t *testing.T) { config: successfulConfig, authMethod: TOKEN, fakeServer: func() *FakeVaultServerConfig { - fakeServer := setupSuccessFakeVaultServer() + fakeServer := setupSuccessFakeVaultServer("test-transit") fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) fakeServer.CertAuthResponse = []byte{} fakeServer.AppRoleAuthResponse = []byte{} @@ -379,7 +363,7 @@ func TestGenerateKey(t *testing.T) { config: successfulConfig, authMethod: TOKEN, fakeServer: func() *FakeVaultServerConfig { - fakeServer := setupSuccessFakeVaultServer() + fakeServer := setupSuccessFakeVaultServer("test-transit") fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) fakeServer.CertAuthResponse = []byte{} fakeServer.AppRoleAuthResponse = []byte{} @@ -397,7 +381,7 @@ func TestGenerateKey(t *testing.T) { config: successfulConfig, authMethod: TOKEN, fakeServer: func() *FakeVaultServerConfig { - fakeServer := setupSuccessFakeVaultServer() + fakeServer := setupSuccessFakeVaultServer("test-transit") fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) fakeServer.CertAuthResponse = []byte{} fakeServer.AppRoleAuthResponse = []byte{} @@ -415,7 +399,7 @@ func TestGenerateKey(t *testing.T) { config: successfulConfig, authMethod: TOKEN, fakeServer: func() *FakeVaultServerConfig { - fakeServer := setupSuccessFakeVaultServer() + fakeServer := setupSuccessFakeVaultServer("test-transit") fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) fakeServer.CertAuthResponse = []byte{} fakeServer.AppRoleAuthResponse = []byte{} @@ -433,7 +417,7 @@ func TestGenerateKey(t *testing.T) { config: successfulConfig, authMethod: TOKEN, fakeServer: func() *FakeVaultServerConfig { - fakeServer := setupSuccessFakeVaultServer() + fakeServer := setupSuccessFakeVaultServer("test-transit") fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) fakeServer.CertAuthResponse = []byte{} fakeServer.AppRoleAuthResponse = []byte{} @@ -451,7 +435,7 @@ func TestGenerateKey(t *testing.T) { config: successfulConfig, authMethod: TOKEN, fakeServer: func() *FakeVaultServerConfig { - fakeServer := setupSuccessFakeVaultServer() + fakeServer := setupSuccessFakeVaultServer("test-transit") fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) fakeServer.CertAuthResponse = []byte{} fakeServer.AppRoleAuthResponse = []byte{} @@ -469,7 +453,7 @@ func TestGenerateKey(t *testing.T) { config: successfulConfig, authMethod: TOKEN, fakeServer: func() *FakeVaultServerConfig { - fakeServer := setupSuccessFakeVaultServer() + fakeServer := setupSuccessFakeVaultServer("test-transit") fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) fakeServer.CertAuthResponse = []byte{} fakeServer.AppRoleAuthResponse = []byte{} @@ -531,6 +515,195 @@ func TestGenerateKey(t *testing.T) { } } +func TestPluginGetKey(t *testing.T) { + for _, tt := range []struct { + name string + config *Config + configTmpl string + authMethod AuthMethod + expectCode codes.Code + expectMsgPrefix string + id string + + fakeServer func() *FakeVaultServerConfig + }{ + { + name: "Get EC P-256 key with token auth", + configTmpl: testTokenAuthConfigTpl, + id: "x509-CA-A", + authMethod: TOKEN, + fakeServer: func() *FakeVaultServerConfig { + fakeServer := setupSuccessFakeVaultServer("transit") + fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) + fakeServer.CertAuthResponse = []byte{} + fakeServer.AppRoleAuthResponse = []byte{} + + return fakeServer + }, + }, + { + name: "Get P-384 key with token auth", + configTmpl: testTokenAuthConfigTpl, + id: "x509-CA-A", + authMethod: TOKEN, + fakeServer: func() *FakeVaultServerConfig { + fakeServer := setupSuccessFakeVaultServer("transit") + fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) + fakeServer.CertAuthResponse = []byte{} + fakeServer.AppRoleAuthResponse = []byte{} + fakeServer.GetKeyResponse = []byte(testGetKeyResponseP384) + + return fakeServer + }, + }, + { + name: "Get RSA 2048 key with token auth", + configTmpl: testTokenAuthConfigTpl, + id: "x509-CA-A", + authMethod: TOKEN, + fakeServer: func() *FakeVaultServerConfig { + fakeServer := setupSuccessFakeVaultServer("transit") + fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) + fakeServer.CertAuthResponse = []byte{} + fakeServer.AppRoleAuthResponse = []byte{} + fakeServer.GetKeyResponse = []byte(testGetKeyResponseRSA2048) + + return fakeServer + }, + }, + { + name: "Get RSA 4096 key with token auth", + configTmpl: testTokenAuthConfigTpl, + id: "x509-CA-A", + authMethod: TOKEN, + fakeServer: func() *FakeVaultServerConfig { + fakeServer := setupSuccessFakeVaultServer("transit") + fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) + fakeServer.CertAuthResponse = []byte{} + fakeServer.AppRoleAuthResponse = []byte{} + fakeServer.GetKeyResponse = []byte(testGetKeyResponseRSA4096) + + return fakeServer + }, + }, + { + name: "Get key with missing id", + configTmpl: testTokenAuthConfigTpl, + authMethod: TOKEN, + fakeServer: func() *FakeVaultServerConfig { + fakeServer := setupSuccessFakeVaultServer("transit") + fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) + fakeServer.CertAuthResponse = []byte{} + fakeServer.AppRoleAuthResponse = []byte{} + fakeServer.GetKeyResponse = []byte(testGetKeyResponseRSA2048) + + return fakeServer + }, + expectCode: codes.InvalidArgument, + expectMsgPrefix: "keymanager(hashicorp_vault): key id is required", + }, + { + name: "Malformed get key response", + configTmpl: testTokenAuthConfigTpl, + id: "x509-CA-A", + authMethod: TOKEN, + fakeServer: func() *FakeVaultServerConfig { + fakeServer := setupSuccessFakeVaultServer("transit") + fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) + fakeServer.CertAuthResponse = []byte{} + fakeServer.AppRoleAuthResponse = []byte{} + fakeServer.GetKeyResponse = []byte("error") + + return fakeServer + }, + expectCode: codes.Internal, + expectMsgPrefix: "failed to get transit engine key:", + }, + { + name: "Bad get key response code", + configTmpl: testTokenAuthConfigTpl, + id: "x509-CA-A", + authMethod: TOKEN, + fakeServer: func() *FakeVaultServerConfig { + fakeServer := setupSuccessFakeVaultServer("transit") + fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) + fakeServer.CertAuthResponse = []byte{} + fakeServer.AppRoleAuthResponse = []byte{} + fakeServer.GetKeyResponseCode = 500 + + return fakeServer + }, + expectCode: codes.Internal, + expectMsgPrefix: "failed to get transit engine key:", + }, + { + name: "Malformed key", + configTmpl: testTokenAuthConfigTpl, + id: "x509-CA-A", + authMethod: TOKEN, + fakeServer: func() *FakeVaultServerConfig { + fakeServer := setupSuccessFakeVaultServer("transit") + fakeServer.LookupSelfResponse = []byte(testLookupSelfResponse) + fakeServer.CertAuthResponse = []byte{} + fakeServer.AppRoleAuthResponse = []byte{} + fakeServer.GetKeyResponse = []byte(testGetKeyResponseMalformed) + + return fakeServer + }, + expectCode: codes.Internal, + expectMsgPrefix: "unable to decode PEM key", + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + fakeVaultServer := tt.fakeServer() + + s, addr, err := fakeVaultServer.NewTLSServer() + require.NoError(t, err) + + s.Start() + defer s.Close() + + p := New() + options := []plugintest.Option{ + plugintest.CaptureConfigureError(&err), + plugintest.Configure(getTestConfigureRequest(t, fmt.Sprintf("https://%v/", addr), tt.configTmpl)), + plugintest.CoreConfig(catalog.CoreConfig{ + TrustDomain: spiffeid.RequireTrustDomainFromString("example.org"), + }), + } + + v1 := new(keymanager.V1) + plugintest.Load(t, builtin(p), v1, + options..., + ) + + if err != nil { + spiretest.RequireGRPCStatusHasPrefix(t, err, tt.expectCode, tt.expectMsgPrefix) + return + } + + key, err := v1.GetKey(context.Background(), tt.id) + + spiretest.RequireGRPCStatusHasPrefix(t, err, tt.expectCode, tt.expectMsgPrefix) + if tt.expectCode != codes.OK { + require.Nil(t, key) + return + } + + require.NotNil(t, key) + require.Equal(t, tt.id, key.ID()) + + if p.cc.clientParams.Namespace != "" { + headers := p.vc.vaultClient.Headers() + require.Equal(t, p.cc.clientParams.Namespace, headers.Get(consts.NamespaceHeaderName)) + } + }) + } +} + +// TODO: Should the Sign function also be tested? + func getTestConfigureRequest(t *testing.T, addr string, tpl string) string { templ, err := template.New("plugin config").Parse(tpl) require.NoError(t, err) @@ -544,7 +717,7 @@ func getTestConfigureRequest(t *testing.T, addr string, tpl string) string { return c.String() } -func setupSuccessFakeVaultServer() *FakeVaultServerConfig { +func setupSuccessFakeVaultServer(transitEnginePath string) *FakeVaultServerConfig { fakeVaultServer := setupFakeVaultServer() fakeVaultServer.CertAuthResponseCode = 200 @@ -564,12 +737,16 @@ func setupSuccessFakeVaultServer() *FakeVaultServerConfig { fakeVaultServer.LookupSelfResponseCode = 200 fakeVaultServer.CreateKeyResponseCode = 200 - fakeVaultServer.CreateKeyReqEndpoint = "PUT /v1/test-transit/keys/{id}" + fakeVaultServer.CreateKeyReqEndpoint = fmt.Sprintf("PUT /v1/%s/keys/{id}", transitEnginePath) fakeVaultServer.GetKeyResponseCode = 200 - fakeVaultServer.GetKeyReqEndpoint = "GET /v1/test-transit/keys/{id}" + fakeVaultServer.GetKeyReqEndpoint = fmt.Sprintf("GET /v1/%s/keys/{id}", transitEnginePath) fakeVaultServer.GetKeyResponse = []byte(testGetKeyResponseP256) + fakeVaultServer.GetKeysResponseCode = 200 + fakeVaultServer.GetKeysReqEndpoint = fmt.Sprintf("GET /v1/%s/keys", transitEnginePath) + fakeVaultServer.GetKeysResponse = []byte(testGetKeysResponseOneKey) + return fakeVaultServer } diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go index 4047777801..f8a476383f 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go @@ -4,10 +4,12 @@ import ( "crypto/tls" "crypto/x509" "encoding/base64" + "encoding/pem" "fmt" "github.com/hashicorp/go-hclog" vapi "github.com/hashicorp/vault/api" "github.com/imdario/mergo" + keymanagerv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/plugin/server/keymanager/v1" "golang.org/x/net/context" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -380,48 +382,6 @@ func (c *Client) CreateKey(ctx context.Context, spireKeyID string, keyType Trans return nil } -// GetKey gets the transit engine key with the specified spire key id. -// See: https://developer.hashicorp.com/vault/api-docs/secret/transit#read-key -func (c *Client) GetKey(ctx context.Context, spireKeyID string) (string, error) { - res, err := c.vaultClient.Logical().ReadWithContext(ctx, fmt.Sprintf("/%s/keys/%s", c.clientParams.TransitEnginePath, spireKeyID)) - if err != nil { - return "", status.Errorf(codes.Internal, "failed to get transit engine key: %v", err) - } - - keys, ok := res.Data["keys"] - if !ok { - return "", status.Errorf(codes.Internal, "transit engine get key call was successful but keys are missing") - } - - keyMap, ok := keys.(map[string]interface{}) - if !ok { - return "", status.Errorf(codes.Internal, "expected key map data type %T but got %T", keyMap, keys) - } - - // TODO: Should we support multiple versions of the key? - currentKey, ok := keyMap["1"] - if !ok { - return "", status.Errorf(codes.Internal, "unable to find key with version 1 in %v", keyMap) - } - - currentKeyMap, ok := currentKey.(map[string]interface{}) - if !ok { - return "", status.Errorf(codes.Internal, "expected key data type %T but got %T", currentKeyMap, currentKey) - } - - pk, ok := currentKeyMap["public_key"] - if !ok { - return "", status.Errorf(codes.Internal, "expected public key to be present") - } - - pkStr, ok := pk.(string) - if !ok { - return "", status.Errorf(codes.Internal, "expected public key data type %T but got %T", pkStr, pk) - } - - return pkStr, nil -} - // SignData signs the data using the transit engine key with the provided spire key id. // See: https://developer.hashicorp.com/vault/api-docs/secret/transit#sign-data func (c *Client) SignData(ctx context.Context, spireKeyID string, data []byte, hashAlgo TransitHashAlgorithm, signatureAlgo TransitSignatureAlgorithm) ([]byte, error) { @@ -462,3 +422,135 @@ func (c *Client) SignData(ctx context.Context, spireKeyID string, data []byte, h return sigData, nil } + +// GetKeys returns all the keys of the transit engine. +// See: https://developer.hashicorp.com/vault/api-docs/secret/transit#list-keys +// TODO: Test this function +func (c *Client) GetKeys(ctx context.Context) ([]*keyEntry, error) { + var keyEntries []*keyEntry + + listResp, err := c.vaultClient.Logical().ListWithContext(ctx, fmt.Sprintf("/%s/keys", c.clientParams.TransitEnginePath)) + if err != nil { + return nil, status.Errorf(codes.Internal, "transit engine list keys call failed: %v", err) + } + + if listResp == nil { + return []*keyEntry{}, nil + } + + keys, ok := listResp.Data["keys"] + if !ok { + return nil, status.Errorf(codes.Internal, "transit engine list keys call was successful but keys are missing") + } + + keyIds, ok := keys.([]interface{}) + if !ok { + return nil, status.Errorf(codes.Internal, "expected keys data type %T but got %T", keyIds, keys) + } + + for _, keyId := range keyIds { + keyIdStr, ok := keyId.(string) + if !ok { + return nil, status.Errorf(codes.Internal, "expected key id data type %T but got %T", keyIdStr, keyId) + } + + keyEntry, err := c.getKeyEntry(ctx, keyIdStr) + if err != nil { + return nil, err + } + + keyEntries = append(keyEntries, keyEntry) + } + + return keyEntries, nil +} + +// TODO: Test this function +// getKeyEntry gets the transit engine key with the specified spire key id and converts it into a key entry. +func (c *Client) getKeyEntry(ctx context.Context, spireKeyID string) (*keyEntry, error) { + keyData, err := c.getKey(ctx, spireKeyID) + if err != nil { + return nil, err + } + + pk, ok := keyData["public_key"] + if !ok { + return nil, status.Errorf(codes.Internal, "expected public key to be present") + } + + pkStr, ok := pk.(string) + if !ok { + return nil, status.Errorf(codes.Internal, "expected public key data type %T but got %T", pkStr, pk) + } + + pemBlock, _ := pem.Decode([]byte(pkStr)) + if pemBlock == nil || pemBlock.Type != "PUBLIC KEY" { + return nil, status.Error(codes.Internal, "unable to decode PEM key") + } + + pubKeyType, ok := keyData["name"] + if !ok { + return nil, status.Errorf(codes.Internal, "expected name to be present") + } + + pubKeyTypeStr, ok := pubKeyType.(string) + if !ok { + return nil, status.Errorf(codes.Internal, "expected public key type to be of type %T but got %T", pubKeyTypeStr, pubKeyType) + } + + var keyType keymanagerv1.KeyType + + switch pubKeyTypeStr { + case "P-256": + keyType = keymanagerv1.KeyType_EC_P256 + case "P-384": + keyType = keymanagerv1.KeyType_EC_P384 + case "rsa-2048": + keyType = keymanagerv1.KeyType_RSA_2048 + case "rsa-4096": + keyType = keymanagerv1.KeyType_RSA_4096 + default: + return nil, status.Errorf(codes.Internal, "unsupported key type: %v", pubKeyTypeStr) + } + + return &keyEntry{ + PublicKey: &keymanagerv1.PublicKey{ + Id: spireKeyID, + Type: keyType, + PkixData: pemBlock.Bytes, + Fingerprint: makeFingerprint(pemBlock.Bytes), + }, + }, nil +} + +// getKey returns a specific key from the transit engine. +// See: https://developer.hashicorp.com/vault/api-docs/secret/transit#read-key +func (c *Client) getKey(ctx context.Context, spireKeyID string) (map[string]interface{}, error) { + res, err := c.vaultClient.Logical().ReadWithContext(ctx, fmt.Sprintf("/%s/keys/%s", c.clientParams.TransitEnginePath, spireKeyID)) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get transit engine key: %v", err) + } + + keys, ok := res.Data["keys"] + if !ok { + return nil, status.Errorf(codes.Internal, "transit engine get key call was successful but keys are missing") + } + + keyMap, ok := keys.(map[string]interface{}) + if !ok { + return nil, status.Errorf(codes.Internal, "expected key map data type %T but got %T", keyMap, keys) + } + + // TODO: Should we support multiple versions of the key? + currentKey, ok := keyMap["1"] + if !ok { + return nil, status.Errorf(codes.Internal, "unable to find key with version 1 in %v", keyMap) + } + + currentKeyMap, ok := currentKey.(map[string]interface{}) + if !ok { + return nil, status.Errorf(codes.Internal, "expected key data type %T but got %T", currentKeyMap, currentKey) + } + + return currentKeyMap, nil +} diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go index 5c0f3551a7..cff5c18bea 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go @@ -699,10 +699,14 @@ func TestGetKey(t *testing.T) { client, err := cc.NewAuthenticatedClient(CERT, renewCh) require.NoError(t, err) - resp, err := client.GetKey(context.Background(), "x509-CA-A") + resp, err := client.getKey(context.Background(), "x509-CA-A") require.NoError(t, err) - require.Equal(t, "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV57LFbIQZzyZ2YcKZfB9mGWkUhJv\niRzIZOqV4wRHoUOZjMuhBMR2WviEsy65TYpcBjreAc6pbneiyhlTwPvgmw==\n-----END PUBLIC KEY-----\n", resp) + require.Equal(t, map[string]interface{}{ + "name": "P-256", + "creation_time": "2024-09-16T18:18:54.284635756Z", + "public_key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV57LFbIQZzyZ2YcKZfB9mGWkUhJv\niRzIZOqV4wRHoUOZjMuhBMR2WviEsy65TYpcBjreAc6pbneiyhlTwPvgmw==\n-----END PUBLIC KEY-----\n", + }, resp) } func TestGetKeyErrorFromEndpoint(t *testing.T) { @@ -734,7 +738,7 @@ func TestGetKeyErrorFromEndpoint(t *testing.T) { client, err := cc.NewAuthenticatedClient(CERT, renewCh) require.NoError(t, err) - resp, err := client.GetKey(context.Background(), "x509-CA-A") + resp, err := client.getKey(context.Background(), "x509-CA-A") spiretest.RequireGRPCStatusHasPrefix(t, err, codes.Internal, "failed to get transit engine key: Error making API request.") require.Empty(t, resp) } diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go index 4e129e95d2..6806cf0c7b 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go @@ -15,6 +15,7 @@ const ( defaultLookupSelfEndpoint = "GET /v1/auth/token/lookup-self" defaultCreateKeyEndpoint = "PUT /v1/transit/keys/{id}" defaultGetKeyEndpoint = "GET /v1/transit/keys/{id}" + defaultGetKeysEndpoint = "GET /v1/transit/keys" defaultSignDataEndpoint = "PUT /v1/transit/sign/{id}/{algo}" listenAddr = "127.0.0.1:0" @@ -387,6 +388,34 @@ k8s_auth { } }` + testGetKeysResponseOneKey = `{ + "request_id": "3d02d2cf-baa4-a4ca-90d8-448b6c3ce6b0", + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": { + "keys": [ + "x509-CA-A" + ] + }, + "wrap_info": null, + "warnings": null, + "auth": null +}` + + testGetKeysResponseNoKeys = `{ + "request_id": "3d02d2cf-baa4-a4ca-90d8-448b6c3ce6b0", + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": { + "keys": [] + }, + "wrap_info": null, + "warnings": null, + "auth": null +}` + testGetKeyResponseP256 = `{ "request_id": "646eddbd-83fd-0cc1-387b-f1a17fa88c3d", "lease_id": "", @@ -610,6 +639,10 @@ type FakeVaultServerConfig struct { GetKeyReqHandler func(code int, resp []byte) func(http.ResponseWriter, *http.Request) GetKeyResponseCode int GetKeyResponse []byte + GetKeysReqEndpoint string + GetKeysReqHandler func(code int, resp []byte) func(http.ResponseWriter, *http.Request) + GetKeysResponseCode int + GetKeysResponse []byte SignDataReqEndpoint string SignDataReqHandler func(code int, resp []byte) func(http.ResponseWriter, *http.Request) SignDataResponseCode int @@ -634,6 +667,8 @@ func NewFakeVaultServerConfig() *FakeVaultServerConfig { CreateKeyReqHandler: defaultReqHandler, GetKeyReqEndpoint: defaultGetKeyEndpoint, GetKeyReqHandler: defaultReqHandler, + GetKeysReqEndpoint: defaultGetKeysEndpoint, + GetKeysReqHandler: defaultReqHandler, SignDataReqEndpoint: defaultSignDataEndpoint, SignDataReqHandler: defaultReqHandler, } @@ -669,6 +704,7 @@ func (v *FakeVaultServerConfig) NewTLSServer() (srv *httptest.Server, addr strin mux.HandleFunc(v.LookupSelfReqEndpoint, v.LookupSelfReqHandler(v.LookupSelfResponseCode, v.LookupSelfResponse)) mux.HandleFunc(v.CreateKeyReqEndpoint, v.CreateKeyReqHandler(v.CreateKeyResponseCode, v.CreateKeyResponse)) mux.HandleFunc(v.GetKeyReqEndpoint, v.GetKeyReqHandler(v.GetKeyResponseCode, v.GetKeyResponse)) + mux.HandleFunc(v.GetKeysReqEndpoint, v.GetKeysReqHandler(v.GetKeysResponseCode, v.GetKeysResponse)) mux.HandleFunc(v.SignDataReqEndpoint, v.SignDataReqHandler(v.SignDataResponseCode, v.SignDataResponse)) srv = httptest.NewUnstartedServer(mux) From 2ba4eb408697b28bb0cfb4efbe7a036c8cdc46f3 Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:23 +0200 Subject: [PATCH 26/29] Test get key entry function (#5058) Signed-off-by: Matteo Kamm --- .../keymanager/hashicorpvault/vault_client.go | 1 - .../hashicorpvault/vault_client_test.go | 72 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go index f8a476383f..502232323d 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go @@ -465,7 +465,6 @@ func (c *Client) GetKeys(ctx context.Context) ([]*keyEntry, error) { return keyEntries, nil } -// TODO: Test this function // getKeyEntry gets the transit engine key with the specified spire key id and converts it into a key entry. func (c *Client) getKeyEntry(ctx context.Context, spireKeyID string) (*keyEntry, error) { keyData, err := c.getKey(ctx, spireKeyID) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go index cff5c18bea..b28ff6dc85 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go @@ -5,8 +5,10 @@ import ( "crypto/tls" "crypto/x509" "encoding/base64" + "encoding/pem" "fmt" vapi "github.com/hashicorp/vault/api" + keymanagerv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/plugin/server/keymanager/v1" "net/http" "os" "testing" @@ -743,6 +745,76 @@ func TestGetKeyErrorFromEndpoint(t *testing.T) { require.Empty(t, resp) } +func TestGetKeyEntry(t *testing.T) { + fakeVaultServer := newFakeVaultServer() + fakeVaultServer.CertAuthResponseCode = 200 + fakeVaultServer.CertAuthResponse = []byte(testCertAuthResponse) + fakeVaultServer.GetKeyResponseCode = 200 + fakeVaultServer.GetKeyResponse = []byte(testGetKeyResponseP256) + + s, addr, err := fakeVaultServer.NewTLSServer() + require.NoError(t, err) + + s.Start() + defer s.Close() + + cp := &ClientParams{ + VaultAddr: fmt.Sprintf("https://%v/", addr), + CACertPath: testRootCert, + ClientCertPath: testClientCert, + ClientKeyPath: testClientKey, + } + + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + renewCh := make(chan struct{}) + client, err := cc.NewAuthenticatedClient(CERT, renewCh) + require.NoError(t, err) + + resp, err := client.getKeyEntry(context.Background(), "x509-CA-A") + require.NoError(t, err) + + block, _ := pem.Decode([]byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV57LFbIQZzyZ2YcKZfB9mGWkUhJv\niRzIZOqV4wRHoUOZjMuhBMR2WviEsy65TYpcBjreAc6pbneiyhlTwPvgmw==\n-----END PUBLIC KEY-----\n")) + + require.Equal(t, "x509-CA-A", resp.PublicKey.Id) + require.Equal(t, keymanagerv1.KeyType_EC_P256, resp.PublicKey.Type) + require.Equal(t, block.Bytes, resp.PublicKey.PkixData) + require.Equal(t, "afd4e26c151ce5c1069414bdb08fe5f7a7fdb271d40d077aa1f77a82e8ac5870", resp.PublicKey.Fingerprint) +} + +func TestGetKeyEntryErrorFromEndpoint(t *testing.T) { + fakeVaultServer := newFakeVaultServer() + fakeVaultServer.CertAuthResponseCode = 200 + fakeVaultServer.CertAuthResponse = []byte(testCertAuthResponse) + fakeVaultServer.GetKeyResponseCode = 500 + fakeVaultServer.GetKeyResponse = []byte("some error") + + s, addr, err := fakeVaultServer.NewTLSServer() + require.NoError(t, err) + + s.Start() + defer s.Close() + + cp := &ClientParams{ + VaultAddr: fmt.Sprintf("https://%v/", addr), + CACertPath: testRootCert, + ClientCertPath: testClientCert, + ClientKeyPath: testClientKey, + } + + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + renewCh := make(chan struct{}) + client, err := cc.NewAuthenticatedClient(CERT, renewCh) + require.NoError(t, err) + + resp, err := client.getKeyEntry(context.Background(), "x509-CA-A") + spiretest.RequireGRPCStatusHasPrefix(t, err, codes.Internal, "failed to get transit engine key: Error making API request.") + require.Empty(t, resp) +} + func TestSignData(t *testing.T) { fakeVaultServer := newFakeVaultServer() fakeVaultServer.CertAuthResponseCode = 200 From 4f1b244add50777e25319fb3c79ae8fa9ee8d816 Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:23 +0200 Subject: [PATCH 27/29] Test get keys function (#5058) Signed-off-by: Matteo Kamm --- .../keymanager/hashicorpvault/vault_client.go | 1 - .../hashicorpvault/vault_client_test.go | 141 ++++++++++++++++++ 2 files changed, 141 insertions(+), 1 deletion(-) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go index 502232323d..1743b8988a 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go @@ -425,7 +425,6 @@ func (c *Client) SignData(ctx context.Context, spireKeyID string, data []byte, h // GetKeys returns all the keys of the transit engine. // See: https://developer.hashicorp.com/vault/api-docs/secret/transit#list-keys -// TODO: Test this function func (c *Client) GetKeys(ctx context.Context) ([]*keyEntry, error) { var keyEntries []*keyEntry diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go index b28ff6dc85..a6bc3560f0 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go @@ -674,6 +674,147 @@ func TestCreateKeyErrorFromEndpoint(t *testing.T) { spiretest.RequireGRPCStatusHasPrefix(t, err, codes.Internal, "failed to create transit engine key: Error making API request.") } +func TestGetKeysSingleKey(t *testing.T) { + fakeVaultServer := newFakeVaultServer() + fakeVaultServer.CertAuthResponseCode = 200 + fakeVaultServer.CertAuthResponse = []byte(testCertAuthResponse) + fakeVaultServer.GetKeysResponseCode = 200 + fakeVaultServer.GetKeysResponse = []byte(testGetKeysResponseOneKey) + fakeVaultServer.GetKeyResponseCode = 200 + fakeVaultServer.GetKeyResponse = []byte(testGetKeyResponseP256) + + s, addr, err := fakeVaultServer.NewTLSServer() + require.NoError(t, err) + + s.Start() + defer s.Close() + + cp := &ClientParams{ + VaultAddr: fmt.Sprintf("https://%v/", addr), + CACertPath: testRootCert, + ClientCertPath: testClientCert, + ClientKeyPath: testClientKey, + } + + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + renewCh := make(chan struct{}) + client, err := cc.NewAuthenticatedClient(CERT, renewCh) + require.NoError(t, err) + + resp, err := client.GetKeys(context.Background()) + require.NoError(t, err) + + block, _ := pem.Decode([]byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV57LFbIQZzyZ2YcKZfB9mGWkUhJv\niRzIZOqV4wRHoUOZjMuhBMR2WviEsy65TYpcBjreAc6pbneiyhlTwPvgmw==\n-----END PUBLIC KEY-----\n")) + + require.Len(t, resp, 1) + + require.Equal(t, "x509-CA-A", resp[0].PublicKey.Id) + require.Equal(t, keymanagerv1.KeyType_EC_P256, resp[0].PublicKey.Type) + require.Equal(t, block.Bytes, resp[0].PublicKey.PkixData) + require.Equal(t, "afd4e26c151ce5c1069414bdb08fe5f7a7fdb271d40d077aa1f77a82e8ac5870", resp[0].PublicKey.Fingerprint) +} + +func TestGetKeysNoKey(t *testing.T) { + fakeVaultServer := newFakeVaultServer() + fakeVaultServer.CertAuthResponseCode = 200 + fakeVaultServer.CertAuthResponse = []byte(testCertAuthResponse) + fakeVaultServer.GetKeysResponseCode = 200 + fakeVaultServer.GetKeysResponse = []byte(testGetKeysResponseNoKeys) + + s, addr, err := fakeVaultServer.NewTLSServer() + require.NoError(t, err) + + s.Start() + defer s.Close() + + cp := &ClientParams{ + VaultAddr: fmt.Sprintf("https://%v/", addr), + CACertPath: testRootCert, + ClientCertPath: testClientCert, + ClientKeyPath: testClientKey, + } + + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + renewCh := make(chan struct{}) + client, err := cc.NewAuthenticatedClient(CERT, renewCh) + require.NoError(t, err) + + resp, err := client.GetKeys(context.Background()) + require.NoError(t, err) + + require.Empty(t, resp) +} + +func TestGetKeysErrorFromListEndpoint(t *testing.T) { + fakeVaultServer := newFakeVaultServer() + fakeVaultServer.CertAuthResponseCode = 200 + fakeVaultServer.CertAuthResponse = []byte(testCertAuthResponse) + fakeVaultServer.GetKeysResponseCode = 500 + fakeVaultServer.GetKeysResponse = []byte("some error") + + s, addr, err := fakeVaultServer.NewTLSServer() + require.NoError(t, err) + + s.Start() + defer s.Close() + + cp := &ClientParams{ + VaultAddr: fmt.Sprintf("https://%v/", addr), + CACertPath: testRootCert, + ClientCertPath: testClientCert, + ClientKeyPath: testClientKey, + } + + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + renewCh := make(chan struct{}) + client, err := cc.NewAuthenticatedClient(CERT, renewCh) + require.NoError(t, err) + + resp, err := client.GetKeys(context.Background()) + spiretest.RequireGRPCStatusHasPrefix(t, err, codes.Internal, "transit engine list keys call failed: Error making API request.") + require.Empty(t, resp) +} + +func TestGetKeysErrorFromKeyEndpoint(t *testing.T) { + fakeVaultServer := newFakeVaultServer() + fakeVaultServer.CertAuthResponseCode = 200 + fakeVaultServer.CertAuthResponse = []byte(testCertAuthResponse) + fakeVaultServer.GetKeysResponseCode = 200 + fakeVaultServer.GetKeysResponse = []byte(testGetKeysResponseOneKey) + fakeVaultServer.GetKeyResponseCode = 500 + fakeVaultServer.GetKeyResponse = []byte("some error") + + s, addr, err := fakeVaultServer.NewTLSServer() + require.NoError(t, err) + + s.Start() + defer s.Close() + + cp := &ClientParams{ + VaultAddr: fmt.Sprintf("https://%v/", addr), + CACertPath: testRootCert, + ClientCertPath: testClientCert, + ClientKeyPath: testClientKey, + } + + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + renewCh := make(chan struct{}) + client, err := cc.NewAuthenticatedClient(CERT, renewCh) + require.NoError(t, err) + + resp, err := client.GetKeys(context.Background()) + spiretest.RequireGRPCStatusHasPrefix(t, err, codes.Internal, "failed to get transit engine key: Error making API request.") + require.Empty(t, resp) +} + func TestGetKey(t *testing.T) { fakeVaultServer := newFakeVaultServer() fakeVaultServer.CertAuthResponseCode = 200 From 307a2a7864d3af7a46bf30d6a43d9c0a300b0781 Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Wed, 18 Sep 2024 22:39:23 +0200 Subject: [PATCH 28/29] Add simple plugin documentation (#5058) Signed-off-by: Matteo Kamm --- ...lugin_server_keymanager_hashicorp_vault.md | 162 ++++++++++++++++++ doc/spire_server.md | 1 + 2 files changed, 163 insertions(+) create mode 100644 doc/plugin_server_keymanager_hashicorp_vault.md diff --git a/doc/plugin_server_keymanager_hashicorp_vault.md b/doc/plugin_server_keymanager_hashicorp_vault.md new file mode 100644 index 0000000000..fb3343e5c4 --- /dev/null +++ b/doc/plugin_server_keymanager_hashicorp_vault.md @@ -0,0 +1,162 @@ +# Server plugin: KeyManager "hashicorp_vault" + +The `hashicorp_vault` key manager plugin leverages HashiCorp Vault to create, maintain, and rotate key pairs, signing +SVIDs as needed. + +## Configuration + +The plugin accepts the following configuration options: + +| key | type | required | description | default | +|:---------------------|:-------|:---------|:---------------------------------------------------------------------------------------------------------|:---------------------| +| vault_addr | string | | The URL of the Vault server. (e.g., ) | `${VAULT_ADDR}` | +| namespace | string | | Name of the Vault namespace. This is only available in the Vault Enterprise. | `${VAULT_NAMESPACE}` | +| transit_engine_path | string | | Path of the transit engine that stores the keys. | transit | +| ca_cert_path | string | | Path to a CA certificate file used to verify the Vault server certificate. Only PEM format is supported. | `${VAULT_CACERT}` | +| insecure_skip_verify | bool | | If true, vault client accepts any server certificates | false | +| cert_auth | struct | | Configuration for the Client Certificate authentication method | | +| token_auth | struct | | Configuration for the Token authentication method | | +| approle_auth | struct | | Configuration for the AppRole authentication method | | +| k8s_auth | struct | | Configuration for the Kubernetes authentication method | | + +The plugin supports **Client Certificate**, **Token** and **AppRole** authentication methods. + +- **Client Certificate** method authenticates to Vault using a TLS client certificate. +- **Token** method authenticates to Vault using the token in a HTTP Request header. +- **AppRole** method authenticates to Vault using a RoleID and SecretID that are issued from Vault. + +The [`ca_ttl` SPIRE Server configurable](https://github.com/spiffe/spire/blob/main/doc/spire_server.md#server-configuration-file) +should be less than or equal to the Vault's PKI secret engine TTL. +To configure the TTL value, tune the engine. + +e.g. + +```shell +$ vault secrets tune -max-lease-ttl=8760h pki +``` + +The configured token needs to be attached to a policy that has at least the following capabilities: + +```hcl +path "pki/root/sign-intermediate" { + capabilities = ["update"] +} +``` + +## Client Certificate Authentication + +| key | type | required | description | default | +|:----------------------|:-------|:---------|:---------------------------------------------------------------------------------------------------------------------|:-----------------------| +| cert_auth_mount_point | string | | Name of the mount point where TLS certificate auth method is mounted | cert | +| cert_auth_role_name | string | | Name of the Vault role. If given, the plugin authenticates against only the named role. Default to trying all roles. | | +| client_cert_path | string | | Path to a client certificate file. Only PEM format is supported. | `${VAULT_CLIENT_CERT}` | +| client_key_path | string | | Path to a client private key file. Only PEM format is supported. | `${VAULT_CLIENT_KEY}` | + +```hcl + UpstreamAuthority "vault" { + plugin_data { + vault_addr = "https://vault.example.org/" + pki_mount_point = "test-pki" + ca_cert_path = "/path/to/ca-cert.pem" + cert_auth { + cert_auth_mount_point = "test-tls-cert-auth" + client_cert_path = "/path/to/client-cert.pem" + client_key_path = "/path/to/client-key.pem" + } + // If specify the role to authenticate with + // cert_auth { + // cert_auth_mount_point = "test-tls-cert-auth" + // cert_auth_role_name = "test" + // client_cert_path = "/path/to/client-cert.pem" + // client_key_path = "/path/to/client-key.pem" + // } + + // If specify the key-pair as an environment variable and use the modified mount point + // cert_auth { + // cert_auth_mount_point = "test-tls-cert-auth" + // } + + // If specify the key-pair as an environment variable and use the default mount point, set the empty structure. + // cert_auth {} + } + } +``` + +## Token Authentication + +| key | type | required | description | default | +|:------|:-------|:---------|:------------------------------------------------|:-----------------| +| token | string | | Token string to set into "X-Vault-Token" header | `${VAULT_TOKEN}` | + +```hcl + UpstreamAuthority "vault" { + plugin_data { + vault_addr = "https://vault.example.org/" + pki_mount_point = "test-pki" + ca_cert_path = "/path/to/ca-cert.pem" + token_auth { + token = "" + } + // If specify the token as an environment variable, set the empty structure. + // token_auth {} + } + } +``` + +## AppRole Authentication + +| key | type | required | description | default | +|:-------------------------|:-------|:---------|:-----------------------------------------------------------------|:-----------------------------| +| approle_auth_mount_point | string | | Name of the mount point where the AppRole auth method is mounted | approle | +| approle_id | string | | An identifier of AppRole | `${VAULT_APPROLE_ID}` | +| approle_secret_id | string | | A credential of AppRole | `${VAULT_APPROLE_SECRET_ID}` | + +```hcl + UpstreamAuthority "vault" { + plugin_data { + vault_addr = "https://vault.example.org/" + pki_mount_point = "test-pki" + ca_cert_path = "/path/to/ca-cert.pem" + approle_auth { + approle_auth_mount_point = "my-approle-auth" + approle_id = "" // or specified by environment variables + approle_secret_id = "" // or specified by environment variables + } + // If specify the approle_id and approle_secret as an environment variable and use the modified mount point + // approle_auth { + // approle_auth_mount_point = "my-approle-auth" + // } + + // If specify the approle_id and approle_secret as an environment variable and use the default mount point, set the empty structure. + // approle_auth {} + } + } +``` + +## Kubernetes Authentication + +| key | type | required | description | default | +|:---------------------|:-------|:---------|:----------------------------------------------------------------------------------|:-----------| +| k8s_auth_mount_point | string | | Name of the mount point where the Kubernetes auth method is mounted | kubernetes | +| k8s_auth_role_name | string | ✔ | Name of the Vault role. The plugin authenticates against the named role | | +| token_path | string | ✔ | Path to the Kubernetes Service Account Token to use authentication with the Vault | | + +```hcl + UpstreamAuthority "vault" { + plugin_data { + vault_addr = "https://vault.example.org/" + pki_mount_point = "test-pki" + ca_cert_path = "/path/to/ca-cert.pem" + k8s_auth { + k8s_auth_mount_point = "my-k8s-auth" + k8s_auth_role_name = "my-role" + token_path = "/path/to/sa-token" + } + + // If specify role name and use the default mount point and token_path + // k8s_auth { + // k8s_auth_role_name = "my-role" + // } + } + } +``` diff --git a/doc/spire_server.md b/doc/spire_server.md index 1304225cc7..dbc9ce773e 100644 --- a/doc/spire_server.md +++ b/doc/spire_server.md @@ -20,6 +20,7 @@ This document is a configuration reference for SPIRE Server. It includes informa |--------------------|--------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------| | DataStore | [sql](/doc/plugin_server_datastore_sql.md) | An SQL database storage for SQLite, PostgreSQL and MySQL databases for the SPIRE datastore | | KeyManager | [aws_kms](/doc/plugin_server_keymanager_aws_kms.md) | A key manager which manages keys in AWS KMS | +| KeyManager | [hashicorp_vault](/doc/plugin_server_keymanager_hashicorp_vault.md) | A key manager which manages unpersisted keys in memory | | KeyManager | [disk](/doc/plugin_server_keymanager_disk.md) | A key manager which manages keys persisted on disk | | KeyManager | [memory](/doc/plugin_server_keymanager_memory.md) | A key manager which manages unpersisted keys in memory | | CredentialComposer | [uniqueid](/doc/plugin_server_credentialcomposer_uniqueid.md) | Adds the x509UniqueIdentifier attribute to workload X509-SVIDs. | From db491f394b529b278b0b692d4130eb680a7c09c3 Mon Sep 17 00:00:00 2001 From: Matteo Kamm Date: Thu, 19 Sep 2024 21:58:16 +0200 Subject: [PATCH 29/29] Remove unused hooks (#5058) Signed-off-by: Matteo Kamm --- .../plugin/keymanager/hashicorpvault/hashicorp_vault.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go index 2c91eddb1d..64952de8fe 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go @@ -39,10 +39,6 @@ type keyEntry struct { type pluginHooks struct { // Used for testing only. - scheduleDeleteSignal chan error - refreshKeysSignal chan error - disposeKeysSignal chan error - lookupEnv func(string) (string, bool) }