diff --git a/deploy/crds/crd-clusterissuers.yaml b/deploy/crds/crd-clusterissuers.yaml index 8d850f68a..55af2abb5 100644 --- a/deploy/crds/crd-clusterissuers.yaml +++ b/deploy/crds/crd-clusterissuers.yaml @@ -1255,6 +1255,30 @@ spec: name: description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' type: string + clientCertSecretRef: + description: Reference to a Secret containing a PEM-encoded Client Certificate to use when the Vault server requires mTLS. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + clientKeySecretRef: + description: Reference to a Secret containing a PEM-encoded Client Private Key to use when the Vault server requires mTLS. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string namespace: description: 'Name of the vault namespace. Namespaces is a set of features within Vault Enterprise that allows Vault environments to support Secure Multi-tenancy. e.g: "ns1" More about namespaces can be found here https://www.vaultproject.io/docs/enterprise/namespaces' type: string diff --git a/deploy/crds/crd-issuers.yaml b/deploy/crds/crd-issuers.yaml index 924181324..f303ee732 100644 --- a/deploy/crds/crd-issuers.yaml +++ b/deploy/crds/crd-issuers.yaml @@ -1255,6 +1255,30 @@ spec: name: description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' type: string + clientCertSecretRef: + description: Reference to a Secret containing a PEM-encoded Client Certificate to use when the Vault server requires mTLS. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + clientKeySecretRef: + description: Reference to a Secret containing a PEM-encoded Client Private Key to use when the Vault server requires mTLS. + type: object + required: + - name + properties: + key: + description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required. + type: string + name: + description: 'Name of the resource being referred to. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string namespace: description: 'Name of the vault namespace. Namespaces is a set of features within Vault Enterprise that allows Vault environments to support Secure Multi-tenancy. e.g: "ns1" More about namespaces can be found here https://www.vaultproject.io/docs/enterprise/namespaces' type: string diff --git a/internal/apis/certmanager/types_issuer.go b/internal/apis/certmanager/types_issuer.go index 74c32b850..e9a91f8db 100644 --- a/internal/apis/certmanager/types_issuer.go +++ b/internal/apis/certmanager/types_issuer.go @@ -197,6 +197,16 @@ type VaultIssuer struct { // If no key for the Secret is specified, cert-manager will default to 'ca.crt'. // +optional CABundleSecretRef *cmmeta.SecretKeySelector + + // Reference to a Secret containing a PEM-encoded Client Certificate to use when the + // Vault server requires mTLS. + // +optional + ClientCertSecretRef *cmmeta.SecretKeySelector + + // Reference to a Secret containing a PEM-encoded Client Private Key to use when the + // Vault server requires mTLS. + // +optional + ClientKeySecretRef *cmmeta.SecretKeySelector } // VaultAuth is configuration used to authenticate with a Vault server. The diff --git a/internal/apis/certmanager/v1/zz_generated.conversion.go b/internal/apis/certmanager/v1/zz_generated.conversion.go index c19ff7c59..d88ce84bc 100644 --- a/internal/apis/certmanager/v1/zz_generated.conversion.go +++ b/internal/apis/certmanager/v1/zz_generated.conversion.go @@ -1546,6 +1546,24 @@ func autoConvert_v1_VaultIssuer_To_certmanager_VaultIssuer(in *v1.VaultIssuer, o } else { out.CABundleSecretRef = nil } + if in.ClientCertSecretRef != nil { + in, out := &in.ClientCertSecretRef, &out.ClientCertSecretRef + *out = new(meta.SecretKeySelector) + if err := internalapismetav1.Convert_v1_SecretKeySelector_To_meta_SecretKeySelector(*in, *out, s); err != nil { + return err + } + } else { + out.ClientCertSecretRef = nil + } + if in.ClientKeySecretRef != nil { + in, out := &in.ClientKeySecretRef, &out.ClientKeySecretRef + *out = new(meta.SecretKeySelector) + if err := internalapismetav1.Convert_v1_SecretKeySelector_To_meta_SecretKeySelector(*in, *out, s); err != nil { + return err + } + } else { + out.ClientKeySecretRef = nil + } return nil } @@ -1571,6 +1589,24 @@ func autoConvert_certmanager_VaultIssuer_To_v1_VaultIssuer(in *certmanager.Vault } else { out.CABundleSecretRef = nil } + if in.ClientCertSecretRef != nil { + in, out := &in.ClientCertSecretRef, &out.ClientCertSecretRef + *out = new(apismetav1.SecretKeySelector) + if err := internalapismetav1.Convert_meta_SecretKeySelector_To_v1_SecretKeySelector(*in, *out, s); err != nil { + return err + } + } else { + out.ClientCertSecretRef = nil + } + if in.ClientKeySecretRef != nil { + in, out := &in.ClientKeySecretRef, &out.ClientKeySecretRef + *out = new(apismetav1.SecretKeySelector) + if err := internalapismetav1.Convert_meta_SecretKeySelector_To_v1_SecretKeySelector(*in, *out, s); err != nil { + return err + } + } else { + out.ClientKeySecretRef = nil + } return nil } diff --git a/internal/apis/certmanager/v1alpha2/types_issuer.go b/internal/apis/certmanager/v1alpha2/types_issuer.go index 7e726e2e3..c0db3ff02 100644 --- a/internal/apis/certmanager/v1alpha2/types_issuer.go +++ b/internal/apis/certmanager/v1alpha2/types_issuer.go @@ -214,6 +214,16 @@ type VaultIssuer struct { // If no key for the Secret is specified, cert-manager will default to 'ca.crt'. // +optional CABundleSecretRef *cmmeta.SecretKeySelector `json:"caBundleSecretRef,omitempty"` + + // Reference to a Secret containing a PEM-encoded Client Certificate to use when the + // Vault server requires mTLS. + // +optional + ClientCertSecretRef *cmmeta.SecretKeySelector `json:"clientCertSecretRef,omitempty"` + + // Reference to a Secret containing a PEM-encoded Client Private Key to use when the + // Vault server requires mTLS. + // +optional + ClientKeySecretRef *cmmeta.SecretKeySelector `json:"clientKeySecretRef,omitempty"` } // Configuration used to authenticate with a Vault server. diff --git a/internal/apis/certmanager/v1alpha2/zz_generated.conversion.go b/internal/apis/certmanager/v1alpha2/zz_generated.conversion.go index 0dc97d222..b1759810c 100644 --- a/internal/apis/certmanager/v1alpha2/zz_generated.conversion.go +++ b/internal/apis/certmanager/v1alpha2/zz_generated.conversion.go @@ -1552,6 +1552,24 @@ func autoConvert_v1alpha2_VaultIssuer_To_certmanager_VaultIssuer(in *VaultIssuer } else { out.CABundleSecretRef = nil } + if in.ClientCertSecretRef != nil { + in, out := &in.ClientCertSecretRef, &out.ClientCertSecretRef + *out = new(meta.SecretKeySelector) + if err := apismetav1.Convert_v1_SecretKeySelector_To_meta_SecretKeySelector(*in, *out, s); err != nil { + return err + } + } else { + out.ClientCertSecretRef = nil + } + if in.ClientKeySecretRef != nil { + in, out := &in.ClientKeySecretRef, &out.ClientKeySecretRef + *out = new(meta.SecretKeySelector) + if err := apismetav1.Convert_v1_SecretKeySelector_To_meta_SecretKeySelector(*in, *out, s); err != nil { + return err + } + } else { + out.ClientKeySecretRef = nil + } return nil } @@ -1577,6 +1595,24 @@ func autoConvert_certmanager_VaultIssuer_To_v1alpha2_VaultIssuer(in *certmanager } else { out.CABundleSecretRef = nil } + if in.ClientCertSecretRef != nil { + in, out := &in.ClientCertSecretRef, &out.ClientCertSecretRef + *out = new(metav1.SecretKeySelector) + if err := apismetav1.Convert_meta_SecretKeySelector_To_v1_SecretKeySelector(*in, *out, s); err != nil { + return err + } + } else { + out.ClientCertSecretRef = nil + } + if in.ClientKeySecretRef != nil { + in, out := &in.ClientKeySecretRef, &out.ClientKeySecretRef + *out = new(metav1.SecretKeySelector) + if err := apismetav1.Convert_meta_SecretKeySelector_To_v1_SecretKeySelector(*in, *out, s); err != nil { + return err + } + } else { + out.ClientKeySecretRef = nil + } return nil } diff --git a/internal/apis/certmanager/v1alpha2/zz_generated.deepcopy.go b/internal/apis/certmanager/v1alpha2/zz_generated.deepcopy.go index f4844ff13..330390150 100644 --- a/internal/apis/certmanager/v1alpha2/zz_generated.deepcopy.go +++ b/internal/apis/certmanager/v1alpha2/zz_generated.deepcopy.go @@ -998,6 +998,16 @@ func (in *VaultIssuer) DeepCopyInto(out *VaultIssuer) { *out = new(metav1.SecretKeySelector) **out = **in } + if in.ClientCertSecretRef != nil { + in, out := &in.ClientCertSecretRef, &out.ClientCertSecretRef + *out = new(metav1.SecretKeySelector) + **out = **in + } + if in.ClientKeySecretRef != nil { + in, out := &in.ClientKeySecretRef, &out.ClientKeySecretRef + *out = new(metav1.SecretKeySelector) + **out = **in + } return } diff --git a/internal/apis/certmanager/v1alpha3/types_issuer.go b/internal/apis/certmanager/v1alpha3/types_issuer.go index f3406132d..73960254b 100644 --- a/internal/apis/certmanager/v1alpha3/types_issuer.go +++ b/internal/apis/certmanager/v1alpha3/types_issuer.go @@ -214,6 +214,16 @@ type VaultIssuer struct { // If no key for the Secret is specified, cert-manager will default to 'ca.crt'. // +optional CABundleSecretRef *cmmeta.SecretKeySelector `json:"caBundleSecretRef,omitempty"` + + // Reference to a Secret containing a PEM-encoded Client Certificate to use when the + // Vault server requires mTLS. + // +optional + ClientCertSecretRef *cmmeta.SecretKeySelector `json:"clientCertSecretRef,omitempty"` + + // Reference to a Secret containing a PEM-encoded Client Private Key to use when the + // Vault server requires mTLS. + // +optional + ClientKeySecretRef *cmmeta.SecretKeySelector `json:"clientKeySecretRef,omitempty"` } // Configuration used to authenticate with a Vault server. diff --git a/internal/apis/certmanager/v1alpha3/zz_generated.conversion.go b/internal/apis/certmanager/v1alpha3/zz_generated.conversion.go index 27ea8a130..d34cdab9c 100644 --- a/internal/apis/certmanager/v1alpha3/zz_generated.conversion.go +++ b/internal/apis/certmanager/v1alpha3/zz_generated.conversion.go @@ -1551,6 +1551,24 @@ func autoConvert_v1alpha3_VaultIssuer_To_certmanager_VaultIssuer(in *VaultIssuer } else { out.CABundleSecretRef = nil } + if in.ClientCertSecretRef != nil { + in, out := &in.ClientCertSecretRef, &out.ClientCertSecretRef + *out = new(meta.SecretKeySelector) + if err := apismetav1.Convert_v1_SecretKeySelector_To_meta_SecretKeySelector(*in, *out, s); err != nil { + return err + } + } else { + out.ClientCertSecretRef = nil + } + if in.ClientKeySecretRef != nil { + in, out := &in.ClientKeySecretRef, &out.ClientKeySecretRef + *out = new(meta.SecretKeySelector) + if err := apismetav1.Convert_v1_SecretKeySelector_To_meta_SecretKeySelector(*in, *out, s); err != nil { + return err + } + } else { + out.ClientKeySecretRef = nil + } return nil } @@ -1576,6 +1594,24 @@ func autoConvert_certmanager_VaultIssuer_To_v1alpha3_VaultIssuer(in *certmanager } else { out.CABundleSecretRef = nil } + if in.ClientCertSecretRef != nil { + in, out := &in.ClientCertSecretRef, &out.ClientCertSecretRef + *out = new(metav1.SecretKeySelector) + if err := apismetav1.Convert_meta_SecretKeySelector_To_v1_SecretKeySelector(*in, *out, s); err != nil { + return err + } + } else { + out.ClientCertSecretRef = nil + } + if in.ClientKeySecretRef != nil { + in, out := &in.ClientKeySecretRef, &out.ClientKeySecretRef + *out = new(metav1.SecretKeySelector) + if err := apismetav1.Convert_meta_SecretKeySelector_To_v1_SecretKeySelector(*in, *out, s); err != nil { + return err + } + } else { + out.ClientKeySecretRef = nil + } return nil } diff --git a/internal/apis/certmanager/v1alpha3/zz_generated.deepcopy.go b/internal/apis/certmanager/v1alpha3/zz_generated.deepcopy.go index 357619a13..52d0d5e6b 100644 --- a/internal/apis/certmanager/v1alpha3/zz_generated.deepcopy.go +++ b/internal/apis/certmanager/v1alpha3/zz_generated.deepcopy.go @@ -993,6 +993,16 @@ func (in *VaultIssuer) DeepCopyInto(out *VaultIssuer) { *out = new(metav1.SecretKeySelector) **out = **in } + if in.ClientCertSecretRef != nil { + in, out := &in.ClientCertSecretRef, &out.ClientCertSecretRef + *out = new(metav1.SecretKeySelector) + **out = **in + } + if in.ClientKeySecretRef != nil { + in, out := &in.ClientKeySecretRef, &out.ClientKeySecretRef + *out = new(metav1.SecretKeySelector) + **out = **in + } return } diff --git a/internal/apis/certmanager/v1beta1/types_issuer.go b/internal/apis/certmanager/v1beta1/types_issuer.go index 4559bc4ac..b4e1262e9 100644 --- a/internal/apis/certmanager/v1beta1/types_issuer.go +++ b/internal/apis/certmanager/v1beta1/types_issuer.go @@ -216,6 +216,16 @@ type VaultIssuer struct { // If no key for the Secret is specified, cert-manager will default to 'ca.crt'. // +optional CABundleSecretRef *cmmeta.SecretKeySelector `json:"caBundleSecretRef,omitempty"` + + // Reference to a Secret containing a PEM-encoded Client Certificate to use when the + // Vault server requires mTLS. + // +optional + ClientCertSecretRef *cmmeta.SecretKeySelector `json:"clientCertSecretRef,omitempty"` + + // Reference to a Secret containing a PEM-encoded Client Private Key to use when the + // Vault server requires mTLS. + // +optional + ClientKeySecretRef *cmmeta.SecretKeySelector `json:"clientKeySecretRef,omitempty"` } // Configuration used to authenticate with a Vault server. diff --git a/internal/apis/certmanager/v1beta1/zz_generated.conversion.go b/internal/apis/certmanager/v1beta1/zz_generated.conversion.go index f075c9b54..c895df576 100644 --- a/internal/apis/certmanager/v1beta1/zz_generated.conversion.go +++ b/internal/apis/certmanager/v1beta1/zz_generated.conversion.go @@ -1534,6 +1534,24 @@ func autoConvert_v1beta1_VaultIssuer_To_certmanager_VaultIssuer(in *VaultIssuer, } else { out.CABundleSecretRef = nil } + if in.ClientCertSecretRef != nil { + in, out := &in.ClientCertSecretRef, &out.ClientCertSecretRef + *out = new(meta.SecretKeySelector) + if err := apismetav1.Convert_v1_SecretKeySelector_To_meta_SecretKeySelector(*in, *out, s); err != nil { + return err + } + } else { + out.ClientCertSecretRef = nil + } + if in.ClientKeySecretRef != nil { + in, out := &in.ClientKeySecretRef, &out.ClientKeySecretRef + *out = new(meta.SecretKeySelector) + if err := apismetav1.Convert_v1_SecretKeySelector_To_meta_SecretKeySelector(*in, *out, s); err != nil { + return err + } + } else { + out.ClientKeySecretRef = nil + } return nil } @@ -1559,6 +1577,24 @@ func autoConvert_certmanager_VaultIssuer_To_v1beta1_VaultIssuer(in *certmanager. } else { out.CABundleSecretRef = nil } + if in.ClientCertSecretRef != nil { + in, out := &in.ClientCertSecretRef, &out.ClientCertSecretRef + *out = new(metav1.SecretKeySelector) + if err := apismetav1.Convert_meta_SecretKeySelector_To_v1_SecretKeySelector(*in, *out, s); err != nil { + return err + } + } else { + out.ClientCertSecretRef = nil + } + if in.ClientKeySecretRef != nil { + in, out := &in.ClientKeySecretRef, &out.ClientKeySecretRef + *out = new(metav1.SecretKeySelector) + if err := apismetav1.Convert_meta_SecretKeySelector_To_v1_SecretKeySelector(*in, *out, s); err != nil { + return err + } + } else { + out.ClientKeySecretRef = nil + } return nil } diff --git a/internal/apis/certmanager/v1beta1/zz_generated.deepcopy.go b/internal/apis/certmanager/v1beta1/zz_generated.deepcopy.go index b8b39cf22..dc1da740a 100644 --- a/internal/apis/certmanager/v1beta1/zz_generated.deepcopy.go +++ b/internal/apis/certmanager/v1beta1/zz_generated.deepcopy.go @@ -993,6 +993,16 @@ func (in *VaultIssuer) DeepCopyInto(out *VaultIssuer) { *out = new(metav1.SecretKeySelector) **out = **in } + if in.ClientCertSecretRef != nil { + in, out := &in.ClientCertSecretRef, &out.ClientCertSecretRef + *out = new(metav1.SecretKeySelector) + **out = **in + } + if in.ClientKeySecretRef != nil { + in, out := &in.ClientKeySecretRef, &out.ClientKeySecretRef + *out = new(metav1.SecretKeySelector) + **out = **in + } return } diff --git a/internal/apis/certmanager/validation/issuer.go b/internal/apis/certmanager/validation/issuer.go index cef697b86..8c8cdf336 100644 --- a/internal/apis/certmanager/validation/issuer.go +++ b/internal/apis/certmanager/validation/issuer.go @@ -281,6 +281,12 @@ func ValidateVaultIssuerConfig(iss *certmanager.VaultIssuer, fldPath *field.Path el = append(el, field.Invalid(fldPath.Child("caBundleSecretRef"), iss.CABundleSecretRef.Name, "specified caBundleSecretRef and caBundle cannot be used together")) } + if iss.ClientCertSecretRef != nil && iss.ClientKeySecretRef == nil { + el = append(el, field.Invalid(fldPath.Child("clientKeySecretRef"), "", "clientKeySecretRef must be provided when defining the clientCertSecretRef")) + } else if iss.ClientCertSecretRef == nil && iss.ClientKeySecretRef != nil { + el = append(el, field.Invalid(fldPath.Child("clientCertSecretRef"), "", "clientCertSecretRef must be provided when defining the clientKeySecretRef")) + } + el = append(el, ValidateVaultIssuerAuth(&iss.Auth, fldPath.Child("auth"))...) return el diff --git a/internal/apis/certmanager/validation/issuer_test.go b/internal/apis/certmanager/validation/issuer_test.go index 256fcdac7..a875bec81 100644 --- a/internal/apis/certmanager/validation/issuer_test.go +++ b/internal/apis/certmanager/validation/issuer_test.go @@ -119,6 +119,54 @@ func TestValidateVaultIssuerConfig(t *testing.T) { field.Invalid(fldPath.Child("caBundle"), "", "cert bundle didn't contain any valid certificates"), }, }, + "vault issuer define clientCertSecretRef but not clientKeySecretRef": { + spec: &cmapi.VaultIssuer{ + Server: "https://vault.example.com", + Path: "secret/path", + CABundleSecretRef: &cmmeta.SecretKeySelector{ + Key: "ca.crt", + LocalObjectReference: cmmeta.LocalObjectReference{ + Name: "test-secret", + }, + }, + ClientCertSecretRef: &cmmeta.SecretKeySelector{ + Key: "tls.crt", + LocalObjectReference: cmmeta.LocalObjectReference{ + Name: "test-secret", + }, + }, + Auth: cmapi.VaultAuth{ + TokenSecretRef: &validSecretKeyRef, + }, + }, + errs: []*field.Error{ + field.Invalid(fldPath.Child("clientKeySecretRef"), "", "clientKeySecretRef must be provided when defining the clientCertSecretRef"), + }, + }, + "vault issuer define clientKeySecretRef but not clientCertSecretRef": { + spec: &cmapi.VaultIssuer{ + Server: "https://vault.example.com", + Path: "secret/path", + CABundleSecretRef: &cmmeta.SecretKeySelector{ + Key: "ca.crt", + LocalObjectReference: cmmeta.LocalObjectReference{ + Name: "test-secret", + }, + }, + ClientKeySecretRef: &cmmeta.SecretKeySelector{ + Key: "tls.key", + LocalObjectReference: cmmeta.LocalObjectReference{ + Name: "test-secret", + }, + }, + Auth: cmapi.VaultAuth{ + TokenSecretRef: &validSecretKeyRef, + }, + }, + errs: []*field.Error{ + field.Invalid(fldPath.Child("clientCertSecretRef"), "", "clientCertSecretRef must be provided when defining the clientKeySecretRef"), + }, + }, } for n, s := range scenarios { t.Run(n, func(t *testing.T) { diff --git a/internal/apis/certmanager/zz_generated.deepcopy.go b/internal/apis/certmanager/zz_generated.deepcopy.go index a24e71fca..631f68e1d 100644 --- a/internal/apis/certmanager/zz_generated.deepcopy.go +++ b/internal/apis/certmanager/zz_generated.deepcopy.go @@ -993,6 +993,16 @@ func (in *VaultIssuer) DeepCopyInto(out *VaultIssuer) { *out = new(meta.SecretKeySelector) **out = **in } + if in.ClientCertSecretRef != nil { + in, out := &in.ClientCertSecretRef, &out.ClientCertSecretRef + *out = new(meta.SecretKeySelector) + **out = **in + } + if in.ClientKeySecretRef != nil { + in, out := &in.ClientKeySecretRef, &out.ClientKeySecretRef + *out = new(meta.SecretKeySelector) + **out = **in + } return } diff --git a/internal/vault/vault.go b/internal/vault/vault.go index b8001d00c..324473c9e 100644 --- a/internal/vault/vault.go +++ b/internal/vault/vault.go @@ -18,6 +18,7 @@ package vault import ( "context" + "crypto/tls" "crypto/x509" "errors" "fmt" @@ -27,6 +28,8 @@ import ( "strings" "time" + corev1 "k8s.io/api/core/v1" + vault "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/sdk/helper/certutil" authv1 "k8s.io/api/authentication/v1" @@ -230,20 +233,24 @@ func (v *Vault) newConfig() (*vault.Config, error) { return nil, fmt.Errorf("failed to load vault CA bundle: %w", err) } - // If no CA bundle was loaded, return early and don't modify the vault config - // further. This will cause the vault client to use the system root CA - // bundle. - if len(caBundle) == 0 { - return cfg, nil + if len(caBundle) != 0 { + caCertPool := x509.NewCertPool() + ok := caCertPool.AppendCertsFromPEM(caBundle) + if !ok { + return nil, fmt.Errorf("no Vault CA bundles loaded, check bundle contents") + } + + cfg.HttpClient.Transport.(*http.Transport).TLSClientConfig.RootCAs = caCertPool } - caCertPool := x509.NewCertPool() - ok := caCertPool.AppendCertsFromPEM(caBundle) - if !ok { - return nil, fmt.Errorf("no Vault CA bundles loaded, check bundle contents") + clientCertificate, err := v.clientCertificate() + if err != nil { + return nil, fmt.Errorf("failed to load vault client certificate: %w", err) } - cfg.HttpClient.Transport.(*http.Transport).TLSClientConfig.RootCAs = caCertPool + if clientCertificate != nil { + cfg.HttpClient.Transport.(*http.Transport).TLSClientConfig.Certificates = []tls.Certificate{*clientCertificate} + } return cfg, nil } @@ -284,6 +291,54 @@ func (v *Vault) caBundle() ([]byte, error) { return certBytes, nil } +// clientCertificate returns the Client Certificate for the Vault server. +// Can be used in Vault client configs when the server requires mTLS. +func (v *Vault) clientCertificate() (*tls.Certificate, error) { + refCert := v.issuer.GetSpec().Vault.ClientCertSecretRef + refPrivateKey := v.issuer.GetSpec().Vault.ClientKeySecretRef + if refCert == nil || refPrivateKey == nil { + return nil, nil + } + + secretCert, err := v.secretsLister.Secrets(v.namespace).Get(refCert.Name) + if err != nil { + return nil, fmt.Errorf("could not access Secret '%s/%s': %s", v.namespace, refCert.Name, err) + } + secretPrivateKey, err := v.secretsLister.Secrets(v.namespace).Get(refPrivateKey.Name) + if err != nil { + return nil, fmt.Errorf("could not access Secret '%s/%s': %s", v.namespace, refPrivateKey.Name, err) + } + + var keyCert string + if refCert.Key != "" { + keyCert = refCert.Key + } else { + keyCert = corev1.TLSCertKey + } + + var keyPrivate string + if refPrivateKey.Key != "" { + keyPrivate = refPrivateKey.Key + } else { + keyPrivate = corev1.TLSPrivateKeyKey + } + + certBytes, ok := secretCert.Data[keyCert] + if !ok { + return nil, fmt.Errorf("no data for %q in Secret '%s/%s'", keyCert, v.namespace, refCert.Name) + } + privateKeyBytes, ok := secretPrivateKey.Data[keyPrivate] + if !ok { + return nil, fmt.Errorf("no data for %q in Secret '%s/%s'", keyPrivate, v.namespace, refPrivateKey.Name) + } + + cert, err := tls.X509KeyPair(certBytes, privateKeyBytes) + if err != nil { + return nil, fmt.Errorf("could not parse the TLS certificate from Secrets '%s/%s'(cert) and '%s/%s'(key): %s", v.namespace, refCert.Name, v.namespace, refPrivateKey.Name, err) + } + return &cert, nil +} + func (v *Vault) tokenRef(name, namespace, key string) (string, error) { secret, err := v.secretsLister.Secrets(namespace).Get(name) if err != nil { diff --git a/internal/vault/vault_test.go b/internal/vault/vault_test.go index 2d7ea51a7..b81e1b5a1 100644 --- a/internal/vault/vault_test.go +++ b/internal/vault/vault_test.go @@ -147,6 +147,58 @@ Pc7TUJiY8gW9SWhPVUPaMkTIBgfN11c6BzLlhzN2r1zaZyghXr8QmcG4kWywkX7k oXeN5eS8iO5fx0EOvIcYQ4yRZLGafZxsLHlsZmt32N/ZZtcl4KDP5LRE7iZEOaE/ UXY5wAUH2A== -----END CERTIFICATE----- +` + testClientCertificate = `-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIBATANBgkqhkiG9w0BAQsFADBZMQswCQYDVQQGEwJVUzEL +MAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFDASBgNVBAoMC0NF +UlRNQU5BR0VSMQ8wDQYDVQQDDAZiYXIuY2EwHhcNMjQwMTA0MTAxMzQ3WhcNNDMx +MjMwMTAxMzQ3WjBgMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcM +DVNhbiBGcmFuY2lzY28xFDASBgNVBAoMC0NFUlRNQU5BR0VSMRYwFAYDVQQDDA1j +bGllbnQuYmFyLmNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvFLC +dXqU33bG8TrVQ2mwTZF6UxNgrQCjLYJZWgK7AUvOzdsMUiZfQ51GVRLw7inRYgYn +QuIjPwgXQDYCHfQJEsBghSMNze+QjmGYwT2dy2QZm47Q0lGi57n2rtgRrM2Q+19E +SlEOZhi45ZVaU2NAEMAD8jIj3XznukrZLUZQSt7lN0xS2w1IvhO7Xb1n+wFcjwMt +f4ciRQyKprHQGTcaTzGn4Fjhua3kyFydg7elE1U23UJT42TbZ0WfgNG9KLGLWYpD +pMDnAgkgwMggguQz4izgFyeD2NnvDIviGbwnTC9WAlogwBZx2SOrBGIjoyWNmfIj +9uu5CytBgCdmhzMx1QIDAQABo4GGMIGDMAkGA1UdEwQCMAAwEQYJYIZIAYb4QgEB +BAQDAgeAMB0GA1UdDgQWBBRQlAnVwsjjnb3lu44c2Rt/zz4l3zAfBgNVHSMEGDAW +gBR/PgLMjVGMUxKzqRqcocLfB500ETAOBgNVHQ8BAf8EBAMCBeAwEwYDVR0lBAww +CgYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADggEBACXv/vfiuC8VXnvFo+Cvpn1H +eG1qsjOHOnPFhvHaMY55wsFchnZd7t0aqRNwkqLEvqpMIMDiXh7nw5pQZZu5IGBi ++cNDtfadmFi6NMFZNqlgPsYmb6pCI6OOG2r8VkmG+OdIg8QOdH60FQamT3MYKelE +JHxBQYgtiJr+vNTzBdrq9/qDgDJdx0OVo2U8+igFKkrWqgbPeJDLJb1NpVJBIhSG +ntdrtA87wmrLkV09SLUpvTYuTm3NMMrlD3hSBBBm3evb+65tsJg9/M5QjtAb8pQT +gtrc5PnSjjZzCeL94DkWQ+A7oLQStJMVePFvizMTozlnjCpVaJJN25nf+yVm22E= +-----END CERTIFICATE----- +` + testClientCertificatePrivateKey = `-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC8UsJ1epTfdsbx +OtVDabBNkXpTE2CtAKMtgllaArsBS87N2wxSJl9DnUZVEvDuKdFiBidC4iM/CBdA +NgId9AkSwGCFIw3N75COYZjBPZ3LZBmbjtDSUaLnufau2BGszZD7X0RKUQ5mGLjl +lVpTY0AQwAPyMiPdfOe6StktRlBK3uU3TFLbDUi+E7tdvWf7AVyPAy1/hyJFDIqm +sdAZNxpPMafgWOG5reTIXJ2Dt6UTVTbdQlPjZNtnRZ+A0b0osYtZikOkwOcCCSDA +yCCC5DPiLOAXJ4PY2e8Mi+IZvCdML1YCWiDAFnHZI6sEYiOjJY2Z8iP267kLK0GA +J2aHMzHVAgMBAAECggEACAEWfcrHgBX6z675+IMJ+MoJonVM4x2HUfxb0t0R2LTB +pfM8+1LhMq0BG8WR0vWZDisHySp2aAvufQ6umVpRdmgR0ibSw+F+SebxCKmXRtlK +01dHHeFVZLb9OqI5YhhcpKqAaw415/X+CdgGvkuWIgAfStCBwLy51quuvmNiL0Rm +XEq0tUAhJ8Z0G4z1e7PCabjq1eGPfsA6K3V/Bguo9ePIXls7AKn8xtyvQHL67RPy +wiupVB77RuViDYArx0ybMIvSKu8n2GAB0cBOICFe9MU8RiSGHl9MbgfDvW4BU/Fj ++YIrGjBV2MYdvjMCb2wlxyR11Yaaupd4u1e0eYxp4wKBgQDdaGHDgYQvTVOlgHrr +pYJoML50OIi9rh5q5HVRb3iLziSCaDUSBg7HuwC1vGLvZjfX3GVHpvwGaV9ZTk1c +C4jTtDAVibbCeL0/t9OzIE1EZbEwpMg0BXm0Jt97PhEKqxM2QHPGBAXKMxzJHYEO +HUHQpMCeHm5fLxZgv9qcTWisGwKBgQDZvxpCJ2qkRVyEhFYDJKBZ7KTlgH1TDZq6 +XvbVL/Q6c+6/iM+7TvytfnN36yb/p22eaBRf19ZkPHjGUqnCQnudwsK/bdyf0smh +6zZVirTL5/5qWIUPgIgWv5trqksMRm0NtRyo6ZwGWJnKTKPoxZJiW/d8v5bUw5IR +boLBMiCYzwKBgH4FCZAzyb76rl+HD2/M1rri86RHAV2lG18QBc6COgSpIpKvKXXG +yObaA39taIqGjcZphaQQ4WXs1/6G2PVJA2osJyo7JjDudBkuUmqkOhZyIzZitCkX +7LujXJRTMXP3B4pbiQnuBDWgfgPirTARawKMo63b+EppDL2otY89aBR9AoGBAK8q +VcRcEyTdC4UrNEpI/5n3jdt2FttmOU+uL2Dmt9ECDFEWjQ4Ah7JF5DvW9sN4++0P +izxi1HxETWA1hYzZkLojwCjhBzenCT9xiX8dGz5hfcAtP7Vtz4yFTVE6aC8SxI3f +YZPcggB07Bratoz9yznHA/vd4Ed+oJXXUeZ7Hc/vAoGBAKOpdDIzkKIkUIrbIKVI +DjlJQMrnnVpHB+7q2z+EnmVHzfn5zduL77hEBA/tXnkIvOyLm8WXDo/9K4E1xa93 +nUclMMQnJneyw8RRXGvgiy7EsLnRY8EhR/AgjoHY37etpj+v6kcaA+B7Q2oSYr11 +beE8ft41eEFS8AnSJd5hE9Ym +-----END PRIVATE KEY----- ` ) @@ -1060,6 +1112,25 @@ func TestNewConfig(t *testing.T) { } }) } + clientCertificateSecretRefFakeSecretLister := func(namespace, secret, caKey, caCert, clientKey, clientCert, privateKey, privateKeyCert string) *listers.FakeSecretLister { + return listers.FakeSecretListerFrom(listers.NewFakeSecretLister(), func(f *listers.FakeSecretLister) { + f.SecretsFn = func(namespace string) clientcorev1.SecretNamespaceLister { + return listers.FakeSecretNamespaceListerFrom(listers.NewFakeSecretNamespaceLister(), func(fn *listers.FakeSecretNamespaceLister) { + fn.GetFn = func(name string) (*corev1.Secret, error) { + if name == secret && namespace == namespace { + return &corev1.Secret{ + Data: map[string][]byte{ + caKey: []byte(caCert), + clientKey: []byte(clientCert), + privateKey: []byte(privateKeyCert), + }}, nil + } + return nil, errors.New("unexpected secret name or namespace passed to FakeSecretLister") + } + }) + } + }) + } tests := map[string]testNewConfigT{ "no CA bundle set in issuer should return nil": { issuer: gen.Issuer("vault-issuer", @@ -1217,6 +1288,72 @@ func TestNewConfig(t *testing.T) { } }, }, + "a good client certificate with default key should be added to the config": { + issuer: gen.Issuer("vault-issuer", + gen.SetIssuerVault(cmapi.VaultIssuer{ + Server: "https://vault.example.com", + CABundleSecretRef: &cmmeta.SecretKeySelector{ + LocalObjectReference: cmmeta.LocalObjectReference{ + Name: "bundle", + }, + }, + ClientCertSecretRef: &cmmeta.SecretKeySelector{ + LocalObjectReference: cmmeta.LocalObjectReference{ + Name: "bundle", + }, + }, + ClientKeySecretRef: &cmmeta.SecretKeySelector{ + LocalObjectReference: cmmeta.LocalObjectReference{ + Name: "bundle", + }, + }, + }, + )), + checkFunc: func(cfg *vault.Config, error error) error { + if error != nil { + return error + } + + certificates := cfg.HttpClient.Transport.(*http.Transport).TLSClientConfig.Certificates + if len(certificates) != 1 { + return fmt.Errorf("got unexpected number of client certificates in config, exp=1 got=%d", len(certificates)) + } + certificate, err := x509.ParseCertificate(certificates[0].Certificate[0]) + if err != nil { + return err + } + if certificate.Subject.CommonName != "client.bar.ca" { + return fmt.Errorf("got unexpected common name from the client certificate in config, exp=client.bar.ca got=%s", certificate.Subject.CommonName) + } + + return nil + }, + fakeLister: clientCertificateSecretRefFakeSecretLister("test-namespace", "bundle", "ca.crt", testLeafCertificate, "tls.crt", testClientCertificate, "tls.key", testClientCertificatePrivateKey), + }, + "a bad client certificate should error": { + issuer: gen.Issuer("vault-issuer", + gen.SetIssuerVault(cmapi.VaultIssuer{ + Server: "https://vault.example.com", + CABundleSecretRef: &cmmeta.SecretKeySelector{ + LocalObjectReference: cmmeta.LocalObjectReference{ + Name: "bundle", + }, + }, + ClientCertSecretRef: &cmmeta.SecretKeySelector{ + LocalObjectReference: cmmeta.LocalObjectReference{ + Name: "bundle", + }, + }, + ClientKeySecretRef: &cmmeta.SecretKeySelector{ + LocalObjectReference: cmmeta.LocalObjectReference{ + Name: "bundle", + }, + }, + }, + )), + expectedErr: errors.New("failed to load vault client certificate: could not parse the TLS certificate from Secrets 'test-namespace/bundle'(cert) and 'test-namespace/bundle'(key): tls: failed to find any PEM data in certificate input"), + fakeLister: clientCertificateSecretRefFakeSecretLister("test-namespace", "bundle", "ca.crt", testLeafCertificate, "tls.crt", "not a valid certificate", "tls.key", "not a valid certificate"), + }, } for name, test := range tests { diff --git a/pkg/apis/certmanager/v1/types_issuer.go b/pkg/apis/certmanager/v1/types_issuer.go index 6757050e3..57645c5c0 100644 --- a/pkg/apis/certmanager/v1/types_issuer.go +++ b/pkg/apis/certmanager/v1/types_issuer.go @@ -218,6 +218,16 @@ type VaultIssuer struct { // If no key for the Secret is specified, cert-manager will default to 'ca.crt'. // +optional CABundleSecretRef *cmmeta.SecretKeySelector `json:"caBundleSecretRef,omitempty"` + + // Reference to a Secret containing a PEM-encoded Client Certificate to use when the + // Vault server requires mTLS. + // +optional + ClientCertSecretRef *cmmeta.SecretKeySelector `json:"clientCertSecretRef,omitempty"` + + // Reference to a Secret containing a PEM-encoded Client Private Key to use when the + // Vault server requires mTLS. + // +optional + ClientKeySecretRef *cmmeta.SecretKeySelector `json:"clientKeySecretRef,omitempty"` } // VaultAuth is configuration used to authenticate with a Vault server. The diff --git a/pkg/apis/certmanager/v1/zz_generated.deepcopy.go b/pkg/apis/certmanager/v1/zz_generated.deepcopy.go index bc078ab95..453560285 100644 --- a/pkg/apis/certmanager/v1/zz_generated.deepcopy.go +++ b/pkg/apis/certmanager/v1/zz_generated.deepcopy.go @@ -993,6 +993,16 @@ func (in *VaultIssuer) DeepCopyInto(out *VaultIssuer) { *out = new(apismetav1.SecretKeySelector) **out = **in } + if in.ClientCertSecretRef != nil { + in, out := &in.ClientCertSecretRef, &out.ClientCertSecretRef + *out = new(apismetav1.SecretKeySelector) + **out = **in + } + if in.ClientKeySecretRef != nil { + in, out := &in.ClientKeySecretRef, &out.ClientKeySecretRef + *out = new(apismetav1.SecretKeySelector) + **out = **in + } return } diff --git a/test/e2e/framework/addon/globals.go b/test/e2e/framework/addon/globals.go index 53caa6b25..411dd1a29 100644 --- a/test/e2e/framework/addon/globals.go +++ b/test/e2e/framework/addon/globals.go @@ -38,8 +38,9 @@ type AddonTransferableData = internal.AddonTransferableData var ( // Base is a base addon containing Kubernetes clients - Base = &base.Base{} - Vault = &vault.Vault{} + Base = &base.Base{} + Vault = &vault.Vault{} + VaultEnforceMtls = &vault.Vault{} // allAddons is populated by InitGlobals and defines the order in which // addons will be provisioned @@ -65,9 +66,16 @@ func InitGlobals(cfg *config.Config) { Namespace: "e2e-vault", Name: "vault", } + *VaultEnforceMtls = vault.Vault{ + Base: Base, + Namespace: "e2e-vault-mtls", + Name: "vault-mtls", + EnforceMtls: true, + } allAddons = []Addon{ Base, Vault, + VaultEnforceMtls, } } diff --git a/test/e2e/framework/addon/vault/setup.go b/test/e2e/framework/addon/vault/setup.go index bd590fca0..ab0a4017e 100644 --- a/test/e2e/framework/addon/vault/setup.go +++ b/test/e2e/framework/addon/vault/setup.go @@ -18,6 +18,7 @@ package vault import ( "context" + "crypto/tls" "crypto/x509" "fmt" "net" @@ -183,6 +184,19 @@ func NewVaultKubernetesSecret(secretName, serviceAccountName string) *corev1.Sec } } +func NewVaultClientCertificateSecret(secretName string, certificate, key []byte) *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + }, + Data: map[string][]byte{ + corev1.TLSCertKey: certificate, + corev1.TLSPrivateKeyKey: key, + }, + Type: corev1.SecretTypeTLS, + } +} + // Set up a new Vault client, port-forward to the Vault instance. func (v *VaultInitializer) Init() error { cfg := vault.DefaultConfiguration() @@ -193,6 +207,13 @@ func (v *VaultInitializer) Init() error { return fmt.Errorf("error loading Vault CA bundle: %s", v.details.VaultCA) } cfg.HTTPClient.Transport.(*http.Transport).TLSClientConfig.RootCAs = caCertPool + if v.details.EnforceMtls { + clientCertificate, err := tls.X509KeyPair(v.details.VaultClientCertificate, v.details.VaultClientPrivateKey) + if err != nil { + return fmt.Errorf("unable to read vault client certificate: %s", err) + } + cfg.HTTPClient.Transport.(*http.Transport).TLSClientConfig.Certificates = []tls.Certificate{clientCertificate} + } client, err := vault.New(vault.WithConfiguration(cfg)) if err != nil { @@ -211,7 +232,10 @@ func (v *VaultInitializer) Init() error { return fmt.Errorf("error parsing proxy URL: %s", err.Error()) } var lastError error - err = wait.PollUntilContextTimeout(context.TODO(), time.Second, 20*time.Second, true, func(ctx context.Context) (bool, error) { + // The timeout below must be aligned with the time taken by the Vault addons to start, + // each addon safely takes about 20 seconds to start and two addons are started one after another, + // one for without mTLS enforced and another with mTLS enforced + err = wait.PollUntilContextTimeout(context.TODO(), time.Second, 45*time.Second, true, func(ctx context.Context) (bool, error) { conn, err := net.DialTimeout("tcp", proxyUrl.Host, time.Second) if err != nil { lastError = err diff --git a/test/e2e/framework/addon/vault/vault.go b/test/e2e/framework/addon/vault/vault.go index 885c835c6..956fa94d3 100644 --- a/test/e2e/framework/addon/vault/vault.go +++ b/test/e2e/framework/addon/vault/vault.go @@ -30,6 +30,7 @@ import ( "math/big" "net" "os" + "strconv" "strings" "time" @@ -63,6 +64,10 @@ type Vault struct { // Namespace is the namespace to deploy Vault into Namespace string + // EnforceMtls defines if mTLS is enforced in the vault server + // and clients must provide client certificates + EnforceMtls bool + // Proxy is the proxy that can be used to connect to Vault proxy *proxy @@ -84,6 +89,16 @@ type Details struct { // VaultCA is the CA used to sign the vault serving certificate VaultCA []byte + + // VaultClientCertificate is the certificate used by clients when connecting to vault + VaultClientCertificate []byte + + // VaultClientPrivateKey is the private key used by clients when connecting to vault + VaultClientPrivateKey []byte + + // EnforceMtls defines if mTLS is enforced in the vault server + // and clients must provide client certificates + EnforceMtls bool } func convertInterfaceToDetails(unmarshalled interface{}) (Details, error) { @@ -149,6 +164,15 @@ func (v *Vault) Setup(cfg *config.Config, leaderData ...internal.AddonTransferab Key: "server.extraEnvironmentVars.VAULT_DEV_ROOT_TOKEN_ID", Value: "vault-root-token", }, + // configure client certificates used in the readiness/liveness probes exec commands + { + Key: "server.extraEnvironmentVars.VAULT_CLIENT_CERT", + Value: "/vault/tls/client.crt", + }, + { + Key: "server.extraEnvironmentVars.VAULT_CLIENT_KEY", + Value: "/vault/tls/client.key", + }, // configure tls certificate { Key: "global.tlsDisable", @@ -156,15 +180,16 @@ func (v *Vault) Setup(cfg *config.Config, leaderData ...internal.AddonTransferab }, { Key: "server.standalone.config", - Value: ` + Value: fmt.Sprintf(` listener "tcp" { - tls_disable = false address = "[::]:8200" cluster_address = "[::]:8201" tls_disable = false + tls_client_ca_file = "/vault/tls/ca.crt" tls_cert_file = "/vault/tls/server.crt" tls_key_file = "/vault/tls/server.key" - }`, + tls_require_and_verify_client_cert = %s + }`, strconv.FormatBool(v.EnforceMtls)), }, { Key: "server.volumes[0].name", @@ -267,6 +292,14 @@ func (v *Vault) Setup(cfg *config.Config, leaderData ...internal.AddonTransferab return nil, err } + vaultClientCertificate, vaultClientPrivateKey, err := generateVaultClientCert(vaultCA, vaultCAPrivateKey) + if err != nil { + return nil, err + } + v.details.VaultClientCertificate = vaultClientCertificate + v.details.VaultClientPrivateKey = vaultClientPrivateKey + v.details.EnforceMtls = v.EnforceMtls + if cfg.Kubectl == "" { return nil, fmt.Errorf("path to kubectl must be specified") } @@ -310,8 +343,11 @@ func (v *Vault) Provision() error { Namespace: v.Namespace, }, StringData: map[string]string{ + "ca.crt": string(v.details.VaultCA), "server.crt": string(v.vaultCert), "server.key": string(v.vaultCertPrivateKey), + "client.crt": string(v.details.VaultClientCertificate), + "client.key": string(v.details.VaultClientPrivateKey), }, } _, err = kubeClient.CoreV1().Secrets(v.Namespace).Create(context.TODO(), tlsSecret, metav1.CreateOptions{}) @@ -438,6 +474,30 @@ func generateVaultServingCert(vaultCA []byte, vaultCAPrivateKey []byte, dnsName return encodePublicKey(certBytes), encodePrivateKey(privateKey), nil } +func generateVaultClientCert(vaultCA []byte, vaultCAPrivateKey []byte) ([]byte, []byte, error) { + catls, _ := tls.X509KeyPair(vaultCA, vaultCAPrivateKey) + ca, _ := x509.ParseCertificate(catls.Certificate[0]) + + cert := &x509.Certificate{ + Version: 3, + SerialNumber: big.NewInt(1658), + Subject: pkix.Name{ + CommonName: "cert-manager vault client", + Organization: []string{"cert-manager"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + SubjectKeyId: []byte{1, 2, 3, 4, 6}, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + } + + privateKey, _ := rsa.GenerateKey(rand.Reader, 2048) + certBytes, _ := x509.CreateCertificate(rand.Reader, cert, ca, &privateKey.PublicKey, catls.PrivateKey) + + return encodePublicKey(certBytes), encodePrivateKey(privateKey), nil +} + func GenerateCA() ([]byte, []byte, error) { ca := &x509.Certificate{ Version: 3, diff --git a/test/e2e/suite/issuers/vault/mtls.go b/test/e2e/suite/issuers/vault/mtls.go new file mode 100644 index 000000000..e706b9531 --- /dev/null +++ b/test/e2e/suite/issuers/vault/mtls.go @@ -0,0 +1,468 @@ +/* +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 vault + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/rand" + + "github.com/cert-manager/cert-manager/e2e-tests/framework" + "github.com/cert-manager/cert-manager/e2e-tests/framework/addon" + vaultaddon "github.com/cert-manager/cert-manager/e2e-tests/framework/addon/vault" + e2eutil "github.com/cert-manager/cert-manager/e2e-tests/util" + v1 "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/test/unit/gen" +) + +var _ = framework.CertManagerDescribe("Vault Issuer [mtls]", func() { + f := framework.NewDefaultFramework("create-vault-issuer") + + issuerName := "test-vault-issuer" + vaultSecretServiceAccount := "vault-serviceaccount" + vaultClientCertificateSecretName := "vault-client-cert-secret-" + rand.String(5) + var roleId, secretId, vaultSecretName string + + appRoleSecretGeneratorName := "vault-approle-secret-" + var setup *vaultaddon.VaultInitializer + + details := addon.VaultEnforceMtls.Details() + + BeforeEach(func() { + By("Configuring the Vault server") + + setup = vaultaddon.NewVaultInitializerAllAuth( + addon.Base.Details().KubeClient, + *details, + false, + "https://kubernetes.default.svc.cluster.local", + ) + Expect(setup.Init()).NotTo(HaveOccurred(), "failed to init vault") + Expect(setup.Setup()).NotTo(HaveOccurred(), "failed to setup vault") + + var err error + roleId, secretId, err = setup.CreateAppRole() + Expect(err).NotTo(HaveOccurred()) + + By("creating a service account for Vault authentication") + err = setup.CreateKubernetesRole(f.KubeClientSet, f.Namespace.Name, vaultSecretServiceAccount) + Expect(err).NotTo(HaveOccurred()) + + By("creating a client certificate for Vault mTLS") + secret := vaultaddon.NewVaultClientCertificateSecret(vaultClientCertificateSecretName, details.VaultClientCertificate, details.VaultClientPrivateKey) + _, err = f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name).Create(context.TODO(), secret, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + }) + + JustAfterEach(func() { + By("Cleaning up AppRole") + f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name).Delete(context.TODO(), issuerName, metav1.DeleteOptions{}) + f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name).Delete(context.TODO(), vaultSecretName, metav1.DeleteOptions{}) + setup.CleanAppRole() + + By("Cleaning up Kubernetes") + setup.CleanKubernetesRole(f.KubeClientSet, f.Namespace.Name, vaultSecretServiceAccount) + + By("Cleaning up Vault") + Expect(setup.Clean()).NotTo(HaveOccurred()) + }) + + It("should be ready with a valid AppRole", func() { + sec, err := f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name).Create(context.TODO(), vaultaddon.NewVaultAppRoleSecret(appRoleSecretGeneratorName, secretId), metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + vaultSecretName = sec.Name + + vaultIssuer := gen.IssuerWithRandomName(issuerName, + gen.SetIssuerNamespace(f.Namespace.Name), + gen.SetIssuerVaultURL(details.URL), + gen.SetIssuerVaultPath(setup.IntermediateSignPath()), + gen.SetIssuerVaultCABundle(details.VaultCA), + gen.SetIssuerVaultClientCertSecretRef(vaultClientCertificateSecretName, corev1.TLSCertKey), + gen.SetIssuerVaultClientKeySecretRef(vaultClientCertificateSecretName, corev1.TLSPrivateKeyKey), + gen.SetIssuerVaultAppRoleAuth("secretkey", vaultSecretName, roleId, setup.AppRoleAuthPath())) + iss, err := f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name).Create(context.TODO(), vaultIssuer, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Issuer to become Ready") + err = e2eutil.WaitForIssuerCondition(f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name), + iss.Name, + v1.IssuerCondition{ + Type: v1.IssuerConditionReady, + Status: cmmeta.ConditionTrue, + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should fail to init with missing client certificates", func() { + sec, err := f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name).Create(context.TODO(), vaultaddon.NewVaultAppRoleSecret(appRoleSecretGeneratorName, secretId), metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + vaultSecretName = sec.Name + + By("Creating an Issuer") + vaultIssuer := gen.IssuerWithRandomName(issuerName, + gen.SetIssuerNamespace(f.Namespace.Name), + gen.SetIssuerVaultURL(details.URL), + gen.SetIssuerVaultPath(setup.IntermediateSignPath()), + gen.SetIssuerVaultCABundle(details.VaultCA), + gen.SetIssuerVaultAppRoleAuth("secretkey", vaultSecretName, roleId, setup.AppRoleAuthPath())) + iss, err := f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name).Create(context.TODO(), vaultIssuer, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Issuer to become Ready") + err = e2eutil.WaitForIssuerCondition(f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name), + iss.Name, + v1.IssuerCondition{ + Type: v1.IssuerConditionReady, + Status: cmmeta.ConditionFalse, + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should fail to init with missing Vault AppRole", func() { + By("Creating an Issuer") + vaultIssuer := gen.IssuerWithRandomName(issuerName, + gen.SetIssuerNamespace(f.Namespace.Name), + gen.SetIssuerVaultURL(details.URL), + gen.SetIssuerVaultPath(setup.IntermediateSignPath()), + gen.SetIssuerVaultCABundle(details.VaultCA), + gen.SetIssuerVaultClientCertSecretRef(vaultClientCertificateSecretName, corev1.TLSCertKey), + gen.SetIssuerVaultClientKeySecretRef(vaultClientCertificateSecretName, corev1.TLSPrivateKeyKey), + gen.SetIssuerVaultAppRoleAuth("secretkey", roleId, setup.Role(), setup.AppRoleAuthPath())) + iss, err := f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name).Create(context.TODO(), vaultIssuer, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Issuer to become Ready") + err = e2eutil.WaitForIssuerCondition(f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name), + iss.Name, + v1.IssuerCondition{ + Type: v1.IssuerConditionReady, + Status: cmmeta.ConditionFalse, + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should fail to init with missing Vault Token", func() { + By("Creating an Issuer") + vaultIssuer := gen.Issuer(issuerName, + gen.SetIssuerNamespace(f.Namespace.Name), + gen.SetIssuerVaultURL(details.URL), + gen.SetIssuerVaultPath(setup.IntermediateSignPath()), + gen.SetIssuerVaultCABundle(details.VaultCA), + gen.SetIssuerVaultClientCertSecretRef(vaultClientCertificateSecretName, corev1.TLSCertKey), + gen.SetIssuerVaultClientKeySecretRef(vaultClientCertificateSecretName, corev1.TLSPrivateKeyKey), + gen.SetIssuerVaultTokenAuth("secretkey", "vault-token")) + _, err := f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name).Create(context.TODO(), vaultIssuer, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Issuer to become Ready") + err = e2eutil.WaitForIssuerCondition(f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name), + issuerName, + v1.IssuerCondition{ + Type: v1.IssuerConditionReady, + Status: cmmeta.ConditionFalse, + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should be ready with a valid Kubernetes Role and ServiceAccount Secret", func() { + saTokenSecretName := "vault-sa-secret-" + rand.String(5) + _, err := f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name).Create(context.TODO(), vaultaddon.NewVaultKubernetesSecret(saTokenSecretName, vaultSecretServiceAccount), metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + vaultIssuer := gen.Issuer(issuerName, + gen.SetIssuerNamespace(f.Namespace.Name), + gen.SetIssuerVaultURL(details.URL), + gen.SetIssuerVaultPath(setup.IntermediateSignPath()), + gen.SetIssuerVaultCABundle(details.VaultCA), + gen.SetIssuerVaultClientCertSecretRef(vaultClientCertificateSecretName, corev1.TLSCertKey), + gen.SetIssuerVaultClientKeySecretRef(vaultClientCertificateSecretName, corev1.TLSPrivateKeyKey), + gen.SetIssuerVaultKubernetesAuthSecret("token", saTokenSecretName, setup.Role(), setup.KubernetesAuthPath())) + _, err = f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name).Create(context.TODO(), vaultIssuer, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Issuer to become Ready") + err = e2eutil.WaitForIssuerCondition(f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name), + issuerName, + v1.IssuerCondition{ + Type: v1.IssuerConditionReady, + Status: cmmeta.ConditionTrue, + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should fail to init with missing Kubernetes Role", func() { + saTokenSecretName := "vault-sa-secret-" + rand.String(5) + // we test without creating the secret + + By("Creating an Issuer") + vaultIssuer := gen.Issuer(issuerName, + gen.SetIssuerNamespace(f.Namespace.Name), + gen.SetIssuerVaultURL(details.URL), + gen.SetIssuerVaultPath(setup.IntermediateSignPath()), + gen.SetIssuerVaultCABundle(details.VaultCA), + gen.SetIssuerVaultClientCertSecretRef(vaultClientCertificateSecretName, corev1.TLSCertKey), + gen.SetIssuerVaultClientKeySecretRef(vaultClientCertificateSecretName, corev1.TLSPrivateKeyKey), + gen.SetIssuerVaultKubernetesAuthSecret("token", saTokenSecretName, setup.Role(), setup.KubernetesAuthPath())) + _, err := f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name).Create(context.TODO(), vaultIssuer, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + By("Waiting for Issuer to become Ready") + err = e2eutil.WaitForIssuerCondition(f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name), + issuerName, + v1.IssuerCondition{ + Type: v1.IssuerConditionReady, + Status: cmmeta.ConditionFalse, + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should fail to init when both caBundle and caBundleSecretRef are set", func() { + By("Creating an Issuer") + vaultIssuer := gen.Issuer(issuerName, + gen.SetIssuerNamespace(f.Namespace.Name), + gen.SetIssuerVaultURL(details.URL), + gen.SetIssuerVaultPath(setup.IntermediateSignPath()), + gen.SetIssuerVaultCABundle(details.VaultCA), + gen.SetIssuerVaultClientCertSecretRef(vaultClientCertificateSecretName, corev1.TLSCertKey), + gen.SetIssuerVaultClientKeySecretRef(vaultClientCertificateSecretName, corev1.TLSPrivateKeyKey), + gen.SetIssuerVaultCABundleSecretRef("ca-bundle", f.Namespace.Name, "ca.crt")) + _, err := f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name).Create(context.TODO(), vaultIssuer, metav1.CreateOptions{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring( + "spec.vault.caBundle: Invalid value: \"\": specified caBundle and caBundleSecretRef cannot be used together", + )) + Expect(err.Error()).To(ContainSubstring("spec.vault.caBundleSecretRef: Invalid value: \"ca-bundle\": specified caBundleSecretRef and caBundle cannot be used together")) + }) + + It("should be ready with a caBundle from a Kubernetes Secret", func() { + saTokenSecretName := "vault-sa-secret-" + rand.String(5) + _, err := f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name).Create(context.TODO(), vaultaddon.NewVaultKubernetesSecret(saTokenSecretName, vaultSecretServiceAccount), metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + _, err = f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name).Create(context.TODO(), &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ca-bundle", + }, + Type: "Opaque", + Data: map[string][]byte{ + "ca.crt": details.VaultCA, + }, + }, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + vaultIssuer := gen.Issuer(issuerName, + gen.SetIssuerNamespace(f.Namespace.Name), + gen.SetIssuerVaultURL(details.URL), + gen.SetIssuerVaultPath(setup.IntermediateSignPath()), + gen.SetIssuerVaultCABundleSecretRef("ca-bundle", f.Namespace.Name, "ca.crt"), + gen.SetIssuerVaultClientCertSecretRef(vaultClientCertificateSecretName, corev1.TLSCertKey), + gen.SetIssuerVaultClientKeySecretRef(vaultClientCertificateSecretName, corev1.TLSPrivateKeyKey), + gen.SetIssuerVaultKubernetesAuthSecret("token", saTokenSecretName, setup.Role(), setup.KubernetesAuthPath())) + _, err = f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name).Create(context.TODO(), vaultIssuer, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Issuer to become Ready") + err = e2eutil.WaitForIssuerCondition(f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name), + issuerName, + v1.IssuerCondition{ + Type: v1.IssuerConditionReady, + Status: cmmeta.ConditionTrue, + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should be eventually ready when the CA bundle secret gets created after the Issuer", func() { + saTokenSecretName := "vault-sa-secret-" + rand.String(5) + _, err := f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name).Create(context.TODO(), vaultaddon.NewVaultKubernetesSecret(saTokenSecretName, vaultSecretServiceAccount), metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + vaultIssuer := gen.Issuer(issuerName, + gen.SetIssuerNamespace(f.Namespace.Name), + gen.SetIssuerVaultURL(details.URL), + gen.SetIssuerVaultPath(setup.IntermediateSignPath()), + gen.SetIssuerVaultCABundleSecretRef("ca-bundle", f.Namespace.Name, "ca.crt"), + gen.SetIssuerVaultClientCertSecretRef(vaultClientCertificateSecretName, corev1.TLSCertKey), + gen.SetIssuerVaultClientKeySecretRef(vaultClientCertificateSecretName, corev1.TLSPrivateKeyKey), + gen.SetIssuerVaultKubernetesAuthSecret("token", saTokenSecretName, setup.Role(), setup.KubernetesAuthPath())) + _, err = f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name).Create(context.TODO(), vaultIssuer, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + By("Validate that the Issuer is not ready yet") + err = e2eutil.WaitForIssuerCondition(f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name), + issuerName, + v1.IssuerCondition{ + Type: v1.IssuerConditionReady, + Status: cmmeta.ConditionFalse, + }) + Expect(err).NotTo(HaveOccurred()) + + _, err = f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name).Create(context.TODO(), &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ca-bundle", + }, + Type: "Opaque", + Data: map[string][]byte{ + "ca.crt": details.VaultCA, + }, + }, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Issuer to become Ready") + err = e2eutil.WaitForIssuerCondition(f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name), + issuerName, + v1.IssuerCondition{ + Type: v1.IssuerConditionReady, + Status: cmmeta.ConditionTrue, + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should be eventually ready when the Vault client certificate secret gets created after the Issuer", func() { + sec, err := f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name).Create(context.TODO(), vaultaddon.NewVaultAppRoleSecret(appRoleSecretGeneratorName, secretId), metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + vaultSecretName = sec.Name + customVaultClientCertificateSecretName := "vault-client-cert-secret-custom-" + rand.String(5) + + vaultIssuer := gen.Issuer(issuerName, + gen.SetIssuerNamespace(f.Namespace.Name), + gen.SetIssuerVaultURL(details.URL), + gen.SetIssuerVaultPath(setup.IntermediateSignPath()), + gen.SetIssuerVaultCABundle(details.VaultCA), + gen.SetIssuerVaultClientCertSecretRef(customVaultClientCertificateSecretName, corev1.TLSCertKey), + gen.SetIssuerVaultClientKeySecretRef(customVaultClientCertificateSecretName, corev1.TLSPrivateKeyKey), + gen.SetIssuerVaultAppRoleAuth("secretkey", vaultSecretName, roleId, setup.AppRoleAuthPath())) + iss, err := f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name).Create(context.TODO(), vaultIssuer, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + By("Validate that the Issuer is not ready yet") + err = e2eutil.WaitForIssuerCondition(f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name), + issuerName, + v1.IssuerCondition{ + Type: v1.IssuerConditionReady, + Status: cmmeta.ConditionFalse, + }) + Expect(err).NotTo(HaveOccurred()) + + By("creating a client certificate for Vault mTLS") + secret := vaultaddon.NewVaultClientCertificateSecret(customVaultClientCertificateSecretName, details.VaultClientCertificate, details.VaultClientPrivateKey) + _, err = f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name).Create(context.TODO(), secret, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Issuer to become Ready") + err = e2eutil.WaitForIssuerCondition(f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name), + iss.Name, + v1.IssuerCondition{ + Type: v1.IssuerConditionReady, + Status: cmmeta.ConditionTrue, + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("it should become not ready when the CA certificate in the secret changes and doesn't match Vault's CA anymore", func() { + saTokenSecretName := "vault-sa-secret-" + rand.String(5) + _, err := f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name).Create(context.TODO(), vaultaddon.NewVaultKubernetesSecret(saTokenSecretName, vaultSecretServiceAccount), metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + _, err = f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name).Create(context.TODO(), &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ca-bundle", + }, + Type: "Opaque", + Data: map[string][]byte{ + "ca.crt": details.VaultCA, + }, + }, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + vaultIssuer := gen.Issuer(issuerName, + gen.SetIssuerNamespace(f.Namespace.Name), + gen.SetIssuerVaultURL(details.URL), + gen.SetIssuerVaultPath(setup.IntermediateSignPath()), + gen.SetIssuerVaultCABundleSecretRef("ca-bundle", f.Namespace.Name, "ca.crt"), + gen.SetIssuerVaultClientCertSecretRef(vaultClientCertificateSecretName, corev1.TLSCertKey), + gen.SetIssuerVaultClientKeySecretRef(vaultClientCertificateSecretName, corev1.TLSPrivateKeyKey), + gen.SetIssuerVaultKubernetesAuthSecret("token", saTokenSecretName, setup.Role(), setup.KubernetesAuthPath())) + _, err = f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name).Create(context.TODO(), vaultIssuer, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Issuer to become Ready") + err = e2eutil.WaitForIssuerCondition(f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name), + issuerName, + v1.IssuerCondition{ + Type: v1.IssuerConditionReady, + Status: cmmeta.ConditionTrue, + }) + Expect(err).NotTo(HaveOccurred()) + + By("Updating CA bundle") + public, _, err := vaultaddon.GenerateCA() + Expect(err).NotTo(HaveOccurred()) + _, err = f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name).Update(context.TODO(), &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ca-bundle", + }, + Data: map[string][]byte{ + "ca.crt": public, + }, + }, metav1.UpdateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + By("Validate that the issuer isn't ready anymore due to Vault still using the old certificate") + err = e2eutil.WaitForIssuerCondition(f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name), + issuerName, + v1.IssuerCondition{ + Type: v1.IssuerConditionReady, + Status: cmmeta.ConditionFalse, + }) + Expect(err).NotTo(HaveOccurred()) + }) + It("should be ready with a valid serviceAccountRef", func() { + // Note that we reuse the same service account as for the Kubernetes + // auth based on secretRef. There should be no problem doing so. + By("Creating the Role and RoleBinding to let cert-manager use TokenRequest for the ServiceAccount") + vaultaddon.CreateKubernetesRoleForServiceAccountRefAuth(f.KubeClientSet, setup.Role(), f.Namespace.Name, vaultSecretServiceAccount) + defer vaultaddon.CleanKubernetesRoleForServiceAccountRefAuth(f.KubeClientSet, setup.Role(), f.Namespace.Name, vaultSecretServiceAccount) + + By("Creating an Issuer") + vaultIssuer := gen.Issuer(issuerName, + gen.SetIssuerNamespace(f.Namespace.Name), + gen.SetIssuerVaultURL(details.URL), + gen.SetIssuerVaultPath(setup.IntermediateSignPath()), + gen.SetIssuerVaultCABundle(details.VaultCA), + gen.SetIssuerVaultClientCertSecretRef(vaultClientCertificateSecretName, corev1.TLSCertKey), + gen.SetIssuerVaultClientKeySecretRef(vaultClientCertificateSecretName, corev1.TLSPrivateKeyKey), + gen.SetIssuerVaultKubernetesAuthServiceAccount(vaultSecretServiceAccount, setup.Role(), setup.KubernetesAuthPath())) + _, err := f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name).Create(context.TODO(), vaultIssuer, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Issuer to become Ready") + err = e2eutil.WaitForIssuerCondition(f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name), + issuerName, + v1.IssuerCondition{ + Type: v1.IssuerConditionReady, + Status: cmmeta.ConditionTrue, + }) + Expect(err).NotTo(HaveOccurred()) + }) +}) diff --git a/test/unit/gen/issuer.go b/test/unit/gen/issuer.go index 1ab46f34f..eb74eff82 100644 --- a/test/unit/gen/issuer.go +++ b/test/unit/gen/issuer.go @@ -302,6 +302,36 @@ func SetIssuerVaultCABundleSecretRef(name, namespace, key string) IssuerModifier } } +func SetIssuerVaultClientCertSecretRef(vaultClientCertificateSecretName, key string) IssuerModifier { + return func(iss v1.GenericIssuer) { + spec := iss.GetSpec() + if spec.Vault == nil { + spec.Vault = &v1.VaultIssuer{} + } + spec.Vault.ClientCertSecretRef = &cmmeta.SecretKeySelector{ + LocalObjectReference: cmmeta.LocalObjectReference{ + Name: vaultClientCertificateSecretName, + }, + Key: key, + } + } +} + +func SetIssuerVaultClientKeySecretRef(vaultClientCertificateSecretName, key string) IssuerModifier { + return func(iss v1.GenericIssuer) { + spec := iss.GetSpec() + if spec.Vault == nil { + spec.Vault = &v1.VaultIssuer{} + } + spec.Vault.ClientKeySecretRef = &cmmeta.SecretKeySelector{ + LocalObjectReference: cmmeta.LocalObjectReference{ + Name: vaultClientCertificateSecretName, + }, + Key: key, + } + } +} + func SetIssuerVaultTokenAuth(keyName, tokenName string) IssuerModifier { return func(iss v1.GenericIssuer) { spec := iss.GetSpec()