Repository: jetstack/cert-manager
Pull request #3641 (2021-02-08 19:41:48)

CertificateRequest UserInfo fields
**What this PR does / why we need it**:

This PR adds the `UserInfo` fields `Username`, `Groups`, `UID`, and `Extra` to the Spec of `CertificateRequests.`

Implements: https://github.com/jetstack/cert-manager/pull/3630

This PR introduces a new mutating registry which behaves similar to the validating registry in that is can register multiple mutating functions for both `CREATE` and `UPDATE` events for different cert-manager types. The registry will attempt to run the mutating functions that are registered for that type, before applying the scheme defaults, and generating a JSON patch.

I have updated the validation registry where the functions now accept an admission request, whose UserInfo is required for the identity validation functions.

I have added e2e tests to ensure that the identity fields are properly populated, and they have they same behaviour as the [design doc](https://github.com/jetstack/cert-manager/pull/3630).

I have also made the `CertificateRequest` immutable upon creation where the spec, and metadata.annotations fields cannot be modified. I have included annotations here as these can be used to change the behaviour of how the `CertificateRequest` gets signed (e.g.  `cert-manager.io/private-key-secret-name`).

**Special notes for your reviewer**:

Hopefully the commits are quite well separated so I would suggest to review by commit. Let me know if I need to split the PR out.

```release-note
Adds UserInfo fields to CertificateRequests containing the UserInfo of the requester: `Username`, `Groups`, `UID`, `Extra`.
```



Patch:
diff --git a/cmd/webhook/app/webhook.go b/cmd/webhook/app/webhook.go
index 098ea1536..bb6497399 100644
--- a/cmd/webhook/app/webhook.go
+++ b/cmd/webhook/app/webhook.go
@@ -35,7 +35,7 @@ import (
 )
 
 var validationHook handlers.ValidatingAdmissionHook = handlers.NewRegistryBackedValidator(logf.Log, webhook.Scheme, webhook.ValidationRegistry)
-var mutationHook handlers.MutatingAdmissionHook = handlers.NewSchemeBackedDefaulter(logf.Log, webhook.Scheme)
+var mutationHook handlers.MutatingAdmissionHook = handlers.NewRegistryBackedMutator(logf.Log, webhook.Scheme, webhook.MutationRegistry)
 var conversionHook handlers.ConversionHook = handlers.NewSchemeBackedConverter(logf.Log, webhook.Scheme)
 
 func NewServerWithOptions(log logr.Logger, opts options.WebhookOptions) (*server.Server, error) {
diff --git a/deploy/crds/crd-certificaterequests.yaml b/deploy/crds/crd-certificaterequests.yaml
index 3b5133912..9155a1967 100644
--- a/deploy/crds/crd-certificaterequests.yaml
+++ b/deploy/crds/crd-certificaterequests.yaml
@@ -81,6 +81,19 @@ spec:
                 duration:
                   description: The requested 'duration' (i.e. lifetime) of the Certificate. This option may be ignored/overridden by some issuer types.
                   type: string
+                extra:
+                  description: Extra contains extra attributes of the user that created the CertificateRequest. Populated by the cert-manager webhook on creation and immutable.
+                  type: object
+                  additionalProperties:
+                    type: array
+                    items:
+                      type: string
+                groups:
+                  description: Groups contains group membership of the user that created the CertificateRequest. Populated by the cert-manager webhook on creation and immutable.
+                  type: array
+                  items:
+                    type: string
+                  x-kubernetes-list-type: atomic
                 isCA:
                   description: IsCA will request to mark the certificate as valid for certificate signing when submitting to the issuer. This will automatically add the `cert sign` usage to the list of `usages`.
                   type: boolean
@@ -99,6 +112,9 @@ spec:
                     name:
                       description: Name of the resource being referred to.
                       type: string
+                uid:
+                  description: UID contains the uid of the user that created the CertificateRequest. Populated by the cert-manager webhook on creation and immutable.
+                  type: string
                 usages:
                   description: Usages is the set of x509 usages that are requested for the certificate. Defaults to `digital signature` and `key encipherment` if not specified.
                   type: array
@@ -129,6 +145,9 @@ spec:
                       - ocsp signing
                       - microsoft sgc
                       - netscape sgc
+                username:
+                  description: Username contains the name of the user that created the CertificateRequest. Populated by the cert-manager webhook on creation and immutable.
+                  type: string
             status:
               description: Status of the CertificateRequest. This is set and managed automatically.
               type: object
@@ -223,6 +242,19 @@ spec:
                 duration:
                   description: The requested 'duration' (i.e. lifetime) of the Certificate. This option may be ignored/overridden by some issuer types.
                   type: string
+                extra:
+                  description: Extra contains extra attributes of the user that created the CertificateRequest. Populated by the cert-manager webhook on creation and immutable.
+                  type: object
+                  additionalProperties:
+                    type: array
+                    items:
+                      type: string
+                groups:
+                  description: Groups contains group membership of the user that created the CertificateRequest. Populated by the cert-manager webhook on creation and immutable.
+                  type: array
+                  items:
+                    type: string
+                  x-kubernetes-list-type: atomic
                 isCA:
                   description: IsCA will request to mark the certificate as valid for certificate signing when submitting to the issuer. This will automatically add the `cert sign` usage to the list of `usages`.
                   type: boolean
@@ -241,6 +273,9 @@ spec:
                     name:
                       description: Name of the resource being referred to.
                       type: string
+                uid:
+                  description: UID contains the uid of the user that created the CertificateRequest. Populated by the cert-manager webhook on creation and immutable.
+                  type: string
                 usages:
                   description: Usages is the set of x509 usages that are requested for the certificate. Defaults to `digital signature` and `key encipherment` if not specified.
                   type: array
@@ -271,6 +306,9 @@ spec:
                       - ocsp signing
                       - microsoft sgc
                       - netscape sgc
+                username:
+                  description: Username contains the name of the user that created the CertificateRequest. Populated by the cert-manager webhook on creation and immutable.
+                  type: string
             status:
               description: Status of the CertificateRequest. This is set and managed automatically.
               type: object
@@ -363,6 +401,19 @@ spec:
                 duration:
                   description: The requested 'duration' (i.e. lifetime) of the Certificate. This option may be ignored/overridden by some issuer types.
                   type: string
+                extra:
+                  description: Extra contains extra attributes of the user that created the CertificateRequest. Populated by the cert-manager webhook on creation and immutable.
+                  type: object
+                  additionalProperties:
+                    type: array
+                    items:
+                      type: string
+                groups:
+                  description: Groups contains group membership of the user that created the CertificateRequest. Populated by the cert-manager webhook on creation and immutable.
+                  type: array
+                  items:
+                    type: string
+                  x-kubernetes-list-type: atomic
                 isCA:
                   description: IsCA will request to mark the certificate as valid for certificate signing when submitting to the issuer. This will automatically add the `cert sign` usage to the list of `usages`.
                   type: boolean
@@ -385,6 +436,9 @@ spec:
                   description: The PEM-encoded x509 certificate signing request to be submitted to the CA for signing.
                   type: string
                   format: byte
+                uid:
+                  description: UID contains the uid of the user that created the CertificateRequest. Populated by the cert-manager webhook on creation and immutable.
+                  type: string
                 usages:
                   description: Usages is the set of x509 usages that are requested for the certificate. Defaults to `digital signature` and `key encipherment` if not specified.
                   type: array
@@ -415,6 +469,9 @@ spec:
                       - ocsp signing
                       - microsoft sgc
                       - netscape sgc
+                username:
+                  description: Username contains the name of the user that created the CertificateRequest. Populated by the cert-manager webhook on creation and immutable.
+                  type: string
             status:
               description: Status of the CertificateRequest. This is set and managed automatically.
               type: object
@@ -507,6 +564,19 @@ spec:
                 duration:
                   description: The requested 'duration' (i.e. lifetime) of the Certificate. This option may be ignored/overridden by some issuer types.
                   type: string
+                extra:
+                  description: Extra contains extra attributes of the user that created the CertificateRequest. Populated by the cert-manager webhook on creation and immutable.
+                  type: object
+                  additionalProperties:
+                    type: array
+                    items:
+                      type: string
+                groups:
+                  description: Groups contains group membership of the user that created the CertificateRequest. Populated by the cert-manager webhook on creation and immutable.
+                  type: array
+                  items:
+                    type: string
+                  x-kubernetes-list-type: atomic
                 isCA:
                   description: IsCA will request to mark the certificate as valid for certificate signing when submitting to the issuer. This will automatically add the `cert sign` usage to the list of `usages`.
                   type: boolean
@@ -529,6 +599,9 @@ spec:
                   description: The PEM-encoded x509 certificate signing request to be submitted to the CA for signing.
                   type: string
                   format: byte
+                uid:
+                  description: UID contains the uid of the user that created the CertificateRequest. Populated by the cert-manager webhook on creation and immutable.
+                  type: string
                 usages:
                   description: Usages is the set of x509 usages that are requested for the certificate. If usages are set they SHOULD be encoded inside the CSR spec Defaults to `digital signature` and `key encipherment` if not specified.
                   type: array
@@ -559,6 +632,9 @@ spec:
                       - ocsp signing
                       - microsoft sgc
                       - netscape sgc
+                username:
+                  description: Username contains the name of the user that created the CertificateRequest. Populated by the cert-manager webhook on creation and immutable.
+                  type: string
             status:
               description: Status of the CertificateRequest. This is set and managed automatically.
               type: object
diff --git a/pkg/apis/certmanager/v1/types_certificaterequest.go b/pkg/apis/certmanager/v1/types_certificaterequest.go
index bf0123816..a86700ff7 100644
--- a/pkg/apis/certmanager/v1/types_certificaterequest.go
+++ b/pkg/apis/certmanager/v1/types_certificaterequest.go
@@ -102,6 +102,24 @@ type CertificateRequestSpec struct {
 	// Defaults to `digital signature` and `key encipherment` if not specified.
 	// +optional
 	Usages []KeyUsage `json:"usages,omitempty"`
+
+	// Username contains the name of the user that created the CertificateRequest.
+	// Populated by the cert-manager webhook on creation and immutable.
+	// +optional
+	Username string `json:"username,omitempty"`
+	// UID contains the uid of the user that created the CertificateRequest.
+	// Populated by the cert-manager webhook on creation and immutable.
+	// +optional
+	UID string `json:"uid,omitempty"`
+	// Groups contains group membership of the user that created the CertificateRequest.
+	// Populated by the cert-manager webhook on creation and immutable.
+	// +listType=atomic
+	// +optional
+	Groups []string `json:"groups,omitempty"`
+	// Extra contains extra attributes of the user that created the CertificateRequest.
+	// Populated by the cert-manager webhook on creation and immutable.
+	// +optional
+	Extra map[string][]string `json:"extra,omitempty"`
 }
 
 // CertificateRequestStatus defines the observed state of CertificateRequest and
diff --git a/pkg/apis/certmanager/v1/zz_generated.deepcopy.go b/pkg/apis/certmanager/v1/zz_generated.deepcopy.go
index 5ed714547..7bc07b144 100644
--- a/pkg/apis/certmanager/v1/zz_generated.deepcopy.go
+++ b/pkg/apis/certmanager/v1/zz_generated.deepcopy.go
@@ -276,6 +276,26 @@ func (in *CertificateRequestSpec) DeepCopyInto(out *CertificateRequestSpec) {
 		*out = make([]KeyUsage, len(*in))
 		copy(*out, *in)
 	}
+	if in.Groups != nil {
+		in, out := &in.Groups, &out.Groups
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.Extra != nil {
+		in, out := &in.Extra, &out.Extra
+		*out = make(map[string][]string, len(*in))
+		for key, val := range *in {
+			var outVal []string
+			if val == nil {
+				(*out)[key] = nil
+			} else {
+				in, out := &val, &outVal
+				*out = make([]string, len(*in))
+				copy(*out, *in)
+			}
+			(*out)[key] = outVal
+		}
+	}
 	return
 }
 
diff --git a/pkg/apis/certmanager/v1alpha2/types_certificaterequest.go b/pkg/apis/certmanager/v1alpha2/types_certificaterequest.go
index f6b189327..be4b38b7a 100644
--- a/pkg/apis/certmanager/v1alpha2/types_certificaterequest.go
+++ b/pkg/apis/certmanager/v1alpha2/types_certificaterequest.go
@@ -99,6 +99,24 @@ type CertificateRequestSpec struct {
 	// Defaults to `digital signature` and `key encipherment` if not specified.
 	// +optional
 	Usages []KeyUsage `json:"usages,omitempty"`
+
+	// Username contains the name of the user that created the CertificateRequest.
+	// Populated by the cert-manager webhook on creation and immutable.
+	// +optional
+	Username string `json:"username,omitempty"`
+	// UID contains the uid of the user that created the CertificateRequest.
+	// Populated by the cert-manager webhook on creation and immutable.
+	// +optional
+	UID string `json:"uid,omitempty"`
+	// Groups contains group membership of the user that created the CertificateRequest.
+	// Populated by the cert-manager webhook on creation and immutable.
+	// +listType=atomic
+	// +optional
+	Groups []string `json:"groups,omitempty"`
+	// Extra contains extra attributes of the user that created the CertificateRequest.
+	// Populated by the cert-manager webhook on creation and immutable.
+	// +optional
+	Extra map[string][]string `json:"extra,omitempty"`
 }
 
 // CertificateRequestStatus defines the observed state of CertificateRequest and
diff --git a/pkg/apis/certmanager/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/certmanager/v1alpha2/zz_generated.deepcopy.go
index 3cd08e1b7..0fa0d4fea 100644
--- a/pkg/apis/certmanager/v1alpha2/zz_generated.deepcopy.go
+++ b/pkg/apis/certmanager/v1alpha2/zz_generated.deepcopy.go
@@ -276,6 +276,26 @@ func (in *CertificateRequestSpec) DeepCopyInto(out *CertificateRequestSpec) {
 		*out = make([]KeyUsage, len(*in))
 		copy(*out, *in)
 	}
+	if in.Groups != nil {
+		in, out := &in.Groups, &out.Groups
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.Extra != nil {
+		in, out := &in.Extra, &out.Extra
+		*out = make(map[string][]string, len(*in))
+		for key, val := range *in {
+			var outVal []string
+			if val == nil {
+				(*out)[key] = nil
+			} else {
+				in, out := &val, &outVal
+				*out = make([]string, len(*in))
+				copy(*out, *in)
+			}
+			(*out)[key] = outVal
+		}
+	}
 	return
 }
 
diff --git a/pkg/apis/certmanager/v1alpha3/types_certificaterequest.go b/pkg/apis/certmanager/v1alpha3/types_certificaterequest.go
index 3a35dc004..46393cab3 100644
--- a/pkg/apis/certmanager/v1alpha3/types_certificaterequest.go
+++ b/pkg/apis/certmanager/v1alpha3/types_certificaterequest.go
@@ -99,6 +99,24 @@ type CertificateRequestSpec struct {
 	// Defaults to `digital signature` and `key encipherment` if not specified.
 	// +optional
 	Usages []KeyUsage `json:"usages,omitempty"`
+
+	// Username contains the name of the user that created the CertificateRequest.
+	// Populated by the cert-manager webhook on creation and immutable.
+	// +optional
+	Username string `json:"username,omitempty"`
+	// UID contains the uid of the user that created the CertificateRequest.
+	// Populated by the cert-manager webhook on creation and immutable.
+	// +optional
+	UID string `json:"uid,omitempty"`
+	// Groups contains group membership of the user that created the CertificateRequest.
+	// Populated by the cert-manager webhook on creation and immutable.
+	// +listType=atomic
+	// +optional
+	Groups []string `json:"groups,omitempty"`
+	// Extra contains extra attributes of the user that created the CertificateRequest.
+	// Populated by the cert-manager webhook on creation and immutable.
+	// +optional
+	Extra map[string][]string `json:"extra,omitempty"`
 }
 
 // CertificateRequestStatus defines the observed state of CertificateRequest and
diff --git a/pkg/apis/certmanager/v1alpha3/zz_generated.deepcopy.go b/pkg/apis/certmanager/v1alpha3/zz_generated.deepcopy.go
index abfa3cf66..401ec5bf1 100644
--- a/pkg/apis/certmanager/v1alpha3/zz_generated.deepcopy.go
+++ b/pkg/apis/certmanager/v1alpha3/zz_generated.deepcopy.go
@@ -276,6 +276,26 @@ func (in *CertificateRequestSpec) DeepCopyInto(out *CertificateRequestSpec) {
 		*out = make([]KeyUsage, len(*in))
 		copy(*out, *in)
 	}
+	if in.Groups != nil {
+		in, out := &in.Groups, &out.Groups
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.Extra != nil {
+		in, out := &in.Extra, &out.Extra
+		*out = make(map[string][]string, len(*in))
+		for key, val := range *in {
+			var outVal []string
+			if val == nil {
+				(*out)[key] = nil
+			} else {
+				in, out := &val, &outVal
+				*out = make([]string, len(*in))
+				copy(*out, *in)
+			}
+			(*out)[key] = outVal
+		}
+	}
 	return
 }
 
diff --git a/pkg/apis/certmanager/v1beta1/types_certificaterequest.go b/pkg/apis/certmanager/v1beta1/types_certificaterequest.go
index 3c345e33d..3af27a8a9 100644
--- a/pkg/apis/certmanager/v1beta1/types_certificaterequest.go
+++ b/pkg/apis/certmanager/v1beta1/types_certificaterequest.go
@@ -100,6 +100,24 @@ type CertificateRequestSpec struct {
 	// Defaults to `digital signature` and `key encipherment` if not specified.
 	// +optional
 	Usages []KeyUsage `json:"usages,omitempty"`
+
+	// Username contains the name of the user that created the CertificateRequest.
+	// Populated by the cert-manager webhook on creation and immutable.
+	// +optional
+	Username string `json:"username,omitempty"`
+	// UID contains the uid of the user that created the CertificateRequest.
+	// Populated by the cert-manager webhook on creation and immutable.
+	// +optional
+	UID string `json:"uid,omitempty"`
+	// Groups contains group membership of the user that created the CertificateRequest.
+	// Populated by the cert-manager webhook on creation and immutable.
+	// +listType=atomic
+	// +optional
+	Groups []string `json:"groups,omitempty"`
+	// Extra contains extra attributes of the user that created the CertificateRequest.
+	// Populated by the cert-manager webhook on creation and immutable.
+	// +optional
+	Extra map[string][]string `json:"extra,omitempty"`
 }
 
 // CertificateRequestStatus defines the observed state of CertificateRequest and
diff --git a/pkg/apis/certmanager/v1beta1/zz_generated.deepcopy.go b/pkg/apis/certmanager/v1beta1/zz_generated.deepcopy.go
index 9a904cfbc..875b73bb6 100644
--- a/pkg/apis/certmanager/v1beta1/zz_generated.deepcopy.go
+++ b/pkg/apis/certmanager/v1beta1/zz_generated.deepcopy.go
@@ -276,6 +276,26 @@ func (in *CertificateRequestSpec) DeepCopyInto(out *CertificateRequestSpec) {
 		*out = make([]KeyUsage, len(*in))
 		copy(*out, *in)
 	}
+	if in.Groups != nil {
+		in, out := &in.Groups, &out.Groups
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.Extra != nil {
+		in, out := &in.Extra, &out.Extra
+		*out = make(map[string][]string, len(*in))
+		for key, val := range *in {
+			var outVal []string
+			if val == nil {
+				(*out)[key] = nil
+			} else {
+				in, out := &val, &outVal
+				*out = make([]string, len(*in))
+				copy(*out, *in)
+			}
+			(*out)[key] = outVal
+		}
+	}
 	return
 }
 
diff --git a/pkg/controller/certificaterequests/BUILD.bazel b/pkg/controller/certificaterequests/BUILD.bazel
index 15ca494f0..53ec20aa4 100644
--- a/pkg/controller/certificaterequests/BUILD.bazel
+++ b/pkg/controller/certificaterequests/BUILD.bazel
@@ -18,11 +18,9 @@ go_library(
         "//pkg/client/listers/certmanager/v1:go_default_library",
         "//pkg/controller:go_default_library",
         "//pkg/controller/certificaterequests/util:go_default_library",
-        "//pkg/internal/apis/certmanager:go_default_library",
         "//pkg/issuer:go_default_library",
         "//pkg/logs:go_default_library",
         "//pkg/util/pki:go_default_library",
-        "//pkg/webhook:go_default_library",
         "@com_github_go_logr_logr//:go_default_library",
         "@com_github_kr_pretty//:go_default_library",
         "@io_k8s_apimachinery//pkg/api/errors:go_default_library",
diff --git a/pkg/controller/certificaterequests/sync.go b/pkg/controller/certificaterequests/sync.go
index 3e622f35a..4c55f6950 100644
--- a/pkg/controller/certificaterequests/sync.go
+++ b/pkg/controller/certificaterequests/sync.go
@@ -31,10 +31,8 @@ import (
 	"github.com/jetstack/cert-manager/pkg/apis/certmanager"
 	v1 "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
 	cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1"
-	internalapi "github.com/jetstack/cert-manager/pkg/internal/apis/certmanager"
 	logf "github.com/jetstack/cert-manager/pkg/logs"
 	"github.com/jetstack/cert-manager/pkg/util/pki"
-	"github.com/jetstack/cert-manager/pkg/webhook"
 )
 
 var (
@@ -112,13 +110,6 @@ func (c *Controller) Sync(ctx context.Context, cr *v1.CertificateRequest) (err e
 
 	dbg.Info("validating CertificateRequest resource object")
 
-	el := webhook.ValidationRegistry.Validate(crCopy, internalapi.SchemeGroupVersion.WithKind("CertificateRequest"))
-	if len(el) > 0 {
-		c.reporter.Failed(crCopy, el.ToAggregate(), "BadConfig",
-			"Resource validation failed")
-		return nil
-	}
-
 	if len(crCopy.Status.Certificate) > 0 {
 		dbg.Info("certificate field is already set in status so skipping processing")
 		return nil
diff --git a/pkg/controller/certificates/requestmanager/requestmanager_controller.go b/pkg/controller/certificates/requestmanager/requestmanager_controller.go
index 5f2bda3ae..4487a501f 100644
--- a/pkg/controller/certificates/requestmanager/requestmanager_controller.go
+++ b/pkg/controller/certificates/requestmanager/requestmanager_controller.go
@@ -338,6 +338,7 @@ func (c *controller) createNewCertificateRequest(ctx context.Context, crt *cmapi
 
 	cr, err = c.client.CertmanagerV1().CertificateRequests(cr.Namespace).Create(ctx, cr, metav1.CreateOptions{})
 	if err != nil {
+		c.recorder.Eventf(crt, corev1.EventTypeWarning, "RequestFailed", "Failed to create CertificateRequest: "+err.Error())
 		return err
 	}
 	c.recorder.Eventf(crt, corev1.EventTypeNormal, "Requested", "Created new CertificateRequest resource %q", cr.Name)
diff --git a/pkg/controller/clusterissuers/BUILD.bazel b/pkg/controller/clusterissuers/BUILD.bazel
index eb44a22a4..f9411e730 100644
--- a/pkg/controller/clusterissuers/BUILD.bazel
+++ b/pkg/controller/clusterissuers/BUILD.bazel
@@ -10,16 +10,12 @@ go_library(
     importpath = "github.com/jetstack/cert-manager/pkg/controller/clusterissuers",
     visibility = ["//visibility:public"],
     deps = [
-        "//pkg/api/util:go_default_library",
         "//pkg/apis/certmanager/v1:go_default_library",
-        "//pkg/apis/meta/v1:go_default_library",
         "//pkg/client/clientset/versioned:go_default_library",
         "//pkg/client/listers/certmanager/v1:go_default_library",
         "//pkg/controller:go_default_library",
-        "//pkg/internal/apis/certmanager:go_default_library",
         "//pkg/issuer:go_default_library",
         "//pkg/logs:go_default_library",
-        "//pkg/webhook:go_default_library",
         "@com_github_go_logr_logr//:go_default_library",
         "@io_k8s_api//core/v1:go_default_library",
         "@io_k8s_apimachinery//pkg/api/errors:go_default_library",
diff --git a/pkg/controller/clusterissuers/sync.go b/pkg/controller/clusterissuers/sync.go
index 42a34e323..2bb422552 100644
--- a/pkg/controller/clusterissuers/sync.go
+++ b/pkg/controller/clusterissuers/sync.go
@@ -18,7 +18,6 @@ package clusterissuers
 
 import (
 	"context"
-	"fmt"
 	"reflect"
 	"time"
 
@@ -26,22 +25,17 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/errors"
 
-	apiutil "github.com/jetstack/cert-manager/pkg/api/util"
-	v1 "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
-	cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1"
-	internalapi "github.com/jetstack/cert-manager/pkg/internal/apis/certmanager"
+	cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
 	logf "github.com/jetstack/cert-manager/pkg/logs"
-	"github.com/jetstack/cert-manager/pkg/webhook"
 )
 
 const (
 	errorInitIssuer = "ErrInitIssuer"
-	errorConfig     = "ConfigError"
 
 	messageErrorInitIssuer = "Error initializing issuer: "
 )
 
-func (c *controller) Sync(ctx context.Context, iss *v1.ClusterIssuer) (err error) {
+func (c *controller) Sync(ctx context.Context, iss *cmapi.ClusterIssuer) (err error) {
 	log := logf.FromContext(ctx)
 
 	issuerCopy := iss.DeepCopy()
@@ -51,25 +45,7 @@ func (c *controller) Sync(ctx context.Context, iss *v1.ClusterIssuer) (err error
 		}
 	}()
 
-	el := webhook.ValidationRegistry.Validate(issuerCopy, internalapi.SchemeGroupVersion.WithKind("ClusterIssuer"))
-	if len(el) > 0 {
-		msg := fmt.Sprintf("Resource validation failed: %v", el.ToAggregate())
-		apiutil.SetIssuerCondition(issuerCopy, v1.IssuerConditionReady, cmmeta.ConditionFalse, errorConfig, msg)
-		return
-	}
-
-	// Remove existing ErrorConfig condition if it exists
-	for i, c := range issuerCopy.Status.Conditions {
-		if c.Type == v1.IssuerConditionReady {
-			if c.Reason == errorConfig && c.Status == cmmeta.ConditionFalse {
-				issuerCopy.Status.Conditions = append(issuerCopy.Status.Conditions[:i], issuerCopy.Status.Conditions[i+1:]...)
-				break
-			}
-		}
-	}
-
 	i, err := c.issuerFactory.IssuerFor(issuerCopy)
-
 	if err != nil {
 		return err
 	}
@@ -88,7 +64,7 @@ func (c *controller) Sync(ctx context.Context, iss *v1.ClusterIssuer) (err error
 	return nil
 }
 
-func (c *controller) updateIssuerStatus(old, new *v1.ClusterIssuer) (*v1.ClusterIssuer, error) {
+func (c *controller) updateIssuerStatus(old, new *cmapi.ClusterIssuer) (*cmapi.ClusterIssuer, error) {
 	if reflect.DeepEqual(old.Status, new.Status) {
 		return nil, nil
 	}
diff --git a/pkg/controller/issuers/BUILD.bazel b/pkg/controller/issuers/BUILD.bazel
index d22469582..5ef1fc818 100644
--- a/pkg/controller/issuers/BUILD.bazel
+++ b/pkg/controller/issuers/BUILD.bazel
@@ -10,16 +10,12 @@ go_library(
     importpath = "github.com/jetstack/cert-manager/pkg/controller/issuers",
     visibility = ["//visibility:public"],
     deps = [
-        "//pkg/api/util:go_default_library",
         "//pkg/apis/certmanager/v1:go_default_library",
-        "//pkg/apis/meta/v1:go_default_library",
         "//pkg/client/clientset/versioned:go_default_library",
         "//pkg/client/listers/certmanager/v1:go_default_library",
         "//pkg/controller:go_default_library",
-        "//pkg/internal/apis/certmanager:go_default_library",
         "//pkg/issuer:go_default_library",
         "//pkg/logs:go_default_library",
-        "//pkg/webhook:go_default_library",
         "@com_github_go_logr_logr//:go_default_library",
         "@io_k8s_api//core/v1:go_default_library",
         "@io_k8s_apimachinery//pkg/api/errors:go_default_library",
diff --git a/pkg/controller/issuers/sync.go b/pkg/controller/issuers/sync.go
index 9e1dd09c7..6e19f752d 100644
--- a/pkg/controller/issuers/sync.go
+++ b/pkg/controller/issuers/sync.go
@@ -18,7 +18,6 @@ package issuers
 
 import (
 	"context"
-	"fmt"
 	"reflect"
 	"time"
 
@@ -26,22 +25,17 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/errors"
 
-	apiutil "github.com/jetstack/cert-manager/pkg/api/util"
-	v1 "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
-	cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1"
-	internalapi "github.com/jetstack/cert-manager/pkg/internal/apis/certmanager"
+	cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
 	logf "github.com/jetstack/cert-manager/pkg/logs"
-	"github.com/jetstack/cert-manager/pkg/webhook"
 )
 
 const (
 	errorInitIssuer = "ErrInitIssuer"
-	errorConfig     = "ConfigError"
 
 	messageErrorInitIssuer = "Error initializing issuer: "
 )
 
-func (c *controller) Sync(ctx context.Context, iss *v1.Issuer) (err error) {
+func (c *controller) Sync(ctx context.Context, iss *cmapi.Issuer) (err error) {
 	log := logf.FromContext(ctx)
 
 	issuerCopy := iss.DeepCopy()
@@ -51,25 +45,7 @@ func (c *controller) Sync(ctx context.Context, iss *v1.Issuer) (err error) {
 		}
 	}()
 
-	el := webhook.ValidationRegistry.Validate(issuerCopy, internalapi.SchemeGroupVersion.WithKind("Issuer"))
-	if len(el) > 0 {
-		msg := fmt.Sprintf("Resource validation failed: %v", el.ToAggregate())
-		apiutil.SetIssuerCondition(issuerCopy, v1.IssuerConditionReady, cmmeta.ConditionFalse, errorConfig, msg)
-		return
-	}
-
-	// Remove existing ErrorConfig condition if it exists
-	for i, c := range issuerCopy.Status.Conditions {
-		if c.Type == v1.IssuerConditionReady {
-			if c.Reason == errorConfig && c.Status == cmmeta.ConditionFalse {
-				issuerCopy.Status.Conditions = append(issuerCopy.Status.Conditions[:i], issuerCopy.Status.Conditions[i+1:]...)
-				break
-			}
-		}
-	}
-
 	i, err := c.issuerFactory.IssuerFor(issuerCopy)
-
 	if err != nil {
 		return err
 	}
@@ -88,7 +64,7 @@ func (c *controller) Sync(ctx context.Context, iss *v1.Issuer) (err error) {
 	return nil
 }
 
-func (c *controller) updateIssuerStatus(old, new *v1.Issuer) (*v1.Issuer, error) {
+func (c *controller) updateIssuerStatus(old, new *cmapi.Issuer) (*cmapi.Issuer, error) {
 	if reflect.DeepEqual(old.Status, new.Status) {
 		return nil, nil
 	}
diff --git a/pkg/internal/BUILD.bazel b/pkg/internal/BUILD.bazel
index 8e209c44d..9aba7f3f4 100644
--- a/pkg/internal/BUILD.bazel
+++ b/pkg/internal/BUILD.bazel
@@ -9,6 +9,7 @@ filegroup(
     name = "all-srcs",
     srcs = [
         ":package-srcs",
+        "//pkg/internal/api/mutation:all-srcs",
         "//pkg/internal/api/validation:all-srcs",
         "//pkg/internal/apis/acme:all-srcs",
         "//pkg/internal/apis/certmanager:all-srcs",
diff --git a/pkg/internal/api/mutation/BUILD.bazel b/pkg/internal/api/mutation/BUILD.bazel
new file mode 100644
index 000000000..d03c3e315
--- /dev/null
+++ b/pkg/internal/api/mutation/BUILD.bazel
@@ -0,0 +1,45 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["registry.go"],
+    importpath = "github.com/jetstack/cert-manager/pkg/internal/api/mutation",
+    visibility = ["//pkg:__subpackages__"],
+    deps = [
+        "@com_github_mattbaird_jsonpatch//:go_default_library",
+        "@io_k8s_api//admission/v1:go_default_library",
+        "@io_k8s_apimachinery//pkg/runtime:go_default_library",
+        "@io_k8s_apimachinery//pkg/runtime/schema:go_default_library",
+        "@io_k8s_apimachinery//pkg/runtime/serializer:go_default_library",
+        "@io_k8s_apimachinery//pkg/runtime/serializer/json:go_default_library",
+    ],
+)
+
+filegroup(
+    name = "package-srcs",
+    srcs = glob(["**"]),
+    tags = ["automanaged"],
+    visibility = ["//visibility:private"],
+)
+
+filegroup(
+    name = "all-srcs",
+    srcs = [":package-srcs"],
+    tags = ["automanaged"],
+    visibility = ["//visibility:public"],
+)
+
+go_test(
+    name = "go_default_test",
+    srcs = ["registry_test.go"],
+    embed = [":go_default_library"],
+    deps = [
+        "//pkg/apis/certmanager:go_default_library",
+        "//pkg/internal/apis/certmanager:go_default_library",
+        "//pkg/webhook:go_default_library",
+        "//test/unit/gen:go_default_library",
+        "@io_k8s_api//admission/v1:go_default_library",
+        "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
+        "@io_k8s_apimachinery//pkg/runtime:go_default_library",
+    ],
+)
diff --git a/pkg/internal/api/mutation/registry.go b/pkg/internal/api/mutation/registry.go
new file mode 100644
index 000000000..88bbcd609
--- /dev/null
+++ b/pkg/internal/api/mutation/registry.go
@@ -0,0 +1,257 @@
+/*
+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 mutation allows a caller to automatically register, lookup and call
+// API mutation functions.
+// It is similar to runtime.Scheme and is designed to make writing and
+// consuming API mutation functions easier.
+// This registry also handles adding scheme defaults, even if no mutation
+// functions are defined for that type. Any type where defaulting is desired
+// should be registered.
+// Functions are designed to update the incoming object, or new object, which
+// will then have a patch generated and returned to the master Mutation.
+package mutation
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"sort"
+
+	"github.com/mattbaird/jsonpatch"
+	admissionv1 "k8s.io/api/admission/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/runtime/serializer"
+	apijson "k8s.io/apimachinery/pkg/runtime/serializer/json"
+)
+
+// Registry is used to store and lookup references to mutation functions for
+// given Kubernetes API types. API types will be converted into internal API
+// versions during mutation, then converted back to the target version when
+// generating the patch.
+type Registry struct {
+	codec  runtime.Codec
+	scheme *runtime.Scheme
+
+	mutateRegister       map[schema.GroupVersionKind]MutateFunc
+	mutateUpdateRegister map[schema.GroupVersionKind]MutateUpdateFunc
+}
+
+type MutateFunc func(req *admissionv1.AdmissionRequest, obj runtime.Object)
+type MutateUpdateFunc func(req *admissionv1.AdmissionRequest, old, new runtime.Object)
+
+// NewRegistry creates a new empty registry, backed by the provided Scheme.
+func NewRegistry(scheme *runtime.Scheme) *Registry {
+	factory := serializer.NewCodecFactory(scheme)
+	serializer := apijson.NewSerializerWithOptions(apijson.DefaultMetaFactory, scheme, scheme, apijson.SerializerOptions{})
+	encoder := factory.WithoutConversion().EncoderForVersion(serializer, nil)
+	decoder := factory.UniversalDeserializer()
+	return &Registry{
+		codec:                runtime.NewCodec(encoder, decoder),
+		scheme:               scheme,
+		mutateRegister:       make(map[schema.GroupVersionKind]MutateFunc),
+		mutateUpdateRegister: make(map[schema.GroupVersionKind]MutateUpdateFunc),
+	}
+}
+
+// AddMutateFunc will add a new mutation function to the register. The function
+// will be run whenever a Mutate is called with a CREATE operation, and API
+// type whose internal version is registered. Registered types MUST be that of the
+// internal version of the target resource kind.
+func (r *Registry) AddMutateFunc(obj runtime.Object, fn MutateFunc) error {
+	gvks, _, err := r.scheme.ObjectKinds(obj)
+	if err != nil {
+		return err
+	}
+
+	for _, gvk := range gvks {
+		r.appendMutate(gvk, fn)
+	}
+
+	return nil
+}
+
+// AddMutateFunc will add a new mutation function to the register. The function
+// will be run whenever a Mutate is called with an UPDATE operation, and API
+// type whose internal version is registered. Registered types MUST be that of the
+// internal version of the target resource kind.
+func (r *Registry) AddMutateUpdateFunc(obj runtime.Object, fn MutateUpdateFunc) error {
+	gvks, _, err := r.scheme.ObjectKinds(obj)
+	if err != nil {
+		return err
+	}
+
+	for _, gvk := range gvks {
+		r.appendMutateUpdate(gvk, fn)
+	}
+
+	return nil
+}
+
+// Mutate will run all mutation functions registed on CREATE and UPDATE
+// operations over the internal type of the given resource.
+// The object is converted to its internal version before either a CREATE or
+// UPDATE mutation is applied to the object. The object is then converted to
+// the requested version, and defaults applied for that schema.
+// A JSON patch is then generated for the target resource version.
+// Defaulting is always applied against the given resource, regardless of
+// whether any mutation functions are defined.
+func (r *Registry) Mutate(req *admissionv1.AdmissionRequest) ([]byte, error) {
+	// Create GroupVersionKind where the Version is set to internal.
+	gvk := schema.GroupVersionKind{
+		Group: req.RequestKind.Group,
+		// Set version to internal API version
+		Version: runtime.APIVersionInternal,
+		Kind:    req.RequestKind.Kind,
+	}
+
+	// Convert the incoming resource to the internal type
+	internal, err := r.convert(req.Object.Raw, gvk)
+	if err != nil {
+		return nil, fmt.Errorf("failed to convert object: %s", err)
+	}
+
+	switch req.Operation {
+	case admissionv1.Create:
+		// Attempt to retrieve the registered CREATE mutating functions, and apply
+		// to the internal type.
+		mutate := r.mutateRegister[gvk]
+		if mutate == nil {
+			break
+		}
+
+		mutate(req, internal)
+		break
+
+	case admissionv1.Update:
+		// Attempt to retrieve the registered UPDATE mutating functions, and apply
+		// to the internal type.
+
+		// decode the old raw object data
+		oldInternal, err := r.convert(req.OldObject.Raw, gvk)
+		if err != nil {
+			return nil, fmt.Errorf("failed to decode old admission object: %s", err)
+		}
+
+		mutate := r.mutateUpdateRegister[gvk]
+		if mutate == nil {
+			break
+		}
+
+		// Pass both the old and new internal types to mutate
+		mutate(req, oldInternal, internal)
+		break
+
+	default:
+		// If not under a CREATE or UPDATE operation, exit early
+		return nil, nil
+	}
+
+	// Convert the mutated internal object into the target resource version.
+	target, err := r.scheme.New(schema.GroupVersionKind{
+		Group:   req.RequestKind.Group,
+		Version: req.RequestKind.Version,
+		Kind:    req.RequestKind.Kind,
+	})
+	if err != nil {
+		return nil, err
+	}
+	if err := r.scheme.Convert(internal, target, nil); err != nil {
+		return nil, err
+	}
+
+	// Apply defaults to the target resource.
+	r.scheme.Default(target)
+
+	// Generate a JSON patch based on the incoming resource and the mutated object.
+	return r.createMutatePatch(req, target)
+}
+
+// convert converts a raw resource byte slice to an internal versioned resource, based on the given GroupKind.
+func (r *Registry) convert(rawObj []byte, gvk schema.GroupVersionKind) (runtime.Object, error) {
+	gvk.Version = runtime.APIVersionInternal
+	obj, _, err := r.codec.Decode(rawObj, nil, nil)
+	if err != nil {
+		return nil, fmt.Errorf("failed to decode admission object: %s", err)
+	}
+
+	targetObj, err := r.scheme.New(gvk)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := r.scheme.Convert(obj, targetObj, nil); err != nil {
+		return nil, err
+	}
+
+	return targetObj, nil
+}
+
+// createMutatePatch will generate a JSON patch based upon the given original
+// raw object, and the mutated typed object.
+func (r *Registry) createMutatePatch(req *admissionv1.AdmissionRequest, obj runtime.Object) ([]byte, error) {
+	var buf bytes.Buffer
+	if err := r.codec.Encode(obj, &buf); err != nil {
+		return nil, fmt.Errorf("failed to encode object after mutation: %s", err)
+	}
+
+	ops, err := jsonpatch.CreatePatch(req.Object.Raw, buf.Bytes())
+	if err != nil {
+		return nil, fmt.Errorf("failed to set mutation patch: %s", err)
+	}
+
+	sortOps(ops)
+
+	patch, err := json.Marshal(ops)
+	if err != nil {
+		return nil, fmt.Errorf("failed to generate json patch: %s", err)
+	}
+
+	return patch, nil
+}
+
+func sortOps(ops []jsonpatch.JsonPatchOperation) {
+	sort.Slice(ops, func(i, j int) bool {
+		return ops[i].Path < ops[j].Path
+	})
+}
+
+func (r *Registry) appendMutate(gvk schema.GroupVersionKind, fn MutateFunc) {
+	existing, ok := r.mutateRegister[gvk]
+	if !ok {
+		r.mutateRegister[gvk] = fn
+		return
+	}
+
+	r.mutateRegister[gvk] = func(aspec *admissionv1.AdmissionRequest, obj runtime.Object) {
+		existing(aspec, obj)
+		fn(aspec, obj)
+	}
+}
+
+func (r *Registry) appendMutateUpdate(gvk schema.GroupVersionKind, fn MutateUpdateFunc) {
+	existing, ok := r.mutateUpdateRegister[gvk]
+	if !ok {
+		r.mutateUpdateRegister[gvk] = fn
+		return
+	}
+
+	r.mutateUpdateRegister[gvk] = func(aspec *admissionv1.AdmissionRequest, oldObj, newObj runtime.Object) {
+		existing(aspec, oldObj, newObj)
+		fn(aspec, oldObj, newObj)
+	}
+}
diff --git a/pkg/internal/api/validation/BUILD.bazel b/pkg/internal/api/validation/BUILD.bazel
index 7b82e506f..f46aa67d1 100644
--- a/pkg/internal/api/validation/BUILD.bazel
+++ b/pkg/internal/api/validation/BUILD.bazel
@@ -6,6 +6,7 @@ go_library(
     importpath = "github.com/jetstack/cert-manager/pkg/internal/api/validation",
     visibility = ["//pkg:__subpackages__"],
     deps = [
+        "@io_k8s_api//admission/v1:go_default_library",
         "@io_k8s_apimachinery//pkg/runtime:go_default_library",
         "@io_k8s_apimachinery//pkg/runtime/schema:go_default_library",
         "@io_k8s_apimachinery//pkg/util/validation/field:go_default_library",
@@ -20,6 +21,7 @@ go_test(
         "//pkg/apis/certmanager/v1:go_default_library",
         "//pkg/internal/apis/certmanager:go_default_library",
         "//pkg/webhook:go_default_library",
+        "@io_k8s_api//admission/v1:go_default_library",
         "@io_k8s_api//core/v1:go_default_library",
         "@io_k8s_apimachinery//pkg/runtime:go_default_library",
         "@io_k8s_apimachinery//pkg/util/runtime:go_default_library",
diff --git a/pkg/internal/api/validation/registry.go b/pkg/internal/api/validation/registry.go
index f58115b8c..afe545837 100644
--- a/pkg/internal/api/validation/registry.go
+++ b/pkg/internal/api/validation/registry.go
@@ -21,6 +21,7 @@ limitations under the License.
 package validation
 
 import (
+	admissionv1 "k8s.io/api/admission/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/util/validation/field"
@@ -34,8 +35,8 @@ type Registry struct {
 	validateUpdateRegister map[schema.GroupVersionKind]ValidateUpdateFunc
 }
 
-type ValidateFunc func(obj runtime.Object) field.ErrorList
-type ValidateUpdateFunc func(oldObj, obj runtime.Object) field.ErrorList
+type ValidateFunc func(req *admissionv1.AdmissionRequest, obj runtime.Object) field.ErrorList
+type ValidateUpdateFunc func(req *admissionv1.AdmissionRequest, oldObj, obj runtime.Object) field.ErrorList
 
 // NewRegistry creates a new empty registry, backed by the provided Scheme.
 func NewRegistry(scheme *runtime.Scheme) *Registry {
@@ -92,7 +93,7 @@ func (r *Registry) AddValidateUpdateFunc(obj runtime.Object, fn ValidateUpdateFu
 // calling the validation functions.
 // Any validation functions registered for the objects internal API version
 // will be run against the object regardless of version.
-func (r *Registry) Validate(obj runtime.Object, requestVersion schema.GroupVersionKind) field.ErrorList {
+func (r *Registry) Validate(req *admissionv1.AdmissionRequest, obj runtime.Object, requestVersion schema.GroupVersionKind) field.ErrorList {
 	versioned, internal := r.lookupValidateFuncs(requestVersion)
 	if versioned == nil && internal == nil {
 		return nil
@@ -105,10 +106,10 @@ func (r *Registry) Validate(obj runtime.Object, requestVersion schema.GroupVersi
 
 	el := field.ErrorList{}
 	if versioned != nil {
-		el = append(el, versioned(targetObj)...)
+		el = append(el, versioned(req, targetObj)...)
 	}
 	if internal != nil {
-		el = append(el, internal(internalObj)...)
+		el = append(el, internal(req, internalObj)...)
 	}
 
 	return el
@@ -121,7 +122,7 @@ func (r *Registry) Validate(obj runtime.Object, requestVersion schema.GroupVersi
 // calling the validation functions.
 // Any validation functions registered for the objects internal API version
 // will be run against the object regardless of version.
-func (r *Registry) ValidateUpdate(oldObj, obj runtime.Object, requestVersion schema.GroupVersionKind) field.ErrorList {
+func (r *Registry) ValidateUpdate(req *admissionv1.AdmissionRequest, oldObj, obj runtime.Object, requestVersion schema.GroupVersionKind) field.ErrorList {
 	versioned, internal := r.lookupValidateUpdateFuncs(requestVersion)
 	if versioned == nil && internal == nil {
 		return nil
@@ -139,10 +140,10 @@ func (r *Registry) ValidateUpdate(oldObj, obj runtime.Object, requestVersion sch
 
 	el := field.ErrorList{}
 	if versioned != nil {
-		el = append(el, versioned(targetOldObj, targetObj)...)
+		el = append(el, versioned(req, targetOldObj, targetObj)...)
 	}
 	if internal != nil {
-		el = append(el, internal(internalOldObj, internalObj)...)
+		el = append(el, internal(req, internalOldObj, internalObj)...)
 	}
 
 	return el
@@ -169,8 +170,8 @@ func (r *Registry) appendValidate(gvk schema.GroupVersionKind, fn ValidateFunc)
 		return
 	}
 
-	r.validateRegister[gvk] = func(obj runtime.Object) field.ErrorList {
-		return append(existing(obj), fn(obj)...)
+	r.validateRegister[gvk] = func(req *admissionv1.AdmissionRequest, obj runtime.Object) field.ErrorList {
+		return append(existing(req, obj), fn(req, obj)...)
 	}
 }
 
@@ -181,8 +182,8 @@ func (r *Registry) appendValidateUpdate(gvk schema.GroupVersionKind, fn Validate
 		return
 	}
 
-	r.validateUpdateRegister[gvk] = func(oldObj, obj runtime.Object) field.ErrorList {
-		return append(existing(oldObj, obj), fn(oldObj, obj)...)
+	r.validateUpdateRegister[gvk] = func(req *admissionv1.AdmissionRequest, oldObj, obj runtime.Object) field.ErrorList {
+		return append(existing(req, oldObj, obj), fn(req, oldObj, obj)...)
 	}
 }
 
diff --git a/pkg/internal/apis/acme/validation/BUILD.bazel b/pkg/internal/apis/acme/validation/BUILD.bazel
index b7c9b4503..02815f20e 100644
--- a/pkg/internal/apis/acme/validation/BUILD.bazel
+++ b/pkg/internal/apis/acme/validation/BUILD.bazel
@@ -12,6 +12,7 @@ go_library(
     deps = [
         "//pkg/internal/api/validation:go_default_library",
         "//pkg/internal/apis/acme:go_default_library",
+        "@io_k8s_api//admission/v1:go_default_library",
         "@io_k8s_apimachinery//pkg/runtime:go_default_library",
         "@io_k8s_apimachinery//pkg/util/validation/field:go_default_library",
     ],
diff --git a/pkg/internal/apis/acme/validation/challenge.go b/pkg/internal/apis/acme/validation/challenge.go
index 576cc6ce6..956a9df9b 100644
--- a/pkg/internal/apis/acme/validation/challenge.go
+++ b/pkg/internal/apis/acme/validation/challenge.go
@@ -19,13 +19,14 @@ package validation
 import (
 	"reflect"
 
+	admissionv1 "k8s.io/api/admission/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/util/validation/field"
 
 	cmacme "github.com/jetstack/cert-manager/pkg/internal/apis/acme"
 )
 
-func ValidateChallengeUpdate(oldObj, newObj runtime.Object) field.ErrorList {
+func ValidateChallengeUpdate(_ *admissionv1.AdmissionRequest, oldObj, newObj runtime.Object) field.ErrorList {
 	old, ok := oldObj.(*cmacme.Challenge)
 	new := newObj.(*cmacme.Challenge)
 	// if oldObj is not set, the Update operation is always valid.
diff --git a/pkg/internal/apis/acme/validation/order.go b/pkg/internal/apis/acme/validation/order.go
index 1a582b4ad..d87606a1d 100644
--- a/pkg/internal/apis/acme/validation/order.go
+++ b/pkg/internal/apis/acme/validation/order.go
@@ -19,13 +19,14 @@ package validation
 import (
 	"bytes"
 
+	admissionv1 "k8s.io/api/admission/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/util/validation/field"
 
 	cmacme "github.com/jetstack/cert-manager/pkg/internal/apis/acme"
 )
 
-func ValidateOrderUpdate(oldObj, newObj runtime.Object) field.ErrorList {
+func ValidateOrderUpdate(_ *admissionv1.AdmissionRequest, oldObj, newObj runtime.Object) field.ErrorList {
 	old, ok := oldObj.(*cmacme.Order)
 	new := newObj.(*cmacme.Order)
 	// if oldObj is not set, the Update operation is always valid.
diff --git a/pkg/internal/apis/certmanager/BUILD.bazel b/pkg/internal/apis/certmanager/BUILD.bazel
index 37abcc71a..8a1961c9c 100644
--- a/pkg/internal/apis/certmanager/BUILD.bazel
+++ b/pkg/internal/apis/certmanager/BUILD.bazel
@@ -36,6 +36,7 @@ filegroup(
     srcs = [
         ":package-srcs",
         "//pkg/internal/apis/certmanager/fuzzer:all-srcs",
+        "//pkg/internal/apis/certmanager/identity:all-srcs",
         "//pkg/internal/apis/certmanager/install:all-srcs",
         "//pkg/internal/apis/certmanager/v1:all-srcs",
         "//pkg/internal/apis/certmanager/v1alpha2:all-srcs",
diff --git a/pkg/internal/apis/certmanager/identity/BUILD.bazel b/pkg/internal/apis/certmanager/identity/BUILD.bazel
new file mode 100644
index 000000000..3a65c1e48
--- /dev/null
+++ b/pkg/internal/apis/certmanager/identity/BUILD.bazel
@@ -0,0 +1,31 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["identity.go"],
+    importpath = "github.com/jetstack/cert-manager/pkg/internal/apis/certmanager/identity",
+    visibility = ["//pkg:__subpackages__"],
+    deps = [
+        "//pkg/internal/api/mutation:go_default_library",
+        "//pkg/internal/api/validation:go_default_library",
+        "//pkg/internal/apis/certmanager:go_default_library",
+        "//pkg/internal/apis/certmanager/identity/certificaterequests:go_default_library",
+    ],
+)
+
+filegroup(
+    name = "package-srcs",
+    srcs = glob(["**"]),
+    tags = ["automanaged"],
+    visibility = ["//visibility:private"],
+)
+
+filegroup(
+    name = "all-srcs",
+    srcs = [
+        ":package-srcs",
+        "//pkg/internal/apis/certmanager/identity/certificaterequests:all-srcs",
+    ],
+    tags = ["automanaged"],
+    visibility = ["//visibility:public"],
+)
diff --git a/pkg/internal/apis/certmanager/identity/certificaterequests/BUILD.bazel b/pkg/internal/apis/certmanager/identity/certificaterequests/BUILD.bazel
new file mode 100644
index 000000000..d8276dc5d
--- /dev/null
+++ b/pkg/internal/apis/certmanager/identity/certificaterequests/BUILD.bazel
@@ -0,0 +1,42 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["certificaterequests.go"],
+    importpath = "github.com/jetstack/cert-manager/pkg/internal/apis/certmanager/identity/certificaterequests",
+    visibility = ["//pkg:__subpackages__"],
+    deps = [
+        "//pkg/internal/apis/certmanager:go_default_library",
+        "//pkg/util:go_default_library",
+        "@io_k8s_api//admission/v1:go_default_library",
+        "@io_k8s_api//authentication/v1:go_default_library",
+        "@io_k8s_apimachinery//pkg/runtime:go_default_library",
+        "@io_k8s_apimachinery//pkg/util/validation/field:go_default_library",
+    ],
+)
+
+filegroup(
+    name = "package-srcs",
+    srcs = glob(["**"]),
+    tags = ["automanaged"],
+    visibility = ["//visibility:private"],
+)
+
+filegroup(
+    name = "all-srcs",
+    srcs = [":package-srcs"],
+    tags = ["automanaged"],
+    visibility = ["//visibility:public"],
+)
+
+go_test(
+    name = "go_default_test",
+    srcs = ["certificaterequests_test.go"],
+    embed = [":go_default_library"],
+    deps = [
+        "//pkg/internal/apis/certmanager:go_default_library",
+        "@io_k8s_api//admission/v1:go_default_library",
+        "@io_k8s_api//authentication/v1:go_default_library",
+        "@io_k8s_apimachinery//pkg/util/validation/field:go_default_library",
+    ],
+)
diff --git a/pkg/internal/apis/certmanager/identity/certificaterequests/certificaterequests.go b/pkg/internal/apis/certmanager/identity/certificaterequests/certificaterequests.go
new file mode 100644
index 000000000..f62c4a26c
--- /dev/null
+++ b/pkg/internal/apis/certmanager/identity/certificaterequests/certificaterequests.go
@@ -0,0 +1,109 @@
+/*
+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 certificaterequests populates and enforces identity on
+// CertificateRequest resources.
+package certificaterequests
+
+import (
+	"reflect"
+
+	admissionv1 "k8s.io/api/admission/v1"
+	authenticationv1 "k8s.io/api/authentication/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/util/validation/field"
+
+	cmapi "github.com/jetstack/cert-manager/pkg/internal/apis/certmanager"
+	"github.com/jetstack/cert-manager/pkg/util"
+)
+
+func ValidateCreate(req *admissionv1.AdmissionRequest, obj runtime.Object) field.ErrorList {
+	cr := obj.(*cmapi.CertificateRequest)
+	fldPath := field.NewPath("spec")
+
+	var el field.ErrorList
+	if cr.Spec.UID != req.UserInfo.UID {
+		el = append(el, field.Forbidden(fldPath.Child("uid"), "uid identity must be that of the requester"))
+	}
+	if cr.Spec.Username != req.UserInfo.Username {
+		el = append(el, field.Forbidden(fldPath.Child("username"), "username identity must be that of the requester"))
+	}
+	if !util.EqualUnsorted(cr.Spec.Groups, req.UserInfo.Groups) {
+		el = append(el, field.Forbidden(fldPath.Child("groups"), "groups identity must be that of the requester"))
+	}
+	if !extrasMatch(cr.Spec.Extra, req.UserInfo.Extra) {
+		el = append(el, field.Forbidden(fldPath.Child("extra"), "extra identity must be that of the requester"))
+	}
+
+	return el
+}
+
+func extrasMatch(crExtra map[string][]string, reqExtra map[string]authenticationv1.ExtraValue) bool {
+	if len(crExtra) != len(reqExtra) {
+		return false
+	}
+
+	for k, v := range crExtra {
+		reqv, ok := reqExtra[k]
+		if !ok {
+			return false
+		}
+
+		if !util.EqualUnsorted(v, reqv) {
+			return false
+		}
+	}
+
+	return true
+}
+
+func ValidateUpdate(_ *admissionv1.AdmissionRequest, oldObj, newObj runtime.Object) field.ErrorList {
+	oldCR, newCR := oldObj.(*cmapi.CertificateRequest), newObj.(*cmapi.CertificateRequest)
+	fldPath := field.NewPath("spec")
+
+	var el field.ErrorList
+	if oldCR.Spec.UID != newCR.Spec.UID {
+		el = append(el, field.Forbidden(fldPath.Child("uid"), "uid identity cannot be changed once set"))
+	}
+	if oldCR.Spec.Username != newCR.Spec.Username {
+		el = append(el, field.Forbidden(fldPath.Child("username"), "username identity cannot be changed once set"))
+	}
+	if !util.EqualUnsorted(oldCR.Spec.Groups, newCR.Spec.Groups) {
+		el = append(el, field.Forbidden(fldPath.Child("groups"), "groups identity cannot be changed once set"))
+	}
+	if !reflect.DeepEqual(oldCR.Spec.Extra, newCR.Spec.Extra) {
+		el = append(el, field.Forbidden(fldPath.Child("extra"), "extra identity cannot be changed once set"))
+	}
+
+	return el
+}
+
+func MutateCreate(req *admissionv1.AdmissionRequest, obj runtime.Object) {
+	cr := obj.(*cmapi.CertificateRequest)
+	userInfo := req.DeepCopy().UserInfo
+
+	cr.Spec.UID = userInfo.UID
+	cr.Spec.Username = userInfo.Username
+	cr.Spec.Groups = userInfo.Groups
+	cr.Spec.Extra = make(map[string][]string)
+	for k, v := range userInfo.Extra {
+		cr.Spec.Extra[k] = v
+	}
+}
+
+func MutateUpdate(_ *admissionv1.AdmissionRequest, _, _ runtime.Object) {
+	return
+}
diff --git a/pkg/internal/apis/certmanager/identity/identity.go b/pkg/internal/apis/certmanager/identity/identity.go
new file mode 100644
index 000000000..0a2876e7a
--- /dev/null
+++ b/pkg/internal/apis/certmanager/identity/identity.go
@@ -0,0 +1,46 @@
+/*
+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 identity
+
+import (
+	"github.com/jetstack/cert-manager/pkg/internal/api/mutation"
+	"github.com/jetstack/cert-manager/pkg/internal/api/validation"
+	cmapi "github.com/jetstack/cert-manager/pkg/internal/apis/certmanager"
+	"github.com/jetstack/cert-manager/pkg/internal/apis/certmanager/identity/certificaterequests"
+)
+
+func AddToValidationRegistry(reg *validation.Registry) error {
+	if err := reg.AddValidateFunc(&cmapi.CertificateRequest{}, certificaterequests.ValidateCreate); err != nil {
+		return err
+	}
+	if err := reg.AddValidateUpdateFunc(&cmapi.CertificateRequest{}, certificaterequests.ValidateUpdate); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func AddToMutationRegistry(reg *mutation.Registry) error {
+	if err := reg.AddMutateFunc(&cmapi.CertificateRequest{}, certificaterequests.MutateCreate); err != nil {
+		return err
+	}
+	if err := reg.AddMutateUpdateFunc(&cmapi.CertificateRequest{}, certificaterequests.MutateUpdate); err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/pkg/internal/apis/certmanager/install/BUILD.bazel b/pkg/internal/apis/certmanager/install/BUILD.bazel
index 2f3ffbe69..298195e09 100644
--- a/pkg/internal/apis/certmanager/install/BUILD.bazel
+++ b/pkg/internal/apis/certmanager/install/BUILD.bazel
@@ -6,8 +6,10 @@ go_library(
     importpath = "github.com/jetstack/cert-manager/pkg/internal/apis/certmanager/install",
     visibility = ["//visibility:public"],
     deps = [
+        "//pkg/internal/api/mutation:go_default_library",
         "//pkg/internal/api/validation:go_default_library",
         "//pkg/internal/apis/certmanager:go_default_library",
+        "//pkg/internal/apis/certmanager/identity:go_default_library",
         "//pkg/internal/apis/certmanager/v1:go_default_library",
         "//pkg/internal/apis/certmanager/v1alpha2:go_default_library",
         "//pkg/internal/apis/certmanager/v1alpha3:go_default_library",
diff --git a/pkg/internal/apis/certmanager/install/install.go b/pkg/internal/apis/certmanager/install/install.go
index 98ccbdbcf..ebfb74403 100644
--- a/pkg/internal/apis/certmanager/install/install.go
+++ b/pkg/internal/apis/certmanager/install/install.go
@@ -22,8 +22,10 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 
+	"github.com/jetstack/cert-manager/pkg/internal/api/mutation"
 	"github.com/jetstack/cert-manager/pkg/internal/api/validation"
 	"github.com/jetstack/cert-manager/pkg/internal/apis/certmanager"
+	cmidentity "github.com/jetstack/cert-manager/pkg/internal/apis/certmanager/identity"
 	v1 "github.com/jetstack/cert-manager/pkg/internal/apis/certmanager/v1"
 	"github.com/jetstack/cert-manager/pkg/internal/apis/certmanager/v1alpha2"
 	"github.com/jetstack/cert-manager/pkg/internal/apis/certmanager/v1alpha3"
@@ -48,4 +50,11 @@ func Install(scheme *runtime.Scheme) {
 // validation registry
 func InstallValidation(registry *validation.Registry) {
 	utilruntime.Must(cmvalidation.AddToValidationRegistry(registry))
+	utilruntime.Must(cmidentity.AddToValidationRegistry(registry))
+}
+
+// InstallMutation registers mutation functions for the API group with a
+// mutation registry
+func InstallMutation(registry *mutation.Registry) {
+	utilruntime.Must(cmidentity.AddToMutationRegistry(registry))
 }
diff --git a/pkg/internal/apis/certmanager/types_certificaterequest.go b/pkg/internal/apis/certmanager/types_certificaterequest.go
index 1e9e467da..a6c3b9bfc 100644
--- a/pkg/internal/apis/certmanager/types_certificaterequest.go
+++ b/pkg/internal/apis/certmanager/types_certificaterequest.go
@@ -94,6 +94,19 @@ type CertificateRequestSpec struct {
 	// Usages is the set of x509 usages that are requested for the certificate.
 	// Defaults to `digital signature` and `key encipherment` if not specified.
 	Usages []KeyUsage
+
+	// Username contains the name of the user that created the CertificateRequest.
+	// Populated by the cert-manager webhook on creation and immutable.
+	Username string
+	// UID contains the uid of the user that created the CertificateRequest.
+	// Populated by the cert-manager webhook on creation and immutable.
+	UID string
+	// Groups contains group membership of the user that created the CertificateRequest.
+	// Populated by the cert-manager webhook on creation and immutable.
+	Groups []string
+	// Extra contains extra attributes of the user that created the CertificateRequest.
+	// Populated by the cert-manager webhook on creation and immutable.
+	Extra map[string][]string
 }
 
 // CertificateRequestStatus defines the observed state of CertificateRequest and
diff --git a/pkg/internal/apis/certmanager/v1/zz_generated.conversion.go b/pkg/internal/apis/certmanager/v1/zz_generated.conversion.go
index c204153e9..0551894af 100644
--- a/pkg/internal/apis/certmanager/v1/zz_generated.conversion.go
+++ b/pkg/internal/apis/certmanager/v1/zz_generated.conversion.go
@@ -629,6 +629,10 @@ func autoConvert_v1_CertificateRequestSpec_To_certmanager_CertificateRequestSpec
 	out.Request = *(*[]byte)(unsafe.Pointer(&in.Request))
 	out.IsCA = in.IsCA
 	out.Usages = *(*[]certmanager.KeyUsage)(unsafe.Pointer(&in.Usages))
+	out.Username = in.Username
+	out.UID = in.UID
+	out.Groups = *(*[]string)(unsafe.Pointer(&in.Groups))
+	out.Extra = *(*map[string][]string)(unsafe.Pointer(&in.Extra))
 	return nil
 }
 
@@ -646,6 +650,10 @@ func autoConvert_certmanager_CertificateRequestSpec_To_v1_CertificateRequestSpec
 	out.Request = *(*[]byte)(unsafe.Pointer(&in.Request))
 	out.IsCA = in.IsCA
 	out.Usages = *(*[]v1.KeyUsage)(unsafe.Pointer(&in.Usages))
+	out.Username = in.Username
+	out.UID = in.UID
+	out.Groups = *(*[]string)(unsafe.Pointer(&in.Groups))
+	out.Extra = *(*map[string][]string)(unsafe.Pointer(&in.Extra))
 	return nil
 }
 
diff --git a/pkg/internal/apis/certmanager/v1alpha2/zz_generated.conversion.go b/pkg/internal/apis/certmanager/v1alpha2/zz_generated.conversion.go
index d30605c05..176669616 100644
--- a/pkg/internal/apis/certmanager/v1alpha2/zz_generated.conversion.go
+++ b/pkg/internal/apis/certmanager/v1alpha2/zz_generated.conversion.go
@@ -641,6 +641,10 @@ func autoConvert_v1alpha2_CertificateRequestSpec_To_certmanager_CertificateReque
 	// WARNING: in.CSRPEM requires manual conversion: does not exist in peer-type
 	out.IsCA = in.IsCA
 	out.Usages = *(*[]certmanager.KeyUsage)(unsafe.Pointer(&in.Usages))
+	out.Username = in.Username
+	out.UID = in.UID
+	out.Groups = *(*[]string)(unsafe.Pointer(&in.Groups))
+	out.Extra = *(*map[string][]string)(unsafe.Pointer(&in.Extra))
 	return nil
 }
 
@@ -653,6 +657,10 @@ func autoConvert_certmanager_CertificateRequestSpec_To_v1alpha2_CertificateReque
 	// WARNING: in.Request requires manual conversion: does not exist in peer-type
 	out.IsCA = in.IsCA
 	out.Usages = *(*[]v1alpha2.KeyUsage)(unsafe.Pointer(&in.Usages))
+	out.Username = in.Username
+	out.UID = in.UID
+	out.Groups = *(*[]string)(unsafe.Pointer(&in.Groups))
+	out.Extra = *(*map[string][]string)(unsafe.Pointer(&in.Extra))
 	return nil
 }
 
diff --git a/pkg/internal/apis/certmanager/v1alpha3/zz_generated.conversion.go b/pkg/internal/apis/certmanager/v1alpha3/zz_generated.conversion.go
index 10ee25b7f..aee97cbb3 100644
--- a/pkg/internal/apis/certmanager/v1alpha3/zz_generated.conversion.go
+++ b/pkg/internal/apis/certmanager/v1alpha3/zz_generated.conversion.go
@@ -641,6 +641,10 @@ func autoConvert_v1alpha3_CertificateRequestSpec_To_certmanager_CertificateReque
 	// WARNING: in.CSRPEM requires manual conversion: does not exist in peer-type
 	out.IsCA = in.IsCA
 	out.Usages = *(*[]certmanager.KeyUsage)(unsafe.Pointer(&in.Usages))
+	out.Username = in.Username
+	out.UID = in.UID
+	out.Groups = *(*[]string)(unsafe.Pointer(&in.Groups))
+	out.Extra = *(*map[string][]string)(unsafe.Pointer(&in.Extra))
 	return nil
 }
 
@@ -653,6 +657,10 @@ func autoConvert_certmanager_CertificateRequestSpec_To_v1alpha3_CertificateReque
 	// WARNING: in.Request requires manual conversion: does not exist in peer-type
 	out.IsCA = in.IsCA
 	out.Usages = *(*[]v1alpha3.KeyUsage)(unsafe.Pointer(&in.Usages))
+	out.Username = in.Username
+	out.UID = in.UID
+	out.Groups = *(*[]string)(unsafe.Pointer(&in.Groups))
+	out.Extra = *(*map[string][]string)(unsafe.Pointer(&in.Extra))
 	return nil
 }
 
diff --git a/pkg/internal/apis/certmanager/v1beta1/zz_generated.conversion.go b/pkg/internal/apis/certmanager/v1beta1/zz_generated.conversion.go
index 9574eabdf..a65b7c1e2 100644
--- a/pkg/internal/apis/certmanager/v1beta1/zz_generated.conversion.go
+++ b/pkg/internal/apis/certmanager/v1beta1/zz_generated.conversion.go
@@ -609,6 +609,10 @@ func autoConvert_v1beta1_CertificateRequestSpec_To_certmanager_CertificateReques
 	out.Request = *(*[]byte)(unsafe.Pointer(&in.Request))
 	out.IsCA = in.IsCA
 	out.Usages = *(*[]certmanager.KeyUsage)(unsafe.Pointer(&in.Usages))
+	out.Username = in.Username
+	out.UID = in.UID
+	out.Groups = *(*[]string)(unsafe.Pointer(&in.Groups))
+	out.Extra = *(*map[string][]string)(unsafe.Pointer(&in.Extra))
 	return nil
 }
 
@@ -626,6 +630,10 @@ func autoConvert_certmanager_CertificateRequestSpec_To_v1beta1_CertificateReques
 	out.Request = *(*[]byte)(unsafe.Pointer(&in.Request))
 	out.IsCA = in.IsCA
 	out.Usages = *(*[]v1beta1.KeyUsage)(unsafe.Pointer(&in.Usages))
+	out.Username = in.Username
+	out.UID = in.UID
+	out.Groups = *(*[]string)(unsafe.Pointer(&in.Groups))
+	out.Extra = *(*map[string][]string)(unsafe.Pointer(&in.Extra))
 	return nil
 }
 
diff --git a/pkg/internal/apis/certmanager/validation/BUILD.bazel b/pkg/internal/apis/certmanager/validation/BUILD.bazel
index fb348a3ea..5e7036cd2 100644
--- a/pkg/internal/apis/certmanager/validation/BUILD.bazel
+++ b/pkg/internal/apis/certmanager/validation/BUILD.bazel
@@ -14,6 +14,8 @@ go_library(
     visibility = ["//visibility:public"],
     deps = [
         "//pkg/api/util:go_default_library",
+        "//pkg/apis/acme:go_default_library",
+        "//pkg/apis/certmanager:go_default_library",
         "//pkg/apis/certmanager/v1:go_default_library",
         "//pkg/internal/api/validation:go_default_library",
         "//pkg/internal/apis/acme:go_default_library",
@@ -23,6 +25,7 @@ go_library(
         "//pkg/util:go_default_library",
         "//pkg/util/pki:go_default_library",
         "@com_github_kr_pretty//:go_default_library",
+        "@io_k8s_api//admission/v1:go_default_library",
         "@io_k8s_api//core/v1:go_default_library",
         "@io_k8s_apimachinery//pkg/runtime:go_default_library",
         "@io_k8s_apimachinery//pkg/util/validation/field:go_default_library",
diff --git a/pkg/internal/apis/certmanager/validation/certificate.go b/pkg/internal/apis/certmanager/validation/certificate.go
index 5661750cd..7be84f6e8 100644
--- a/pkg/internal/apis/certmanager/validation/certificate.go
+++ b/pkg/internal/apis/certmanager/validation/certificate.go
@@ -21,6 +21,7 @@ import (
 	"net"
 	"net/mail"
 
+	admissionv1 "k8s.io/api/admission/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/util/validation/field"
 
@@ -81,13 +82,13 @@ func ValidateCertificateSpec(crt *internalcmapi.CertificateSpec, fldPath *field.
 	return el
 }
 
-func ValidateCertificate(obj runtime.Object) field.ErrorList {
+func ValidateCertificate(_ *admissionv1.AdmissionRequest, obj runtime.Object) field.ErrorList {
 	crt := obj.(*internalcmapi.Certificate)
 	allErrs := ValidateCertificateSpec(&crt.Spec, field.NewPath("spec"))
 	return allErrs
 }
 
-func ValidateUpdateCertificate(oldObj, obj runtime.Object) field.ErrorList {
+func ValidateUpdateCertificate(_ *admissionv1.AdmissionRequest, oldObj, obj runtime.Object) field.ErrorList {
 	crt := obj.(*internalcmapi.Certificate)
 	allErrs := ValidateCertificateSpec(&crt.Spec, field.NewPath("spec"))
 	return allErrs
diff --git a/pkg/internal/apis/certmanager/validation/certificaterequest.go b/pkg/internal/apis/certmanager/validation/certificaterequest.go
index 29c6b0634..1c7b33b08 100644
--- a/pkg/internal/apis/certmanager/validation/certificaterequest.go
+++ b/pkg/internal/apis/certmanager/validation/certificaterequest.go
@@ -20,33 +20,61 @@ import (
 	"crypto/x509"
 	"encoding/asn1"
 	"fmt"
-
-	"github.com/jetstack/cert-manager/pkg/util"
-
 	"reflect"
+	"strings"
 
 	"github.com/kr/pretty"
-
+	admissionv1 "k8s.io/api/admission/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/util/validation/field"
 
+	"github.com/jetstack/cert-manager/pkg/apis/acme"
+	"github.com/jetstack/cert-manager/pkg/apis/certmanager"
 	cmapi "github.com/jetstack/cert-manager/pkg/internal/apis/certmanager"
+	"github.com/jetstack/cert-manager/pkg/util"
 	"github.com/jetstack/cert-manager/pkg/util/pki"
 )
 
 var defaultInternalKeyUsages = []cmapi.KeyUsage{cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment}
 
-func ValidateCertificateRequest(obj runtime.Object) field.ErrorList {
+func ValidateCertificateRequest(_ *admissionv1.AdmissionRequest, obj runtime.Object) field.ErrorList {
 	cr := obj.(*cmapi.CertificateRequest)
 	allErrs := ValidateCertificateRequestSpec(&cr.Spec, field.NewPath("spec"), true)
 	return allErrs
 }
 
-func ValidateUpdateCertificateRequest(oldObj, obj runtime.Object) field.ErrorList {
-	cr := obj.(*cmapi.CertificateRequest)
-	// do not check the CSR content here not to break existing resources on upgrade
-	allErrs := ValidateCertificateRequestSpec(&cr.Spec, field.NewPath("spec"), false)
-	return allErrs
+func ValidateUpdateCertificateRequest(_ *admissionv1.AdmissionRequest, oldObj, newObj runtime.Object) field.ErrorList {
+	oldCR, newCR := oldObj.(*cmapi.CertificateRequest), newObj.(*cmapi.CertificateRequest)
+
+	var el field.ErrorList
+
+	// Enforce that no cert-manager annotations may be modified after creation.
+	// This is to prevent changing the request during processing resulting in
+	// undefined behaviour, and breaking the concept of requests being made by a
+	// single user.
+	annotationField := field.NewPath("metadata", "annotations")
+	el = append(el, validateCertificateRequestAnnotations(oldCR, newCR, annotationField)...)
+	el = append(el, validateCertificateRequestAnnotations(newCR, oldCR, annotationField)...)
+
+	if !reflect.DeepEqual(oldCR.Spec, newCR.Spec) {
+		el = append(el, field.Forbidden(field.NewPath("spec"), "cannot change spec after creation"))
+	}
+
+	return el
+}
+
+func validateCertificateRequestAnnotations(objA, objB *cmapi.CertificateRequest, fieldPath *field.Path) field.ErrorList {
+	var el field.ErrorList
+	for k, v := range objA.Annotations {
+		if strings.HasPrefix(k, certmanager.GroupName) ||
+			strings.HasPrefix(k, acme.GroupName) {
+			if vnew, ok := objB.Annotations[k]; !ok || v != vnew {
+				el = append(el, field.Forbidden(fieldPath.Child(k), "cannot change cert-manager annotation after creation"))
+			}
+		}
+	}
+
+	return el
 }
 
 func ValidateCertificateRequestSpec(crSpec *cmapi.CertificateRequestSpec, fldPath *field.Path, validateCSRContent bool) field.ErrorList {
diff --git a/pkg/internal/apis/certmanager/validation/clusterissuer.go b/pkg/internal/apis/certmanager/validation/clusterissuer.go
index 600802947..f7e149ee8 100644
--- a/pkg/internal/apis/certmanager/validation/clusterissuer.go
+++ b/pkg/internal/apis/certmanager/validation/clusterissuer.go
@@ -17,6 +17,7 @@ limitations under the License.
 package validation
 
 import (
+	admissionv1 "k8s.io/api/admission/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/util/validation/field"
 
@@ -25,13 +26,13 @@ import (
 
 // Validation functions for cert-manager v1alpha2 ClusterIssuer types
 
-func ValidateClusterIssuer(obj runtime.Object) field.ErrorList {
+func ValidateClusterIssuer(_ *admissionv1.AdmissionRequest, obj runtime.Object) field.ErrorList {
 	iss := obj.(*cmapi.ClusterIssuer)
 	allErrs := ValidateIssuerSpec(&iss.Spec, field.NewPath("spec"))
 	return allErrs
 }
 
-func ValidateUpdateClusterIssuer(oldObj, obj runtime.Object) field.ErrorList {
+func ValidateUpdateClusterIssuer(_ *admissionv1.AdmissionRequest, oldObj, obj runtime.Object) field.ErrorList {
 	iss := obj.(*cmapi.ClusterIssuer)
 	allErrs := ValidateIssuerSpec(&iss.Spec, field.NewPath("spec"))
 	return allErrs
diff --git a/pkg/internal/apis/certmanager/validation/issuer.go b/pkg/internal/apis/certmanager/validation/issuer.go
index ac562895f..ccc861769 100644
--- a/pkg/internal/apis/certmanager/validation/issuer.go
+++ b/pkg/internal/apis/certmanager/validation/issuer.go
@@ -21,6 +21,7 @@ import (
 	"fmt"
 	"strings"
 
+	admissionv1 "k8s.io/api/admission/v1"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/util/validation/field"
@@ -33,13 +34,13 @@ import (
 
 // Validation functions for cert-manager v1alpha2 Issuer types
 
-func ValidateIssuer(obj runtime.Object) field.ErrorList {
+func ValidateIssuer(_ *admissionv1.AdmissionRequest, obj runtime.Object) field.ErrorList {
 	iss := obj.(*certmanager.Issuer)
 	allErrs := ValidateIssuerSpec(&iss.Spec, field.NewPath("spec"))
 	return allErrs
 }
 
-func ValidateUpdateIssuer(oldObj, obj runtime.Object) field.ErrorList {
+func ValidateUpdateIssuer(_ *admissionv1.AdmissionRequest, oldObj, obj runtime.Object) field.ErrorList {
 	iss := obj.(*certmanager.Issuer)
 	allErrs := ValidateIssuerSpec(&iss.Spec, field.NewPath("spec"))
 	return allErrs
diff --git a/pkg/internal/apis/certmanager/zz_generated.deepcopy.go b/pkg/internal/apis/certmanager/zz_generated.deepcopy.go
index 3ce4a5df5..77c51896e 100644
--- a/pkg/internal/apis/certmanager/zz_generated.deepcopy.go
+++ b/pkg/internal/apis/certmanager/zz_generated.deepcopy.go
@@ -276,6 +276,26 @@ func (in *CertificateRequestSpec) DeepCopyInto(out *CertificateRequestSpec) {
 		*out = make([]KeyUsage, len(*in))
 		copy(*out, *in)
 	}
+	if in.Groups != nil {
+		in, out := &in.Groups, &out.Groups
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.Extra != nil {
+		in, out := &in.Extra, &out.Extra
+		*out = make(map[string][]string, len(*in))
+		for key, val := range *in {
+			var outVal []string
+			if val == nil {
+				(*out)[key] = nil
+			} else {
+				in, out := &val, &outVal
+				*out = make([]string, len(*in))
+				copy(*out, *in)
+			}
+			(*out)[key] = outVal
+		}
+	}
 	return
 }
 
diff --git a/pkg/webhook/BUILD.bazel b/pkg/webhook/BUILD.bazel
index 76c06d17c..22e4fbe26 100644
--- a/pkg/webhook/BUILD.bazel
+++ b/pkg/webhook/BUILD.bazel
@@ -6,6 +6,7 @@ go_library(
     importpath = "github.com/jetstack/cert-manager/pkg/webhook",
     visibility = ["//visibility:public"],
     deps = [
+        "//pkg/internal/api/mutation:go_default_library",
         "//pkg/internal/api/validation:go_default_library",
         "//pkg/internal/apis/acme/install:go_default_library",
         "//pkg/internal/apis/certmanager/install:go_default_library",
diff --git a/pkg/webhook/handlers/BUILD.bazel b/pkg/webhook/handlers/BUILD.bazel
index 8e5de3186..1a35fb3aa 100644
--- a/pkg/webhook/handlers/BUILD.bazel
+++ b/pkg/webhook/handlers/BUILD.bazel
@@ -11,10 +11,10 @@ go_library(
     importpath = "github.com/jetstack/cert-manager/pkg/webhook/handlers",
     visibility = ["//visibility:public"],
     deps = [
+        "//pkg/internal/api/mutation:go_default_library",
         "//pkg/internal/api/validation:go_default_library",
         "//pkg/logs:go_default_library",
         "@com_github_go_logr_logr//:go_default_library",
-        "@com_github_mattbaird_jsonpatch//:go_default_library",
         "@io_k8s_api//admission/v1:go_default_library",
         "@io_k8s_apiextensions_apiserver//pkg/apis/apiextensions/v1:go_default_library",
         "@io_k8s_apiextensions_apiserver//pkg/apis/apiextensions/v1beta1:go_default_library",
@@ -37,6 +37,7 @@ go_test(
     ],
     embed = [":go_default_library"],
     deps = [
+        "//pkg/internal/api/mutation:go_default_library",
         "//pkg/internal/api/validation:go_default_library",
         "//pkg/logs:go_default_library",
         "//pkg/webhook/handlers/testdata/apis/testgroup:go_default_library",
diff --git a/pkg/webhook/handlers/mutation.go b/pkg/webhook/handlers/mutation.go
index 36636725a..281ceebc3 100644
--- a/pkg/webhook/handlers/mutation.go
+++ b/pkg/webhook/handlers/mutation.go
@@ -17,85 +17,44 @@ limitations under the License.
 package handlers
 
 import (
-	"bytes"
-	"encoding/json"
 	"fmt"
 	"net/http"
-	"sort"
 
 	"github.com/go-logr/logr"
-	"github.com/mattbaird/jsonpatch"
 	admissionv1 "k8s.io/api/admission/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/serializer"
-	apijson "k8s.io/apimachinery/pkg/runtime/serializer/json"
 
+	"github.com/jetstack/cert-manager/pkg/internal/api/mutation"
 	logf "github.com/jetstack/cert-manager/pkg/logs"
 )
 
-type SchemeBackedDefaulter struct {
-	log    logr.Logger
-	scheme *runtime.Scheme
-	codec  runtime.Codec
+type RegistryBackedMutator struct {
+	log      logr.Logger
+	decoder  runtime.Decoder
+	registry *mutation.Registry
 }
 
-func NewSchemeBackedDefaulter(log logr.Logger, scheme *runtime.Scheme) *SchemeBackedDefaulter {
+func NewRegistryBackedMutator(log logr.Logger, scheme *runtime.Scheme, registry *mutation.Registry) *RegistryBackedMutator {
 	factory := serializer.NewCodecFactory(scheme)
-	serializer := apijson.NewSerializerWithOptions(apijson.DefaultMetaFactory, scheme, scheme, apijson.SerializerOptions{})
-	encoder := factory.WithoutConversion().EncoderForVersion(serializer, nil)
-	decoder := factory.UniversalDeserializer()
-	return &SchemeBackedDefaulter{
-		log:    log,
-		scheme: scheme,
-		codec:  runtime.NewCodec(encoder, decoder),
+	return &RegistryBackedMutator{
+		log:      log,
+		decoder:  factory.UniversalDecoder(),
+		registry: registry,
 	}
 }
 
-func (c *SchemeBackedDefaulter) Mutate(admissionSpec *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse {
+func (c *RegistryBackedMutator) Mutate(admissionSpec *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse {
 	status := &admissionv1.AdmissionResponse{}
 	status.UID = admissionSpec.UID
 
-	// decode the raw object data
-	obj, _, err := c.codec.Decode(admissionSpec.Object.Raw, nil, nil)
+	// Generate a patch from the appropriate functions installed in the mutation registry
+	patch, err := c.registry.Mutate(admissionSpec)
 	if err != nil {
 		status.Result = &metav1.Status{
 			Status: metav1.StatusFailure, Code: http.StatusInternalServerError, Reason: metav1.StatusReasonInternalError,
-			Message: fmt.Sprintf("Failed to decode object: %v", err.Error()),
-		}
-		return status
-	}
-
-	// create a copy of the resource
-	defaultedObj := obj.DeepCopyObject()
-	// apply defaults to the object
-	c.scheme.Default(defaultedObj)
-	// encode the default object to JSON
-	buf := bytes.Buffer{}
-	if err := c.codec.Encode(defaultedObj, &buf); err != nil {
-		status.Result = &metav1.Status{
-			Status: metav1.StatusFailure, Code: http.StatusInternalServerError, Reason: metav1.StatusReasonInternalError,
-			Message: fmt.Sprintf("Failed to encode defaulted data: %v", err.Error()),
-		}
-		return status
-	}
-	// create a merge patch between the old and the new json data
-	ops, err := jsonpatch.CreatePatch(admissionSpec.Object.Raw, buf.Bytes())
-	if err != nil {
-		status.Result = &metav1.Status{
-			Status: metav1.StatusFailure, Code: http.StatusInternalServerError, Reason: metav1.StatusReasonInternalError,
-			Message: fmt.Sprintf("Failed to generate json patch: %v", err.Error()),
-		}
-		return status
-	}
-	// sort options by path to ensure the output of CreatePatch is deterministic
-	sortOps(ops)
-
-	patch, err := json.Marshal(ops)
-	if err != nil {
-		status.Result = &metav1.Status{
-			Status: metav1.StatusFailure, Code: http.StatusInternalServerError, Reason: metav1.StatusReasonInternalError,
-			Message: fmt.Sprintf("Failed to generate json patch: %v", err.Error()),
+			Message: fmt.Sprintf("Failed to mutate object: %v", err.Error()),
 		}
 		return status
 	}
@@ -110,9 +69,3 @@ func (c *SchemeBackedDefaulter) Mutate(admissionSpec *admissionv1.AdmissionReque
 
 	return status
 }
-
-func sortOps(ops []jsonpatch.JsonPatchOperation) {
-	sort.Slice(ops, func(i, j int) bool {
-		return ops[i].Path < ops[j].Path
-	})
-}
diff --git a/pkg/webhook/handlers/validation.go b/pkg/webhook/handlers/validation.go
index 5a011e1a2..3627b9202 100644
--- a/pkg/webhook/handlers/validation.go
+++ b/pkg/webhook/handlers/validation.go
@@ -92,10 +92,10 @@ func (r *registryBackedValidator) Validate(admissionSpec *admissionv1.AdmissionR
 
 	if admissionSpec.Operation == admissionv1.Create {
 		// perform validation on new version of resource
-		errs = append(errs, r.registry.Validate(obj, gvk)...)
+		errs = append(errs, r.registry.Validate(admissionSpec, obj, gvk)...)
 	} else if admissionSpec.Operation == admissionv1.Update {
 		// perform update validation on resource
-		errs = append(errs, r.registry.ValidateUpdate(oldObj, obj, gvk)...)
+		errs = append(errs, r.registry.ValidateUpdate(admissionSpec, oldObj, obj, gvk)...)
 	}
 	// return with allowed = false if any errors occurred
 	if err := errs.ToAggregate(); err != nil {
diff --git a/pkg/webhook/scheme.go b/pkg/webhook/scheme.go
index 030a711aa..ad7269913 100644
--- a/pkg/webhook/scheme.go
+++ b/pkg/webhook/scheme.go
@@ -19,6 +19,7 @@ package webhook
 import (
 	"k8s.io/apimachinery/pkg/runtime"
 
+	"github.com/jetstack/cert-manager/pkg/internal/api/mutation"
 	"github.com/jetstack/cert-manager/pkg/internal/api/validation"
 	acmeinstall "github.com/jetstack/cert-manager/pkg/internal/apis/acme/install"
 	cminstall "github.com/jetstack/cert-manager/pkg/internal/apis/certmanager/install"
@@ -40,6 +41,10 @@ var (
 	// ValidationRegistry is a validation registry with all required
 	// validations that should be enforced by the webhook component.
 	ValidationRegistry = validation.NewRegistry(Scheme)
+
+	// MutationRegistry is a mutation registry with all required
+	// mutations that should be enforced by the webhook component.
+	MutationRegistry = mutation.NewRegistry(Scheme)
 )
 
 func init() {
@@ -49,4 +54,6 @@ func init() {
 
 	cminstall.InstallValidation(ValidationRegistry)
 	acmeinstall.InstallValidation(ValidationRegistry)
+
+	cminstall.InstallMutation(MutationRegistry)
 }


Test patch:
diff --git a/pkg/controller/certificaterequests/acme/acme_test.go b/pkg/controller/certificaterequests/acme/acme_test.go
index db055101f..6d976ca21 100644
--- a/pkg/controller/certificaterequests/acme/acme_test.go
+++ b/pkg/controller/certificaterequests/acme/acme_test.go
@@ -174,37 +174,6 @@ func TestSign(t *testing.T) {
 
 	metaFixedClockStart := metav1.NewTime(fixedClockStart)
 	tests := map[string]testT{
-		"a badly formed CSR should report failure": {
-			certificateRequest: gen.CertificateRequestFrom(baseCR,
-				gen.SetCertificateRequestCSR([]byte("a bad csr")),
-			),
-			builder: &testpkg.Builder{
-				KubeObjects:        []runtime.Object{},
-				CertManagerObjects: []runtime.Object{baseCR.DeepCopy(), baseIssuer.DeepCopy()},
-				ExpectedEvents: []string{
-					"Warning BadConfig Resource validation failed: spec.request: Invalid value: []byte{0x61, 0x20, 0x62, 0x61, 0x64, 0x20, 0x63, 0x73, 0x72}: failed to decode csr: error decoding certificate request PEM block",
-				},
-				ExpectedActions: []testpkg.Action{
-					testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
-						cmapi.SchemeGroupVersion.WithResource("certificaterequests"),
-						"status",
-						gen.DefaultTestNamespace,
-						gen.CertificateRequestFrom(baseCR,
-							gen.SetCertificateRequestCSR([]byte("a bad csr")),
-							gen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
-								Type:               cmapi.CertificateRequestConditionReady,
-								Status:             cmmeta.ConditionFalse,
-								Reason:             cmapi.CertificateRequestReasonFailed,
-								Message:            "Resource validation failed: spec.request: Invalid value: []byte{0x61, 0x20, 0x62, 0x61, 0x64, 0x20, 0x63, 0x73, 0x72}: failed to decode csr: error decoding certificate request PEM block",
-								LastTransitionTime: &metaFixedClockStart,
-							}),
-							gen.SetCertificateRequestFailureTime(metaFixedClockStart),
-						),
-					)),
-				},
-			},
-		},
-
 		"if the common name is not present in the DNS names then should hard fail": {
 			certificateRequest: gen.CertificateRequestFrom(baseCR,
 				gen.SetCertificateRequestCSR(csrPEMExampleNotPresent),
diff --git a/pkg/controller/certificaterequests/selfsigned/selfsigned_test.go b/pkg/controller/certificaterequests/selfsigned/selfsigned_test.go
index da7e19a4e..edaf419eb 100644
--- a/pkg/controller/certificaterequests/selfsigned/selfsigned_test.go
+++ b/pkg/controller/certificaterequests/selfsigned/selfsigned_test.go
@@ -354,38 +354,6 @@ func TestSign(t *testing.T) {
 			},
 			expectedErr: true,
 		},
-		"a CertificateRequest with a bad CSR should fail": {
-			certificateRequest: gen.CertificateRequestFrom(baseCR,
-				gen.SetCertificateRequestCSR([]byte("this is a bad CSR")),
-			),
-			builder: &testpkg.Builder{
-				KubeObjects: []runtime.Object{rsaKeySecret},
-				CertManagerObjects: []runtime.Object{gen.CertificateRequestFrom(baseCR,
-					gen.SetCertificateRequestCSR([]byte("this is a bad CSR")),
-				), baseIssuer},
-				ExpectedEvents: []string{
-					"Warning BadConfig Resource validation failed: spec.request: Invalid value: []byte{0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x62, 0x61, 0x64, 0x20, 0x43, 0x53, 0x52}: failed to decode csr: error decoding certificate request PEM block",
-				},
-				ExpectedActions: []testpkg.Action{
-					testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
-						cmapi.SchemeGroupVersion.WithResource("certificaterequests"),
-						"status",
-						gen.DefaultTestNamespace,
-						gen.CertificateRequestFrom(baseCR,
-							gen.SetCertificateRequestCSR([]byte("this is a bad CSR")),
-							gen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
-								Type:               cmapi.CertificateRequestConditionReady,
-								Status:             cmmeta.ConditionFalse,
-								Reason:             cmapi.CertificateRequestReasonFailed,
-								Message:            "Resource validation failed: spec.request: Invalid value: []byte{0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x62, 0x61, 0x64, 0x20, 0x43, 0x53, 0x52}: failed to decode csr: error decoding certificate request PEM block",
-								LastTransitionTime: &metaFixedClockStart,
-							}),
-							gen.SetCertificateRequestFailureTime(metaFixedClockStart),
-						),
-					)),
-				},
-			},
-		},
 		"a CSR that has not been signed with the same public key as the referenced private key should fail": {
 			certificateRequest: ecCR.DeepCopy(),
 			builder: &testpkg.Builder{
diff --git a/pkg/controller/certificaterequests/sync_test.go b/pkg/controller/certificaterequests/sync_test.go
index e4c2ce293..bf9830681 100644
--- a/pkg/controller/certificaterequests/sync_test.go
+++ b/pkg/controller/certificaterequests/sync_test.go
@@ -294,35 +294,6 @@ func TestSync(t *testing.T) {
 				ExpectedEvents:  []string{},
 			},
 		},
-		"report failure if the CertificateRequest fails validation": {
-			certificateRequest: gen.CertificateRequestFrom(baseCR,
-				gen.SetCertificateRequestCSR([]byte("bad csr")),
-			),
-			builder: &testpkg.Builder{
-				CertManagerObjects: []runtime.Object{baseCR, baseIssuer},
-				ExpectedEvents: []string{
-					"Warning BadConfig Resource validation failed: spec.request: Invalid value: []byte{0x62, 0x61, 0x64, 0x20, 0x63, 0x73, 0x72}: failed to decode csr: error decoding certificate request PEM block",
-				},
-				ExpectedActions: []testpkg.Action{
-					testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
-						cmapi.SchemeGroupVersion.WithResource("certificaterequests"),
-						"status",
-						gen.DefaultTestNamespace,
-						gen.CertificateRequestFrom(baseCR,
-							gen.SetCertificateRequestCSR([]byte("bad csr")),
-							gen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
-								Type:               cmapi.CertificateRequestConditionReady,
-								Status:             cmmeta.ConditionFalse,
-								Reason:             "Failed",
-								Message:            "Resource validation failed: spec.request: Invalid value: []byte{0x62, 0x61, 0x64, 0x20, 0x63, 0x73, 0x72}: failed to decode csr: error decoding certificate request PEM block",
-								LastTransitionTime: &nowMetaTime,
-							}),
-							gen.SetCertificateRequestFailureTime(nowMetaTime),
-						),
-					)),
-				},
-			},
-		},
 		"if the Certificate is already set in the status then return nil and no-op, regardless of condition": {
 			certificateRequest: gen.CertificateRequestFrom(baseCR,
 				gen.SetCertificateRequestCertificate([]byte("a cert")),
diff --git a/pkg/internal/api/mutation/registry_test.go b/pkg/internal/api/mutation/registry_test.go
new file mode 100644
index 000000000..db2cad787
--- /dev/null
+++ b/pkg/internal/api/mutation/registry_test.go
@@ -0,0 +1,385 @@
+/*
+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 mutation_test
+
+import (
+	"bytes"
+	"encoding/json"
+	"testing"
+
+	admissionv1 "k8s.io/api/admission/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+
+	"github.com/jetstack/cert-manager/pkg/apis/certmanager"
+	"github.com/jetstack/cert-manager/pkg/internal/api/mutation"
+	cminternal "github.com/jetstack/cert-manager/pkg/internal/apis/certmanager"
+	"github.com/jetstack/cert-manager/pkg/webhook"
+	"github.com/jetstack/cert-manager/test/unit/gen"
+)
+
+var (
+	// use the webhook's Scheme during test fixtures as it has all internal and
+	// external cert-manager kinds registered
+	scheme = webhook.Scheme
+)
+
+func TestMutate(t *testing.T) {
+	crGVK := &metav1.GroupVersionKind{
+		Group:   certmanager.GroupName,
+		Version: "v1",
+		Kind:    "CertificateRequest",
+	}
+
+	testCR := gen.CertificateRequest("test-cr",
+		gen.SetCertificateRequestTypeMeta(metav1.TypeMeta{
+			Kind:       "CertificateRequest",
+			APIVersion: "cert-manager.io/v1",
+		}),
+	)
+	testCRBytes, err := json.Marshal(testCR)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	testNotRegistered := gen.CertificateRequest("test-cr",
+		gen.SetCertificateRequestTypeMeta(metav1.TypeMeta{
+			Kind:       "NotRegistered",
+			APIVersion: "not-registered.io/v1",
+		}),
+	)
+	testNotRegisteredBytes, err := json.Marshal(testNotRegistered)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	type mutationsEntry struct {
+		obj runtime.Object
+		fn  func(t *testing.T) mutation.MutateFunc
+	}
+	type mutationUpdatesEntry struct {
+		obj runtime.Object
+		fn  func(t *testing.T) mutation.MutateUpdateFunc
+	}
+	tests := map[string]struct {
+		mutations       []mutationsEntry
+		mutationUpdates []mutationUpdatesEntry
+		req             *admissionv1.AdmissionRequest
+
+		expErr   bool
+		expPatch []byte
+	}{
+		"exit early if operation is not UPDATE or CREATE": {
+			mutations:       nil,
+			mutationUpdates: nil,
+			req: &admissionv1.AdmissionRequest{
+				RequestKind: crGVK.DeepCopy(),
+				Operation:   admissionv1.Delete,
+				Object: runtime.RawExtension{
+					Raw: testCRBytes,
+				},
+			},
+			expErr:   false,
+			expPatch: nil,
+		},
+		"if no functions registered, expect only default patch on CREATE": {
+			mutations:       nil,
+			mutationUpdates: nil,
+			req: &admissionv1.AdmissionRequest{
+				RequestKind: crGVK.DeepCopy(),
+				Operation:   admissionv1.Create,
+				Object: runtime.RawExtension{
+					Raw: testCRBytes,
+				},
+			},
+			expErr:   false,
+			expPatch: []byte("[]"),
+		},
+		"if no functions registered, expect only default patch on UPDATE": {
+			mutations:       nil,
+			mutationUpdates: nil,
+			req: &admissionv1.AdmissionRequest{
+				RequestKind: crGVK.DeepCopy(),
+				Operation:   admissionv1.Update,
+				OldObject: runtime.RawExtension{
+					Raw: testCRBytes,
+				},
+				Object: runtime.RawExtension{
+					Raw: testCRBytes,
+				},
+			},
+			expErr:   false,
+			expPatch: []byte("[]"),
+		},
+		"if kind presented for mutation hasn't been registered for CREATE, error": {
+			mutations:       nil,
+			mutationUpdates: nil,
+			req: &admissionv1.AdmissionRequest{
+				RequestKind: crGVK.DeepCopy(),
+				Operation:   admissionv1.Create,
+				Object: runtime.RawExtension{
+					Raw: testNotRegisteredBytes,
+				},
+			},
+			expErr:   true,
+			expPatch: nil,
+		},
+		"if kind presented for mutation hasn't been registered for UPDATE, error": {
+			mutations:       nil,
+			mutationUpdates: nil,
+			req: &admissionv1.AdmissionRequest{
+				RequestKind: crGVK.DeepCopy(),
+				Operation:   admissionv1.Update,
+				OldObject: runtime.RawExtension{
+					Raw: testNotRegisteredBytes,
+				},
+				Object: runtime.RawExtension{
+					Raw: testNotRegisteredBytes,
+				},
+			},
+			expErr:   true,
+			expPatch: nil,
+		},
+		"if update mutation function registered for different kind, ignore": {
+			mutations: []mutationsEntry{
+				{
+					obj: new(cminternal.Certificate),
+					fn: func(t *testing.T) mutation.MutateFunc {
+						return func(_ *admissionv1.AdmissionRequest, _ runtime.Object) {
+							t.Error("unexpected call")
+						}
+					},
+				},
+			},
+			mutationUpdates: nil,
+			req: &admissionv1.AdmissionRequest{
+				RequestKind: crGVK.DeepCopy(),
+				Operation:   admissionv1.Create,
+				Object: runtime.RawExtension{
+					Raw: testCRBytes,
+				},
+			},
+			expErr:   false,
+			expPatch: []byte("[]"),
+		},
+		"if create mutation function registered for different kind, ignore": {
+			mutations: nil,
+			mutationUpdates: []mutationUpdatesEntry{
+				{
+					obj: new(cminternal.Certificate),
+					fn: func(t *testing.T) mutation.MutateUpdateFunc {
+						return func(_ *admissionv1.AdmissionRequest, _, _ runtime.Object) {
+							t.Error("unexpected call")
+						}
+					},
+				},
+			},
+			req: &admissionv1.AdmissionRequest{
+				RequestKind: crGVK.DeepCopy(),
+				Operation:   admissionv1.Create,
+				Object: runtime.RawExtension{
+					Raw: testCRBytes,
+				},
+			},
+			expErr:   false,
+			expPatch: []byte("[]"),
+		},
+		"if create mutation function registered for kind, run mutation": {
+			mutations: []mutationsEntry{
+				{
+					obj: new(cminternal.CertificateRequest),
+					fn: func(t *testing.T) mutation.MutateFunc {
+						return func(_ *admissionv1.AdmissionRequest, obj runtime.Object) {
+							cr := obj.(*cminternal.CertificateRequest)
+							cr.Spec.Request = []byte("mutation called")
+						}
+					},
+				},
+			},
+			mutationUpdates: []mutationUpdatesEntry{
+				{
+					obj: new(cminternal.Certificate),
+					fn: func(t *testing.T) mutation.MutateUpdateFunc {
+						return func(_ *admissionv1.AdmissionRequest, _, _ runtime.Object) {
+							t.Error("unexpected call")
+						}
+					},
+				},
+			},
+			req: &admissionv1.AdmissionRequest{
+				RequestKind: crGVK.DeepCopy(),
+				Operation:   admissionv1.Create,
+				Object: runtime.RawExtension{
+					Raw: testCRBytes,
+				},
+			},
+			expErr:   false,
+			expPatch: []byte(`[{"op":"replace","path":"/spec/request","value":"bXV0YXRpb24gY2FsbGVk"}]`),
+		},
+		"if update mutation function registered for kind, run mutation": {
+			mutations: []mutationsEntry{
+				{
+					obj: new(cminternal.Certificate),
+					fn: func(t *testing.T) mutation.MutateFunc {
+						return func(_ *admissionv1.AdmissionRequest, _ runtime.Object) {
+							t.Error("unexpected call")
+						}
+					},
+				},
+			},
+			mutationUpdates: []mutationUpdatesEntry{
+				{
+					obj: new(cminternal.CertificateRequest),
+					fn: func(t *testing.T) mutation.MutateUpdateFunc {
+						return func(_ *admissionv1.AdmissionRequest, _, obj runtime.Object) {
+							cr := obj.(*cminternal.CertificateRequest)
+							cr.Spec.Request = []byte("mutation called")
+						}
+					},
+				},
+			},
+			req: &admissionv1.AdmissionRequest{
+				RequestKind: crGVK.DeepCopy(),
+				Operation:   admissionv1.Update,
+				Object: runtime.RawExtension{
+					Raw: testCRBytes,
+				},
+				OldObject: runtime.RawExtension{
+					Raw: testCRBytes,
+				},
+			},
+			expErr:   false,
+			expPatch: []byte(`[{"op":"replace","path":"/spec/request","value":"bXV0YXRpb24gY2FsbGVk"}]`),
+		},
+		"if multiple create mutation functions registered for kind, run mutation": {
+			mutations: []mutationsEntry{
+				{
+					obj: new(cminternal.CertificateRequest),
+					fn: func(t *testing.T) mutation.MutateFunc {
+						return func(_ *admissionv1.AdmissionRequest, obj runtime.Object) {
+							cr := obj.(*cminternal.CertificateRequest)
+							cr.Spec.Request = []byte("mutation called")
+						}
+					},
+				},
+				{
+					obj: new(cminternal.CertificateRequest),
+					fn: func(t *testing.T) mutation.MutateFunc {
+						return func(_ *admissionv1.AdmissionRequest, obj runtime.Object) {
+							cr := obj.(*cminternal.CertificateRequest)
+							if cr.Annotations == nil {
+								cr.Annotations = make(map[string]string)
+							}
+							cr.Annotations["second-mutation"] = "called"
+						}
+					},
+				},
+			},
+			mutationUpdates: []mutationUpdatesEntry{
+				{
+					obj: new(cminternal.Certificate),
+					fn: func(t *testing.T) mutation.MutateUpdateFunc {
+						return func(_ *admissionv1.AdmissionRequest, _, _ runtime.Object) {
+							t.Error("unexpected call")
+						}
+					},
+				},
+			},
+			req: &admissionv1.AdmissionRequest{
+				RequestKind: crGVK.DeepCopy(),
+				Operation:   admissionv1.Create,
+				Object: runtime.RawExtension{
+					Raw: testCRBytes,
+				},
+			},
+			expErr:   false,
+			expPatch: []byte(`[{"op":"add","path":"/metadata/annotations","value":{"second-mutation":"called"}},{"op":"replace","path":"/spec/request","value":"bXV0YXRpb24gY2FsbGVk"}]`),
+		},
+		"if multiple update mutation function registered for kind, run mutation": {
+			mutations: []mutationsEntry{
+				{
+					obj: new(cminternal.Certificate),
+					fn: func(t *testing.T) mutation.MutateFunc {
+						return func(_ *admissionv1.AdmissionRequest, _ runtime.Object) {
+							t.Error("unexpected call")
+						}
+					},
+				},
+			},
+			mutationUpdates: []mutationUpdatesEntry{
+				{
+					obj: new(cminternal.CertificateRequest),
+					fn: func(t *testing.T) mutation.MutateUpdateFunc {
+						return func(_ *admissionv1.AdmissionRequest, _, obj runtime.Object) {
+							cr := obj.(*cminternal.CertificateRequest)
+							cr.Spec.Request = []byte("mutation called")
+						}
+					},
+				},
+				{
+					obj: new(cminternal.CertificateRequest),
+					fn: func(t *testing.T) mutation.MutateUpdateFunc {
+						return func(_ *admissionv1.AdmissionRequest, _, obj runtime.Object) {
+							cr := obj.(*cminternal.CertificateRequest)
+							if cr.Annotations == nil {
+								cr.Annotations = make(map[string]string)
+							}
+							cr.Annotations["second-mutation"] = "called"
+						}
+					},
+				},
+			},
+			req: &admissionv1.AdmissionRequest{
+				RequestKind: crGVK.DeepCopy(),
+				Operation:   admissionv1.Update,
+				Object: runtime.RawExtension{
+					Raw: testCRBytes,
+				},
+				OldObject: runtime.RawExtension{
+					Raw: testCRBytes,
+				},
+			},
+			expErr:   false,
+			expPatch: []byte(`[{"op":"add","path":"/metadata/annotations","value":{"second-mutation":"called"}},{"op":"replace","path":"/spec/request","value":"bXV0YXRpb24gY2FsbGVk"}]`),
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			reg := mutation.NewRegistry(scheme)
+
+			// Add mutation functions to registry
+			for _, m := range test.mutations {
+				reg.AddMutateFunc(m.obj, m.fn(t))
+			}
+			for _, m := range test.mutationUpdates {
+				reg.AddMutateUpdateFunc(m.obj, m.fn(t))
+			}
+
+			patch, err := reg.Mutate(test.req)
+			if test.expErr != (err != nil) {
+				t.Errorf("unexpected error, exp=%t got=%v",
+					test.expErr, err)
+			}
+
+			if !bytes.Equal(test.expPatch, patch) {
+				t.Errorf("unexpected patch, exp=%s got=%s",
+					test.expPatch, patch)
+			}
+		})
+	}
+}
diff --git a/pkg/internal/api/validation/registry_test.go b/pkg/internal/api/validation/registry_test.go
index 7403a06ac..919a249cb 100644
--- a/pkg/internal/api/validation/registry_test.go
+++ b/pkg/internal/api/validation/registry_test.go
@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"testing"
 
+	admissionv1 "k8s.io/api/admission/v1"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
@@ -40,11 +41,11 @@ var (
 func TestValidateType(t *testing.T) {
 	reg := validation.NewRegistry(scheme)
 	called := false
-	utilruntime.Must(reg.AddValidateFunc(&cmapi.Certificate{}, func(obj runtime.Object) field.ErrorList {
+	utilruntime.Must(reg.AddValidateFunc(&cmapi.Certificate{}, func(req *admissionv1.AdmissionRequest, obj runtime.Object) field.ErrorList {
 		called = true
 		return nil
 	}))
-	errs := reg.Validate(&cmapi.Certificate{}, cmapi.SchemeGroupVersion.WithKind("Certificate"))
+	errs := reg.Validate(&admissionv1.AdmissionRequest{}, &cmapi.Certificate{}, cmapi.SchemeGroupVersion.WithKind("Certificate"))
 	if len(errs) > 0 {
 		t.Errorf("expected to not get an error but got: %v", errs.ToAggregate())
 	}
@@ -58,19 +59,19 @@ func TestValidateTypeMultiple(t *testing.T) {
 	called1 := false
 	called2 := false
 	calledInternal := false
-	utilruntime.Must(reg.AddValidateFunc(&cmapi.Certificate{}, func(obj runtime.Object) field.ErrorList {
+	utilruntime.Must(reg.AddValidateFunc(&cmapi.Certificate{}, func(req *admissionv1.AdmissionRequest, obj runtime.Object) field.ErrorList {
 		called1 = true
 		return nil
 	}))
-	utilruntime.Must(reg.AddValidateFunc(&cmapi.Certificate{}, func(obj runtime.Object) field.ErrorList {
+	utilruntime.Must(reg.AddValidateFunc(&cmapi.Certificate{}, func(req *admissionv1.AdmissionRequest, obj runtime.Object) field.ErrorList {
 		called2 = true
 		return nil
 	}))
-	utilruntime.Must(reg.AddValidateFunc(&cmapiinternal.Certificate{}, func(obj runtime.Object) field.ErrorList {
+	utilruntime.Must(reg.AddValidateFunc(&cmapiinternal.Certificate{}, func(req *admissionv1.AdmissionRequest, obj runtime.Object) field.ErrorList {
 		calledInternal = true
 		return nil
 	}))
-	errs := reg.Validate(&cmapi.Certificate{}, cmapi.SchemeGroupVersion.WithKind("Certificate"))
+	errs := reg.Validate(&admissionv1.AdmissionRequest{}, &cmapi.Certificate{}, cmapi.SchemeGroupVersion.WithKind("Certificate"))
 	if len(errs) > 0 {
 		t.Errorf("expected to not get an error but got: %v", errs.ToAggregate())
 	}
@@ -87,19 +88,19 @@ func TestValidateUpdateTypeMultiple(t *testing.T) {
 	called1 := false
 	called2 := false
 	calledInternal := false
-	utilruntime.Must(reg.AddValidateUpdateFunc(&cmapi.Certificate{}, func(_, _ runtime.Object) field.ErrorList {
+	utilruntime.Must(reg.AddValidateUpdateFunc(&cmapi.Certificate{}, func(_ *admissionv1.AdmissionRequest, _, _ runtime.Object) field.ErrorList {
 		called1 = true
 		return nil
 	}))
-	utilruntime.Must(reg.AddValidateUpdateFunc(&cmapi.Certificate{}, func(_, _ runtime.Object) field.ErrorList {
+	utilruntime.Must(reg.AddValidateUpdateFunc(&cmapi.Certificate{}, func(_ *admissionv1.AdmissionRequest, _, _ runtime.Object) field.ErrorList {
 		called2 = true
 		return nil
 	}))
-	utilruntime.Must(reg.AddValidateUpdateFunc(&cmapiinternal.Certificate{}, func(_, _ runtime.Object) field.ErrorList {
+	utilruntime.Must(reg.AddValidateUpdateFunc(&cmapiinternal.Certificate{}, func(_ *admissionv1.AdmissionRequest, _, _ runtime.Object) field.ErrorList {
 		calledInternal = true
 		return nil
 	}))
-	errs := reg.ValidateUpdate(&cmapi.Certificate{}, &cmapi.Certificate{}, cmapi.SchemeGroupVersion.WithKind("Certificate"))
+	errs := reg.ValidateUpdate(&admissionv1.AdmissionRequest{}, &cmapi.Certificate{}, &cmapi.Certificate{}, cmapi.SchemeGroupVersion.WithKind("Certificate"))
 	if len(errs) > 0 {
 		t.Errorf("expected to not get an error but got: %v", errs.ToAggregate())
 	}
@@ -114,11 +115,11 @@ func TestValidateUpdateTypeMultiple(t *testing.T) {
 func TestValidateUpdateType(t *testing.T) {
 	reg := validation.NewRegistry(scheme)
 	called := false
-	utilruntime.Must(reg.AddValidateUpdateFunc(&cmapi.Certificate{}, func(oldObj, obj runtime.Object) field.ErrorList {
+	utilruntime.Must(reg.AddValidateUpdateFunc(&cmapi.Certificate{}, func(_ *admissionv1.AdmissionRequest, oldObj, obj runtime.Object) field.ErrorList {
 		called = true
 		return nil
 	}))
-	errs := reg.ValidateUpdate(&cmapi.Certificate{}, &cmapi.Certificate{}, cmapi.SchemeGroupVersion.WithKind("Certificate"))
+	errs := reg.ValidateUpdate(&admissionv1.AdmissionRequest{}, &cmapi.Certificate{}, &cmapi.Certificate{}, cmapi.SchemeGroupVersion.WithKind("Certificate"))
 	if len(errs) > 0 {
 		t.Errorf("expected to not get an error but got: %v", errs.ToAggregate())
 	}
@@ -131,11 +132,11 @@ func TestValidateTypeReturnsErrors(t *testing.T) {
 	reg := validation.NewRegistry(scheme)
 	called := false
 	expectedErr := field.InternalError(nil, fmt.Errorf("failed"))
-	utilruntime.Must(reg.AddValidateFunc(&cmapi.Certificate{}, func(obj runtime.Object) field.ErrorList {
+	utilruntime.Must(reg.AddValidateFunc(&cmapi.Certificate{}, func(_ *admissionv1.AdmissionRequest, obj runtime.Object) field.ErrorList {
 		called = true
 		return field.ErrorList{expectedErr}
 	}))
-	errs := reg.Validate(&cmapi.Certificate{}, cmapi.SchemeGroupVersion.WithKind("Certificate"))
+	errs := reg.Validate(&admissionv1.AdmissionRequest{}, &cmapi.Certificate{}, cmapi.SchemeGroupVersion.WithKind("Certificate"))
 	if len(errs) != 1 {
 		t.Error("expected to get an error but got none")
 	} else if err := errs[0]; err.Error() != expectedErr.Error() {
@@ -149,11 +150,11 @@ func TestValidateTypeReturnsErrors(t *testing.T) {
 func TestValidateInternalType(t *testing.T) {
 	reg := validation.NewRegistry(scheme)
 	called := false
-	utilruntime.Must(reg.AddValidateFunc(&cmapiinternal.Certificate{}, func(obj runtime.Object) field.ErrorList {
+	utilruntime.Must(reg.AddValidateFunc(&cmapiinternal.Certificate{}, func(_ *admissionv1.AdmissionRequest, obj runtime.Object) field.ErrorList {
 		called = true
 		return nil
 	}))
-	errs := reg.Validate(&cmapi.Certificate{}, cmapi.SchemeGroupVersion.WithKind("Certificate"))
+	errs := reg.Validate(&admissionv1.AdmissionRequest{}, &cmapi.Certificate{}, cmapi.SchemeGroupVersion.WithKind("Certificate"))
 	if len(errs) > 0 {
 		t.Errorf("expected to not get an error but got: %v", errs.ToAggregate())
 	}
@@ -164,7 +165,7 @@ func TestValidateInternalType(t *testing.T) {
 
 func TestValidateNoErrorNoneRegistered(t *testing.T) {
 	reg := validation.NewRegistry(scheme)
-	errs := reg.Validate(&cmapi.Certificate{}, cmapi.SchemeGroupVersion.WithKind("Certificate"))
+	errs := reg.Validate(&admissionv1.AdmissionRequest{}, &cmapi.Certificate{}, cmapi.SchemeGroupVersion.WithKind("Certificate"))
 	if len(errs) > 0 {
 		t.Errorf("expected to not get an error but got: %v", errs.ToAggregate())
 	}
@@ -172,7 +173,7 @@ func TestValidateNoErrorNoneRegistered(t *testing.T) {
 
 func TestValidateUpdateNoErrorNoneRegistered(t *testing.T) {
 	reg := validation.NewRegistry(scheme)
-	errs := reg.ValidateUpdate(&cmapi.Certificate{}, &cmapi.Certificate{}, cmapi.SchemeGroupVersion.WithKind("Certificate"))
+	errs := reg.ValidateUpdate(&admissionv1.AdmissionRequest{}, &cmapi.Certificate{}, &cmapi.Certificate{}, cmapi.SchemeGroupVersion.WithKind("Certificate"))
 	if len(errs) > 0 {
 		t.Errorf("expected to not get an error but got: %v", errs.ToAggregate())
 	}
@@ -180,7 +181,7 @@ func TestValidateUpdateNoErrorNoneRegistered(t *testing.T) {
 
 func TestValidateUnrecognisedType(t *testing.T) {
 	reg := validation.NewRegistry(scheme)
-	err := reg.AddValidateFunc(&corev1.Pod{}, func(obj runtime.Object) field.ErrorList {
+	err := reg.AddValidateFunc(&corev1.Pod{}, func(_ *admissionv1.AdmissionRequest, obj runtime.Object) field.ErrorList {
 		return nil
 	})
 	if err == nil {
@@ -190,7 +191,7 @@ func TestValidateUnrecognisedType(t *testing.T) {
 
 func TestValidateUpdateUnrecognisedType(t *testing.T) {
 	reg := validation.NewRegistry(scheme)
-	err := reg.AddValidateUpdateFunc(&corev1.Pod{}, func(oldObj, obj runtime.Object) field.ErrorList {
+	err := reg.AddValidateUpdateFunc(&corev1.Pod{}, func(_ *admissionv1.AdmissionRequest, oldObj, obj runtime.Object) field.ErrorList {
 		return nil
 	})
 	if err == nil {
diff --git a/pkg/internal/apis/acme/validation/challenge_test.go b/pkg/internal/apis/acme/validation/challenge_test.go
index 09d896840..c182b3a0c 100644
--- a/pkg/internal/apis/acme/validation/challenge_test.go
+++ b/pkg/internal/apis/acme/validation/challenge_test.go
@@ -67,7 +67,7 @@ func TestValidateChallengeUpdate(t *testing.T) {
 	}
 	for n, s := range scenarios {
 		t.Run(n, func(t *testing.T) {
-			errs := ValidateChallengeUpdate(s.old, s.new)
+			errs := ValidateChallengeUpdate(nil, s.old, s.new)
 			if len(errs) != len(s.errs) {
 				t.Errorf("Expected %v but got %v", s.errs, errs)
 				return
diff --git a/pkg/internal/apis/acme/validation/order_test.go b/pkg/internal/apis/acme/validation/order_test.go
index fb46f28f9..df4e08629 100644
--- a/pkg/internal/apis/acme/validation/order_test.go
+++ b/pkg/internal/apis/acme/validation/order_test.go
@@ -47,7 +47,7 @@ func testImmutableOrderField(t *testing.T, fldPath *field.Path, setter func(*cma
 		new := &cmacme.Order{}
 		setter(old, testValueOptionOne)
 		setter(new, testValueOptionTwo)
-		errs := ValidateOrderUpdate(old, new)
+		errs := ValidateOrderUpdate(nil, old, new)
 		if len(errs) != len(expectedErrs) {
 			t.Errorf("Expected %v but got %v", expectedErrs, errs)
 			return
@@ -65,7 +65,7 @@ func testImmutableOrderField(t *testing.T, fldPath *field.Path, setter func(*cma
 		new := &cmacme.Order{}
 		setter(old, testValueNone)
 		setter(new, testValueOptionOne)
-		errs := ValidateOrderUpdate(old, new)
+		errs := ValidateOrderUpdate(nil, old, new)
 		if len(errs) != len(expectedErrs) {
 			t.Errorf("Expected %v but got %v", expectedErrs, errs)
 			return
@@ -211,7 +211,7 @@ func TestValidateCertificateUpdate(t *testing.T) {
 	}
 	for n, s := range scenarios {
 		t.Run(n, func(t *testing.T) {
-			errs := ValidateOrderUpdate(s.old, s.new)
+			errs := ValidateOrderUpdate(nil, s.old, s.new)
 			if len(errs) != len(s.errs) {
 				t.Errorf("Expected %v but got %v", s.errs, errs)
 				return
diff --git a/pkg/internal/apis/certmanager/identity/certificaterequests/certificaterequests_test.go b/pkg/internal/apis/certmanager/identity/certificaterequests/certificaterequests_test.go
new file mode 100644
index 000000000..097b950ec
--- /dev/null
+++ b/pkg/internal/apis/certmanager/identity/certificaterequests/certificaterequests_test.go
@@ -0,0 +1,255 @@
+/*
+Copyright 2021 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 certificaterequests
+
+import (
+	"reflect"
+	"testing"
+
+	admissionv1 "k8s.io/api/admission/v1"
+	authenticationv1 "k8s.io/api/authentication/v1"
+	"k8s.io/apimachinery/pkg/util/validation/field"
+
+	cmapi "github.com/jetstack/cert-manager/pkg/internal/apis/certmanager"
+)
+
+func TestValidateCreate(t *testing.T) {
+	fldPath := field.NewPath("spec")
+
+	tests := map[string]struct {
+		req  *admissionv1.AdmissionRequest
+		cr   *cmapi.CertificateRequest
+		want field.ErrorList
+	}{
+		"if identity fields don't match that of requester, should fail": {
+			req: &admissionv1.AdmissionRequest{
+				UserInfo: authenticationv1.UserInfo{
+					UID:      "abc",
+					Username: "user-1",
+					Groups:   []string{"group-1", "group-2"},
+					Extra: map[string]authenticationv1.ExtraValue{
+						"1": []string{"abc", "efg"},
+						"2": []string{"efg", "abc"},
+					},
+				},
+			},
+			cr: &cmapi.CertificateRequest{
+				Spec: cmapi.CertificateRequestSpec{
+					UID:      "efg",
+					Username: "user-2",
+					Groups:   []string{"group-3", "group-4"},
+					Extra: map[string][]string{
+						"1": {"123", "456"},
+						"2": {"efg", "abc"},
+					},
+				},
+			},
+			want: field.ErrorList{
+				field.Forbidden(fldPath.Child("uid"), "uid identity must be that of the requester"),
+				field.Forbidden(fldPath.Child("username"), "username identity must be that of the requester"),
+				field.Forbidden(fldPath.Child("groups"), "groups identity must be that of the requester"),
+				field.Forbidden(fldPath.Child("extra"), "extra identity must be that of the requester"),
+			},
+		},
+		"if identity fields match that of requester, should pass": {
+			req: &admissionv1.AdmissionRequest{
+				UserInfo: authenticationv1.UserInfo{
+					UID:      "abc",
+					Username: "user-1",
+					Groups:   []string{"group-1", "group-2"},
+					Extra: map[string]authenticationv1.ExtraValue{
+						"1": []string{"abc", "efg"},
+						"2": []string{"efg", "abc"},
+					},
+				},
+			},
+			cr: &cmapi.CertificateRequest{
+				Spec: cmapi.CertificateRequestSpec{
+					UID:      "abc",
+					Username: "user-1",
+					Groups:   []string{"group-1", "group-2"},
+					Extra: map[string][]string{
+						"1": {"abc", "efg"},
+						"2": {"efg", "abc"},
+					},
+				},
+			},
+			want: nil,
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			got := ValidateCreate(test.req, test.cr)
+			if !reflect.DeepEqual(got, test.want) {
+				t.Errorf("ValidateCreate() = %v, want %v", got, test.want)
+			}
+		})
+	}
+}
+
+func TestValidateUpdate(t *testing.T) {
+	fldPath := field.NewPath("spec")
+
+	tests := map[string]struct {
+		oldCR, newCR *cmapi.CertificateRequest
+		want         field.ErrorList
+	}{
+		"if identity fields don't match that of the old CertificateRequest, should fail": {
+			oldCR: &cmapi.CertificateRequest{
+				Spec: cmapi.CertificateRequestSpec{
+					UID:      "abc",
+					Username: "user-1",
+					Groups:   []string{"group-1", "group-2"},
+					Extra: map[string][]string{
+						"1": {"abc", "efg"},
+						"2": {"efg", "abc"},
+					},
+				},
+			},
+			newCR: &cmapi.CertificateRequest{
+				Spec: cmapi.CertificateRequestSpec{
+					UID:      "efg",
+					Username: "user-2",
+					Groups:   []string{"group-3", "group-4"},
+					Extra: map[string][]string{
+						"1": {"123", "456"},
+						"2": {"efg", "abc"},
+					},
+				},
+			},
+			want: field.ErrorList{
+				field.Forbidden(fldPath.Child("uid"), "uid identity cannot be changed once set"),
+				field.Forbidden(fldPath.Child("username"), "username identity cannot be changed once set"),
+				field.Forbidden(fldPath.Child("groups"), "groups identity cannot be changed once set"),
+				field.Forbidden(fldPath.Child("extra"), "extra identity cannot be changed once set"),
+			},
+		},
+		"if identity fields match that of requester, should pass": {
+			oldCR: &cmapi.CertificateRequest{
+				Spec: cmapi.CertificateRequestSpec{
+					UID:      "abc",
+					Username: "user-1",
+					Groups:   []string{"group-1", "group-2"},
+					Extra: map[string][]string{
+						"1": {"abc", "efg"},
+						"2": {"efg", "abc"},
+					},
+				},
+			},
+			newCR: &cmapi.CertificateRequest{
+				Spec: cmapi.CertificateRequestSpec{
+					UID:      "abc",
+					Username: "user-1",
+					Groups:   []string{"group-1", "group-2"},
+					Extra: map[string][]string{
+						"1": {"abc", "efg"},
+						"2": {"efg", "abc"},
+					},
+				},
+			},
+			want: nil,
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			got := ValidateUpdate(nil, test.newCR, test.oldCR)
+			if !reflect.DeepEqual(got, test.want) {
+				t.Errorf("ValidateUpdate() = %v, want %v", got, test.want)
+			}
+		})
+	}
+}
+
+func TestMutateCreate(t *testing.T) {
+	tests := map[string]struct {
+		req                    *admissionv1.AdmissionRequest
+		existingCR, expectedCR *cmapi.CertificateRequest
+	}{
+		"should set the identity of CertificateRequest to that of the requester": {
+			req: &admissionv1.AdmissionRequest{
+				UserInfo: authenticationv1.UserInfo{
+					UID:      "abc",
+					Username: "user-1",
+					Groups:   []string{"group-1", "group-2"},
+					Extra: map[string]authenticationv1.ExtraValue{
+						"1": []string{"abc", "efg"},
+						"2": []string{"efg", "abc"},
+					},
+				},
+			},
+			existingCR: new(cmapi.CertificateRequest),
+			expectedCR: &cmapi.CertificateRequest{
+				Spec: cmapi.CertificateRequestSpec{
+					UID:      "abc",
+					Username: "user-1",
+					Groups:   []string{"group-1", "group-2"},
+					Extra: map[string][]string{
+						"1": {"abc", "efg"},
+						"2": {"efg", "abc"},
+					},
+				},
+			},
+		},
+		"should overrite existing user info fields if they exist on a CREATE operation": {
+			req: &admissionv1.AdmissionRequest{
+				UserInfo: authenticationv1.UserInfo{
+					UID:      "abc",
+					Username: "user-1",
+					Groups:   []string{"group-1", "group-2"},
+					Extra: map[string]authenticationv1.ExtraValue{
+						"1": []string{"abc", "efg"},
+						"2": []string{"efg", "abc"},
+					},
+				},
+			},
+			existingCR: &cmapi.CertificateRequest{
+				Spec: cmapi.CertificateRequestSpec{
+					UID:      "1234",
+					Username: "user-2",
+					Groups:   []string{"group-3", "group-4"},
+					Extra: map[string][]string{
+						"3": {"abc", "efg"},
+						"4": {"efg", "abc"},
+					},
+				},
+			},
+			expectedCR: &cmapi.CertificateRequest{
+				Spec: cmapi.CertificateRequestSpec{
+					UID:      "abc",
+					Username: "user-1",
+					Groups:   []string{"group-1", "group-2"},
+					Extra: map[string][]string{
+						"1": {"abc", "efg"},
+						"2": {"efg", "abc"},
+					},
+				},
+			},
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			cr := test.expectedCR.DeepCopy()
+			MutateCreate(test.req, cr)
+			if !reflect.DeepEqual(test.expectedCR, cr) {
+				t.Errorf("MutateCreate() = %v, want %v", cr, test.expectedCR)
+			}
+		})
+	}
+}
diff --git a/pkg/internal/apis/certmanager/validation/certificate_test.go b/pkg/internal/apis/certmanager/validation/certificate_test.go
index 7a5cd419c..671d357d4 100644
--- a/pkg/internal/apis/certmanager/validation/certificate_test.go
+++ b/pkg/internal/apis/certmanager/validation/certificate_test.go
@@ -487,7 +487,7 @@ func TestValidateCertificate(t *testing.T) {
 	}
 	for n, s := range scenarios {
 		t.Run(n, func(t *testing.T) {
-			errs := ValidateCertificate(s.cfg)
+			errs := ValidateCertificate(nil, s.cfg)
 			if len(errs) != len(s.errs) {
 				t.Errorf("Expected %v but got %v", s.errs, errs)
 				return
diff --git a/pkg/internal/apis/certmanager/validation/certificaterequest_test.go b/pkg/internal/apis/certmanager/validation/certificaterequest_test.go
index 7e44d3c68..ab667837a 100644
--- a/pkg/internal/apis/certmanager/validation/certificaterequest_test.go
+++ b/pkg/internal/apis/certmanager/validation/certificaterequest_test.go
@@ -22,6 +22,7 @@ import (
 	"reflect"
 	"testing"
 
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/validation/field"
 
 	cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
@@ -31,6 +32,87 @@ import (
 	"github.com/jetstack/cert-manager/test/unit/gen"
 )
 
+func TestValidateCertificateRequestUpdate(t *testing.T) {
+	baseCR := &cminternal.CertificateRequest{
+		ObjectMeta: metav1.ObjectMeta{
+			Annotations: map[string]string{
+				"abc":                      "123",
+				"cert-manager.io/foo":      "abc",
+				"acme.cert-manager.io/bar": "123",
+			},
+		},
+		Spec: cminternal.CertificateRequestSpec{
+			Request:   mustGenerateCSR(t, gen.Certificate("test", gen.SetCertificateDNSNames("example.com"))),
+			IssuerRef: validIssuerRef,
+			Usages:    nil,
+			UID:       "abc",
+			Username:  "user-1",
+			Groups:    []string{"group-1", "group-2"},
+			Extra: map[string][]string{
+				"1": {"abc", "efg"},
+				"2": {"efg", "abc"},
+			},
+		},
+	}
+
+	tests := map[string]struct {
+		oldCR, newCR *cminternal.CertificateRequest
+		want         field.ErrorList
+	}{
+		"if CertificateRequest spec and cert-manager.io annotations change, error": {
+			oldCR: baseCR.DeepCopy(),
+			newCR: &cminternal.CertificateRequest{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						"acme.cert-manager.io/bar": "123",
+						"123":                      "abc",
+					},
+				},
+				Spec: cminternal.CertificateRequestSpec{
+					Request: mustGenerateCSR(t, gen.Certificate("test", gen.SetCertificateDNSNames("example.com"))),
+				},
+			},
+			want: []*field.Error{
+				field.Forbidden(field.NewPath("metadata", "annotations", "cert-manager.io/foo"), "cannot change cert-manager annotation after creation"),
+				field.Forbidden(field.NewPath("spec"), "cannot change spec after creation"),
+			},
+		},
+		"if CertificateRequest spec and acme.cert-manager.io annotations change, error": {
+			oldCR: baseCR.DeepCopy(),
+			newCR: &cminternal.CertificateRequest{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						"cert-manager.io/foo": "abc",
+						"123":                 "abc",
+					},
+				},
+				Spec: cminternal.CertificateRequestSpec{
+					Request: mustGenerateCSR(t, gen.Certificate("test", gen.SetCertificateDNSNames("example.com"))),
+				},
+			},
+			want: []*field.Error{
+				field.Forbidden(field.NewPath("metadata", "annotations", "acme.cert-manager.io/bar"), "cannot change cert-manager annotation after creation"),
+				field.Forbidden(field.NewPath("spec"), "cannot change spec after creation"),
+			},
+		},
+		"if CertificateRequest spec and annotations do not change, don't error": {
+			oldCR: baseCR.DeepCopy(),
+			newCR: baseCR.DeepCopy(),
+			want:  nil,
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			err := ValidateUpdateCertificateRequest(nil, test.oldCR, test.newCR)
+			if !reflect.DeepEqual(err, test.want) {
+				t.Errorf("got unexpected error response, exp=%v got=%v",
+					test.want, err)
+			}
+		})
+	}
+}
+
 func TestValidateCertificateRequestSpec(t *testing.T) {
 	fldPath := field.NewPath("test")
 
diff --git a/pkg/webhook/handlers/mutation_test.go b/pkg/webhook/handlers/mutation_test.go
index d093a76a6..d8eba4fbf 100644
--- a/pkg/webhook/handlers/mutation_test.go
+++ b/pkg/webhook/handlers/mutation_test.go
@@ -23,11 +23,13 @@ import (
 
 	"github.com/mattbaird/jsonpatch"
 	admissionv1 "k8s.io/api/admission/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/klog/v2/klogr"
 	"k8s.io/utils/diff"
 
+	"github.com/jetstack/cert-manager/pkg/internal/api/mutation"
 	"github.com/jetstack/cert-manager/pkg/webhook/handlers/testdata/apis/testgroup/install"
 )
 
@@ -46,14 +48,21 @@ func responseForOperations(ops ...jsonpatch.JsonPatchOperation) []byte {
 
 func TestDefaultCertificate(t *testing.T) {
 	scheme := runtime.NewScheme()
+	registry := mutation.NewRegistry(scheme)
 	install.Install(scheme)
 
 	log := klogr.New()
-	c := NewSchemeBackedDefaulter(log, scheme)
+	c := NewRegistryBackedMutator(log, scheme, registry)
 	tests := map[string]admissionTestT{
 		"apply defaults to TestType": {
 			inputRequest: admissionv1.AdmissionRequest{
-				UID: types.UID("abc"),
+				UID:       types.UID("abc"),
+				Operation: admissionv1.Create,
+				RequestKind: &metav1.GroupVersionKind{
+					Group:   "testgroup.testing.cert-manager.io",
+					Version: "v1",
+					Kind:    "TestType",
+				},
 				Object: runtime.RawExtension{
 					Raw: []byte(`
 {
diff --git a/pkg/webhook/handlers/testdata/apis/testgroup/v2/BUILD.bazel b/pkg/webhook/handlers/testdata/apis/testgroup/v2/BUILD.bazel
index 668e6e3b0..e3f04b332 100644
--- a/pkg/webhook/handlers/testdata/apis/testgroup/v2/BUILD.bazel
+++ b/pkg/webhook/handlers/testdata/apis/testgroup/v2/BUILD.bazel
@@ -18,6 +18,7 @@ go_library(
     deps = [
         "//pkg/internal/api/validation:go_default_library",
         "//pkg/webhook/handlers/testdata/apis/testgroup:go_default_library",
+        "@io_k8s_api//admission/v1:go_default_library",
         "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
         "@io_k8s_apimachinery//pkg/conversion:go_default_library",
         "@io_k8s_apimachinery//pkg/runtime:go_default_library",
diff --git a/pkg/webhook/handlers/testdata/apis/testgroup/v2/validation.go b/pkg/webhook/handlers/testdata/apis/testgroup/v2/validation.go
index 92630c838..e25f1bce7 100644
--- a/pkg/webhook/handlers/testdata/apis/testgroup/v2/validation.go
+++ b/pkg/webhook/handlers/testdata/apis/testgroup/v2/validation.go
@@ -17,11 +17,12 @@ limitations under the License.
 package v2
 
 import (
+	admissionv1 "k8s.io/api/admission/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/util/validation/field"
 )
 
-func ValidateTestType(obj runtime.Object) field.ErrorList {
+func ValidateTestType(_ *admissionv1.AdmissionRequest, obj runtime.Object) field.ErrorList {
 	el := field.ErrorList{}
 	tt := obj.(*TestType)
 	if tt.TestField == DisallowedTestFieldValue {
diff --git a/pkg/webhook/handlers/testdata/apis/testgroup/validation/BUILD.bazel b/pkg/webhook/handlers/testdata/apis/testgroup/validation/BUILD.bazel
index ea76bb36a..c0f4b37fa 100644
--- a/pkg/webhook/handlers/testdata/apis/testgroup/validation/BUILD.bazel
+++ b/pkg/webhook/handlers/testdata/apis/testgroup/validation/BUILD.bazel
@@ -12,6 +12,7 @@ go_library(
         "//pkg/internal/api/validation:go_default_library",
         "//pkg/webhook/handlers/testdata/apis/testgroup:go_default_library",
         "//pkg/webhook/handlers/testdata/apis/testgroup/v1:go_default_library",
+        "@io_k8s_api//admission/v1:go_default_library",
         "@io_k8s_apimachinery//pkg/runtime:go_default_library",
         "@io_k8s_apimachinery//pkg/util/validation/field:go_default_library",
     ],
diff --git a/pkg/webhook/handlers/testdata/apis/testgroup/validation/validation.go b/pkg/webhook/handlers/testdata/apis/testgroup/validation/validation.go
index 85b9dc04a..2ae47036b 100644
--- a/pkg/webhook/handlers/testdata/apis/testgroup/validation/validation.go
+++ b/pkg/webhook/handlers/testdata/apis/testgroup/validation/validation.go
@@ -17,6 +17,7 @@ limitations under the License.
 package validation
 
 import (
+	admissionv1 "k8s.io/api/admission/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/util/validation/field"
 
@@ -24,7 +25,7 @@ import (
 	v1 "github.com/jetstack/cert-manager/pkg/webhook/handlers/testdata/apis/testgroup/v1"
 )
 
-func ValidateTestType(obj runtime.Object) field.ErrorList {
+func ValidateTestType(_ *admissionv1.AdmissionRequest, obj runtime.Object) field.ErrorList {
 	testType := obj.(*testgroup.TestType)
 	el := field.ErrorList{}
 	if testType.TestField == v1.TestFieldValueNotAllowed {
@@ -33,7 +34,7 @@ func ValidateTestType(obj runtime.Object) field.ErrorList {
 	return el
 }
 
-func ValidateTestTypeUpdate(oldObj, newObj runtime.Object) field.ErrorList {
+func ValidateTestTypeUpdate(_ *admissionv1.AdmissionRequest, oldObj, newObj runtime.Object) field.ErrorList {
 	old, ok := oldObj.(*testgroup.TestType)
 	new := newObj.(*testgroup.TestType)
 	// if oldObj is not set, the Update operation is always valid.
diff --git a/pkg/webhook/handlers/testdata/apis/testgroup/validation/validation_test.go b/pkg/webhook/handlers/testdata/apis/testgroup/validation/validation_test.go
index 2335c2136..a2642728b 100644
--- a/pkg/webhook/handlers/testdata/apis/testgroup/validation/validation_test.go
+++ b/pkg/webhook/handlers/testdata/apis/testgroup/validation/validation_test.go
@@ -42,7 +42,7 @@ func TestValidateTestType(t *testing.T) {
 	}
 	for n, s := range scenarios {
 		t.Run(n, func(t *testing.T) {
-			errs := ValidateTestType(s.obj)
+			errs := ValidateTestType(nil, s.obj)
 			if len(errs) != len(s.errs) {
 				t.Errorf("Expected %v but got %v", s.errs, errs)
 				return
@@ -74,7 +74,7 @@ func TestValidateTestTypeUpdate(t *testing.T) {
 	}
 	for n, s := range scenarios {
 		t.Run(n, func(t *testing.T) {
-			errs := ValidateTestTypeUpdate(s.old, s.new)
+			errs := ValidateTestTypeUpdate(nil, s.old, s.new)
 			if len(errs) != len(s.errs) {
 				t.Errorf("Expected %v but got %v", s.errs, errs)
 				return
@@ -109,7 +109,7 @@ func testImmutableTestTypeField(t *testing.T, fldPath *field.Path, setter func(*
 		new := &testgroup.TestType{}
 		setter(old, testValueOptionOne)
 		setter(new, testValueOptionTwo)
-		errs := ValidateTestTypeUpdate(old, new)
+		errs := ValidateTestTypeUpdate(nil, old, new)
 		if len(errs) != len(expectedErrs) {
 			t.Errorf("Expected %v but got %v", expectedErrs, errs)
 			return
@@ -127,7 +127,7 @@ func testImmutableTestTypeField(t *testing.T, fldPath *field.Path, setter func(*
 		new := &testgroup.TestType{}
 		setter(old, testValueNone)
 		setter(new, testValueOptionOne)
-		errs := ValidateTestTypeUpdate(old, new)
+		errs := ValidateTestTypeUpdate(nil, old, new)
 		if len(errs) != len(expectedErrs) {
 			t.Errorf("Expected %v but got %v", expectedErrs, errs)
 			return
diff --git a/test/e2e/suite/BUILD.bazel b/test/e2e/suite/BUILD.bazel
index 7505f0995..6067bac9b 100644
--- a/test/e2e/suite/BUILD.bazel
+++ b/test/e2e/suite/BUILD.bazel
@@ -8,6 +8,7 @@ go_library(
     visibility = ["//visibility:public"],
     deps = [
         "//test/e2e/suite/conformance:go_default_library",
+        "//test/e2e/suite/identity:go_default_library",
         "//test/e2e/suite/issuers:go_default_library",
         "//test/e2e/suite/serving:go_default_library",
     ],
@@ -25,6 +26,7 @@ filegroup(
     srcs = [
         ":package-srcs",
         "//test/e2e/suite/conformance:all-srcs",
+        "//test/e2e/suite/identity:all-srcs",
         "//test/e2e/suite/issuers:all-srcs",
         "//test/e2e/suite/serving:all-srcs",
     ],
diff --git a/test/e2e/suite/doc.go b/test/e2e/suite/doc.go
index 67cd044aa..9de32833b 100644
--- a/test/e2e/suite/doc.go
+++ b/test/e2e/suite/doc.go
@@ -18,6 +18,7 @@ package suite
 
 import (
 	_ "github.com/jetstack/cert-manager/test/e2e/suite/conformance"
+	_ "github.com/jetstack/cert-manager/test/e2e/suite/identity"
 	_ "github.com/jetstack/cert-manager/test/e2e/suite/issuers"
 	_ "github.com/jetstack/cert-manager/test/e2e/suite/serving"
 )
diff --git a/test/e2e/suite/identity/BUILD.bazel b/test/e2e/suite/identity/BUILD.bazel
new file mode 100644
index 000000000..245f11886
--- /dev/null
+++ b/test/e2e/suite/identity/BUILD.bazel
@@ -0,0 +1,36 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["certificaterequests.go"],
+    importpath = "github.com/jetstack/cert-manager/test/e2e/suite/identity",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//pkg/apis/meta/v1:go_default_library",
+        "//pkg/client/clientset/versioned:go_default_library",
+        "//pkg/util:go_default_library",
+        "//test/e2e/framework:go_default_library",
+        "//test/e2e/framework/util:go_default_library",
+        "//test/unit/gen:go_default_library",
+        "@com_github_onsi_ginkgo//:go_default_library",
+        "@com_github_onsi_gomega//:go_default_library",
+        "@io_k8s_api//core/v1:go_default_library",
+        "@io_k8s_api//rbac/v1:go_default_library",
+        "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
+        "@io_k8s_apimachinery//pkg/util/wait:go_default_library",
+    ],
+)
+
+filegroup(
+    name = "package-srcs",
+    srcs = glob(["**"]),
+    tags = ["automanaged"],
+    visibility = ["//visibility:private"],
+)
+
+filegroup(
+    name = "all-srcs",
+    srcs = [":package-srcs"],
+    tags = ["automanaged"],
+    visibility = ["//visibility:public"],
+)
diff --git a/test/e2e/suite/identity/certificaterequests.go b/test/e2e/suite/identity/certificaterequests.go
new file mode 100644
index 000000000..61e49f4a7
--- /dev/null
+++ b/test/e2e/suite/identity/certificaterequests.go
@@ -0,0 +1,195 @@
+/*
+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 certificaterequests
+
+import (
+	"context"
+	"crypto/x509"
+	"fmt"
+	"time"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
+	rbacv1 "k8s.io/api/rbac/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/wait"
+
+	cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1"
+	clientset "github.com/jetstack/cert-manager/pkg/client/clientset/versioned"
+	"github.com/jetstack/cert-manager/pkg/util"
+	"github.com/jetstack/cert-manager/test/e2e/framework"
+	testutil "github.com/jetstack/cert-manager/test/e2e/framework/util"
+	"github.com/jetstack/cert-manager/test/unit/gen"
+)
+
+// Check that the identity fields on CertificateRequests are populated
+// correctly, and they cannot be modified.
+var _ = framework.CertManagerDescribe("Identity CertificateRequests", func() {
+	f := framework.NewDefaultFramework("identity-certificaterequests")
+
+	It("should appropriately create set identity of CertificateRequests, and reject changes", func() {
+		var (
+			adminUsername = "kubernetes-admin"
+			adminGroups   = []string{"system:masters", "system:authenticated"}
+		)
+		csr, _, err := gen.CSR(x509.RSA)
+		Expect(err).NotTo(HaveOccurred())
+
+		cr := gen.CertificateRequest("test-v1",
+			gen.SetCertificateRequestNamespace(f.Namespace.Name),
+			gen.SetCertificateRequestCSR(csr),
+			gen.SetCertificateRequestIssuer(cmmeta.ObjectReference{
+				Name: "issuer",
+			}),
+		)
+
+		By("Creating CertificateRequest")
+		cr, err = f.CertManagerClientSet.CertmanagerV1().CertificateRequests(f.Namespace.Name).Create(context.TODO(), cr, metav1.CreateOptions{})
+		Expect(err).NotTo(HaveOccurred())
+
+		By("Ensure identity fields are set")
+		if cr.Spec.Username != adminUsername {
+			Fail(fmt.Sprintf("Unexpected username in CertificateRequest, exp=%s got=%s", adminUsername, cr.Spec.Username))
+		}
+		if !util.EqualUnsorted(cr.Spec.Groups, adminGroups) {
+			Fail(fmt.Sprintf("Unexpected groups in CertificateRequest, exp=%s got=%s", adminGroups, cr.Spec.Groups))
+		}
+
+		By("Should error when attempting to update identity fields")
+		cr.Spec.Username = "abc"
+		cr.Spec.UID = "123"
+		cr, err = f.CertManagerClientSet.CertmanagerV1().CertificateRequests(f.Namespace.Name).Update(context.TODO(), cr, metav1.UpdateOptions{})
+		Expect(err).To(HaveOccurred())
+	})
+
+	It("should populate identity with ServiceAccount if is the requester", func() {
+		By("Creating ServiceAccount")
+		sa, err := f.KubeClientSet.CoreV1().ServiceAccounts(f.Namespace.Name).Create(context.TODO(), &corev1.ServiceAccount{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "test-sa",
+				Namespace: f.Namespace.Name,
+			},
+		}, metav1.CreateOptions{})
+		Expect(err).NotTo(HaveOccurred())
+
+		By("Creating certificaterequest-creator role")
+		role, err := f.KubeClientSet.RbacV1().Roles(f.Namespace.Name).Create(context.TODO(), &rbacv1.Role{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "certificaterequest-creator",
+				Namespace: f.Namespace.Name,
+			},
+			Rules: []rbacv1.PolicyRule{
+				{
+					Verbs:     []string{"create"},
+					APIGroups: []string{"cert-manager.io"},
+					Resources: []string{"certificaterequests"},
+				},
+			},
+		}, metav1.CreateOptions{})
+		Expect(err).NotTo(HaveOccurred())
+
+		By("Creating certificaterequest-creator rolebinding for ServiceAccount")
+		_, err = f.KubeClientSet.RbacV1().RoleBindings(f.Namespace.Name).Create(context.TODO(), &rbacv1.RoleBinding{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "certificaterequest-creator",
+				Namespace: f.Namespace.Name,
+			},
+			Subjects: []rbacv1.Subject{
+				{
+					Kind:      "ServiceAccount",
+					Name:      sa.Name,
+					Namespace: f.Namespace.Name,
+				},
+			},
+			RoleRef: rbacv1.RoleRef{
+				APIGroup: "rbac.authorization.k8s.io",
+				Kind:     "Role",
+				Name:     role.Name,
+			},
+		}, metav1.CreateOptions{})
+		Expect(err).NotTo(HaveOccurred())
+
+		By("Waiting for service account secret to be created")
+		err = wait.PollImmediate(time.Second, time.Second*10,
+			func() (bool, error) {
+				sa, err = f.KubeClientSet.CoreV1().ServiceAccounts(f.Namespace.Name).Get(context.TODO(), sa.Name, metav1.GetOptions{})
+				if err != nil {
+					return false, err
+				}
+
+				if len(sa.Secrets) == 0 {
+					return false, nil
+				}
+
+				return true, nil
+			},
+		)
+		Expect(err).NotTo(HaveOccurred())
+
+		By("Building ServiceAccount kubernetes clientset")
+		sec, err := f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name).Get(context.TODO(), sa.Secrets[0].Name, metav1.GetOptions{})
+		Expect(err).NotTo(HaveOccurred())
+
+		kubeConfig, err := testutil.LoadConfig(f.Config.KubeConfig, f.Config.KubeContext)
+		Expect(err).NotTo(HaveOccurred())
+
+		kubeConfig.BearerToken = fmt.Sprintf("%s", sec.Data["token"])
+		kubeConfig.CertData = nil
+		kubeConfig.KeyData = nil
+
+		client, err := clientset.NewForConfig(kubeConfig)
+		Expect(err).NotTo(HaveOccurred())
+
+		csr, _, err := gen.CSR(x509.RSA)
+		Expect(err).NotTo(HaveOccurred())
+
+		cr := gen.CertificateRequest("test-v1",
+			gen.SetCertificateRequestNamespace(f.Namespace.Name),
+			gen.SetCertificateRequestCSR(csr),
+			gen.SetCertificateRequestIssuer(cmmeta.ObjectReference{
+				Name: "issuer",
+			}),
+		)
+
+		By("Creating CertificateRequest")
+		cr, err = client.CertmanagerV1().CertificateRequests(f.Namespace.Name).Create(context.TODO(), cr, metav1.CreateOptions{})
+		Expect(err).NotTo(HaveOccurred())
+
+		fmt.Printf("%s %s %s %s", cr.Spec.UID, cr.Spec.Username, cr.Spec.Groups, cr.Spec.Extra)
+
+		expUsername := fmt.Sprintf("system:serviceaccount:%s:%s", f.Namespace.Name, sa.Name)
+		expGroups := []string{
+			"system:serviceaccounts",
+			"system:authenticated",
+			fmt.Sprintf("system:serviceaccounts:%s", f.Namespace.Name),
+		}
+		By("Ensure identity fields are set")
+		if cr.Spec.UID != string(sa.UID) {
+			Fail(fmt.Sprintf("Unexpected UID in CertificateRequest, exp=%s got=%s", sa.UID, cr.Spec.UID))
+		}
+		if cr.Spec.Username != expUsername {
+			Fail(fmt.Sprintf("Unexpected username in CertificateRequest, exp=%s got=%s", expUsername, cr.Spec.Username))
+		}
+		if !util.EqualUnsorted(cr.Spec.Groups, expGroups) {
+			Fail(fmt.Sprintf("Unexpected groups in CertificateRequest, exp=%s got=%s", expGroups, cr.Spec.Groups))
+		}
+		if len(cr.Spec.Extra) > 0 {
+			Fail(fmt.Sprintf("Unexpected extra in CertificateRequest, exp=nil got=%s", cr.Spec.Groups))
+		}
+	})
+})
diff --git a/test/integration/validation/certificaterequest_test.go b/test/integration/validation/certificaterequest_test.go
index c797bcc4b..934ae5201 100644
--- a/test/integration/validation/certificaterequest_test.go
+++ b/test/integration/validation/certificaterequest_test.go
@@ -28,7 +28,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	"github.com/jetstack/cert-manager/pkg/api"
-	v1 "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
+	cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
 	cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1"
 	"github.com/jetstack/cert-manager/pkg/util/pki"
 	"github.com/jetstack/cert-manager/test/integration/framework"
@@ -47,53 +47,53 @@ func TestValidationCertificateRequests(t *testing.T) {
 		expectError bool
 	}{
 		"No errors on valid certificaterequest with no usages set": {
-			input: &v1.CertificateRequest{
+			input: &cmapi.CertificateRequest{
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test",
 					Namespace: "default",
 				},
-				Spec: v1.CertificateRequestSpec{
-					Request: mustGenerateCSR(t, &v1.Certificate{
-						Spec: v1.CertificateSpec{
+				Spec: cmapi.CertificateRequestSpec{
+					Request: mustGenerateCSR(t, &cmapi.Certificate{
+						Spec: cmapi.CertificateSpec{
 							DNSNames: []string{"example.com"},
 						},
 					}),
-					Usages:    []v1.KeyUsage{},
+					Usages:    []cmapi.KeyUsage{},
 					IssuerRef: cmmeta.ObjectReference{Name: "test"},
 				},
 			},
 			expectError: false,
 		},
 		"No errors on valid certificaterequest with special usages set": {
-			input: &v1.CertificateRequest{
+			input: &cmapi.CertificateRequest{
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test",
 					Namespace: "default",
 				},
-				Spec: v1.CertificateRequestSpec{
-					Request: mustGenerateCSR(t, &v1.Certificate{
-						Spec: v1.CertificateSpec{
+				Spec: cmapi.CertificateRequestSpec{
+					Request: mustGenerateCSR(t, &cmapi.Certificate{
+						Spec: cmapi.CertificateSpec{
 							DNSNames: []string{"example.com"},
-							Usages:   []v1.KeyUsage{v1.UsageDigitalSignature, v1.UsageKeyEncipherment, v1.UsageClientAuth},
+							Usages:   []cmapi.KeyUsage{cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment, cmapi.UsageClientAuth},
 						},
 					}),
-					Usages:    []v1.KeyUsage{v1.UsageDigitalSignature, v1.UsageKeyEncipherment, v1.UsageClientAuth},
+					Usages:    []cmapi.KeyUsage{cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment, cmapi.UsageClientAuth},
 					IssuerRef: cmmeta.ObjectReference{Name: "test"},
 				},
 			},
 			expectError: false,
 		},
 		"No errors on valid certificaterequest with special usages set only in CSR": {
-			input: &v1.CertificateRequest{
+			input: &cmapi.CertificateRequest{
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test",
 					Namespace: "default",
 				},
-				Spec: v1.CertificateRequestSpec{
-					Request: mustGenerateCSR(t, &v1.Certificate{
-						Spec: v1.CertificateSpec{
+				Spec: cmapi.CertificateRequestSpec{
+					Request: mustGenerateCSR(t, &cmapi.Certificate{
+						Spec: cmapi.CertificateSpec{
 							DNSNames: []string{"example.com"},
-							Usages:   []v1.KeyUsage{v1.UsageDigitalSignature, v1.UsageKeyEncipherment, v1.UsageClientAuth},
+							Usages:   []cmapi.KeyUsage{cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment, cmapi.UsageClientAuth},
 						},
 					}),
 					IssuerRef: cmmeta.ObjectReference{Name: "test"},
@@ -102,48 +102,69 @@ func TestValidationCertificateRequests(t *testing.T) {
 			expectError: false,
 		},
 		"No errors on valid certificaterequest with special usages only set in spec": {
-			input: &v1.CertificateRequest{
+			input: &cmapi.CertificateRequest{
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test",
 					Namespace: "default",
 				},
-				Spec: v1.CertificateRequestSpec{
-					Request: mustGenerateCSR(t, &v1.Certificate{
-						Spec: v1.CertificateSpec{
+				Spec: cmapi.CertificateRequestSpec{
+					Request: mustGenerateCSR(t, &cmapi.Certificate{
+						Spec: cmapi.CertificateSpec{
 							DNSNames: []string{"example.com"},
-							Usages:   []v1.KeyUsage{},
+							Usages:   []cmapi.KeyUsage{},
 						},
 					}),
-					Usages:    []v1.KeyUsage{v1.UsageDigitalSignature, v1.UsageKeyEncipherment, v1.UsageClientAuth},
+					Usages:    []cmapi.KeyUsage{cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment, cmapi.UsageClientAuth},
 					IssuerRef: cmmeta.ObjectReference{Name: "test"},
 				},
 			},
 			expectError: false,
 		},
 		"Errors on certificaterequest with mismatch of usages": {
-			input: &v1.CertificateRequest{
+			input: &cmapi.CertificateRequest{
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test",
 					Namespace: "default",
 				},
-				Spec: v1.CertificateRequestSpec{
-					Request: mustGenerateCSR(t, &v1.Certificate{
-						Spec: v1.CertificateSpec{
+				Spec: cmapi.CertificateRequestSpec{
+					Request: mustGenerateCSR(t, &cmapi.Certificate{
+						Spec: cmapi.CertificateSpec{
 							DNSNames: []string{"example.com"},
-							Usages:   []v1.KeyUsage{v1.UsageDigitalSignature, v1.UsageKeyEncipherment, v1.UsageClientAuth},
+							Usages:   []cmapi.KeyUsage{cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment, cmapi.UsageClientAuth},
 						},
 					}),
-					Usages:    []v1.KeyUsage{v1.UsageDigitalSignature, v1.UsageKeyEncipherment, v1.UsageCodeSigning},
+					Usages:    []cmapi.KeyUsage{cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment, cmapi.UsageCodeSigning},
 					IssuerRef: cmmeta.ObjectReference{Name: "test"},
 				},
 			},
 			expectError: true,
 			errorSuffix: "csr key usages do not match specified usages, these should match if both are set: [[2]: \"client auth\" != \"code signing\"]",
 		},
+		"Shouldn't error when setting user info, since this will be overwritten by the mutating webhook": {
+			input: &cmapi.CertificateRequest{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test",
+					Namespace: "default",
+				},
+				Spec: cmapi.CertificateRequestSpec{
+					Request: mustGenerateCSR(t, &cmapi.Certificate{
+						Spec: cmapi.CertificateSpec{
+							DNSNames: []string{"example.com"},
+							Usages:   []cmapi.KeyUsage{},
+						},
+					}),
+					Usages:    []cmapi.KeyUsage{cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment, cmapi.UsageClientAuth},
+					IssuerRef: cmmeta.ObjectReference{Name: "test"},
+					Username:  "user-1",
+					Groups:    []string{"group-1", "group-2"},
+				},
+			},
+			expectError: false,
+		},
 	}
 	for name, test := range tests {
 		t.Run(name, func(t *testing.T) {
-			cert := test.input.(*v1.CertificateRequest)
+			cert := test.input.(*cmapi.CertificateRequest)
 			cert.SetGroupVersionKind(certGVK)
 
 			config, stop := framework.RunControlPlane(t)
@@ -157,20 +178,19 @@ func TestValidationCertificateRequests(t *testing.T) {
 			}
 
 			err = cl.Create(context.Background(), cert)
-
-			if !test.expectError && err != nil {
-				t.Fatalf("Didn't expect error and got error: %v", err)
-			} else if test.expectError && err == nil {
-				t.Errorf("Expected error %v but got nil", test.errorSuffix)
-
-			} else if test.expectError && !strings.HasSuffix(err.Error(), test.errorSuffix) {
-				t.Errorf("Expected error %q but got %q", test.errorSuffix, err)
+			if test.expectError != (err != nil) {
+				t.Errorf("unexpected error, exp=%t got=%v",
+					test.expectError, err)
+			}
+			if test.expectError && !strings.HasSuffix(err.Error(), test.errorSuffix) {
+				t.Errorf("unexpected error suffix, exp=%s got=%s",
+					test.errorSuffix, err)
 			}
 		})
 	}
 }
 
-func mustGenerateCSR(t *testing.T, cert *v1.Certificate) []byte {
+func mustGenerateCSR(t *testing.T, cert *cmapi.Certificate) []byte {
 	request, err := pki.GenerateCSR(cert)
 	if err != nil {
 		t.Fatal(err)
diff --git a/test/unit/gen/certificaterequest.go b/test/unit/gen/certificaterequest.go
index 707ea6664..a24fbd0fe 100644
--- a/test/unit/gen/certificaterequest.go
+++ b/test/unit/gen/certificaterequest.go
@@ -159,3 +159,21 @@ func SetCertificateRequestFailureTime(p metav1.Time) CertificateRequestModifier
 		cr.Status.FailureTime = &p
 	}
 }
+
+func SetCertificateRequestTypeMeta(tm metav1.TypeMeta) CertificateRequestModifier {
+	return func(cr *v1.CertificateRequest) {
+		cr.TypeMeta = tm
+	}
+}
+
+func SetCertificateRequestUsername(username string) CertificateRequestModifier {
+	return func(cr *v1.CertificateRequest) {
+		cr.Spec.Username = username
+	}
+}
+
+func SetCertificateRequestGroups(groups []string) CertificateRequestModifier {
+	return func(cr *v1.CertificateRequest) {
+		cr.Spec.Groups = groups
+	}
+}