Skip to content

Commit

Permalink
Add user-agent header to Venafi API client
Browse files Browse the repository at this point in the history
Signed-off-by: Richard Wall <richard.wall@venafi.com>
  • Loading branch information
wallrj committed Mar 20, 2024
1 parent f56fc1e commit 09125e7
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 61 deletions.
174 changes: 113 additions & 61 deletions pkg/issuer/venafi/client/venaficlient.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
"github.com/cert-manager/cert-manager/pkg/issuer/venafi/client/api"
"github.com/cert-manager/cert-manager/pkg/metrics"
"github.com/cert-manager/cert-manager/pkg/util"
)

const (
Expand Down Expand Up @@ -128,6 +129,8 @@ func New(namespace string, secretsLister internalinformers.SecretLister, issuer
// that can be used to instantiate an API client.
func configForIssuer(iss cmapi.GenericIssuer, secretsLister internalinformers.SecretLister, namespace string) (*vcert.Config, error) {
venCfg := iss.GetSpec().Venafi
var vcertConfig *vcert.Config

switch {
case venCfg.TPP != nil:
tpp := venCfg.TPP
Expand All @@ -139,13 +142,53 @@ func configForIssuer(iss cmapi.GenericIssuer, secretsLister internalinformers.Se
username := string(tppSecret.Data[tppUsernameKey])
password := string(tppSecret.Data[tppPasswordKey])
accessToken := string(tppSecret.Data[tppAccessTokenKey])

return &vcert.Config{
// httpClientForVcert creates an HTTP client and customises it to allow client TLS renegotiation.
//
// Here's why:
//
// 1. The TPP API server is served by Microsoft Windows Server and IIS.
// 2. IIS uses TLS-1.2 by default[1] and it uses a
// TLS-1.2 feature called "renegotiation" to allow client certificate
// settings to be configured at the folder level. e.g.
// https://tpp.example.com/vedauth may Require or Accept client
// certificates while https://tpp.example.com/vedsdk may Ignore
// client certificates.
// 3. When IIS is configured this way it behaves as follows[2]:
// "Server receives a connection request on port 443; it begins a
// handshake. The server does not ask for a client certificate. Once
// the handshake is completed, the client sends the actual target URL
// as a HTTP request in the SSL tunnel. Up to that point, the server
// did not know which page was targeted; it only knew, at best, the
// intended server name (through the Server Name Indication). Now
// that the server knows which page is targeted, he knows which
// "site" (i.e. part of the server, in IIS terminology) is to be
// used."
// 4. In this scenario, the Go HTTP client MUST be configured to
// renegotiate (by default it will refuse to renegotiate).
// We use RenegotiateOnceAsClient rather than RenegotiateFreelyAsClient
// because cert-manager establishes a new HTTPS connection for each API
// request and therefore should only ever need to renegotiate once in this
// scenario.
// 5. But overriding the HTTP client causes vcert to ignore the
// `vcert.Config.ConnectionTrust` field, so we also have to set up the root
// CA trust pool ourselves.
// 6. And the value of RootCAs MUST be nil unless the user has supplied a
// custom CA, because a nil value causes the Go HTTP client to load the
// system default root CAs.
//
// [1] TLS protocol version support in Microsoft Windows: https://learn.microsoft.com/en-us/windows/win32/secauthn/protocols-in-tls-ssl--schannel-ssp-#tls-protocol-version-support
// [2] Should I use SSL/TLS renegotiation?: https://security.stackexchange.com/a/24569
httpClient, err := httpClientForVcert(
withCABundle(tpp.CABundle),
// Enable TLS 1.2 renegotiation (see earlier comment for justification).
withTLSRenegotiation(tls.RenegotiateOnceAsClient),
)
if err != nil {
return nil, err
}
vcertConfig = &vcert.Config{
ConnectorType: endpoint.ConnectorTypeTPP,
BaseUrl: tpp.URL,
Zone: venCfg.Zone,
// always enable verbose logging for now
LogVerbose: true,
// We supply the CA bundle here, to trigger the vcert's builtin
// validation of the supplied PEM content.
// This is somewhat redundant because the value (if valid) will be
Expand All @@ -159,8 +202,8 @@ func configForIssuer(iss cmapi.GenericIssuer, secretsLister internalinformers.Se
Password: password,
AccessToken: accessToken,
},
Client: httpClientForVcertTPP(tpp.CABundle),
}, nil
Client: httpClient,
}
case venCfg.Cloud != nil:
cloud := venCfg.Cloud
cloudSecret, err := secretsLister.Secrets(namespace).Get(cloud.APITokenSecretRef.Name)
Expand All @@ -174,59 +217,70 @@ func configForIssuer(iss cmapi.GenericIssuer, secretsLister internalinformers.Se
}
apiKey := string(cloudSecret.Data[k])

return &vcert.Config{
httpClient, err := httpClientForVcert()
if err != nil {
return nil, err
}

vcertConfig = &vcert.Config{
ConnectorType: endpoint.ConnectorTypeCloud,
BaseUrl: cloud.URL,
Zone: venCfg.Zone,
// always enable verbose logging for now
LogVerbose: true,
Credentials: &endpoint.Authentication{
APIKey: apiKey,
},
}, nil

Client: httpClient,
}
default:
// API validation in webhook and in the ClusterIssuer and Issuer controller
// Sync functions should make this unreachable in production.
return nil, fmt.Errorf("neither Venafi Cloud or TPP configuration found")
}
// API validation in webhook and in the ClusterIssuer and Issuer controller
// Sync functions should make this unreachable in production.
return nil, fmt.Errorf("neither Venafi Cloud or TPP configuration found")

// TPP and Cloud both use the same Zone parameter
vcertConfig.Zone = venCfg.Zone

// always enable verbose logging for now
vcertConfig.LogVerbose = true

// Set the user-agent header
vcertConfig.Client.Transport = util.UserAgentRoundTripper(vcertConfig.Client.Transport, "cert-manager/v0.0.0")

return vcertConfig, nil
}

// httpClientForVcertTPP creates an HTTP client and customises it to allow client TLS renegotiation.
//
// Here's why:
//
// 1. The TPP API server is served by Microsoft Windows Server and IIS.
// 2. IIS uses TLS-1.2 by default[1] and it uses a
// TLS-1.2 feature called "renegotiation" to allow client certificate
// settings to be configured at the folder level. e.g.
// https://tpp.example.com/vedauth may Require or Accept client
// certificates while https://tpp.example.com/vedsdk may Ignore
// client certificates.
// 3. When IIS is configured this way it behaves as follows[2]:
// "Server receives a connection request on port 443; it begins a
// handshake. The server does not ask for a client certificate. Once
// the handshake is completed, the client sends the actual target URL
// as a HTTP request in the SSL tunnel. Up to that point, the server
// did not know which page was targeted; it only knew, at best, the
// intended server name (through the Server Name Indication). Now
// that the server knows which page is targeted, he knows which
// "site" (i.e. part of the server, in IIS terminology) is to be
// used."
// 4. In this scenario, the Go HTTP client MUST be configured to
// renegotiate (by default it will refuse to renegotiate).
// We use RenegotiateOnceAsClient rather than RenegotiateFreelyAsClient
// because cert-manager establishes a new HTTPS connection for each API
// request and therefore should only ever need to renegotiate once in this
// scenario.
// 5. But overriding the HTTP client causes vcert to ignore the
// `vcert.Config.ConnectionTrust` field, so we also have to set up the root
// CA trust pool ourselves.
// 6. And the value of RootCAs MUST be nil unless the user has supplied a
// custom CA, because a nil value causes the Go HTTP client to load the
// system default root CAs.
//
// [1] TLS protocol version support in Microsoft Windows: https://learn.microsoft.com/en-us/windows/win32/secauthn/protocols-in-tls-ssl--schannel-ssp-#tls-protocol-version-support
// [2] Should I use SSL/TLS renegotiation?: https://security.stackexchange.com/a/24569
func httpClientForVcertTPP(caBundle []byte) *http.Client {
type option func(*http.Client) error

func withCABundle(caBundle []byte) option {
return func(c *http.Client) error {
if len(caBundle) == 0 {
return nil
}
rootCAs := x509.NewCertPool()
if !rootCAs.AppendCertsFromPEM(caBundle) {
return fmt.Errorf("failed to add caBundle to pool")
}
transport, ok := c.Transport.(*http.Transport)
if !ok {
return fmt.Errorf("failed to cast http client transport")
}
transport.TLSClientConfig.RootCAs = rootCAs
return nil
}
}

func withTLSRenegotiation(tlsRenegotiation tls.RenegotiationSupport) option {
return func(c *http.Client) error {
transport, ok := c.Transport.(*http.Transport)
if !ok {
return fmt.Errorf("failed to cast http client transport")
}
transport.TLSClientConfig.Renegotiation = tlsRenegotiation
return nil
}
}

func httpClientForVcert(options ...option) (*http.Client, error) {
// Copy vcert's default HTTP transport, which is mostly identical to the
// http.DefaultTransport settings in Go's stdlib.
// https://github.com/Venafi/vcert/blob/89645a7710a7b529765274cb60dc5e28066217a1/pkg/venafi/tpp/tpp.go#L481-L513
Expand All @@ -250,22 +304,20 @@ func httpClientForVcertTPP(caBundle []byte) *http.Client {
if tlsClientConfig == nil {
tlsClientConfig = &tls.Config{}
}
if len(caBundle) > 0 {
rootCAs := x509.NewCertPool()
rootCAs.AppendCertsFromPEM(caBundle)
tlsClientConfig.RootCAs = rootCAs
}
transport.TLSClientConfig = tlsClientConfig

// Enable TLS 1.2 renegotiation (see earlier comment for justification).
transport.TLSClientConfig.Renegotiation = tls.RenegotiateOnceAsClient

// Copy vcert's initialization of the HTTP client, which overrides the default timeout.
// https://github.com/Venafi/vcert/blob/89645a7710a7b529765274cb60dc5e28066217a1/pkg/venafi/tpp/tpp.go#L481-L513
return &http.Client{
c := &http.Client{
Transport: transport,
Timeout: time.Second * 30,
}
for _, o := range options {
if err := o(c); err != nil {
return nil, err
}
}
return c, nil
}

func (v *Venafi) Ping() error {
Expand Down
23 changes: 23 additions & 0 deletions pkg/util/useragent.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package util
import (
"bytes"
"fmt"
"net/http"
"strings"
"unicode"
"unicode/utf8"
Expand Down Expand Up @@ -58,3 +59,25 @@ func PrefixFromUserAgent(u string) string {
}
return buf.String()
}

// UserAgentRoundTripper implements the http.RoundTripper interface and adds a User-Agent
// header.
type userAgentRoundTripper struct {
inner http.RoundTripper
userAgent string
}

// UserAgentRoundTripper returns a RoundTripper that functions identically to
// the provided 'inner' round tripper, other than also setting a user agent.
func UserAgentRoundTripper(inner http.RoundTripper, userAgent string) http.RoundTripper {
return userAgentRoundTripper{
inner: inner,
userAgent: userAgent,
}
}

// RoundTrip implements http.RoundTripper
func (u userAgentRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Set("User-Agent", u.userAgent)
return u.inner.RoundTrip(req)
}

0 comments on commit 09125e7

Please sign in to comment.