421 lines
14 KiB
Go
421 lines
14 KiB
Go
/*
|
|
Copyright 2020 The cert-manager Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package client
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"time"
|
|
|
|
vcert "github.com/Venafi/vcert/v5"
|
|
"github.com/Venafi/vcert/v5/pkg/certificate"
|
|
"github.com/Venafi/vcert/v5/pkg/endpoint"
|
|
"github.com/Venafi/vcert/v5/pkg/venafi/cloud"
|
|
"github.com/Venafi/vcert/v5/pkg/venafi/tpp"
|
|
"github.com/go-logr/logr"
|
|
"k8s.io/utils/ptr"
|
|
|
|
internalinformers "github.com/cert-manager/cert-manager/internal/informers"
|
|
cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
|
|
cmmeta "github.com/cert-manager/cert-manager/pkg/apis/meta/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 (
|
|
tppUsernameKey = "username"
|
|
tppPasswordKey = "password"
|
|
tppAccessTokenKey = "access-token"
|
|
|
|
defaultAPIKeyKey = "api-key"
|
|
)
|
|
|
|
type VenafiClientBuilder func(namespace string, secretsLister internalinformers.SecretLister,
|
|
issuer cmapi.GenericIssuer, metrics *metrics.Metrics, logger logr.Logger, userAgent string) (Interface, error)
|
|
|
|
// Interface implements a Venafi client
|
|
type Interface interface {
|
|
RequestCertificate(csrPEM []byte, duration time.Duration, customFields []api.CustomField) (string, error)
|
|
RetrieveCertificate(pickupID string, csrPEM []byte, duration time.Duration, customFields []api.CustomField) ([]byte, error)
|
|
Ping() error
|
|
ReadZoneConfiguration() (*endpoint.ZoneConfiguration, error)
|
|
SetClient(endpoint.Connector)
|
|
VerifyCredentials() error
|
|
}
|
|
|
|
// Venafi is a implementation of vcert library to manager certificates from TPP or Venafi Cloud
|
|
type Venafi struct {
|
|
// Namespace in which to read resources related to this Issuer from.
|
|
// For Issuers, this will be the namespace of the Issuer.
|
|
// For ClusterIssuers, this will be the cluster resource namespace.
|
|
namespace string
|
|
secretsLister internalinformers.SecretLister
|
|
|
|
vcertClient connector
|
|
tppClient *tpp.Connector
|
|
cloudClient *cloud.Connector
|
|
config *vcert.Config
|
|
}
|
|
|
|
// connector exposes a subset of the vcert Connector interface to make stubbing
|
|
// out its functionality during tests easier.
|
|
type connector interface {
|
|
Ping() (err error)
|
|
ReadZoneConfiguration() (config *endpoint.ZoneConfiguration, err error)
|
|
RequestCertificate(req *certificate.Request) (requestID string, err error)
|
|
RetrieveCertificate(req *certificate.Request) (certificates *certificate.PEMCollection, err error)
|
|
// TODO: (irbekrm) this method is never used- can it be removed?
|
|
RenewCertificate(req *certificate.RenewalRequest) (requestID string, err error)
|
|
}
|
|
|
|
// New constructs a Venafi client Interface. Errors may be network errors and
|
|
// should be considered for retrying.
|
|
func New(namespace string, secretsLister internalinformers.SecretLister, issuer cmapi.GenericIssuer, metrics *metrics.Metrics, logger logr.Logger, userAgent string) (Interface, error) {
|
|
cfg, err := configForIssuer(issuer, secretsLister, namespace, userAgent)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
vcertClient, err := vcert.NewClient(cfg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating Venafi client: %s", err.Error())
|
|
}
|
|
|
|
var tppc *tpp.Connector
|
|
var cc *cloud.Connector
|
|
|
|
switch vcertClient.GetType() {
|
|
case endpoint.ConnectorTypeTPP:
|
|
c, ok := vcertClient.(*tpp.Connector)
|
|
if ok {
|
|
tppc = c
|
|
}
|
|
case endpoint.ConnectorTypeCloud:
|
|
c, ok := vcertClient.(*cloud.Connector)
|
|
if ok {
|
|
cc = c
|
|
}
|
|
}
|
|
|
|
instrumentedVCertClient := newInstumentedConnector(vcertClient, metrics, logger)
|
|
|
|
return &Venafi{
|
|
namespace: namespace,
|
|
secretsLister: secretsLister,
|
|
vcertClient: instrumentedVCertClient,
|
|
cloudClient: cc,
|
|
tppClient: tppc,
|
|
config: cfg,
|
|
}, nil
|
|
}
|
|
|
|
// configForIssuer will convert a cert-manager Venafi issuer into a vcert.Config
|
|
// that can be used to instantiate an API client.
|
|
func configForIssuer(iss cmapi.GenericIssuer, secretsLister internalinformers.SecretLister, namespace string, userAgent string) (*vcert.Config, error) {
|
|
venCfg := iss.GetSpec().Venafi
|
|
|
|
switch {
|
|
case venCfg.TPP != nil:
|
|
tpp := venCfg.TPP
|
|
tppSecret, err := secretsLister.Secrets(namespace).Get(tpp.CredentialsRef.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
caBundle, err := caBundleForVcertTPP(tpp, secretsLister, namespace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
username := string(tppSecret.Data[tppUsernameKey])
|
|
password := string(tppSecret.Data[tppPasswordKey])
|
|
accessToken := string(tppSecret.Data[tppAccessTokenKey])
|
|
|
|
return &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
|
|
// ignored by vcert since we also supply a custom HTTP client,
|
|
// below. But we want to retain the CA bundle validation errors that
|
|
// were returned in previous versions of this code.
|
|
// https://github.com/Venafi/vcert/blob/89645a7710a7b529765274cb60dc5e28066217a1/client.go#L55-L61
|
|
ConnectionTrust: string(caBundle),
|
|
Credentials: &endpoint.Authentication{
|
|
User: username,
|
|
Password: password,
|
|
AccessToken: accessToken,
|
|
},
|
|
Client: httpClientForVcert(&httpClientForVcertOptions{
|
|
UserAgent: ptr.To(userAgent),
|
|
CABundle: caBundle,
|
|
TLSRenegotiationSupport: ptr.To(tls.RenegotiateOnceAsClient),
|
|
}),
|
|
}, nil
|
|
case venCfg.Cloud != nil:
|
|
cloud := venCfg.Cloud
|
|
cloudSecret, err := secretsLister.Secrets(namespace).Get(cloud.APITokenSecretRef.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
k := defaultAPIKeyKey
|
|
if cloud.APITokenSecretRef.Key != "" {
|
|
k = cloud.APITokenSecretRef.Key
|
|
}
|
|
apiKey := string(cloudSecret.Data[k])
|
|
|
|
return &vcert.Config{
|
|
ConnectorType: endpoint.ConnectorTypeCloud,
|
|
BaseUrl: cloud.URL,
|
|
Zone: venCfg.Zone,
|
|
// always enable verbose logging for now
|
|
LogVerbose: true,
|
|
Credentials: &endpoint.Authentication{
|
|
APIKey: apiKey,
|
|
},
|
|
Client: httpClientForVcert(&httpClientForVcertOptions{
|
|
UserAgent: ptr.To(userAgent),
|
|
}),
|
|
}, nil
|
|
}
|
|
// 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")
|
|
}
|
|
|
|
// httpClientForVcertOptions contains options for `httpClientForVcert`, to allow
|
|
// you to customize the HTTP client.
|
|
type httpClientForVcertOptions struct {
|
|
// UserAgent will add a User-Agent header to all HTTP requests.
|
|
UserAgent *string
|
|
// CABundle will override the CA certificates used to verify server
|
|
// certificates.
|
|
CABundle []byte
|
|
// TLSRenegotiationSupport will override the TLSRenegotiationSupport setting
|
|
// of the client.
|
|
TLSRenegotiationSupport *tls.RenegotiationSupport
|
|
}
|
|
|
|
// httpClientForVcert creates an HTTP client which matches the default HTTP client of vcert,
|
|
// but allows you to customize client TLS renegotiation, and User-Agent.
|
|
//
|
|
// Why is it necessary to create our own HTTP client for vcert?
|
|
//
|
|
// 1. We need to customize the client TLS renegotiation setting when connecting
|
|
// to certain TPP servers.
|
|
// 2. We need to customize the User-Agent header for all HTTP requests to Venafi
|
|
// REST API endpoints.
|
|
// 3. The vcert package does not currently provide an easier way to change those
|
|
// settings. See:
|
|
// * https://github.com/Venafi/vcert/issues/437
|
|
// * https://github.com/Venafi/vcert/issues/438
|
|
//
|
|
// Why is it necessary to customize the client TLS renegotiation?
|
|
//
|
|
// 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.
|
|
//
|
|
// Why do we supply CA bundle in the HTTP client **and** in the vcert.Config?
|
|
//
|
|
// 1. 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.
|
|
// 2. 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 httpClientForVcert(options *httpClientForVcertOptions) *http.Client {
|
|
// 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
|
|
transport := &http.Transport{
|
|
Proxy: http.ProxyFromEnvironment,
|
|
DialContext: (&net.Dialer{
|
|
Timeout: 30 * time.Second,
|
|
KeepAlive: 30 * time.Second,
|
|
// Note: This DualStack setting is copied from vcert but
|
|
// deviates from the http.DefaultTransport in Go's stdlib.
|
|
DualStack: true,
|
|
}).DialContext,
|
|
MaxIdleConns: 100,
|
|
IdleConnTimeout: 90 * time.Second,
|
|
TLSHandshakeTimeout: 10 * time.Second,
|
|
ExpectContinueTimeout: 1 * time.Second,
|
|
}
|
|
|
|
// Copy vcert's initialization of the TLS client config
|
|
tlsClientConfig := http.DefaultTransport.(*http.Transport).TLSClientConfig.Clone()
|
|
if tlsClientConfig == nil {
|
|
tlsClientConfig = &tls.Config{}
|
|
}
|
|
if len(options.CABundle) > 0 {
|
|
rootCAs := x509.NewCertPool()
|
|
rootCAs.AppendCertsFromPEM(options.CABundle)
|
|
tlsClientConfig.RootCAs = rootCAs
|
|
}
|
|
transport.TLSClientConfig = tlsClientConfig
|
|
|
|
if options.TLSRenegotiationSupport != nil {
|
|
transport.TLSClientConfig.Renegotiation = *options.TLSRenegotiationSupport
|
|
}
|
|
|
|
var roundTripper http.RoundTripper = transport
|
|
if options.UserAgent != nil {
|
|
roundTripper = util.UserAgentRoundTripper(transport, *options.UserAgent)
|
|
}
|
|
|
|
// 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{
|
|
Transport: roundTripper,
|
|
Timeout: time.Second * 30,
|
|
}
|
|
}
|
|
|
|
// caBundleForVcertTPP is used to by ConnectionTrust and Client fields of vcert.Config.
|
|
// This function sets appropriate CA based on provided bundle or kubernetes secret
|
|
// If no custom CA bundle is configured, an empty byte slice is returned.
|
|
// Assumes exactly one of the in-line/Secret CA bundles are defined.
|
|
// If the `key` of the Secret CA bundle is not defined, its value defaults to
|
|
// `ca.crt`.
|
|
func caBundleForVcertTPP(tpp *cmapi.VenafiTPP, secretsLister internalinformers.SecretLister, namespace string) (caBundle []byte, err error) {
|
|
if len(tpp.CABundle) > 0 {
|
|
return tpp.CABundle, nil
|
|
}
|
|
|
|
secretRef := tpp.CABundleSecretRef
|
|
if secretRef == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
var certBytes []byte
|
|
var ok bool
|
|
|
|
if secretRef != nil {
|
|
secret, err := secretsLister.Secrets(namespace).Get(secretRef.Name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not access secret '%s/%s': %s", namespace, secretRef.Name, err)
|
|
}
|
|
|
|
var key string
|
|
if secretRef.Key != "" {
|
|
key = secretRef.Key
|
|
} else {
|
|
key = cmmeta.TLSCAKey
|
|
}
|
|
|
|
certBytes, ok = secret.Data[key]
|
|
if !ok {
|
|
return nil, fmt.Errorf("no data for %q in secret '%s/%s'", key, namespace, secretRef.Name)
|
|
}
|
|
|
|
}
|
|
|
|
return certBytes, nil
|
|
}
|
|
|
|
func (v *Venafi) Ping() error {
|
|
return v.vcertClient.Ping()
|
|
}
|
|
|
|
func (v *Venafi) ReadZoneConfiguration() (*endpoint.ZoneConfiguration, error) {
|
|
return v.vcertClient.ReadZoneConfiguration()
|
|
}
|
|
|
|
func (v *Venafi) SetClient(client endpoint.Connector) {
|
|
v.vcertClient = client
|
|
}
|
|
|
|
// VerifyCredentials will remotely verify the credentials for the client, both for TPP and Cloud
|
|
func (v *Venafi) VerifyCredentials() error {
|
|
switch {
|
|
case v.cloudClient != nil:
|
|
err := v.cloudClient.Authenticate(&endpoint.Authentication{
|
|
APIKey: v.config.Credentials.APIKey,
|
|
})
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("cloudClient.Authenticate: %v", err)
|
|
}
|
|
|
|
return nil
|
|
case v.tppClient != nil:
|
|
if v.config.Credentials == nil {
|
|
return fmt.Errorf("credentials not configured")
|
|
}
|
|
|
|
if v.config.Credentials.AccessToken != "" {
|
|
_, err := v.tppClient.VerifyAccessToken(&endpoint.Authentication{
|
|
AccessToken: v.config.Credentials.AccessToken,
|
|
})
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("tppClient.VerifyAccessToken: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
if v.config.Credentials.User != "" && v.config.Credentials.Password != "" {
|
|
err := v.tppClient.Authenticate(&endpoint.Authentication{
|
|
User: v.config.Credentials.User,
|
|
Password: v.config.Credentials.Password,
|
|
})
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("tppClient.Authenticate: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("neither tppClient or cloudClient have been set")
|
|
}
|