diff --git a/doc/plugin_server_keymanager_hashicorp_vault.md b/doc/plugin_server_keymanager_hashicorp_vault.md
new file mode 100644
index 0000000000..73ec02beb1
--- /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
+ KeyManager "hashicorp_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
+ KeyManager "hashicorp_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
+ KeyManager "hashicorp_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
+ KeyManager "hashicorp_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 79852c5f21..6ebf95897a 100644
--- a/doc/spire_server.md
+++ b/doc/spire_server.md
@@ -16,34 +16,35 @@ This document is a configuration reference for SPIRE Server. It includes informa
## Built-in plugins
-| Type | Name | Description |
-|--------------------|------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|
-| 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 | [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. |
-| NodeAttestor | [aws_iid](/doc/plugin_server_nodeattestor_aws_iid.md) | A node attestor which attests agent identity using an AWS Instance Identity Document |
-| NodeAttestor | [azure_msi](/doc/plugin_server_nodeattestor_azure_msi.md) | A node attestor which attests agent identity using an Azure MSI token |
-| NodeAttestor | [gcp_iit](/doc/plugin_server_nodeattestor_gcp_iit.md) | A node attestor which attests agent identity using a GCP Instance Identity Token |
-| NodeAttestor | [join_token](/doc/plugin_server_nodeattestor_jointoken.md) | A node attestor which validates agents attesting with server-generated join tokens |
-| NodeAttestor | [k8s_sat](/doc/plugin_server_nodeattestor_k8s_sat.md) (deprecated) | A node attestor which attests agent identity using a Kubernetes Service Account token |
-| NodeAttestor | [k8s_psat](/doc/plugin_server_nodeattestor_k8s_psat.md) | A node attestor which attests agent identity using a Kubernetes Projected Service Account token |
-| NodeAttestor | [sshpop](/doc/plugin_server_nodeattestor_sshpop.md) | A node attestor which attests agent identity using an existing ssh certificate |
-| NodeAttestor | [tpm_devid](/doc/plugin_server_nodeattestor_tpm_devid.md) | A node attestor which attests agent identity using a TPM that has been provisioned with a DevID certificate |
-| NodeAttestor | [x509pop](/doc/plugin_server_nodeattestor_x509pop.md) | A node attestor which attests agent identity using an existing X.509 certificate |
-| UpstreamAuthority | [disk](/doc/plugin_server_upstreamauthority_disk.md) | Uses a CA loaded from disk to sign SPIRE server intermediate certificates. |
-| UpstreamAuthority | [aws_pca](/doc/plugin_server_upstreamauthority_aws_pca.md) | Uses a Private Certificate Authority from AWS Certificate Manager to sign SPIRE server intermediate certificates. |
-| UpstreamAuthority | [awssecret](/doc/plugin_server_upstreamauthority_awssecret.md) | Uses a CA loaded from AWS SecretsManager to sign SPIRE server intermediate certificates. |
-| UpstreamAuthority | [gcp_cas](/doc/plugin_server_upstreamauthority_gcp_cas.md) | Uses a Private Certificate Authority from GCP Certificate Authority Service to sign SPIRE Server intermediate certificates. |
-| UpstreamAuthority | [vault](/doc/plugin_server_upstreamauthority_vault.md) | Uses a PKI Secret Engine from HashiCorp Vault to sign SPIRE server intermediate certificates. |
-| UpstreamAuthority | [spire](/doc/plugin_server_upstreamauthority_spire.md) | Uses an upstream SPIRE server in the same trust domain to obtain intermediate signing certificates for SPIRE server. |
-| UpstreamAuthority | [cert-manager](/doc/plugin_server_upstreamauthority_cert_manager.md) | Uses a referenced cert-manager Issuer to request intermediate signing certificates. |
-| Notifier | [gcs_bundle](/doc/plugin_server_notifier_gcs_bundle.md) | A notifier that pushes the latest trust bundle contents into an object in Google Cloud Storage. |
-| Notifier | [k8sbundle](/doc/plugin_server_notifier_k8sbundle.md) | A notifier that pushes the latest trust bundle contents into a Kubernetes ConfigMap. |
-| BundlePublisher | [aws_s3](/doc/plugin_server_bundlepublisher_aws_s3.md) | Publishes the trust bundle to an Amazon S3 bucket. |
-| BundlePublisher | [gcp_cloudstorage](/doc/plugin_server_bundlepublisher_gcp_cloudstorage.md) | Publishes the trust bundle to a Google Cloud Storage bucket. |
-| BundlePublisher | [aws_rolesanywhere_trustanchor](/doc/plugin_server_bundlepublisher_aws_rolesanywhere_trustanchor.md) | Publishes the trust bundle to an AWS IAM Roles Anywhere trust anchor. |
+| Type | Name | Description |
+|--------------------|--------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|
+| 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. |
+| NodeAttestor | [aws_iid](/doc/plugin_server_nodeattestor_aws_iid.md) | A node attestor which attests agent identity using an AWS Instance Identity Document |
+| NodeAttestor | [azure_msi](/doc/plugin_server_nodeattestor_azure_msi.md) | A node attestor which attests agent identity using an Azure MSI token |
+| NodeAttestor | [gcp_iit](/doc/plugin_server_nodeattestor_gcp_iit.md) | A node attestor which attests agent identity using a GCP Instance Identity Token |
+| NodeAttestor | [join_token](/doc/plugin_server_nodeattestor_jointoken.md) | A node attestor which validates agents attesting with server-generated join tokens |
+| NodeAttestor | [k8s_sat](/doc/plugin_server_nodeattestor_k8s_sat.md) (deprecated) | A node attestor which attests agent identity using a Kubernetes Service Account token |
+| NodeAttestor | [k8s_psat](/doc/plugin_server_nodeattestor_k8s_psat.md) | A node attestor which attests agent identity using a Kubernetes Projected Service Account token |
+| NodeAttestor | [sshpop](/doc/plugin_server_nodeattestor_sshpop.md) | A node attestor which attests agent identity using an existing ssh certificate |
+| NodeAttestor | [tpm_devid](/doc/plugin_server_nodeattestor_tpm_devid.md) | A node attestor which attests agent identity using a TPM that has been provisioned with a DevID certificate |
+| NodeAttestor | [x509pop](/doc/plugin_server_nodeattestor_x509pop.md) | A node attestor which attests agent identity using an existing X.509 certificate |
+| UpstreamAuthority | [disk](/doc/plugin_server_upstreamauthority_disk.md) | Uses a CA loaded from disk to sign SPIRE server intermediate certificates. |
+| UpstreamAuthority | [aws_pca](/doc/plugin_server_upstreamauthority_aws_pca.md) | Uses a Private Certificate Authority from AWS Certificate Manager to sign SPIRE server intermediate certificates. |
+| UpstreamAuthority | [awssecret](/doc/plugin_server_upstreamauthority_awssecret.md) | Uses a CA loaded from AWS SecretsManager to sign SPIRE server intermediate certificates. |
+| UpstreamAuthority | [gcp_cas](/doc/plugin_server_upstreamauthority_gcp_cas.md) | Uses a Private Certificate Authority from GCP Certificate Authority Service to sign SPIRE Server intermediate certificates. |
+| UpstreamAuthority | [vault](/doc/plugin_server_upstreamauthority_vault.md) | Uses a PKI Secret Engine from HashiCorp Vault to sign SPIRE server intermediate certificates. |
+| UpstreamAuthority | [spire](/doc/plugin_server_upstreamauthority_spire.md) | Uses an upstream SPIRE server in the same trust domain to obtain intermediate signing certificates for SPIRE server. |
+| UpstreamAuthority | [cert-manager](/doc/plugin_server_upstreamauthority_cert_manager.md) | Uses a referenced cert-manager Issuer to request intermediate signing certificates. |
+| Notifier | [gcs_bundle](/doc/plugin_server_notifier_gcs_bundle.md) | A notifier that pushes the latest trust bundle contents into an object in Google Cloud Storage. |
+| Notifier | [k8sbundle](/doc/plugin_server_notifier_k8sbundle.md) | A notifier that pushes the latest trust bundle contents into a Kubernetes ConfigMap. |
+| BundlePublisher | [aws_s3](/doc/plugin_server_bundlepublisher_aws_s3.md) | Publishes the trust bundle to an Amazon S3 bucket. |
+| BundlePublisher | [gcp_cloudstorage](/doc/plugin_server_bundlepublisher_gcp_cloudstorage.md) | Publishes the trust bundle to a Google Cloud Storage bucket. |
+| BundlePublisher | [aws_rolesanywhere_trustanchor](/doc/plugin_server_bundlepublisher_rolesanywhere_trustanchor.md) | Publishes the trust bundle to an AWS IAM Roles Anywhere trust anchor. |
## Server configuration file
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..64952de8fe
--- /dev/null
+++ b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go
@@ -0,0 +1,490 @@
+package hashicorpvault
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "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.
+ 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"`
+ // 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"`
+
+ // 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"`
+ // 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"`
+ // 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"`
+ // Configuration for the Kubernetes authentication method
+ K8sAuth *K8sAuthConfig `hcl:"k8s_auth" json:"k8s_auth,omitempty"`
+}
+
+// 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"`
+}
+
+// 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"`
+}
+
+// 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
+ 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(ctx 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
+
+ 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
+}
+
+func parseAuthMethod(config *Config) (AuthMethod, error) {
+ var authMethod AuthMethod
+ if config.TokenAuth != nil {
+ authMethod = TOKEN
+ }
+
+ if config.AppRoleAuth != nil {
+ if err := checkForAuthMethodConfigured(authMethod); err != nil {
+ return 0, err
+ }
+ authMethod = APPROLE
+ }
+
+ if config.CertAuth != nil {
+ if err := checkForAuthMethodConfigured(authMethod); err != nil {
+ return 0, err
+ }
+ authMethod = CERT
+ }
+
+ if config.K8sAuth != nil {
+ if err := checkForAuthMethodConfigured(authMethod); err != nil {
+ return 0, err
+ }
+ authMethod = K8S
+ }
+
+ if authMethod != 0 {
+ return authMethod, nil
+ }
+
+ 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) {
+ cp := &ClientParams{
+ 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,
+ }
+
+ 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)
+ 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)
+ 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
+}
+
+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) {
+ 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())
+ }
+
+ if p.vc == nil {
+ err := p.genVaultClient()
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ signature, err := p.vc.SignData(ctx, req.KeyId, req.Data, hashAlgo, signingAlgo)
+ if err != nil {
+ return nil, err
+ }
+
+ return &keymanagerv1.SignDataResponse{
+ Signature: signature,
+ KeyFingerprint: keyEntry.PublicKey.Fingerprint,
+ }, nil
+}
+
+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, 0, len(p.entries))
+
+ p.mu.RLock()
+ defer p.mu.RUnlock()
+
+ for _, key := range p.entries {
+ keys = append(keys, key.PublicKey)
+ }
+
+ 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 || 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:
+ 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) {
+ if p.vc == nil {
+ err := p.genVaultClient()
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ kt, err := convertToTransitKeyType(keyType)
+ if err != nil {
+ return nil, err
+ }
+
+ err = p.vc.CreateKey(ctx, spireKeyID, *kt)
+ if err != nil {
+ return nil, err
+ }
+
+ return p.vc.getKeyEntry(ctx, spireKeyID)
+}
+
+func convertToTransitKeyType(keyType keymanagerv1.KeyType) (*TransitKeyType, error) {
+ switch keyType {
+ case keymanagerv1.KeyType_EC_P256:
+ return to.Ptr(TransitKeyTypeECDSAP256), nil
+ case keymanagerv1.KeyType_EC_P384:
+ return to.Ptr(TransitKeyTypeECDSAP384), nil
+ case keymanagerv1.KeyType_RSA_2048:
+ return to.Ptr(TransitKeyTypeRSA2048), nil
+ case keymanagerv1.KeyType_RSA_4096:
+ return to.Ptr(TransitKeyTypeRSA4096), nil
+ default:
+ return nil, status.Errorf(codes.Internal, "unsupported key type: %v", keyType)
+ }
+}
+
+func (p *Plugin) genVaultClient() error {
+ 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[:])
+}
+
+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
new file mode 100644
index 0000000000..5260ac47c5
--- /dev/null
+++ b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault_test.go
@@ -0,0 +1,760 @@
+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"
+ "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 TestPluginConfigure(t *testing.T) {
+ 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",
+ 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:",
+ 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",
+ expectTransitEnginePath: "transit",
+ },
+ {
+ 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) {
+ 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) {
+ 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 TestPluginGenerateKey(t *testing.T) {
+ successfulConfig := &Config{
+ TransitEnginePath: "test-transit",
+ CACertPath: "testdata/root-cert.pem",
+ TokenAuth: &TokenAuthConfig{
+ Token: "test-token",
+ },
+ }
+
+ for _, tt := range []struct {
+ name string
+ 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("test-transit")
+ 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("test-transit")
+ 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("test-transit")
+ 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("test-transit")
+ 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("test-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: "Generate key with missing key type",
+ id: "x509-CA-A",
+ config: successfulConfig,
+ authMethod: TOKEN,
+ fakeServer: func() *FakeVaultServerConfig {
+ fakeServer := setupSuccessFakeVaultServer("test-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 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("test-transit")
+ 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("test-transit")
+ 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("test-transit")
+ 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("test-transit")
+ 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("test-transit")
+ 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("test-transit")
+ 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 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)
+
+ cp := &struct{ Addr string }{Addr: addr}
+
+ var c bytes.Buffer
+ err = templ.Execute(&c, cp)
+ require.NoError(t, err)
+
+ return c.String()
+}
+
+func setupSuccessFakeVaultServer(transitEnginePath string) *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 = fmt.Sprintf("PUT /v1/%s/keys/{id}", transitEnginePath)
+
+ fakeVaultServer.GetKeyResponseCode = 200
+ 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
+}
+
+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/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/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/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/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/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
new file mode 100644
index 0000000000..1743b8988a
--- /dev/null
+++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client.go
@@ -0,0 +1,554 @@
+package hashicorpvault
+
+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"
+ "net/http"
+ "os"
+ "strings"
+
+ "github.com/spiffe/spire/pkg/common/pemutil"
+)
+
+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"
+ envVaultTransitEnginePath = "VAULT_TRANSIT_ENGINE_PATH"
+
+ defaultCertMountPoint = "cert"
+ defaultPKIMountPoint = "pki"
+ defaultTransitEnginePath = "transit"
+ 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
+ // TransitEnginePath specifies the path to the transit engine to perform key operations.
+ TransitEnginePath string
+}
+
+type Client struct {
+ vaultClient *vapi.Client
+ clientParams *ClientParams
+}
+
+// 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,
+ TransitEnginePath: defaultTransitEnginePath,
+ }
+ 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 (
+ TransitKeyTypeRSA2048 TransitKeyType = "rsa-2048"
+ TransitKeyTypeRSA4096 TransitKeyType = "rsa-4096"
+ TransitKeyTypeECDSAP256 TransitKeyType = "ecdsa-p256"
+ TransitKeyTypeECDSAP384 TransitKeyType = "ecdsa-p384"
+)
+
+type TransitHashAlgorithm string
+
+const (
+ TransitHashAlgorithmSHA256 TransitHashAlgorithm = "sha2-256"
+ TransitHashAlgorithmSHA384 TransitHashAlgorithm = "sha2-384"
+ TransitHashAlgorithmSHA512 TransitHashAlgorithm = "sha2-512"
+ TransitHashAlgorithmNone TransitHashAlgorithm = "none"
+)
+
+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) error {
+ arguments := map[string]interface{}{
+ "type": keyType,
+ "exportable": "false", // TODO: Maybe make this configurable
+ }
+
+ _, err := c.vaultClient.Logical().WriteWithContext(ctx, fmt.Sprintf("/%s/keys/%s", c.clientParams.TransitEnginePath, spireKeyID), arguments)
+ if err != nil {
+ return status.Errorf(codes.Internal, "failed to create transit engine key: %v", err)
+ }
+
+ return 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)
+
+ body := map[string]interface{}{
+ "input": encodedData,
+ "signature_algorithm": signatureAlgo,
+ "marshalling_algorithm": "asn1",
+ "prehashed": "true",
+ }
+
+ 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)
+ }
+
+ sig, ok := sigResp.Data["signature"]
+ if !ok {
+ return nil, status.Errorf(codes.Internal, "transit engine sign call was successful but signature is missing")
+ }
+
+ sigStr, ok := sig.(string)
+ if !ok {
+ 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)
+ }
+
+ 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
+}
+
+// GetKeys returns all the keys of the transit engine.
+// See: https://developer.hashicorp.com/vault/api-docs/secret/transit#list-keys
+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
+}
+
+// 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
new file mode 100644
index 0000000000..a6bc3560f0
--- /dev/null
+++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go
@@ -0,0 +1,1059 @@
+package hashicorpvault
+
+import (
+ "context"
+ "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"
+ "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"
+ 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) {
+ 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.
+ TransitEnginePath: "", // 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)
+ require.Equal(t, defaultTransitEnginePath, cc.clientParams.TransitEnginePath)
+}
+
+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",
+ TransitEnginePath: "test-transit",
+ }
+
+ 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)
+ require.Equal(t, "test-transit", cc.clientParams.TransitEnginePath)
+}
+
+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 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 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
+ 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 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)
+ 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 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")
+}
+
+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.")
+}
+
+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
+ 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.getKey(context.Background(), "x509-CA-A")
+ require.NoError(t, err)
+
+ 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) {
+ 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)
+}
+
+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
+ 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()
+ 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
+}
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..6806cf0c7b
--- /dev/null
+++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go
@@ -0,0 +1,713 @@
+package hashicorpvault
+
+import (
+ "crypto/tls"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+)
+
+const (
+ 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}"
+ defaultGetKeysEndpoint = "GET /v1/transit/keys"
+ defaultSignDataEndpoint = "PUT /v1/transit/sign/{id}/{algo}"
+
+ listenAddr = "127.0.0.1:0"
+)
+
+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",
+ "policies": [
+ "web",
+ "stage"
+ ],
+ "lease_duration": 3600,
+ "renewable": true
+ }
+}`
+
+ 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",
+ "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
+}`
+
+ testCertAuthResponseNotRenewable = `{
+ "auth": {
+ "client_token": "cf95f87d-f95b-47ff-b1f5-ba7bff850425",
+ "policies": [
+ "web",
+ "stage"
+ ],
+ "lease_duration": 3600,
+ "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
+ }
+}`
+
+ 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": "",
+ "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
+}`
+
+ 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": "",
+ "renewable": false,
+ "lease_duration": 0,
+ "data": {
+ "key_version": 1,
+ "signature": "vault:v1:MEQCIHw3maFgxsmzAUsUXnw2ahUgPcomjF8+XxflwH4CsouhAiAYL3RhWx8dP2ymm7hjSUvc9EQ8GPXmLrvgacqkEKQPGw=="
+ },
+ "wrap_info": null,
+ "warnings": null,
+ "auth": 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
+ 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
+ CreateKeyReqEndpoint string
+ 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
+ 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
+ SignDataResponse []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,
+ RenewReqEndpoint: defaultRenewEndpoint,
+ RenewReqHandler: defaultReqHandler,
+ LookupSelfReqEndpoint: defaultLookupSelfEndpoint,
+ LookupSelfReqHandler: defaultReqHandler,
+ CreateKeyReqEndpoint: defaultCreateKeyEndpoint,
+ CreateKeyReqHandler: defaultReqHandler,
+ GetKeyReqEndpoint: defaultGetKeyEndpoint,
+ GetKeyReqHandler: defaultReqHandler,
+ GetKeysReqEndpoint: defaultGetKeysEndpoint,
+ GetKeysReqHandler: defaultReqHandler,
+ SignDataReqEndpoint: defaultSignDataEndpoint,
+ SignDataReqHandler: 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.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))
+ mux.HandleFunc(v.GetKeysReqEndpoint, v.GetKeysReqHandler(v.GetKeysResponseCode, v.GetKeysResponse))
+ mux.HandleFunc(v.SignDataReqEndpoint, v.SignDataReqHandler(v.SignDataResponseCode, v.SignDataResponse))
+
+ srv = httptest.NewUnstartedServer(mux)
+ srv.Listener = l
+ return srv, l.Addr().String(), nil
+}
diff --git a/test/integration/common b/test/integration/common
index baf0b7d13f..eef46d56b9 100644
--- a/test/integration/common
+++ b/test/integration/common
@@ -224,7 +224,7 @@ download-kind() {
elif [ "${ARCH}" = "aarch64" ]; then
ARCH=arm64
fi
- echo "Ensuring kind version $KINDVERSION is available..."
+ echo "Ensuring kind version $KINDVERSION is available..."
KINDURL="https://github.com/kubernetes-sigs/kind/releases/download/$KINDVERSION/kind-$UNAME-$ARCH"
local kind_link_or_path=$1
@@ -247,7 +247,7 @@ download-helm() {
ARCH=arm64
fi
- echo "Ensuring helm version $HELMVERSION is available..."
+ echo "Ensuring helm version $HELMVERSION is available..."
HELMURL="https://get.helm.sh/helm-${HELMVERSION}-${UNAME}-${ARCH}.tar.gz"
local helm_link_or_path=$1
@@ -274,7 +274,7 @@ download-kubectl() {
ARCH=arm64
fi
- KUBECTLURL="https://storage.googleapis.com/kubernetes-release/release/$WANTVERSION/bin/$UNAME/$ARCH/kubectl"
+ KUBECTLURL="https://dl.k8s.io/release//$WANTVERSION/bin/$UNAME/$ARCH/kubectl"
HAVEVERSION=""
if [ -x "${KUBECTLPATH}" ]; then
diff --git a/test/integration/suites/key-manager-vault/00-setup-kind b/test/integration/suites/key-manager-vault/00-setup-kind
new file mode 100755
index 0000000000..eac0016c0d
--- /dev/null
+++ b/test/integration/suites/key-manager-vault/00-setup-kind
@@ -0,0 +1,33 @@
+#!/bin/bash
+
+# Create a temporary path that will be added to the PATH to avoid picking up
+# binaries from the environment that aren't a version match.
+mkdir -p ./bin
+
+KINDVERSION=v0.24.0
+KUBECTLVERSION=v1.31.2
+K8SIMAGE=kindest/node:v1.31.0
+HELMVERSION=v3.16.2
+
+KIND_PATH=./bin/kind
+KUBECTL_PATH=./bin/kubectl
+HELM_PATH=./bin/helm
+
+# Download kind at the expected version at the given path.
+download-kind "${KIND_PATH}"
+
+# Download kubectl at the expected version.
+download-kubectl "${KUBECTL_PATH}"
+
+# Download helm at the expected version.
+download-helm "${HELM_PATH}"
+
+# Start the kind cluster.
+start-kind-cluster "${KIND_PATH}" vault-test
+
+# Load the given images in the cluster.
+container_images=("spire-server:latest-local")
+load-images "${KIND_PATH}" vault-test "${container_images[@]}"
+
+# Set the kubectl context.
+set-kubectl-context "${KUBECTL_PATH}" kind-vault-test
diff --git a/test/integration/suites/key-manager-vault/01-setup-vault b/test/integration/suites/key-manager-vault/01-setup-vault
new file mode 100755
index 0000000000..fd295725c2
--- /dev/null
+++ b/test/integration/suites/key-manager-vault/01-setup-vault
@@ -0,0 +1,102 @@
+#!/bin/bash
+
+set -e -o pipefail
+
+source init-kubectl
+
+CHARTVERSION=0.28.1
+
+log-info "installing hashicorp vault..."
+
+kubectl-exec-vault() {
+ ./bin/kubectl exec -n vault vault-0 -- $@
+}
+
+log-info "preparing certificates..."
+# Prepare CSR for Vault instance
+openssl ecparam -name prime256v1 -genkey -noout -out vault_key.pem
+openssl req -new \
+ -key vault_key.pem \
+ -out vault_csr.pem \
+ -subj "/C=US/O=system:nodes/CN=system:node:vault" \
+ -reqexts v3 \
+ -config <(cat /etc/ssl/openssl.cnf ; printf "\n[v3]\nsubjectAltName=@alt_names\n[alt_names]\nDNS.1=vault\nDNS.2=vault.vault.svc\nIP.1=127.0.0.1")
+cat > csr.yaml </dev/null) ]]; do sleep 1; done'
+./bin/kubectl get csr -n vault vault.svc -o jsonpath='{.status.certificate}' | openssl base64 -d -A -out vault.pem
+./bin/kubectl config view --raw --minify --flatten -o jsonpath='{.clusters[].cluster.certificate-authority-data}' \
+ | base64 -d > vault_ca.pem
+./bin/kubectl create secret generic vault-tls -n vault \
+ --from-file=vault_key.pem=vault_key.pem \
+ --from-file=vault.pem=vault.pem \
+ --from-file=vault_ca.pem=vault_ca.pem
+
+./bin/helm repo add hashicorp https://helm.releases.hashicorp.com
+./bin/helm install vault hashicorp/vault --namespace vault --version $CHARTVERSION -f conf/helm-values.yaml
+./bin/kubectl wait -n kube-system --for=condition=available deployment --all --timeout=90s
+./bin/kubectl wait pods -n vault --for=jsonpath='{.status.phase}'=Running vault-0 --timeout=90s
+
+# Initialize and unseal
+log-info "initializing hashicorp vault..."
+kubectl-exec-vault vault operator init -key-shares=1 -key-threshold=1 -format=json > cluster-keys.json
+VAULT_UNSEAL_KEY=$(cat cluster-keys.json | jq -r ".unseal_keys_b64[]")
+kubectl-exec-vault vault operator unseal $VAULT_UNSEAL_KEY
+./bin/kubectl wait pods -n vault --for=condition=Ready vault-0 --timeout=60s
+VAULT_ROOT_TOKEN=$(cat cluster-keys.json | jq -r ".root_token")
+kubectl-exec-vault vault login $VAULT_ROOT_TOKEN > /dev/null
+
+./bin/kubectl cp -n vault cert_auth_ca.pem vault-0:/tmp/.
+./bin/kubectl cp -n vault conf/configure-transit-secret-engine.sh vault-0:/tmp/.
+./bin/kubectl cp -n vault conf/spire.hcl vault-0:tmp/.
+./bin/kubectl cp -n vault conf/configure-auth-method.sh vault-0:/tmp/.
+
+# Configure Vault
+log-info "configuring transit secret engine..."
+kubectl-exec-vault /tmp/configure-transit-secret-engine.sh
+log-info "configuring auth methods..."
+kubectl-exec-vault /tmp/configure-auth-method.sh
diff --git a/test/integration/suites/key-manager-vault/02-deploy-spire-and-verify-auth b/test/integration/suites/key-manager-vault/02-deploy-spire-and-verify-auth
new file mode 100755
index 0000000000..a760bff610
--- /dev/null
+++ b/test/integration/suites/key-manager-vault/02-deploy-spire-and-verify-auth
@@ -0,0 +1,46 @@
+#!/bin/bash
+
+set -e -o pipefail
+
+./bin/kubectl create namespace spire
+./bin/kubectl create secret -n spire generic vault-tls \
+ --from-file=vault_ca.pem=vault_ca.pem
+
+# Verify AppRole Auth
+log-info "verifying approle auth..."
+APPROLE_ID=$(./bin/kubectl exec -n vault vault-0 -- vault read --format json auth/approle/role/spire/role-id | jq -r .data.role_id)
+SECRET_ID=$(./bin/kubectl exec -n vault vault-0 -- vault write --format json -f auth/approle/role/spire/secret-id | jq -r .data.secret_id)
+./bin/kubectl create secret -n spire generic vault-credential \
+ --from-literal=approle_id=$APPROLE_ID \
+ --from-literal=secret_id=$SECRET_ID
+./bin/kubectl apply -k ./conf/server/approle-auth
+./bin/kubectl wait pods -n spire -l app=spire-server --for condition=Ready --timeout=60s
+./bin/kubectl delete -k ./conf/server/approle-auth
+./bin/kubectl delete secret -n spire vault-credential
+./bin/kubectl wait pods -n spire -l app=spire-server --for=delete --timeout=60s
+
+# Verify K8s Auth
+log-info "verifying k8s auth..."
+./bin/kubectl apply -k ./conf/server/k8s-auth
+./bin/kubectl wait pods -n spire -l app=spire-server --for condition=Ready --timeout=60s
+./bin/kubectl delete -k ./conf/server/k8s-auth
+./bin/kubectl wait pods -n spire -l app=spire-server --for=delete --timeout=60s
+
+# Verify Cert Auth
+log-info "verifying cert auth..."
+./bin/kubectl create secret -n spire generic vault-credential \
+ --from-file=client.pem=client.pem \
+ --from-file=client_key.pem=client_key.pem
+./bin/kubectl apply -k ./conf/server/cert-auth
+./bin/kubectl wait pods -n spire -l app=spire-server --for condition=Ready --timeout=60s
+./bin/kubectl delete -k ./conf/server/cert-auth
+./bin/kubectl delete secret -n spire vault-credential
+./bin/kubectl wait pods -n spire -l app=spire-server --for=delete --timeout=60s
+
+# Verify Token Auth
+log-info "verifying token auth..."
+TOKEN=$(./bin/kubectl exec -n vault vault-0 -- vault token create -policy=spire -ttl=1m -field=token)
+./bin/kubectl create secret -n spire generic vault-credential \
+ --from-literal=token=$TOKEN
+./bin/kubectl apply -k ./conf/server/token-auth
+./bin/kubectl wait pods -n spire -l app=spire-server --for condition=Ready --timeout=60s
diff --git a/test/integration/suites/key-manager-vault/04-verify-token-renewal.sh b/test/integration/suites/key-manager-vault/04-verify-token-renewal.sh
new file mode 100755
index 0000000000..a467e4bd50
--- /dev/null
+++ b/test/integration/suites/key-manager-vault/04-verify-token-renewal.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+set -eo pipefail
+
+log-debug "verifying token renewal..."
+
+timeout=$(date -ud "1 minute 30 second" +%s)
+count=0
+
+while [ $(date -u +%s) -lt $timeout ]; do
+ count=`./bin/kubectl logs -n spire $(./bin/kubectl get pod -n spire -o name) | echo "$(grep "Successfully renew auth token" || [[ $? == 1 ]])" | wc -l`
+ if [ $count -ge 2 ]; then
+ log-info "token renewal is verified"
+ exit 0
+ fi
+ sleep 10
+done
+
+fail-now "expected number of token renewal log not found"
diff --git a/test/integration/suites/key-manager-vault/README.md b/test/integration/suites/key-manager-vault/README.md
new file mode 100644
index 0000000000..5f4809397f
--- /dev/null
+++ b/test/integration/suites/key-manager-vault/README.md
@@ -0,0 +1,9 @@
+# KeyManager HashiCorp Vault plugin suite
+
+## Description
+
+This suite sets up a Kubernetes cluster using [Kind](https://kind.sigs.k8s.io),
+installs HashiCorp Vault. It then asserts the following:
+
+* SPIRE server successfully requests a key from the referenced Vault Transit Secret Engine
+* Verifies that Auth Methods are configured successfully
diff --git a/test/integration/suites/key-manager-vault/conf/configure-auth-method.sh b/test/integration/suites/key-manager-vault/conf/configure-auth-method.sh
new file mode 100755
index 0000000000..82bb9bc22f
--- /dev/null
+++ b/test/integration/suites/key-manager-vault/conf/configure-auth-method.sh
@@ -0,0 +1,30 @@
+#!/bin/sh
+
+set -e -o pipefail
+
+# Create Policy
+vault policy write spire /tmp/spire.hcl
+
+# Configure Vault Auth Method
+vault auth enable approle
+vault write auth/approle/role/spire \
+ secret_id_ttl=120m \
+ token_ttl=1m \
+ policies="spire"
+
+# Configure K8s Auth Method
+vault auth enable kubernetes
+vault write auth/kubernetes/config kubernetes_host=https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT_HTTPS
+vault write auth/kubernetes/role/my-role \
+ bound_service_account_names=spire-server \
+ bound_service_account_namespaces=spire \
+ token_ttl=1m \
+ policies=spire
+
+# Configure Cert Auth Method
+vault auth enable cert
+vault write auth/cert/certs/my-role \
+ display_name=spire \
+ token_ttl=1m \
+ policies=spire \
+ certificate=@/tmp/cert_auth_ca.pem
diff --git a/test/integration/suites/key-manager-vault/conf/configure-transit-secret-engine.sh b/test/integration/suites/key-manager-vault/conf/configure-transit-secret-engine.sh
new file mode 100755
index 0000000000..ddcd987a13
--- /dev/null
+++ b/test/integration/suites/key-manager-vault/conf/configure-transit-secret-engine.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+set -e -o pipefail
+
+# Configure Root CA
+vault secrets enable transit
+vault secrets tune -max-lease-ttl=8760h transit
diff --git a/test/integration/suites/key-manager-vault/conf/helm-values.yaml b/test/integration/suites/key-manager-vault/conf/helm-values.yaml
new file mode 100644
index 0000000000..6607a9cbad
--- /dev/null
+++ b/test/integration/suites/key-manager-vault/conf/helm-values.yaml
@@ -0,0 +1,33 @@
+global:
+ enabled: true
+ tlsDisable: false
+server:
+ extraEnvironmentVars:
+ VAULT_CACERT: /vault/userconfig/vault-tls/vault_ca.pem
+ VAULT_TLSCERT: /vault/userconfig/vault-tls/vault.pem
+ VAULT_TLSKEY: /vault/userconfig/vault-tls/vault_key.pem
+ volumes:
+ - name: userconfig-vault-tls
+ secret:
+ defaultMode: 420
+ secretName: vault-tls
+ volumeMounts:
+ - mountPath: /vault/userconfig/vault-tls
+ name: userconfig-vault-tls
+ readOnly: true
+ standalone:
+ enabled: "-"
+ config: |
+ listener "tcp" {
+ address = "[::]:8200"
+ cluster_address = "[::]:8201"
+
+ tls_cert_file = "/vault/userconfig/vault-tls/vault.pem"
+ tls_key_file = "/vault/userconfig/vault-tls/vault_key.pem"
+
+ tls_disable_client_certs = false
+ }
+
+ storage "file" {
+ path = "/vault/data"
+ }
diff --git a/test/integration/suites/key-manager-vault/conf/server/approle-auth/kustomization.yaml b/test/integration/suites/key-manager-vault/conf/server/approle-auth/kustomization.yaml
new file mode 100644
index 0000000000..fca71c7ae2
--- /dev/null
+++ b/test/integration/suites/key-manager-vault/conf/server/approle-auth/kustomization.yaml
@@ -0,0 +1,8 @@
+# kustomization.yaml
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+ - ../base
+patchesStrategicMerge:
+ - spire-server.yaml
diff --git a/test/integration/suites/key-manager-vault/conf/server/approle-auth/spire-server.yaml b/test/integration/suites/key-manager-vault/conf/server/approle-auth/spire-server.yaml
new file mode 100644
index 0000000000..af2ea53eed
--- /dev/null
+++ b/test/integration/suites/key-manager-vault/conf/server/approle-auth/spire-server.yaml
@@ -0,0 +1,93 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: spire-server
+ namespace: spire
+data:
+ server.conf: |
+ server {
+ bind_address = "0.0.0.0"
+ bind_port = "8081"
+ trust_domain = "example.org"
+ data_dir = "/run/spire/data"
+ log_level = "DEBUG"
+ default_x509_svid_ttl = "1h"
+ ca_ttl = "12h"
+ ca_subject {
+ country = ["US"]
+ organization = ["SPIFFE"]
+ common_name = ""
+ }
+ }
+
+ plugins {
+ DataStore "sql" {
+ plugin_data {
+ database_type = "sqlite3"
+ connection_string = "/run/spire/data/datastore.sqlite3"
+ }
+ }
+
+ NodeAttestor "k8s_psat" {
+ plugin_data {
+ clusters = {
+ "example-cluster" = {
+ service_account_allow_list = ["spire:spire-agent"]
+ }
+ }
+ }
+ }
+
+ KeyManager "hashicorp_vault" {
+ plugin_data {
+ vault_addr="https://vault.vault.svc:8200/"
+ ca_cert_path="/run/spire/vault/vault_ca.pem"
+ pki_mount_point="pki_int"
+ approle_auth {}
+ }
+ }
+
+ Notifier "k8sbundle" {
+ plugin_data {
+ # This plugin updates the bundle.crt value in the spire:spire-bundle
+ # ConfigMap by default, so no additional configuration is necessary.
+ }
+ }
+ }
+
+ health_checks {
+ listener_enabled = true
+ bind_address = "0.0.0.0"
+ bind_port = "8080"
+ live_path = "/live"
+ ready_path = "/ready"
+ }
+
+---
+
+# This is the Deployment for the SPIRE server. It waits for SPIRE database to
+# initialize and uses the SPIRE healthcheck command for liveness/readiness
+# probes.
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: spire-server
+ namespace: spire
+ labels:
+ app: spire-server
+spec:
+ template:
+ spec:
+ containers:
+ - name: spire-server
+ env:
+ - name: VAULT_APPROLE_ID
+ valueFrom:
+ secretKeyRef:
+ name: vault-credential
+ key: approle_id
+ - name: VAULT_APPROLE_SECRET_ID
+ valueFrom:
+ secretKeyRef:
+ name: vault-credential
+ key: secret_id
diff --git a/test/integration/suites/key-manager-vault/conf/server/base/kustomization.yaml b/test/integration/suites/key-manager-vault/conf/server/base/kustomization.yaml
new file mode 100644
index 0000000000..c87a9a25d0
--- /dev/null
+++ b/test/integration/suites/key-manager-vault/conf/server/base/kustomization.yaml
@@ -0,0 +1,10 @@
+# kustomization.yaml
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+# list of Resource Config to be Applied
+resources:
+ - spire-server.yaml
+
+# namespace to deploy all Resources to
+namespace: spire
diff --git a/test/integration/suites/key-manager-vault/conf/server/base/spire-server.yaml b/test/integration/suites/key-manager-vault/conf/server/base/spire-server.yaml
new file mode 100644
index 0000000000..05b8bfe53c
--- /dev/null
+++ b/test/integration/suites/key-manager-vault/conf/server/base/spire-server.yaml
@@ -0,0 +1,243 @@
+# ServiceAccount used by the SPIRE server.
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: spire-server
+ namespace: spire
+
+---
+
+# Required cluster role to allow spire-server to query k8s API server
+kind: ClusterRole
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+ name: spire-server-cluster-role
+rules:
+ - apiGroups: [""]
+ resources: ["pods", "nodes"]
+ verbs: ["get", "list", "watch"]
+
+---
+
+# Binds above cluster role to spire-server service account
+kind: ClusterRoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+ name: spire-server-cluster-role-binding
+ namespace: spire
+subjects:
+ - kind: ServiceAccount
+ name: spire-server
+ namespace: spire
+roleRef:
+ kind: ClusterRole
+ name: spire-server-cluster-role
+ apiGroup: rbac.authorization.k8s.io
+
+---
+
+# Role for the SPIRE server
+kind: Role
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+ namespace: spire
+ name: spire-server-role
+rules:
+ # allow "get" access to pods (to resolve selectors for PSAT attestation)
+ - apiGroups: [""]
+ resources: ["pods"]
+ verbs: ["get"]
+ # allow access to "get" and "patch" the spire-bundle ConfigMap (for SPIRE
+ # agent bootstrapping, see the spire-bundle ConfigMap below)
+ - apiGroups: [""]
+ resources: ["configmaps"]
+ resourceNames: ["spire-bundle"]
+ verbs: ["get", "patch"]
+ - apiGroups: [""]
+ resources: ["configmaps"]
+ verbs: ["create"]
+ - apiGroups: ["coordination.k8s.io"]
+ resources: ["leases"]
+ verbs: ["create", "update", "get"]
+ - apiGroups: [""]
+ resources: ["events"]
+ verbs: ["create"]
+
+---
+
+# RoleBinding granting the spire-server-role to the SPIRE server
+# service account.
+kind: RoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+ name: spire-server-role-binding
+ namespace: spire
+subjects:
+ - kind: ServiceAccount
+ name: spire-server
+ namespace: spire
+roleRef:
+ kind: Role
+ name: spire-server-role
+ apiGroup: rbac.authorization.k8s.io
+
+---
+
+# ConfigMap containing the latest trust bundle for the trust domain. It is
+# updated by SPIRE using the k8sbundle notifier plugin. SPIRE agents mount
+# this config map and use the certificate to bootstrap trust with the SPIRE
+# server during attestation.
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: spire-bundle
+ namespace: spire
+
+---
+
+# ConfigMap containing the SPIRE server configuration.
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: spire-server
+ namespace: spire
+data:
+ server.conf: |
+ server {
+ bind_address = "0.0.0.0"
+ bind_port = "8081"
+ trust_domain = "example.org"
+ data_dir = "/run/spire/data"
+ log_level = "DEBUG"
+ default_x509_svid_ttl = "1h"
+ ca_ttl = "12h"
+ ca_subject {
+ country = ["US"]
+ organization = ["SPIFFE"]
+ common_name = ""
+ }
+ }
+
+ plugins {
+ DataStore "sql" {
+ plugin_data {
+ database_type = "sqlite3"
+ connection_string = "/run/spire/data/datastore.sqlite3"
+ }
+ }
+
+ NodeAttestor "k8s_psat" {
+ plugin_data {
+ clusters = {
+ "example-cluster" = {
+ service_account_allow_list = ["spire:spire-agent"]
+ }
+ }
+ }
+ }
+
+ KeyManager "disk" {
+ plugin_data {
+ keys_path = "/run/spire/data/keys.json"
+ }
+ }
+
+ UpstreamAuthority "vault" {
+ plugin_data {
+ vault_addr="http://vault.vault.svc:8200/"
+ token_auth {}
+ }
+ }
+
+ Notifier "k8sbundle" {
+ plugin_data {
+ # This plugin updates the bundle.crt value in the spire:spire-bundle
+ # ConfigMap by default, so no additional configuration is necessary.
+ }
+ }
+ }
+
+ health_checks {
+ listener_enabled = true
+ bind_address = "0.0.0.0"
+ bind_port = "8080"
+ live_path = "/live"
+ ready_path = "/ready"
+ }
+
+---
+
+# This is the Deployment for the SPIRE server. It waits for SPIRE database to
+# initialize and uses the SPIRE healthcheck command for liveness/readiness
+# probes.
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: spire-server
+ namespace: spire
+ labels:
+ app: spire-server
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: spire-server
+ template:
+ metadata:
+ namespace: spire
+ labels:
+ app: spire-server
+ spec:
+ serviceAccountName: spire-server
+ shareProcessNamespace: true
+ containers:
+ - name: spire-server
+ image: spire-server:latest-local
+ imagePullPolicy: Never
+ args: ["-config", "/run/spire/config/server.conf"]
+ ports:
+ - containerPort: 8081
+ volumeMounts:
+ - name: spire-config
+ mountPath: /run/spire/config
+ readOnly: true
+ - name: vault-tls
+ mountPath: "/run/spire/vault"
+ readOnly: true
+ livenessProbe:
+ httpGet:
+ path: /live
+ port: 8080
+ initialDelaySeconds: 5
+ periodSeconds: 5
+ readinessProbe:
+ httpGet:
+ path: /ready
+ port: 8080
+ initialDelaySeconds: 5
+ periodSeconds: 5
+ volumes:
+ - name: spire-config
+ configMap:
+ name: spire-server
+ - name: vault-tls
+ secret:
+ secretName: vault-tls
+
+---
+
+# Service definition for SPIRE server defining the gRPC port.
+apiVersion: v1
+kind: Service
+metadata:
+ name: spire-server
+ namespace: spire
+spec:
+ type: NodePort
+ ports:
+ - name: grpc
+ port: 8081
+ targetPort: 8081
+ protocol: TCP
+ selector:
+ app: spire-server
diff --git a/test/integration/suites/key-manager-vault/conf/server/cert-auth/kustomization.yaml b/test/integration/suites/key-manager-vault/conf/server/cert-auth/kustomization.yaml
new file mode 100644
index 0000000000..fca71c7ae2
--- /dev/null
+++ b/test/integration/suites/key-manager-vault/conf/server/cert-auth/kustomization.yaml
@@ -0,0 +1,8 @@
+# kustomization.yaml
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+ - ../base
+patchesStrategicMerge:
+ - spire-server.yaml
diff --git a/test/integration/suites/key-manager-vault/conf/server/cert-auth/spire-server.yaml b/test/integration/suites/key-manager-vault/conf/server/cert-auth/spire-server.yaml
new file mode 100644
index 0000000000..975ca24e44
--- /dev/null
+++ b/test/integration/suites/key-manager-vault/conf/server/cert-auth/spire-server.yaml
@@ -0,0 +1,93 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: spire-server
+ namespace: spire
+data:
+ server.conf: |
+ server {
+ bind_address = "0.0.0.0"
+ bind_port = "8081"
+ trust_domain = "example.org"
+ data_dir = "/run/spire/data"
+ log_level = "DEBUG"
+ default_x509_svid_ttl = "1h"
+ ca_ttl = "12h"
+ ca_subject {
+ country = ["US"]
+ organization = ["SPIFFE"]
+ common_name = ""
+ }
+ }
+
+ plugins {
+ DataStore "sql" {
+ plugin_data {
+ database_type = "sqlite3"
+ connection_string = "/run/spire/data/datastore.sqlite3"
+ }
+ }
+
+ NodeAttestor "k8s_psat" {
+ plugin_data {
+ clusters = {
+ "example-cluster" = {
+ service_account_allow_list = ["spire:spire-agent"]
+ }
+ }
+ }
+ }
+
+ KeyManager "hashicorp_vault" {
+ plugin_data {
+ vault_addr="https://vault.vault.svc:8200/"
+ ca_cert_path="/run/spire/vault/vault_ca.pem"
+ pki_mount_point="pki_int"
+ cert_auth {
+ client_cert_path="/run/spire/vault-auth/client.pem"
+ client_key_path="/run/spire/vault-auth/client_key.pem"
+ }
+ }
+ }
+
+ Notifier "k8sbundle" {
+ plugin_data {
+ # This plugin updates the bundle.crt value in the spire:spire-bundle
+ # ConfigMap by default, so no additional configuration is necessary.
+ }
+ }
+ }
+
+ health_checks {
+ listener_enabled = true
+ bind_address = "0.0.0.0"
+ bind_port = "8080"
+ live_path = "/live"
+ ready_path = "/ready"
+ }
+
+---
+
+# This is the Deployment for the SPIRE server. It waits for SPIRE database to
+# initialize and uses the SPIRE healthcheck command for liveness/readiness
+# probes.
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: spire-server
+ namespace: spire
+ labels:
+ app: spire-server
+spec:
+ template:
+ spec:
+ volumes:
+ - name: vault-credential
+ secret:
+ secretName: vault-credential
+ containers:
+ - name: spire-server
+ volumeMounts:
+ - name: vault-credential
+ mountPath: "/run/spire/vault-auth"
+ readOnly: true
diff --git a/test/integration/suites/key-manager-vault/conf/server/k8s-auth/kustomization.yaml b/test/integration/suites/key-manager-vault/conf/server/k8s-auth/kustomization.yaml
new file mode 100644
index 0000000000..fca71c7ae2
--- /dev/null
+++ b/test/integration/suites/key-manager-vault/conf/server/k8s-auth/kustomization.yaml
@@ -0,0 +1,8 @@
+# kustomization.yaml
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+ - ../base
+patchesStrategicMerge:
+ - spire-server.yaml
diff --git a/test/integration/suites/key-manager-vault/conf/server/k8s-auth/spire-server.yaml b/test/integration/suites/key-manager-vault/conf/server/k8s-auth/spire-server.yaml
new file mode 100644
index 0000000000..a964962596
--- /dev/null
+++ b/test/integration/suites/key-manager-vault/conf/server/k8s-auth/spire-server.yaml
@@ -0,0 +1,67 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: spire-server
+ namespace: spire
+data:
+ server.conf: |
+ server {
+ bind_address = "0.0.0.0"
+ bind_port = "8081"
+ trust_domain = "example.org"
+ data_dir = "/run/spire/data"
+ log_level = "DEBUG"
+ default_x509_svid_ttl = "1h"
+ ca_ttl = "12h"
+ ca_subject {
+ country = ["US"]
+ organization = ["SPIFFE"]
+ common_name = ""
+ }
+ }
+
+ plugins {
+ DataStore "sql" {
+ plugin_data {
+ database_type = "sqlite3"
+ connection_string = "/run/spire/data/datastore.sqlite3"
+ }
+ }
+
+ NodeAttestor "k8s_psat" {
+ plugin_data {
+ clusters = {
+ "example-cluster" = {
+ service_account_allow_list = ["spire:spire-agent"]
+ }
+ }
+ }
+ }
+
+ KeyManager "hashicorp_vault" {
+ plugin_data {
+ vault_addr="https://vault.vault.svc:8200/"
+ ca_cert_path="/run/spire/vault/vault_ca.pem"
+ pki_mount_point="pki_int"
+ k8s_auth {
+ k8s_auth_role_name = "my-role"
+ token_path = "/var/run/secrets/kubernetes.io/serviceaccount/token"
+ }
+ }
+ }
+
+ Notifier "k8sbundle" {
+ plugin_data {
+ # This plugin updates the bundle.crt value in the spire:spire-bundle
+ # ConfigMap by default, so no additional configuration is necessary.
+ }
+ }
+ }
+
+ health_checks {
+ listener_enabled = true
+ bind_address = "0.0.0.0"
+ bind_port = "8080"
+ live_path = "/live"
+ ready_path = "/ready"
+ }
diff --git a/test/integration/suites/key-manager-vault/conf/server/token-auth/kustomization.yaml b/test/integration/suites/key-manager-vault/conf/server/token-auth/kustomization.yaml
new file mode 100644
index 0000000000..fca71c7ae2
--- /dev/null
+++ b/test/integration/suites/key-manager-vault/conf/server/token-auth/kustomization.yaml
@@ -0,0 +1,8 @@
+# kustomization.yaml
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+ - ../base
+patchesStrategicMerge:
+ - spire-server.yaml
diff --git a/test/integration/suites/key-manager-vault/conf/server/token-auth/spire-server.yaml b/test/integration/suites/key-manager-vault/conf/server/token-auth/spire-server.yaml
new file mode 100644
index 0000000000..3613ed7841
--- /dev/null
+++ b/test/integration/suites/key-manager-vault/conf/server/token-auth/spire-server.yaml
@@ -0,0 +1,88 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: spire-server
+ namespace: spire
+data:
+ server.conf: |
+ server {
+ bind_address = "0.0.0.0"
+ bind_port = "8081"
+ trust_domain = "example.org"
+ data_dir = "/run/spire/data"
+ log_level = "DEBUG"
+ default_x509_svid_ttl = "1h"
+ ca_ttl = "12h"
+ ca_subject {
+ country = ["US"]
+ organization = ["SPIFFE"]
+ common_name = ""
+ }
+ }
+
+ plugins {
+ DataStore "sql" {
+ plugin_data {
+ database_type = "sqlite3"
+ connection_string = "/run/spire/data/datastore.sqlite3"
+ }
+ }
+
+ NodeAttestor "k8s_psat" {
+ plugin_data {
+ clusters = {
+ "example-cluster" = {
+ service_account_allow_list = ["spire:spire-agent"]
+ }
+ }
+ }
+ }
+
+ KeyManager "hashicorp_vault" {
+ plugin_data {
+ vault_addr="https://vault.vault.svc:8200/"
+ ca_cert_path="/run/spire/vault/vault_ca.pem"
+ pki_mount_point="pki_int"
+ token_auth {}
+ }
+ }
+
+ Notifier "k8sbundle" {
+ plugin_data {
+ # This plugin updates the bundle.crt value in the spire:spire-bundle
+ # ConfigMap by default, so no additional configuration is necessary.
+ }
+ }
+ }
+
+ health_checks {
+ listener_enabled = true
+ bind_address = "0.0.0.0"
+ bind_port = "8080"
+ live_path = "/live"
+ ready_path = "/ready"
+ }
+
+---
+
+# This is the Deployment for the SPIRE server. It waits for SPIRE database to
+# initialize and uses the SPIRE healthcheck command for liveness/readiness
+# probes.
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: spire-server
+ namespace: spire
+ labels:
+ app: spire-server
+spec:
+ template:
+ spec:
+ containers:
+ - name: spire-server
+ env:
+ - name: VAULT_TOKEN
+ valueFrom:
+ secretKeyRef:
+ name: vault-credential
+ key: token
diff --git a/test/integration/suites/key-manager-vault/conf/spire.hcl b/test/integration/suites/key-manager-vault/conf/spire.hcl
new file mode 100644
index 0000000000..0f3bb2116a
--- /dev/null
+++ b/test/integration/suites/key-manager-vault/conf/spire.hcl
@@ -0,0 +1,3 @@
+path "transit/*" {
+ capabilities = [ "create", "read", "update", "delete", "list" ]
+}
diff --git a/test/integration/suites/key-manager-vault/init-kubectl b/test/integration/suites/key-manager-vault/init-kubectl
new file mode 100644
index 0000000000..a167777059
--- /dev/null
+++ b/test/integration/suites/key-manager-vault/init-kubectl
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+KUBECONFIG="${RUNDIR}/kubeconfig"
+if [ ! -f "${RUNDIR}/kubeconfig" ]; then
+ ./bin/kind get kubeconfig --name=vault-test > "${RUNDIR}/kubeconfig"
+fi
+export KUBECONFIG
+
diff --git a/test/integration/suites/key-manager-vault/teardown b/test/integration/suites/key-manager-vault/teardown
new file mode 100755
index 0000000000..83c7ae74e5
--- /dev/null
+++ b/test/integration/suites/key-manager-vault/teardown
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+source init-kubectl
+
+if [ -z "$SUCCESS" ]; then
+ ./bin/kubectl -nspire logs deployment/spire-server --all-containers || true
+fi
+
+export KUBECONFIG=
+./bin/kind delete cluster --name vault-test