serviceAccountRef: the vault issuer can now use bound SA tokens

Previously, the Vault issuer was only able to use a Secret in order to
use the "Kubernetes authentication" method. The downside to this service
account Secret token is that it has the default JWT iss
"kubernetes/serviceaccount" (along with the fact that the token is not
bound to a particular pod and has no expiry).

With the new serviceAccountRef, cert-manager now requests the token on
behalf of the pod in order to authenticate with Vault.

Signed-off-by: Maël Valais <mael@vls.dev>
This commit is contained in:
Maël Valais 2021-10-15 14:11:23 +02:00
parent ba0bb5d503
commit 76eef68730
28 changed files with 536 additions and 104 deletions

View File

@ -70,7 +70,6 @@ rules:
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "patch"]
---
# ClusterIssuer controller role

View File

@ -1127,7 +1127,6 @@ spec:
type: object
required:
- role
- secretRef
properties:
mountPath:
description: The Vault mountPath here is the mount path to use when authenticating with Vault. For example, setting a value to `/v1/auth/foo`, will use the path `/v1/auth/foo/login` to authenticate with Vault. If unspecified, the default value "/v1/auth/kubernetes" will be used.
@ -1147,6 +1146,22 @@ 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
serviceAccountRef:
description: A reference to a service account that will be used to request a bound token (also known as "projected token"). Compared to using "secretRef", using this field means that you don't rely on statically bound tokens. To use this field, you must configure an RBAC rule to let cert-manager request a token. See <link to a page in cert-manager.io> to learn more.
type: object
required:
- name
properties:
audience:
description: Audience is the intended audience of the token. A recipient of a token must identify itself with an identifier specified in the audience of the token, and otherwise should reject the token. The audience defaults to the identifier of the apiserver.
type: string
expirationSeconds:
description: ExpirationSeconds is the requested duration of validity of the service account token. Defaults to 1 hour and must be at least 10 minutes.
type: integer
format: int64
name:
description: Name of the ServiceAccount used to request a token.
type: string
tokenSecretRef:
description: TokenSecretRef authenticates with Vault by presenting a token.
type: object

View File

@ -1127,7 +1127,6 @@ spec:
type: object
required:
- role
- secretRef
properties:
mountPath:
description: The Vault mountPath here is the mount path to use when authenticating with Vault. For example, setting a value to `/v1/auth/foo`, will use the path `/v1/auth/foo/login` to authenticate with Vault. If unspecified, the default value "/v1/auth/kubernetes" will be used.
@ -1147,6 +1146,22 @@ 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
serviceAccountRef:
description: A reference to a service account that will be used to request a bound token (also known as "projected token"). Compared to using "secretRef", using this field means that you don't rely on statically bound tokens. To use this field, you must configure an RBAC rule to let cert-manager request a token. See <link to a page in cert-manager.io> to learn more.
type: object
required:
- name
properties:
audience:
description: Audience is the intended audience of the token. A recipient of a token must identify itself with an identifier specified in the audience of the token, and otherwise should reject the token. The audience defaults to the identifier of the apiserver.
type: string
expirationSeconds:
description: ExpirationSeconds is the requested duration of validity of the service account token. Defaults to 1 hour and must be at least 10 minutes.
type: integer
format: int64
name:
description: Name of the ServiceAccount used to request a token.
type: string
tokenSecretRef:
description: TokenSecretRef authenticates with Vault by presenting a token.
type: object

View File

@ -243,14 +243,41 @@ type VaultKubernetesAuth struct {
// The required Secret field containing a Kubernetes ServiceAccount JWT used
// for authenticating with Vault. Use of 'ambient credentials' is not
// supported.
// supported. This field should not be set if serviceAccountRef is set.
SecretRef cmmeta.SecretKeySelector
// A reference to a service account that will be used to request a bound
// token (also known as "projected token"). Compared to using "secretRef",
// using this field means that you don't rely on statically bound tokens. To
// use this field, you must configure an RBAC rule to let cert-manager
// request a token. See <link to a page in cert-manager.io> to learn more.
// +optional
ServiceAccountRef ServiceAccountRef `json:"serviceAccountRef,omitempty"`
// A required field containing the Vault Role to assume. A Role binds a
// Kubernetes ServiceAccount with a set of Vault policies.
Role string
}
// ServiceAccountRef is a service account used by cert-manager to request a
// token.
type ServiceAccountRef struct {
// Name of the ServiceAccount used to request a token.
Name string `json:"name"`
// Audience is the intended audience of the token. A recipient of a token
// must identify itself with an identifier specified in the audience of the
// token, and otherwise should reject the token. The audience defaults to the
// identifier of the apiserver.
// +optional
Audience string `json:"audience,omitempty"`
// ExpirationSeconds is the requested duration of validity of the service
// account token. Defaults to 1 hour and must be at least 10 minutes.
// +optional
ExpirationSeconds int64 `json:"expirationSeconds,omitempty"`
}
// CAIssuer configures an issuer that can issue certificates from its provided
// CA certificate. It contains the name of the private key to sign certificates,
// holds the location for Certificate Revocation Lists (CRL) distribution

View File

@ -294,6 +294,16 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*v1.ServiceAccountRef)(nil), (*certmanager.ServiceAccountRef)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1_ServiceAccountRef_To_certmanager_ServiceAccountRef(a.(*v1.ServiceAccountRef), b.(*certmanager.ServiceAccountRef), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*certmanager.ServiceAccountRef)(nil), (*v1.ServiceAccountRef)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_certmanager_ServiceAccountRef_To_v1_ServiceAccountRef(a.(*certmanager.ServiceAccountRef), b.(*v1.ServiceAccountRef), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*v1.VaultAppRole)(nil), (*certmanager.VaultAppRole)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1_VaultAppRole_To_certmanager_VaultAppRole(a.(*v1.VaultAppRole), b.(*certmanager.VaultAppRole), scope)
}); err != nil {
@ -1277,6 +1287,30 @@ func Convert_certmanager_SelfSignedIssuer_To_v1_SelfSignedIssuer(in *certmanager
return autoConvert_certmanager_SelfSignedIssuer_To_v1_SelfSignedIssuer(in, out, s)
}
func autoConvert_v1_ServiceAccountRef_To_certmanager_ServiceAccountRef(in *v1.ServiceAccountRef, out *certmanager.ServiceAccountRef, s conversion.Scope) error {
out.Name = in.Name
out.Audience = in.Audience
out.ExpirationSeconds = in.ExpirationSeconds
return nil
}
// Convert_v1_ServiceAccountRef_To_certmanager_ServiceAccountRef is an autogenerated conversion function.
func Convert_v1_ServiceAccountRef_To_certmanager_ServiceAccountRef(in *v1.ServiceAccountRef, out *certmanager.ServiceAccountRef, s conversion.Scope) error {
return autoConvert_v1_ServiceAccountRef_To_certmanager_ServiceAccountRef(in, out, s)
}
func autoConvert_certmanager_ServiceAccountRef_To_v1_ServiceAccountRef(in *certmanager.ServiceAccountRef, out *v1.ServiceAccountRef, s conversion.Scope) error {
out.Name = in.Name
out.Audience = in.Audience
out.ExpirationSeconds = in.ExpirationSeconds
return nil
}
// Convert_certmanager_ServiceAccountRef_To_v1_ServiceAccountRef is an autogenerated conversion function.
func Convert_certmanager_ServiceAccountRef_To_v1_ServiceAccountRef(in *certmanager.ServiceAccountRef, out *v1.ServiceAccountRef, s conversion.Scope) error {
return autoConvert_certmanager_ServiceAccountRef_To_v1_ServiceAccountRef(in, out, s)
}
func autoConvert_v1_VaultAppRole_To_certmanager_VaultAppRole(in *v1.VaultAppRole, out *certmanager.VaultAppRole, s conversion.Scope) error {
out.Path = in.Path
out.RoleId = in.RoleId
@ -1432,6 +1466,9 @@ func autoConvert_v1_VaultKubernetesAuth_To_certmanager_VaultKubernetesAuth(in *v
if err := internalapismetav1.Convert_v1_SecretKeySelector_To_meta_SecretKeySelector(&in.SecretRef, &out.SecretRef, s); err != nil {
return err
}
if err := Convert_v1_ServiceAccountRef_To_certmanager_ServiceAccountRef(&in.ServiceAccountRef, &out.ServiceAccountRef, s); err != nil {
return err
}
out.Role = in.Role
return nil
}
@ -1446,6 +1483,9 @@ func autoConvert_certmanager_VaultKubernetesAuth_To_v1_VaultKubernetesAuth(in *c
if err := internalapismetav1.Convert_meta_SecretKeySelector_To_v1_SecretKeySelector(&in.SecretRef, &out.SecretRef, s); err != nil {
return err
}
if err := Convert_certmanager_ServiceAccountRef_To_v1_ServiceAccountRef(&in.ServiceAccountRef, &out.ServiceAccountRef, s); err != nil {
return err
}
out.Role = in.Role
return nil
}

View File

@ -125,3 +125,7 @@ func Convert_certmanager_CertificateRequestSpec_To_v1alpha2_CertificateRequestSp
out.CSRPEM = in.Request
return nil
}
func Convert_certmanager_VaultKubernetesAuth_To_v1alpha2_VaultKubernetesAuth(in *certmanager.VaultKubernetesAuth, out *VaultKubernetesAuth, s conversion.Scope) error {
return autoConvert_certmanager_VaultKubernetesAuth_To_v1alpha2_VaultKubernetesAuth(in, out, s)
}

View File

@ -265,7 +265,8 @@ type VaultKubernetesAuth struct {
// The required Secret field containing a Kubernetes ServiceAccount JWT used
// for authenticating with Vault. Use of 'ambient credentials' is not
// supported.
SecretRef cmmeta.SecretKeySelector `json:"secretRef"`
// +optional
SecretRef cmmeta.SecretKeySelector `json:"secretRef,omitempty"`
// A required field containing the Vault Role to assume. A Role binds a
// Kubernetes ServiceAccount with a set of Vault policies.

View File

@ -1462,15 +1462,11 @@ func autoConvert_certmanager_VaultKubernetesAuth_To_v1alpha2_VaultKubernetesAuth
if err := apismetav1.Convert_meta_SecretKeySelector_To_v1_SecretKeySelector(&in.SecretRef, &out.SecretRef, s); err != nil {
return err
}
// WARNING: in.ServiceAccountRef requires manual conversion: does not exist in peer-type
out.Role = in.Role
return nil
}
// Convert_certmanager_VaultKubernetesAuth_To_v1alpha2_VaultKubernetesAuth is an autogenerated conversion function.
func Convert_certmanager_VaultKubernetesAuth_To_v1alpha2_VaultKubernetesAuth(in *certmanager.VaultKubernetesAuth, out *VaultKubernetesAuth, s conversion.Scope) error {
return autoConvert_certmanager_VaultKubernetesAuth_To_v1alpha2_VaultKubernetesAuth(in, out, s)
}
func autoConvert_v1alpha2_VenafiCloud_To_certmanager_VenafiCloud(in *VenafiCloud, out *certmanager.VenafiCloud, s conversion.Scope) error {
out.URL = in.URL
if err := apismetav1.Convert_v1_SecretKeySelector_To_meta_SecretKeySelector(&in.APITokenSecretRef, &out.APITokenSecretRef, s); err != nil {

View File

@ -111,3 +111,8 @@ func Convert_certmanager_CertificateRequestSpec_To_v1alpha3_CertificateRequestSp
out.CSRPEM = in.Request
return nil
}
// Convert_certmanager_VaultKubernetesAuth_To_v1alpha3_VaultKubernetesAuth is an autogenerated conversion function.
func Convert_certmanager_VaultKubernetesAuth_To_v1alpha3_VaultKubernetesAuth(in *certmanager.VaultKubernetesAuth, out *VaultKubernetesAuth, s conversion.Scope) error {
return autoConvert_certmanager_VaultKubernetesAuth_To_v1alpha3_VaultKubernetesAuth(in, out, s)
}

View File

@ -265,7 +265,8 @@ type VaultKubernetesAuth struct {
// The required Secret field containing a Kubernetes ServiceAccount JWT used
// for authenticating with Vault. Use of 'ambient credentials' is not
// supported.
SecretRef cmmeta.SecretKeySelector `json:"secretRef"`
// +optional
SecretRef cmmeta.SecretKeySelector `json:"secretRef,omitempty"`
// A required field containing the Vault Role to assume. A Role binds a
// Kubernetes ServiceAccount with a set of Vault policies.

View File

@ -312,11 +312,6 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*certmanager.VaultKubernetesAuth)(nil), (*VaultKubernetesAuth)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_certmanager_VaultKubernetesAuth_To_v1alpha3_VaultKubernetesAuth(a.(*certmanager.VaultKubernetesAuth), b.(*VaultKubernetesAuth), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*VenafiCloud)(nil), (*certmanager.VenafiCloud)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha3_VenafiCloud_To_certmanager_VenafiCloud(a.(*VenafiCloud), b.(*certmanager.VenafiCloud), scope)
}); err != nil {
@ -367,6 +362,11 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
if err := s.AddConversionFunc((*certmanager.VaultKubernetesAuth)(nil), (*VaultKubernetesAuth)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_certmanager_VaultKubernetesAuth_To_v1alpha3_VaultKubernetesAuth(a.(*certmanager.VaultKubernetesAuth), b.(*VaultKubernetesAuth), scope)
}); err != nil {
return err
}
if err := s.AddConversionFunc((*certmanager.X509Subject)(nil), (*X509Subject)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_certmanager_X509Subject_To_v1alpha3_X509Subject(a.(*certmanager.X509Subject), b.(*X509Subject), scope)
}); err != nil {
@ -1461,15 +1461,11 @@ func autoConvert_certmanager_VaultKubernetesAuth_To_v1alpha3_VaultKubernetesAuth
if err := apismetav1.Convert_meta_SecretKeySelector_To_v1_SecretKeySelector(&in.SecretRef, &out.SecretRef, s); err != nil {
return err
}
// WARNING: in.ServiceAccountRef requires manual conversion: does not exist in peer-type
out.Role = in.Role
return nil
}
// Convert_certmanager_VaultKubernetesAuth_To_v1alpha3_VaultKubernetesAuth is an autogenerated conversion function.
func Convert_certmanager_VaultKubernetesAuth_To_v1alpha3_VaultKubernetesAuth(in *certmanager.VaultKubernetesAuth, out *VaultKubernetesAuth, s conversion.Scope) error {
return autoConvert_certmanager_VaultKubernetesAuth_To_v1alpha3_VaultKubernetesAuth(in, out, s)
}
func autoConvert_v1alpha3_VenafiCloud_To_certmanager_VenafiCloud(in *VenafiCloud, out *certmanager.VenafiCloud, s conversion.Scope) error {
out.URL = in.URL
if err := apismetav1.Convert_v1_SecretKeySelector_To_meta_SecretKeySelector(&in.APITokenSecretRef, &out.APITokenSecretRef, s); err != nil {

View File

@ -0,0 +1,10 @@
package v1beta1
import (
certmanager "github.com/cert-manager/cert-manager/internal/apis/certmanager"
conversion "k8s.io/apimachinery/pkg/conversion"
)
func Convert_certmanager_VaultKubernetesAuth_To_v1beta1_VaultKubernetesAuth(in *certmanager.VaultKubernetesAuth, out *VaultKubernetesAuth, s conversion.Scope) error {
return autoConvert_certmanager_VaultKubernetesAuth_To_v1beta1_VaultKubernetesAuth(in, out, s)
}

View File

@ -267,7 +267,8 @@ type VaultKubernetesAuth struct {
// The required Secret field containing a Kubernetes ServiceAccount JWT used
// for authenticating with Vault. Use of 'ambient credentials' is not
// supported.
SecretRef cmmeta.SecretKeySelector `json:"secretRef"`
// +optional
SecretRef cmmeta.SecretKeySelector `json:"secretRef,omitempty"`
// A required field containing the Vault Role to assume. A Role binds a
// Kubernetes ServiceAccount with a set of Vault policies.

View File

@ -1454,15 +1454,11 @@ func autoConvert_certmanager_VaultKubernetesAuth_To_v1beta1_VaultKubernetesAuth(
if err := apismetav1.Convert_meta_SecretKeySelector_To_v1_SecretKeySelector(&in.SecretRef, &out.SecretRef, s); err != nil {
return err
}
// WARNING: in.ServiceAccountRef requires manual conversion: does not exist in peer-type
out.Role = in.Role
return nil
}
// Convert_certmanager_VaultKubernetesAuth_To_v1beta1_VaultKubernetesAuth is an autogenerated conversion function.
func Convert_certmanager_VaultKubernetesAuth_To_v1beta1_VaultKubernetesAuth(in *certmanager.VaultKubernetesAuth, out *VaultKubernetesAuth, s conversion.Scope) error {
return autoConvert_certmanager_VaultKubernetesAuth_To_v1beta1_VaultKubernetesAuth(in, out, s)
}
func autoConvert_v1beta1_VenafiCloud_To_certmanager_VenafiCloud(in *VenafiCloud, out *certmanager.VenafiCloud, s conversion.Scope) error {
out.URL = in.URL
if err := apismetav1.Convert_v1_SecretKeySelector_To_meta_SecretKeySelector(&in.APITokenSecretRef, &out.APITokenSecretRef, s); err != nil {

View File

@ -817,6 +817,22 @@ func (in *SelfSignedIssuer) DeepCopy() *SelfSignedIssuer {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ServiceAccountRef) DeepCopyInto(out *ServiceAccountRef) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceAccountRef.
func (in *ServiceAccountRef) DeepCopy() *ServiceAccountRef {
if in == nil {
return nil
}
out := new(ServiceAccountRef)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VaultAppRole) DeepCopyInto(out *VaultAppRole) {
*out = *in
@ -896,6 +912,7 @@ func (in *VaultIssuer) DeepCopy() *VaultIssuer {
func (in *VaultKubernetesAuth) DeepCopyInto(out *VaultKubernetesAuth) {
*out = *in
out.SecretRef = in.SecretRef
out.ServiceAccountRef = in.ServiceAccountRef
return
}

View File

@ -18,18 +18,20 @@ package fake
import (
"errors"
"testing"
vault "github.com/hashicorp/vault/api"
)
type Client struct {
type FakeClient struct {
NewRequestS *vault.Request
RawRequestFn func(r *vault.Request) (*vault.Response, error)
token string
GotToken string
T *testing.T
}
func NewFakeClient() *Client {
return &Client{
func NewFakeClient() *FakeClient {
return &FakeClient{
NewRequestS: new(vault.Request),
RawRequestFn: func(r *vault.Request) (*vault.Response, error) {
return nil, errors.New("unexpected RawRequest call")
@ -37,30 +39,33 @@ func NewFakeClient() *Client {
}
}
func (c *Client) WithNewRequest(r *vault.Request) *Client {
func (c *FakeClient) WithNewRequest(r *vault.Request) *FakeClient {
c.NewRequestS = r
return c
}
func (c *Client) WithRawRequest(resp *vault.Response, err error) *Client {
func (c *FakeClient) WithRawRequest(resp *vault.Response, err error) *FakeClient {
c.RawRequestFn = func(r *vault.Request) (*vault.Response, error) {
return resp, err
}
return c
}
func (c *Client) NewRequest(method, requestPath string) *vault.Request {
func (c *FakeClient) WithRawRequestFn(fn func(t *testing.T, r *vault.Request) (*vault.Response, error)) *FakeClient {
c.RawRequestFn = func(req *vault.Request) (*vault.Response, error) {
return fn(c.T, req)
}
return c
}
func (c *FakeClient) NewRequest(method, requestPath string) *vault.Request {
return c.NewRequestS
}
func (c *Client) SetToken(v string) {
c.token = v
func (c *FakeClient) SetToken(v string) {
c.GotToken = v
}
func (c *Client) Token() string {
return c.token
}
func (c *Client) RawRequest(r *vault.Request) (*vault.Response, error) {
func (c *FakeClient) RawRequest(r *vault.Request) (*vault.Response, error) {
return c.RawRequestFn(r)
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package vault
import (
"context"
"crypto/x509"
"errors"
"fmt"
@ -28,6 +29,8 @@ import (
vault "github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/sdk/helper/certutil"
authv1 "k8s.io/api/authentication/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
corelisters "k8s.io/client-go/listers/core/v1"
v1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
@ -39,8 +42,7 @@ var _ Interface = &Vault{}
// ClientBuilder is a function type that returns a new Interface.
// Can be used in tests to create a mock signer of Vault certificate requests.
type ClientBuilder func(namespace string, secretsLister corelisters.SecretLister,
issuer v1.GenericIssuer) (Interface, error)
type ClientBuilder func(namespace string, _ func(ns string) CreateToken, _ corelisters.SecretLister, _ v1.GenericIssuer) (Interface, error)
// Interface implements various high level functionality related to connecting
// with a Vault server, verifying its status and signing certificate request for
@ -57,9 +59,13 @@ type Client interface {
SetToken(v string)
}
// For mocking purposes.
type CreateToken func(ctx context.Context, saName string, req *authv1.TokenRequest, opts metav1.CreateOptions) (*authv1.TokenRequest, error)
// Vault implements Interface and holds a Vault issuer, secrets lister and a
// Vault client.
type Vault struct {
createToken CreateToken // Uses the same namespace as below.
secretsLister corelisters.SecretLister
issuer v1.GenericIssuer
namespace string
@ -86,8 +92,9 @@ type Vault struct {
// secrets lister.
// Returned errors may be network failures and should be considered for
// retrying.
func New(namespace string, secretsLister corelisters.SecretLister, issuer v1.GenericIssuer) (Interface, error) {
func New(namespace string, createTokenFn func(ns string) CreateToken, secretsLister corelisters.SecretLister, issuer v1.GenericIssuer) (Interface, error) {
v := &Vault{
createToken: createTokenFn(namespace),
secretsLister: secretsLister,
namespace: namespace,
issuer: issuer,
@ -197,7 +204,7 @@ func (v *Vault) setToken(client Client) error {
if kubernetesAuth != nil {
token, err := v.requestTokenWithKubernetesAuth(client, kubernetesAuth)
if err != nil {
return fmt.Errorf("error reading Kubernetes service account token from %s: %s", kubernetesAuth.SecretRef.Name, err.Error())
return fmt.Errorf("while requesting a Vault token using the Kubernetes auth: %w", err)
}
client.SetToken(token)
return nil
@ -361,22 +368,41 @@ func (v *Vault) requestTokenWithAppRoleRef(client Client, appRole *v1.VaultAppRo
}
func (v *Vault) requestTokenWithKubernetesAuth(client Client, kubernetesAuth *v1.VaultKubernetesAuth) (string, error) {
secret, err := v.secretsLister.Secrets(v.namespace).Get(kubernetesAuth.SecretRef.Name)
if err != nil {
return "", err
}
var jwt string
switch {
case kubernetesAuth.SecretRef.Name != "":
secret, err := v.secretsLister.Secrets(v.namespace).Get(kubernetesAuth.SecretRef.Name)
if err != nil {
return "", err
}
key := kubernetesAuth.SecretRef.Key
if key == "" {
key = v1.DefaultVaultTokenAuthSecretKey
}
key := kubernetesAuth.SecretRef.Key
if key == "" {
key = v1.DefaultVaultTokenAuthSecretKey
}
keyBytes, ok := secret.Data[key]
if !ok {
return "", fmt.Errorf("no data for %q in secret '%s/%s'", key, v.namespace, kubernetesAuth.SecretRef.Name)
}
keyBytes, ok := secret.Data[key]
if !ok {
return "", fmt.Errorf("no data for %q in secret '%s/%s'", key, v.namespace, kubernetesAuth.SecretRef.Name)
}
jwt := string(keyBytes)
jwt = string(keyBytes)
case kubernetesAuth.ServiceAccountRef.Name != "":
tokenrequest, err := v.createToken(context.Background(), kubernetesAuth.ServiceAccountRef.Name, &authv1.TokenRequest{
Spec: authv1.TokenRequestSpec{
Audiences: []string{kubernetesAuth.ServiceAccountRef.Audience},
ExpirationSeconds: &kubernetesAuth.ServiceAccountRef.ExpirationSeconds,
},
}, metav1.CreateOptions{})
if err != nil {
return "", fmt.Errorf("while requesting a token for the service account %s/%s: %s", v.issuer.GetNamespace(), kubernetesAuth.ServiceAccountRef.Name, err.Error())
}
jwt = tokenrequest.Status.Token
default:
return "", fmt.Errorf("programmer mistake: both serviceAccountRef.name and tokenRef.name are empty")
}
parameters := map[string]string{
"role": kubernetesAuth.Role,
@ -390,7 +416,7 @@ func (v *Vault) requestTokenWithKubernetesAuth(client Client, kubernetesAuth *v1
url := filepath.Join(mountPath, "login")
request := client.NewRequest("POST", url)
err = request.SetJSONBody(parameters)
err := request.SetJSONBody(parameters)
if err != nil {
return "", fmt.Errorf("error encoding Vault parameters: %s", err.Error())
}
@ -464,3 +490,16 @@ func (v *Vault) IsVaultInitializedAndUnsealed() error {
return nil
}
func (v *Vault) addVaultNamespaceToRequest(request *vault.Request) {
vaultIssuer := v.issuer.GetSpec().Vault
if vaultIssuer != nil && vaultIssuer.Namespace != "" {
if request.Headers != nil {
request.Headers.Add("X-VAULT-NAMESPACE", vaultIssuer.Namespace)
} else {
vaultReqHeaders := http.Header{}
vaultReqHeaders.Add("X-VAULT-NAMESPACE", vaultIssuer.Namespace)
request.Headers = vaultReqHeaders
}
}
}

View File

@ -18,6 +18,7 @@ package vault
import (
"bytes"
"context"
"crypto"
"crypto/rsa"
"crypto/x509"
@ -35,6 +36,7 @@ import (
"github.com/hashicorp/vault/sdk/helper/jsonutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
authv1 "k8s.io/api/authentication/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientcorev1 "k8s.io/client-go/listers/core/v1"
@ -171,7 +173,7 @@ func generateCSR(t *testing.T, secretKey crypto.Signer) []byte {
type testSignT struct {
issuer *cmapi.Issuer
fakeLister *listers.FakeSecretLister
fakeClient *vaultfake.Client
fakeClient *vaultfake.FakeClient
csrPEM []byte
expectedErr error
@ -364,15 +366,6 @@ func TestExtractCertificatesFromVaultCertificateSecret(t *testing.T) {
}
}
type testSetTokenT struct {
expectedToken string
expectedErr error
issuer *cmapi.Issuer
fakeLister *listers.FakeSecretLister
fakeClient *vaultfake.Client
}
func TestSetToken(t *testing.T) {
tokenSecret := &corev1.Secret{
Data: map[string][]byte{
@ -391,7 +384,16 @@ func TestSetToken(t *testing.T) {
"my-kube-key": []byte("my-secret-kube-token"),
},
}
tests := map[string]testSetTokenT{
tests := map[string]struct {
expectedToken string
expectedErr error
issuer *cmapi.Issuer
fakeLister *listers.FakeSecretLister
mockCreateToken func(t *testing.T) CreateToken
fakeClient *vaultfake.FakeClient
}{
"if neither token secret ref, app role secret ref, or kube auth then not found then error": {
issuer: gen.Issuer("vault-issuer",
gen.SetIssuerVault(cmapi.VaultIssuer{
@ -400,7 +402,6 @@ func TestSetToken(t *testing.T) {
}),
),
fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister()),
fakeClient: vaultfake.NewFakeClient(),
expectedToken: "",
expectedErr: errors.New(
"error initializing Vault client: tokenSecretRef, appRoleSecretRef, or Kubernetes auth role not set",
@ -423,7 +424,6 @@ func TestSetToken(t *testing.T) {
fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
listers.SetFakeSecretNamespaceListerGet(nil, errors.New("secret does not exists")),
),
fakeClient: vaultfake.NewFakeClient(),
expectedToken: "",
expectedErr: errors.New("secret does not exists"),
},
@ -445,7 +445,7 @@ func TestSetToken(t *testing.T) {
fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
listers.SetFakeSecretNamespaceListerGet(tokenSecret, nil),
),
fakeClient: vaultfake.NewFakeClient(),
expectedToken: "my-secret-token",
expectedErr: nil,
},
@ -470,7 +470,6 @@ func TestSetToken(t *testing.T) {
fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
listers.SetFakeSecretNamespaceListerGet(nil, errors.New("secret not found")),
),
fakeClient: vaultfake.NewFakeClient(),
expectedToken: "",
expectedErr: errors.New("secret not found"),
},
@ -527,7 +526,6 @@ func TestSetToken(t *testing.T) {
fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
listers.SetFakeSecretNamespaceListerGet(nil, errors.New("secret does not exists")),
),
fakeClient: vaultfake.NewFakeClient(),
expectedToken: "",
expectedErr: errors.New("error reading Kubernetes service account token from secret-ref-name: secret does not exists"),
},
@ -552,7 +550,6 @@ func TestSetToken(t *testing.T) {
fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
listers.SetFakeSecretNamespaceListerGet(&corev1.Secret{}, nil),
),
fakeClient: vaultfake.NewFakeClient(),
expectedToken: "",
expectedErr: errors.New(`error reading Kubernetes service account token from secret-ref-name: no data for "my-kube-key" in secret 'test-namespace/secret-ref-name'`),
},
@ -614,7 +611,7 @@ func TestSetToken(t *testing.T) {
expectedErr: nil,
},
"if app role secret ref and token secret set, take preference on token secret": {
"if appRole.secretRef, tokenSecretRef set, take preference on tokenSecretRef": {
issuer: gen.Issuer("vault-issuer",
gen.SetIssuerVault(cmapi.VaultIssuer{
CABundle: []byte(testLeafCertificate),
@ -640,17 +637,65 @@ func TestSetToken(t *testing.T) {
fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
listers.SetFakeSecretNamespaceListerGet(tokenSecret, nil),
),
fakeClient: vaultfake.NewFakeClient(),
expectedToken: "my-secret-token",
expectedErr: nil,
},
"if kubernetes.serviceAccountRef set, request token and exchange it for a vault token": {
issuer: gen.Issuer("vault-issuer",
gen.SetIssuerVault(cmapi.VaultIssuer{
CABundle: []byte(testLeafCertificate),
Auth: cmapi.VaultAuth{
Kubernetes: &cmapi.VaultKubernetesAuth{
Role: "kube-vault-role",
ServiceAccountRef: v1.ServiceAccountRef{
Name: "my-service-account",
Audience: "my-audience",
ExpirationSeconds: 100,
},
Path: "my-path",
},
},
}),
),
mockCreateToken: func(t *testing.T) CreateToken {
return func(_ context.Context, saName string, req *authv1.TokenRequest, _ metav1.CreateOptions) (*authv1.TokenRequest, error) {
assert.Equal(t, "my-service-account", saName)
assert.Equal(t, "my-audience", req.Spec.Audiences[0])
assert.Equal(t, int64(100), *req.Spec.ExpirationSeconds)
return &authv1.TokenRequest{Status: authv1.TokenRequestStatus{
Token: "kube-sa-token",
}}, nil
}
},
fakeClient: vaultfake.NewFakeClient().WithRawRequestFn(func(t *testing.T, req *vault.Request) (*vault.Response, error) {
// Vault exhanges the Kubernetes token with a Vault token.
assert.Equal(t, "kube-sa-token", req.Obj.(map[string]string)["jwt"])
assert.Equal(t, "kube-vault-role", req.Obj.(map[string]string)["role"])
return &vault.Response{Response: &http.Response{Body: io.NopCloser(strings.NewReader(
`{"request_id":"","lease_id":"","lease_duration":0,"renewable":false,"data":null,"warnings":null,"data":{"id":"vault-token"}}`,
))}}, nil
}),
expectedToken: "vault-token",
expectedErr: nil,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
if test.fakeClient == nil {
test.fakeClient = &vaultfake.FakeClient{T: t}
} else {
test.fakeClient.T = t
}
var mockCreateToken CreateToken
if test.mockCreateToken != nil {
mockCreateToken = test.mockCreateToken(t)
}
v := &Vault{
namespace: "test-namespace",
secretsLister: test.fakeLister,
createToken: mockCreateToken,
issuer: test.issuer,
}
@ -662,9 +707,9 @@ func TestSetToken(t *testing.T) {
test.expectedErr, err)
}
if test.fakeClient.Token() != test.expectedToken {
if test.fakeClient.GotToken != test.expectedToken {
t.Errorf("got unexpected client token, exp=%s got=%s",
test.expectedToken, test.fakeClient.Token())
test.expectedToken, test.fakeClient.GotToken)
}
})
}
@ -873,7 +918,8 @@ type testNewConfigT struct {
issuer *cmapi.Issuer
checkFunc func(cfg *vault.Config, err error) error
fakeLister *listers.FakeSecretLister
fakeLister *listers.FakeSecretLister
fakeCreateToken func(t *testing.T) CreateToken
}
func TestNewConfig(t *testing.T) {
@ -1023,6 +1069,30 @@ func TestNewConfig(t *testing.T) {
expectedErr: errors.New("no Vault CA bundles loaded, check bundle contents"),
fakeLister: caBundleSecretRefFakeSecretLister("test-namespace", "bundle", "my-bundle.crt", "not a valid certificate"),
},
"the tokenCreate func should be called with the correct namespace": {
issuer: gen.Issuer("vault-issuer",
gen.SetIssuerVault(cmapi.VaultIssuer{
Path: "my-path",
Auth: cmapi.VaultAuth{
Kubernetes: &cmapi.VaultKubernetesAuth{
Role: "my-role",
ServiceAccountRef: v1.ServiceAccountRef{
Name: "my-sa",
Audience: "my-audience",
ExpirationSeconds: 100,
},
},
}})),
fakeCreateToken: func(t *testing.T) CreateToken {
return func(_ context.Context, saName string, req *authv1.TokenRequest, opts metav1.CreateOptions) (*authv1.TokenRequest, error) {
assert.Equal(t, "test-namespace", req.Namespace)
assert.Equal(t, "my-sa", saName)
return &authv1.TokenRequest{Status: authv1.TokenRequestStatus{
Token: "foo",
}}, nil
}
},
},
}
for name, test := range tests {
@ -1079,7 +1149,6 @@ func TestRequestTokenWithAppRoleRef(t *testing.T) {
tests := map[string]requestTokenWithAppRoleRefT{
"a secret reference that does not exist should error": {
client: vaultfake.NewFakeClient(),
appRole: basicAppRoleRef,
fakeLister: listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
listers.SetFakeSecretNamespaceListerGet(nil, errors.New("secret not found")),
@ -1200,6 +1269,7 @@ func TestNewWithVaultNamespaces(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
c, err := New(
"k8s-ns1",
func(ns string) CreateToken { return nil },
listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
listers.SetFakeSecretNamespaceListerGet(
&corev1.Secret{
@ -1254,6 +1324,7 @@ func TestIsVaultInitiatedAndUnsealedIntegration(t *testing.T) {
v, err := New(
"k8s-ns1",
func(ns string) CreateToken { return nil },
listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
listers.SetFakeSecretNamespaceListerGet(
&corev1.Secret{
@ -1318,6 +1389,7 @@ func TestSignIntegration(t *testing.T) {
v, err := New(
"k8s-ns1",
func(ns string) CreateToken { return nil },
listers.FakeSecretListerFrom(listers.NewFakeSecretLister(),
listers.SetFakeSecretNamespaceListerGet(
&corev1.Secret{

View File

@ -269,13 +269,41 @@ type VaultKubernetesAuth struct {
// The required Secret field containing a Kubernetes ServiceAccount JWT used
// for authenticating with Vault. Use of 'ambient credentials' is not
// supported.
SecretRef cmmeta.SecretKeySelector `json:"secretRef"`
// +optional
SecretRef cmmeta.SecretKeySelector `json:"secretRef,omitempty"`
// A reference to a service account that will be used to request a bound
// token (also known as "projected token"). Compared to using "secretRef",
// using this field means that you don't rely on statically bound tokens. To
// use this field, you must configure an RBAC rule to let cert-manager
// request a token. See <link to a page in cert-manager.io> to learn more.
// +optional
ServiceAccountRef ServiceAccountRef `json:"serviceAccountRef,omitempty"`
// A required field containing the Vault Role to assume. A Role binds a
// Kubernetes ServiceAccount with a set of Vault policies.
Role string `json:"role"`
}
// ServiceAccountRef is a service account used by cert-manager to request a
// token.
type ServiceAccountRef struct {
// Name of the ServiceAccount used to request a token.
Name string `json:"name"`
// Audience is the intended audience of the token. A recipient of a token
// must identify itself with an identifier specified in the audience of the
// token, and otherwise should reject the token. The audience defaults to the
// identifier of the apiserver.
// +optional
Audience string `json:"audience,omitempty"`
// ExpirationSeconds is the requested duration of validity of the service
// account token. Defaults to 1 hour and must be at least 10 minutes.
// +optional
ExpirationSeconds int64 `json:"expirationSeconds,omitempty"`
}
type CAIssuer struct {
// SecretName is the name of the secret used to sign Certificates issued
// by this Issuer.

View File

@ -817,6 +817,22 @@ func (in *SelfSignedIssuer) DeepCopy() *SelfSignedIssuer {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ServiceAccountRef) DeepCopyInto(out *ServiceAccountRef) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceAccountRef.
func (in *ServiceAccountRef) DeepCopy() *ServiceAccountRef {
if in == nil {
return nil
}
out := new(ServiceAccountRef)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VaultAppRole) DeepCopyInto(out *VaultAppRole) {
*out = *in
@ -896,6 +912,7 @@ func (in *VaultIssuer) DeepCopy() *VaultIssuer {
func (in *VaultKubernetesAuth) DeepCopyInto(out *VaultKubernetesAuth) {
*out = *in
out.SecretRef = in.SecretRef
out.ServiceAccountRef = in.ServiceAccountRef
return
}

View File

@ -41,6 +41,7 @@ const (
// pkg/controller/certificaterequests.Issuer interface.
type Vault struct {
issuerOptions controllerpkg.IssuerOptions
createTokenFn func(ns string) vaultinternal.CreateToken
secretsLister corelisters.SecretLister
reporter *crutil.Reporter
@ -59,7 +60,10 @@ func init() {
// NewVault returns a new Vault instance with the given controller context.
func NewVault(ctx *controllerpkg.Context) certificaterequests.Issuer {
return &Vault{
issuerOptions: ctx.IssuerOptions,
issuerOptions: ctx.IssuerOptions,
createTokenFn: func(ns string) vaultinternal.CreateToken {
return ctx.Client.CoreV1().ServiceAccounts(ns).CreateToken
},
secretsLister: ctx.KubeSharedInformerFactory.Core().V1().Secrets().Lister(),
reporter: crutil.NewReporter(ctx.Clock, ctx.Recorder),
vaultClientBuilder: vaultinternal.New,
@ -74,7 +78,7 @@ func (v *Vault) Sign(ctx context.Context, cr *v1.CertificateRequest, issuerObj v
resourceNamespace := v.issuerOptions.ResourceNamespace(issuerObj)
client, err := v.vaultClientBuilder(resourceNamespace, v.secretsLister, issuerObj)
client, err := v.vaultClientBuilder(resourceNamespace, v.createTokenFn, v.secretsLister, issuerObj)
if k8sErrors.IsNotFound(err) {
message := "Required secret resource not found"

View File

@ -518,7 +518,7 @@ func runTest(t *testing.T, test testT) {
vault := NewVault(test.builder.Context).(*Vault)
if test.fakeVault != nil {
vault.vaultClientBuilder = func(ns string, sl corelisters.SecretLister,
vault.vaultClientBuilder = func(ns string, _ func(ns string) internalvault.CreateToken, sl corelisters.SecretLister,
iss cmapi.GenericIssuer) (internalvault.Interface, error) {
return test.fakeVault.New(ns, sl, iss)
}

View File

@ -25,6 +25,7 @@ import (
certificatesv1 "k8s.io/api/certificates/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/client-go/kubernetes"
certificatesclient "k8s.io/client-go/kubernetes/typed/certificates/v1"
corelisters "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/tools/record"
@ -49,6 +50,7 @@ type signingFn func(*x509.Certificate, *x509.Certificate, crypto.PublicKey, inte
// using Vault Issuers.
type Vault struct {
issuerOptions controllerpkg.IssuerOptions
kclient kubernetes.Interface
secretsLister corelisters.SecretLister
recorder record.EventRecorder
@ -71,6 +73,7 @@ func init() {
func NewVault(ctx *controllerpkg.Context) certificatesigningrequests.Signer {
return &Vault{
issuerOptions: ctx.IssuerOptions,
kclient: ctx.Client,
secretsLister: ctx.KubeSharedInformerFactory.Core().V1().Secrets().Lister(),
recorder: ctx.Recorder,
certClient: ctx.Client.CertificatesV1().CertificateSigningRequests(),
@ -89,7 +92,8 @@ func (v *Vault) Sign(ctx context.Context, csr *certificatesv1.CertificateSigning
resourceNamespace := v.issuerOptions.ResourceNamespace(issuerObj)
client, err := v.clientBuilder(resourceNamespace, v.secretsLister, issuerObj)
createTokenFn := func(ns string) internalvault.CreateToken { return v.kclient.CoreV1().ServiceAccounts(ns).CreateToken }
client, err := v.clientBuilder(resourceNamespace, createTokenFn, v.secretsLister, issuerObj)
if apierrors.IsNotFound(err) {
message := "Required secret resource not found"
log.Error(err, message)

View File

@ -129,7 +129,7 @@ func TestProcessItem(t *testing.T) {
Status: corev1.ConditionTrue,
}),
),
clientBuilder: func(_ string, _ corelisters.SecretLister, _ cmapi.GenericIssuer) (internalvault.Interface, error) {
clientBuilder: func(_ string, _ func(ns string) internalvault.CreateToken, _ corelisters.SecretLister, _ cmapi.GenericIssuer) (internalvault.Interface, error) {
return nil, apierrors.NewNotFound(schema.GroupResource{}, "test-secret")
},
builder: &testpkg.Builder{
@ -190,7 +190,7 @@ func TestProcessItem(t *testing.T) {
Status: corev1.ConditionTrue,
}),
),
clientBuilder: func(_ string, _ corelisters.SecretLister, _ cmapi.GenericIssuer) (internalvault.Interface, error) {
clientBuilder: func(_ string, _ func(ns string) internalvault.CreateToken, _ corelisters.SecretLister, _ cmapi.GenericIssuer) (internalvault.Interface, error) {
return nil, errors.New("generic error")
},
expectedErr: true,
@ -234,7 +234,7 @@ func TestProcessItem(t *testing.T) {
Status: corev1.ConditionTrue,
}),
),
clientBuilder: func(_ string, _ corelisters.SecretLister, _ cmapi.GenericIssuer) (internalvault.Interface, error) {
clientBuilder: func(_ string, _ func(ns string) internalvault.CreateToken, _ corelisters.SecretLister, _ cmapi.GenericIssuer) (internalvault.Interface, error) {
return fakevault.New(), nil
},
builder: &testpkg.Builder{
@ -296,7 +296,7 @@ func TestProcessItem(t *testing.T) {
Status: corev1.ConditionTrue,
}),
),
clientBuilder: func(_ string, _ corelisters.SecretLister, _ cmapi.GenericIssuer) (internalvault.Interface, error) {
clientBuilder: func(_ string, _ func(ns string) internalvault.CreateToken, _ corelisters.SecretLister, _ cmapi.GenericIssuer) (internalvault.Interface, error) {
return fakevault.New().WithSign(nil, nil, errors.New("sign error")), nil
},
builder: &testpkg.Builder{
@ -357,7 +357,7 @@ func TestProcessItem(t *testing.T) {
Status: corev1.ConditionTrue,
}),
),
clientBuilder: func(_ string, _ corelisters.SecretLister, _ cmapi.GenericIssuer) (internalvault.Interface, error) {
clientBuilder: func(_ string, _ func(ns string) internalvault.CreateToken, _ corelisters.SecretLister, _ cmapi.GenericIssuer) (internalvault.Interface, error) {
return fakevault.New().WithSign([]byte("signed-cert"), []byte("signing-ca"), nil), nil
},
builder: &testpkg.Builder{

View File

@ -40,7 +40,9 @@ const (
messageAuthFieldsRequired = "Vault tokenSecretRef, appRole, or kubernetes is required"
messageMultipleAuthFieldsSet = "Multiple auth methods cannot be set on the same Vault issuer"
messageKubeAuthFieldsRequired = "Vault Kubernetes auth requires both role and secretRef.name"
messageKubeAuthRoleRequired = "Vault Kubernetes auth requires a role to be set"
messageKubeAuthEitherRequired = "Vault Kubernetes auth requires either secretRef.name or serviceAccountRef.name to be set"
messageKubeAuthSingleRequired = "Vault Kubernetes auth cannot be used with both secretRef.name and serviceAccountRef.name"
messageTokenAuthNameRequired = "Vault Token auth requires tokenSecretRef.name"
messageAppRoleAuthFieldsRequired = "Vault AppRole auth requires both roleId and tokenSecretRef.name"
)
@ -95,14 +97,31 @@ func (v *Vault) Setup(ctx context.Context) error {
return nil
}
// check if all mandatory Vault Kubernetes fields are set.
if kubeAuth != nil && (len(kubeAuth.SecretRef.Name) == 0 || len(kubeAuth.Role) == 0) {
logf.V(logf.WarnLevel).Infof("%s: %s", v.issuer.GetObjectMeta().Name, messageKubeAuthFieldsRequired)
apiutil.SetIssuerCondition(v.issuer, v.issuer.GetGeneration(), v1.IssuerConditionReady, cmmeta.ConditionFalse, errorVault, messageKubeAuthFieldsRequired)
// When using the Kubernetes auth, giving a role is mandatory.
if kubeAuth != nil && len(kubeAuth.Role) == 0 {
logf.V(logf.WarnLevel).Infof("%s: %s", v.issuer.GetObjectMeta().Name, messageKubeAuthRoleRequired)
apiutil.SetIssuerCondition(v.issuer, v.issuer.GetGeneration(), v1.IssuerConditionReady, cmmeta.ConditionFalse, errorVault, messageKubeAuthRoleRequired)
return nil
}
client, err := vaultinternal.New(v.resourceNamespace, v.secretsLister, v.issuer)
// When using the Kubernetes auth, you must either set secretRef or
// serviceAccountRef.
if kubeAuth != nil && (len(kubeAuth.SecretRef.Name) == 0 && len(kubeAuth.ServiceAccountRef.Name) == 0) {
logf.V(logf.WarnLevel).Infof("%s: %s", v.issuer.GetObjectMeta().Name, messageKubeAuthEitherRequired)
apiutil.SetIssuerCondition(v.issuer, v.issuer.GetGeneration(), v1.IssuerConditionReady, cmmeta.ConditionFalse, errorVault, messageKubeAuthEitherRequired)
return nil
}
// When using the Kubernetes auth, you can't use secretRef and
// serviceAccountRef simultaneously.
if kubeAuth != nil && (len(kubeAuth.SecretRef.Name) != 0 && len(kubeAuth.ServiceAccountRef.Name) != 0) {
logf.V(logf.WarnLevel).Infof("%s: %s", v.issuer.GetObjectMeta().Name, messageKubeAuthSingleRequired)
apiutil.SetIssuerCondition(v.issuer, v.issuer.GetGeneration(), v1.IssuerConditionReady, cmmeta.ConditionFalse, errorVault, messageKubeAuthSingleRequired)
return nil
}
createTokenFn := func(ns string) vaultinternal.CreateToken { return v.Client.CoreV1().ServiceAccounts(ns).CreateToken }
client, err := vaultinternal.New(v.resourceNamespace, createTokenFn, v.secretsLister, v.issuer)
if err != nil {
s := messageVaultClientInitFailed + err.Error()
logf.V(logf.WarnLevel).Infof("%s: %s", v.issuer.GetObjectMeta().Name, s)

View File

@ -434,6 +434,15 @@ func (v *VaultInitializer) setupKubernetesBasedAuth() error {
params := map[string]string{
"kubernetes_host": v.APIServerURL,
"kubernetes_ca_cert": v.APIServerCA,
// Since Vault 1.9, HashiCorp recommends disabling the iss validation.
// If we don't disable the iss validation, we can't use the same
// Kubernetes auth config for both testing the "secretRef" Kubernetes
// auth and the "serviceAccountRef" Kubernetes auth because the former
// relies on static tokens for which "iss" is
// "kubernetes/serviceaccount", and the later relies on bound tokens for
// which "iss" is "https://kubernetes.default.svc.cluster.local".
// https://www.vaultproject.io/docs/auth/kubernetes#kubernetes-1-21
"disable_iss_validation": "true",
}
url := fmt.Sprintf("/v1/auth/%s/config", v.KubernetesAuthPath)
@ -551,3 +560,69 @@ func (v *VaultInitializer) CleanKubernetesRole(client kubernetes.Interface, vaul
return nil
}
func RoleAndBindingForServiceAccountRefAuth(roleName, namespace, serviceAccount string) (*rbacv1.Role, *rbacv1.RoleBinding) {
return &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
Name: roleName,
Namespace: namespace,
},
Rules: []rbacv1.PolicyRule{
{
APIGroups: []string{""},
Resources: []string{"serviceaccounts/token"},
ResourceNames: []string{serviceAccount},
Verbs: []string{"create"},
},
},
},
&rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: roleName,
},
RoleRef: rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "Role",
Name: roleName,
},
Subjects: []rbacv1.Subject{
{
Name: "cert-manager",
Namespace: "cert-manager",
Kind: "ServiceAccount",
},
},
}
}
// CreateKubernetesRoleForServiceAccountRefAuth creates a service account and a
// role for using the "serviceAccountRef" field.
func CreateKubernetesRoleForServiceAccountRefAuth(client kubernetes.Interface, roleName, saNS, saName string) error {
role, binding := RoleAndBindingForServiceAccountRefAuth(roleName, saNS, saName)
_, err := client.RbacV1().Roles(saNS).Create(context.TODO(), role, metav1.CreateOptions{})
if err != nil {
return fmt.Errorf("error creating Role for Kubernetes auth ServiceAccount with serviceAccountRef: %s", err.Error())
}
_, err = client.RbacV1().RoleBindings(saNS).Create(context.TODO(), binding, metav1.CreateOptions{})
if err != nil {
return fmt.Errorf("error creating RoleBinding for Kubernetes auth ServiceAccount with serviceAccountRef: %s", err.Error())
}
return nil
}
func CleanKubernetesRoleForServiceAccountRefAuth(client kubernetes.Interface, roleName, saNS, saName string) error {
if err := client.RbacV1().RoleBindings(saNS).Delete(context.TODO(), roleName, metav1.DeleteOptions{}); err != nil {
return err
}
if err := client.RbacV1().Roles(saNS).Delete(context.TODO(), roleName, metav1.DeleteOptions{}); err != nil {
return err
}
if err := client.CoreV1().ServiceAccounts(saNS).Delete(context.TODO(), saName, metav1.DeleteOptions{}); err != nil {
return err
}
return nil
}

View File

@ -188,7 +188,7 @@ var _ = framework.CertManagerDescribe("Vault Issuer", func() {
gen.SetIssuerVaultURL(vault.Details().Host),
gen.SetIssuerVaultPath(vaultPath),
gen.SetIssuerVaultCABundle(vault.Details().VaultCA),
gen.SetIssuerVaultKubernetesAuth("token", vaultSecretServiceAccount, vaultKubernetesRoleName, kubernetesAuthPath))
gen.SetIssuerVaultKubernetesAuthSecret("token", vaultSecretServiceAccount, vaultKubernetesRoleName, kubernetesAuthPath))
_, err = f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name).Create(context.TODO(), vaultIssuer, metav1.CreateOptions{})
Expect(err).NotTo(HaveOccurred())
@ -209,7 +209,7 @@ var _ = framework.CertManagerDescribe("Vault Issuer", func() {
gen.SetIssuerVaultURL(vault.Details().Host),
gen.SetIssuerVaultPath(vaultPath),
gen.SetIssuerVaultCABundle(vault.Details().VaultCA),
gen.SetIssuerVaultKubernetesAuth("token", vaultSecretServiceAccount, vaultKubernetesRoleName, kubernetesAuthPath))
gen.SetIssuerVaultKubernetesAuthSecret("token", vaultSecretServiceAccount, vaultKubernetesRoleName, 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")
@ -258,7 +258,7 @@ var _ = framework.CertManagerDescribe("Vault Issuer", func() {
gen.SetIssuerVaultURL(vault.Details().Host),
gen.SetIssuerVaultPath(vaultPath),
gen.SetIssuerVaultCABundleSecretRef("ca-bundle", f.Namespace.Name, "ca.crt"),
gen.SetIssuerVaultKubernetesAuth("token", vaultSecretServiceAccount, vaultKubernetesRoleName, kubernetesAuthPath))
gen.SetIssuerVaultKubernetesAuthSecret("token", vaultSecretServiceAccount, vaultKubernetesRoleName, kubernetesAuthPath))
_, err = f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name).Create(context.TODO(), vaultIssuer, metav1.CreateOptions{})
Expect(err).NotTo(HaveOccurred())
@ -281,7 +281,7 @@ var _ = framework.CertManagerDescribe("Vault Issuer", func() {
gen.SetIssuerVaultURL(vault.Details().Host),
gen.SetIssuerVaultPath(vaultPath),
gen.SetIssuerVaultCABundleSecretRef("ca-bundle", f.Namespace.Name, "ca.crt"),
gen.SetIssuerVaultKubernetesAuth("token", vaultSecretServiceAccount, vaultKubernetesRoleName, kubernetesAuthPath))
gen.SetIssuerVaultKubernetesAuthSecret("token", vaultSecretServiceAccount, vaultKubernetesRoleName, kubernetesAuthPath))
_, err = f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name).Create(context.TODO(), vaultIssuer, metav1.CreateOptions{})
Expect(err).NotTo(HaveOccurred())
@ -335,7 +335,7 @@ var _ = framework.CertManagerDescribe("Vault Issuer", func() {
gen.SetIssuerVaultURL(vault.Details().Host),
gen.SetIssuerVaultPath(vaultPath),
gen.SetIssuerVaultCABundleSecretRef("ca-bundle", f.Namespace.Name, "ca.crt"),
gen.SetIssuerVaultKubernetesAuth("token", vaultSecretServiceAccount, vaultKubernetesRoleName, kubernetesAuthPath))
gen.SetIssuerVaultKubernetesAuthSecret("token", vaultSecretServiceAccount, vaultKubernetesRoleName, kubernetesAuthPath))
_, err = f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name).Create(context.TODO(), vaultIssuer, metav1.CreateOptions{})
Expect(err).NotTo(HaveOccurred())
@ -369,4 +369,30 @@ var _ = framework.CertManagerDescribe("Vault Issuer", func() {
})
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, vaultKubernetesRoleName, f.Namespace.Name, vaultSecretServiceAccount)
defer vaultaddon.CleanKubernetesRoleForServiceAccountRefAuth(f.KubeClientSet, vaultKubernetesRoleName, f.Namespace.Name, vaultSecretServiceAccount)
By("Creating an Issuer")
vaultIssuer := gen.Issuer(issuerName,
gen.SetIssuerNamespace(f.Namespace.Name),
gen.SetIssuerVaultURL(vault.Details().Host),
gen.SetIssuerVaultPath(vaultPath),
gen.SetIssuerVaultCABundle(vault.Details().VaultCA),
gen.SetIssuerVaultKubernetesAuthServiceAccount(vaultSecretServiceAccount, vaultKubernetesRoleName, 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 = util.WaitForIssuerCondition(f.CertManagerClientSet.CertmanagerV1().Issuers(f.Namespace.Name),
issuerName,
v1.IssuerCondition{
Type: v1.IssuerConditionReady,
Status: cmmeta.ConditionTrue,
})
Expect(err).NotTo(HaveOccurred())
})
})

View File

@ -325,7 +325,7 @@ func SetIssuerVaultAppRoleAuth(keyName, approleName, roleId, path string) Issuer
}
}
func SetIssuerVaultKubernetesAuth(keyName, secretServiceAccount, role, path string) IssuerModifier {
func SetIssuerVaultKubernetesAuthSecret(keyName, secretServiceAccount, role, path string) IssuerModifier {
return func(iss v1.GenericIssuer) {
spec := iss.GetSpec()
if spec.Vault == nil {
@ -344,6 +344,26 @@ func SetIssuerVaultKubernetesAuth(keyName, secretServiceAccount, role, path stri
}
}
func SetIssuerVaultKubernetesAuthServiceAccount(serviceAccount, role, path string) IssuerModifier {
return func(iss v1.GenericIssuer) {
spec := iss.GetSpec()
if spec.Vault == nil {
spec.Vault = &v1.VaultIssuer{}
}
spec.Vault.Auth.Kubernetes = &v1.VaultKubernetesAuth{
Path: path,
Role: role,
ServiceAccountRef: v1.ServiceAccountRef{
Name: serviceAccount,
Audience: "vault",
ExpirationSeconds: 600,
},
}
}
}
func SetIssuerSelfSigned(a v1.SelfSignedIssuer) IssuerModifier {
return func(iss v1.GenericIssuer) {
iss.GetSpec().SelfSigned = &a