From 0adbae3687a8aebced3087eb9e0795b69bd28177 Mon Sep 17 00:00:00 2001 From: Inverse Integral Date: Wed, 11 Sep 2024 22:56:37 +0200 Subject: [PATCH] Support certificate authentication (#5058) --- .../hashicorpvault/hashicorp_vault.go | 34 +++++++ .../hashicorpvault/testdata/client-cert.pem | 9 ++ .../hashicorpvault/testdata/client-key.pem | 5 + .../hashicorpvault/vault_client_test.go | 93 +++++++++++++++++++ .../hashicorpvault/vault_fake_test.go | 12 +++ 5 files changed, 153 insertions(+) create mode 100644 pkg/server/plugin/keymanager/hashicorpvault/testdata/client-cert.pem create mode 100644 pkg/server/plugin/keymanager/hashicorpvault/testdata/client-key.pem diff --git a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go index f6485625ef9..a4862b3da2f 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go @@ -55,11 +55,16 @@ type Config struct { Namespace string `hcl:"namespace" json:"namespace"` // TransitEnginePath specifies the path to the transit engine to perform key operations. TransitEnginePath string `hcl:"transit_engine_path" json:"transit_engine_path"` + // If true, vault client accepts any server certificates. + // It should be used only test environment so on. + InsecureSkipVerify bool `hcl:"insecure_skip_verify" json:"insecure_skip_verify"` // Configuration for the Token authentication method TokenAuth *TokenAuthConfig `hcl:"token_auth" json:"token_auth,omitempty"` // Configuration for the AppRole authentication method AppRoleAuth *AppRoleAuthConfig `hcl:"approle_auth" json:"approle_auth,omitempty"` + // Configuration for the Client Certificate authentication method + CertAuth *CertAuthConfig `hcl:"cert_auth" json:"cert_auth,omitempty"` // TODO: Support other auth methods // TODO: Support client certificate and key @@ -82,6 +87,22 @@ type AppRoleAuthConfig struct { SecretID string `hcl:"approle_secret_id" json:"approle_secret_id"` } +// CertAuthConfig represents parameters for cert auth method +type CertAuthConfig struct { + // Name of the mount point where Client Certificate Auth method is mounted. (e.g., /auth//login) + // If the value is empty, use default mount point (/auth/cert) + CertAuthMountPoint string `hcl:"cert_auth_mount_point" json:"cert_auth_mount_point"` + // Name of the Vault role. + // If given, the plugin authenticates against only the named role. + CertAuthRoleName string `hcl:"cert_auth_role_name" json:"cert_auth_role_name"` + // Path to a client certificate file. + // Only PEM format is supported. + ClientCertPath string `hcl:"client_cert_path" json:"client_cert_path"` + // Path to a client private key file. + // Only PEM format is supported. + ClientKeyPath string `hcl:"client_key_path" json:"client_key_path"` +} + // Plugin is the main representation of this keymanager plugin type Plugin struct { keymanagerv1.UnsafeKeyManagerServer @@ -160,6 +181,13 @@ func parseAuthMethod(config *Config) (AuthMethod, error) { authMethod = APPROLE } + if config.CertAuth != nil { + if err := checkForAuthMethodConfigured(authMethod); err != nil { + return 0, err + } + authMethod = CERT + } + if authMethod != 0 { return authMethod, nil } @@ -179,6 +207,7 @@ func (p *Plugin) genClientParams(method AuthMethod, config *Config) (*ClientPara VaultAddr: p.getEnvOrDefault(envVaultAddr, config.VaultAddr), Namespace: p.getEnvOrDefault(envVaultNamespace, config.Namespace), TransitEnginePath: p.getEnvOrDefault(envVaultTransitEnginePath, config.TransitEnginePath), + TLSSKipVerify: config.InsecureSkipVerify, } switch method { @@ -188,6 +217,11 @@ func (p *Plugin) genClientParams(method AuthMethod, config *Config) (*ClientPara cp.AppRoleAuthMountPoint = config.AppRoleAuth.AppRoleMountPoint cp.AppRoleID = p.getEnvOrDefault(envVaultAppRoleID, config.AppRoleAuth.RoleID) cp.AppRoleSecretID = p.getEnvOrDefault(envVaultAppRoleSecretID, config.AppRoleAuth.SecretID) + case CERT: + cp.CertAuthMountPoint = config.CertAuth.CertAuthMountPoint + cp.CertAuthRoleName = config.CertAuth.CertAuthRoleName + cp.ClientCertPath = p.getEnvOrDefault(envVaultClientCert, config.CertAuth.ClientCertPath) + cp.ClientKeyPath = p.getEnvOrDefault(envVaultClientKey, config.CertAuth.ClientKeyPath) } return cp, nil diff --git a/pkg/server/plugin/keymanager/hashicorpvault/testdata/client-cert.pem b/pkg/server/plugin/keymanager/hashicorpvault/testdata/client-cert.pem new file mode 100644 index 00000000000..ab411834a04 --- /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 00000000000..c9fcac5019c --- /dev/null +++ b/pkg/server/plugin/keymanager/hashicorpvault/testdata/client-key.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgC3sFQg3WCrosxeWb +pT67H8HE/lOcPq+zc6BMss947J6hRANCAAQymtYUje8Cue4bRUr76kUGb5F2iyM/ +Isxt8khYmCRi3TsW21NrOGHmFpIWQ6OVya7UHR0vQbutQJAflrR12cqe +-----END PRIVATE KEY----- diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go index 0f7801650bd..25c561bd527 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go @@ -16,6 +16,8 @@ const ( testRootCert = "testdata/root-cert.pem" testServerCert = "testdata/server-cert.pem" testServerKey = "testdata/server-key.pem" + testClientCert = "testdata/client-cert.pem" + testClientKey = "testdata/client-key.pem" ) func TestNewClientConfigWithDefaultValues(t *testing.T) { @@ -211,6 +213,97 @@ func TestNewAuthenticatedClientAppRoleAuth(t *testing.T) { } } +func TestNewAuthenticatedClientCertAuth(t *testing.T) { + fakeVaultServer := newFakeVaultServer() + fakeVaultServer.CertAuthResponseCode = 200 + for _, tt := range []struct { + name string + response []byte + renew bool + namespace string + }{ + { + name: "Cert Authentication success / Token is renewable", + response: []byte(testCertAuthResponse), + renew: true, + }, + { + name: "Cert Authentication success / Token is not renewable", + response: []byte(testCertAuthResponseNotRenewable), + }, + { + name: "Cert Authentication success / Token is renewable / Namespace is given", + response: []byte(testCertAuthResponse), + renew: true, + namespace: "test-ns", + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + fakeVaultServer.CertAuthResponse = tt.response + + s, addr, err := fakeVaultServer.NewTLSServer() + require.NoError(t, err) + + s.Start() + defer s.Close() + + cp := &ClientParams{ + VaultAddr: fmt.Sprintf("https://%v/", addr), + Namespace: tt.namespace, + CACertPath: testRootCert, + ClientCertPath: testClientCert, + ClientKeyPath: testClientKey, + } + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + renewCh := make(chan struct{}) + client, err := cc.NewAuthenticatedClient(CERT, renewCh) + require.NoError(t, err) + + select { + case <-renewCh: + require.Equal(t, false, tt.renew) + default: + require.Equal(t, true, tt.renew) + } + + if cp.Namespace != "" { + headers := client.vaultClient.Headers() + require.Equal(t, cp.Namespace, headers.Get(consts.NamespaceHeaderName)) + } + }) + } +} + +func TestNewAuthenticatedClientCertAuthFailed(t *testing.T) { + fakeVaultServer := newFakeVaultServer() + fakeVaultServer.CertAuthResponseCode = 500 + + s, addr, err := fakeVaultServer.NewTLSServer() + require.NoError(t, err) + + s.Start() + defer s.Close() + + retry := 0 // Disable retry + cp := &ClientParams{ + MaxRetries: &retry, + VaultAddr: fmt.Sprintf("https://%v/", addr), + CACertPath: testRootCert, + ClientCertPath: testClientCert, + ClientKeyPath: testClientKey, + } + + cc, err := NewClientConfig(cp, hclog.Default()) + require.NoError(t, err) + + renewCh := make(chan struct{}) + _, err = cc.NewAuthenticatedClient(CERT, renewCh) + spiretest.RequireGRPCStatusHasPrefix(t, err, codes.Unauthenticated, "authentication failed auth/cert/login: Error making API request.") +} + func TestRenewTokenFailed(t *testing.T) { fakeVaultServer := newFakeVaultServer() fakeVaultServer.LookupSelfResponse = []byte(testLookupSelfResponseShortTTL) diff --git a/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go b/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go index f709aab4e89..fa1aa14648a 100644 --- a/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go +++ b/pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go @@ -199,6 +199,18 @@ var ( }, "warnings": null }` + + testCertAuthResponseNotRenewable = `{ + "auth": { + "client_token": "cf95f87d-f95b-47ff-b1f5-ba7bff850425", + "policies": [ + "web", + "stage" + ], + "lease_duration": 3600, + "renewable": false + } +}` ) type FakeVaultServerConfig struct {