Skip to content

Commit

Permalink
Support certificate authentication (spiffe#5058)
Browse files Browse the repository at this point in the history
  • Loading branch information
InverseIntegral committed Sep 11, 2024
1 parent d2e173c commit 0adbae3
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 0 deletions.
34 changes: 34 additions & 0 deletions pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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/<mount_point>/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
Expand Down Expand Up @@ -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
}
Expand All @@ -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 {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN CERTIFICATE-----
MIIBKDCBz6ADAgECAgEDMAoGCCqGSM49BAMCMAAwIBgPMDAwMTAxMDEwMDAwMDBa
Fw0zMjA0MTIxNjA4NDRaMAAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQymtYU
je8Cue4bRUr76kUGb5F2iyM/Isxt8khYmCRi3TsW21NrOGHmFpIWQ6OVya7UHR0v
QbutQJAflrR12cqeozgwNjATBgNVHSUEDDAKBggrBgEFBQcDAjAfBgNVHSMEGDAW
gBSYSzYwHNQsGiZXSVYDs59w3+UYNzAKBggqhkjOPQQDAgNIADBFAiEAzcRL2tVT
GpPtq6sJKN9quQcX8xxHq7NAxQ8u10C6UegCIECAEW+D8mNP2nM5J+6eSE7DGQ5d
FQZvf0i+L7y0UQQ3
-----END CERTIFICATE-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgC3sFQg3WCrosxeWb
pT67H8HE/lOcPq+zc6BMss947J6hRANCAAQymtYUje8Cue4bRUr76kUGb5F2iyM/
Isxt8khYmCRi3TsW21NrOGHmFpIWQ6OVya7UHR0vQbutQJAflrR12cqe
-----END PRIVATE KEY-----
93 changes: 93 additions & 0 deletions pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 0adbae3

Please sign in to comment.