Merge pull request #6614 from rodrigorfk/feat-vault-mtls

feat: Add the ability to communicate with Vault via mTLS
This commit is contained in:
jetstack-bot 2024-02-16 18:11:26 +00:00 committed by GitHub
commit 7f92e38988
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 1144 additions and 16 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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.

View File

@ -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
}

View File

@ -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
}

View File

@ -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.

View File

@ -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
}

View File

@ -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
}

View File

@ -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.

View File

@ -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
}

View File

@ -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
}

View File

@ -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"), "<snip>", "clientKeySecretRef must be provided when defining the clientCertSecretRef"))
} else if iss.ClientCertSecretRef == nil && iss.ClientKeySecretRef != nil {
el = append(el, field.Invalid(fldPath.Child("clientCertSecretRef"), "<snip>", "clientCertSecretRef must be provided when defining the clientKeySecretRef"))
}
el = append(el, ValidateVaultIssuerAuth(&iss.Auth, fldPath.Child("auth"))...)
return el

View File

@ -119,6 +119,54 @@ func TestValidateVaultIssuerConfig(t *testing.T) {
field.Invalid(fldPath.Child("caBundle"), "<snip>", "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"), "<snip>", "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"), "<snip>", "clientCertSecretRef must be provided when defining the clientKeySecretRef"),
},
},
}
for n, s := range scenarios {
t.Run(n, func(t *testing.T) {

View File

@ -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
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -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

View File

@ -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,

View File

@ -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: \"<snip>\": 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())
})
})

View File

@ -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()