Skip to content

Commit e31d455

Browse files
Rework certificate authentication client api (#29931)
* Rework certificate authentication api - Use the passed in Vault api client to perform the connection - This provides namespace support, retry behaviors and uses the existing secret parsing logic instead of re-implementing it - Change the cert auth role to be an optional argument - Allow users to use a different cert auth mount point * Clean up test name Co-authored-by: Amir Aslamov <[email protected]> --------- Co-authored-by: Amir Aslamov <[email protected]>
1 parent 758727b commit e31d455

File tree

5 files changed

+321
-158
lines changed

5 files changed

+321
-158
lines changed

CODEOWNERS

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
/enos/ @hashicorp/quality-team
6868

6969
# Cryptosec
70+
/api/auth/cert/ @hashicorp/vault-crypto
7071
/builtin/logical/pki/ @hashicorp/vault-crypto
7172
/builtin/logical/pkiext/ @hashicorp/vault-crypto
7273
/website/content/docs/secrets/pki/ @hashicorp/vault-crypto @hashicorp/vault-education-approvers

api/auth/cert/cert.go

+80-96
Original file line numberDiff line numberDiff line change
@@ -4,49 +4,32 @@
44
package cert
55

66
import (
7-
"bytes"
87
"context"
9-
"crypto/tls"
10-
"encoding/json"
8+
"errors"
119
"fmt"
12-
"io"
13-
"net/http"
1410

15-
"github.com/hashicorp/go-rootcerts"
1611
"github.com/hashicorp/vault/api"
1712
)
1813

14+
const defaultMountPath = "cert"
15+
1916
type CertAuth struct {
20-
role string
21-
caCert string
22-
caCertBytes []byte
23-
clientCert string
24-
clientKey string
25-
insecureSkipVerify bool
17+
role string
18+
mountPath string
2619
}
2720

2821
var _ api.AuthMethod = (*CertAuth)(nil)
2922

3023
type LoginOption func(a *CertAuth) error
3124

3225
// NewCertAuth initializes a new Cert auth method interface to be
33-
// passed as a parameter to the client.Auth().Login method.
26+
// passed as a parameter to the client.Auth().Login method. The client and other
27+
// TLS configuration should be set up on the passed in client, you can use
28+
// the NewCertAuthClient function to set up the client with the proper TLS client attributes.
3429
//
35-
// Supported options: WithCACert, WithCACertBytes, WithInsecure
36-
func NewCertAuth(roleName, clientCert, clientKey string, opts ...LoginOption) (*CertAuth, error) {
37-
if roleName == "" {
38-
return nil, fmt.Errorf("no role name provided for login")
39-
}
40-
41-
if clientCert == "" || clientKey == "" {
42-
return nil, fmt.Errorf("client certificate and key must be provided")
43-
}
44-
45-
a := &CertAuth{
46-
role: roleName,
47-
clientCert: clientCert,
48-
clientKey: clientKey,
49-
}
30+
// Supported options: WithRole, WithMountPath
31+
func NewCertAuth(opts ...LoginOption) (*CertAuth, error) {
32+
a := &CertAuth{}
5033

5134
// Loop through each option
5235
for _, opt := range opts {
@@ -58,111 +41,112 @@ func NewCertAuth(roleName, clientCert, clientKey string, opts ...LoginOption) (*
5841
}
5942
}
6043

44+
if a.mountPath == "" {
45+
a.mountPath = defaultMountPath
46+
}
47+
6148
// return the modified auth struct instance
6249
return a, nil
6350
}
6451

6552
// Login sets up the required request body for the Cert auth method's /login
66-
// endpoint, and performs a write to it.
67-
// It adds the client cert and key to the request.
53+
// endpoint, and performs a write to it. We assume the passed in client has the
54+
// proper TLS client certificates set up.
6855
func (a *CertAuth) Login(ctx context.Context, client *api.Client) (*api.Secret, error) {
6956
if ctx == nil {
7057
ctx = context.Background()
7158
}
7259

73-
c, err := a.httpClient()
74-
if err != nil {
75-
return nil, err
60+
if client == nil {
61+
return nil, fmt.Errorf("client is required for login with the associated client certs initialized")
7662
}
7763

78-
reqBody, err := json.Marshal(map[string]interface{}{
79-
"name": a.role,
80-
})
81-
if err != nil {
82-
return nil, fmt.Errorf("unable to marshal login data: %w", err)
64+
loginData := make(map[string]interface{})
65+
if a.role != "" {
66+
loginData["name"] = a.role
8367
}
8468

85-
url := fmt.Sprintf("%s/v1/auth/cert/login", client.Address())
86-
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(reqBody))
87-
if err != nil {
88-
return nil, fmt.Errorf("unable to create request: %w", err)
89-
}
90-
91-
resp, err := c.Do(req)
69+
certAuthPath := fmt.Sprintf("auth/%s/login", a.mountPath)
70+
resp, err := client.Logical().WriteWithContext(ctx, certAuthPath, loginData)
9271
if err != nil {
9372
return nil, fmt.Errorf("unable to log in with cert auth: %w", err)
9473
}
9574

96-
defer resp.Body.Close()
75+
return resp, nil
76+
}
9777

98-
body, err := io.ReadAll(resp.Body)
99-
if err != nil {
100-
return nil, fmt.Errorf("unable to read response body: %w", err)
78+
// WithRole specifies the role to use for the login request.
79+
func WithRole(roleName string) LoginOption {
80+
return func(a *CertAuth) error {
81+
a.role = roleName
82+
return nil
10183
}
84+
}
10285

103-
if resp.StatusCode >= 400 {
104-
return nil, fmt.Errorf("unable to log in with cert auth, response code: %d. response body: %s", resp.StatusCode, string(body))
86+
// WithMountPath specifies the mount path to use for the login request.
87+
func WithMountPath(mountPath string) LoginOption {
88+
return func(a *CertAuth) error {
89+
a.mountPath = mountPath
90+
return nil
10591
}
92+
}
10693

107-
var secret api.Secret
108-
if err := json.Unmarshal(body, &secret); err != nil {
109-
return nil, fmt.Errorf("unable to unmarshal response body: %w", err)
94+
// NewDefaultCertAuthClient initializes a new client with a default configuration
95+
// with the provided address and TLS configuration. The TLSConfig must have the ClientCert and ClientKey
96+
// fields set.
97+
func NewDefaultCertAuthClient(address string, tlsConfig *api.TLSConfig) (*api.Client, error) {
98+
if tlsConfig == nil {
99+
return nil, errors.New("tls config is required for cert auth client")
110100
}
111101

112-
return &secret, nil
113-
}
102+
if tlsConfig.ClientCert == "" || tlsConfig.ClientKey == "" {
103+
return nil, errors.New("client cert and key are required for cert auth client")
104+
}
114105

115-
func (a *CertAuth) httpClient() (*http.Client, error) {
116-
cert, err := tls.LoadX509KeyPair(a.clientCert, a.clientKey)
117-
if err != nil {
118-
return nil, fmt.Errorf("unable to load cert: %w", err)
106+
if len(address) == 0 {
107+
return nil, errors.New("address is required for cert auth client")
119108
}
120109

121-
tlsConfig := &tls.Config{
122-
InsecureSkipVerify: a.insecureSkipVerify,
123-
Certificates: []tls.Certificate{cert},
110+
cfg := api.DefaultConfig()
111+
cfg.Address = address
112+
err := cfg.ConfigureTLS(tlsConfig)
113+
if err != nil {
114+
return nil, fmt.Errorf("failed configuring TLS on client config: %w", err)
124115
}
125116

126-
if a.caCert != "" || len(a.caCertBytes) > 0 {
127-
err = rootcerts.ConfigureTLS(tlsConfig, &rootcerts.Config{
128-
CAPath: a.caCert,
129-
CACertificate: a.caCertBytes,
130-
})
131-
if err != nil {
132-
return nil, fmt.Errorf("unable to configure TLS: %w", err)
133-
}
117+
client, err := api.NewClient(cfg)
118+
if err != nil {
119+
return nil, fmt.Errorf("failed to create client: %v", err)
134120
}
135121

136-
return &http.Client{
137-
Transport: &http.Transport{
138-
TLSClientConfig: tlsConfig,
139-
},
140-
}, nil
122+
return client, nil
141123
}
142124

143-
// WithCACert sets the CA cert to be used for the login request.
144-
// caCert is the path to the CA cert file.
145-
func WithCACert(caCert string) LoginOption {
146-
return func(a *CertAuth) error {
147-
a.caCert = caCert
148-
return nil
125+
// NewCertAuthClient initializes a new client based on the passed in client
126+
// with the provided TLS configuration. The TLSConfig must have the ClientCert and ClientKey
127+
// fields set.
128+
func NewCertAuthClient(c *api.Client, config *api.TLSConfig) (*api.Client, error) {
129+
if c == nil {
130+
return nil, errors.New("base client is required for cert auth client")
131+
}
132+
if config == nil {
133+
return nil, errors.New("tls config is required for cert auth client")
149134
}
150-
}
151135

152-
// WithCACertBytes sets the CA cert to be used for the login request.
153-
// caCertBytes is the bytes of the CA cert.
154-
// caCertBytes takes precedence over caCert.
155-
func WithCACertBytes(caCertBytes []byte) LoginOption {
156-
return func(a *CertAuth) error {
157-
a.caCertBytes = caCertBytes
158-
return nil
136+
if config.ClientCert == "" || config.ClientKey == "" {
137+
return nil, errors.New("client cert and key are required for cert auth client")
159138
}
160-
}
161139

162-
// WithInsecure skips the verification of the server's certificate chain and host name.
163-
func WithInsecure() LoginOption {
164-
return func(a *CertAuth) error {
165-
a.insecureSkipVerify = true
166-
return nil
140+
conf := c.CloneConfig()
141+
err := conf.ConfigureTLS(config)
142+
if err != nil {
143+
return nil, fmt.Errorf("failed to configure TLS on client: %w", err)
167144
}
145+
146+
client, err := api.NewClient(conf)
147+
if err != nil {
148+
return nil, fmt.Errorf("failed to create client: %v", err)
149+
}
150+
151+
return client, nil
168152
}

0 commit comments

Comments
 (0)