Repository: jetstack/cert-manager
Pull request #4064 (2021-05-26 23:42:45)

Certificate Signing Request Issuer CA
**What this PR does / why we need it**:
This PR adds an _optional_ CertificateSigningRequest CA issuer. The controller is only enabled if it is explicitly done so via the `--feature-gates` flag (i.e. `--feature-gates=ExperimentalCertificateSigningRequestControllers=true`)

This begins the work to integrating kubernetes CertificateSigningRequests as part of cert-manager. (ref #3646).

Users who request a certificate referencing a namespaced issuer, must have permissions to do so in the following form:

```
group: cert-manager.io
resource: signers
verb: reference
namespace: <referenced signer namespace>
name: <either the name of the signer or '*' for all signer names in that namespace>
```

A SubjectAccessReview is performed during processing to check whether the user has sufficient permissions to reference the Issuer encoded into the signer name.

**Special notes for your reviewer**:

This work is based on the design in #3646 

**Release note**:
<!--  Steps to write your release note:
1. Use the release-note-* labels to set the release note state (if you have access)
2. Enter your extended release note in the below block; leaving it blank means using the PR title as the release note. If no release note is required, just write `NONE`.
-->
```release-note
Adds an option for a Kubernetes CertificateSigningRequest controller to implement the CA Issuer.
```

/cc @munnerz 


Patch:
diff --git a/cmd/controller/app/options/BUILD.bazel b/cmd/controller/app/options/BUILD.bazel
index 821ac27bd..fb0228d0d 100644
--- a/cmd/controller/app/options/BUILD.bazel
+++ b/cmd/controller/app/options/BUILD.bazel
@@ -22,10 +22,14 @@ go_library(
         "//pkg/controller/certificates/requestmanager:go_default_library",
         "//pkg/controller/certificates/revisionmanager:go_default_library",
         "//pkg/controller/certificates/trigger:go_default_library",
+        "//pkg/controller/certificatesigningrequests/ca:go_default_library",
         "//pkg/controller/clusterissuers:go_default_library",
         "//pkg/controller/ingress-shim:go_default_library",
         "//pkg/controller/issuers:go_default_library",
+        "//pkg/feature:go_default_library",
+        "//pkg/logs:go_default_library",
         "//pkg/util:go_default_library",
+        "//pkg/util/feature:go_default_library",
         "@com_github_spf13_pflag//:go_default_library",
         "@io_k8s_apimachinery//pkg/util/sets:go_default_library",
     ],
diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go
index be40e9661..628e99249 100644
--- a/cmd/controller/app/options/options.go
+++ b/cmd/controller/app/options/options.go
@@ -41,10 +41,14 @@ import (
 	"github.com/jetstack/cert-manager/pkg/controller/certificates/requestmanager"
 	"github.com/jetstack/cert-manager/pkg/controller/certificates/revisionmanager"
 	"github.com/jetstack/cert-manager/pkg/controller/certificates/trigger"
+	csrcacontroller "github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests/ca"
 	clusterissuerscontroller "github.com/jetstack/cert-manager/pkg/controller/clusterissuers"
 	ingressshimcontroller "github.com/jetstack/cert-manager/pkg/controller/ingress-shim"
 	issuerscontroller "github.com/jetstack/cert-manager/pkg/controller/issuers"
+	"github.com/jetstack/cert-manager/pkg/feature"
+	logf "github.com/jetstack/cert-manager/pkg/logs"
 	"github.com/jetstack/cert-manager/pkg/util"
+	utilfeature "github.com/jetstack/cert-manager/pkg/util/feature"
 )
 
 type ControllerOptions struct {
@@ -162,7 +166,31 @@ var (
 		revisionmanager.ControllerName,
 	}
 
-	defaultEnabledControllers = []string{"*"}
+	defaultEnabledControllers = []string{
+		issuerscontroller.ControllerName,
+		clusterissuerscontroller.ControllerName,
+		certificatesmetricscontroller.ControllerName,
+		ingressshimcontroller.ControllerName,
+		orderscontroller.ControllerName,
+		challengescontroller.ControllerName,
+		cracmecontroller.CRControllerName,
+		crapprovercontroller.ControllerName,
+		crcacontroller.CRControllerName,
+		crselfsignedcontroller.CRControllerName,
+		crvaultcontroller.CRControllerName,
+		crvenaficontroller.CRControllerName,
+		// certificate controllers
+		trigger.ControllerName,
+		issuing.ControllerName,
+		keymanager.ControllerName,
+		requestmanager.ControllerName,
+		readiness.ControllerName,
+		revisionmanager.ControllerName,
+	}
+
+	experimentalCertificateSigningRequestControllers = []string{
+		csrcacontroller.CSRControllerName,
+	}
 )
 
 func NewControllerOptions() *ControllerOptions {
@@ -226,7 +254,7 @@ func (s *ControllerOptions) AddFlags(fs *pflag.FlagSet) {
 		"The duration the clients should wait between attempting acquisition and renewal "+
 		"of a leadership. This is only applicable if leader election is enabled.")
 
-	fs.StringSliceVar(&s.controllers, "controllers", defaultEnabledControllers, fmt.Sprintf(""+
+	fs.StringSliceVar(&s.controllers, "controllers", []string{"*"}, fmt.Sprintf(""+
 		"A list of controllers to enable. '--controllers=*' enables all "+
 		"on-by-default controllers, '--controllers=foo' enables just the controller "+
 		"named 'foo', '--controllers=*,-foo' disables the controller named "+
@@ -352,7 +380,7 @@ func (o *ControllerOptions) EnabledControllers() sets.String {
 	for _, controller := range o.controllers {
 		switch {
 		case controller == "*":
-			enabled = enabled.Insert(allControllers...)
+			enabled = enabled.Insert(defaultEnabledControllers...)
 		case strings.HasPrefix(controller, "-"):
 			disabled = append(disabled, strings.TrimPrefix(controller, "-"))
 		default:
@@ -362,5 +390,10 @@ func (o *ControllerOptions) EnabledControllers() sets.String {
 
 	enabled = enabled.Delete(disabled...)
 
+	if utilfeature.DefaultFeatureGate.Enabled(feature.ExperimentalCertificateSigningRequestControllers) {
+		logf.Log.Info("enabling all experimental certificatesigningrequest controllers")
+		enabled = enabled.Insert(experimentalCertificateSigningRequestControllers...)
+	}
+
 	return enabled
 }
diff --git a/deploy/charts/cert-manager/templates/rbac.yaml b/deploy/charts/cert-manager/templates/rbac.yaml
index 07f74a4de..0618ee06b 100644
--- a/deploy/charts/cert-manager/templates/rbac.yaml
+++ b/deploy/charts/cert-manager/templates/rbac.yaml
@@ -504,4 +504,57 @@ subjects:
     namespace: {{ .Release.Namespace | quote }}
     kind: ServiceAccount
 
+---
+
+# Permission to:
+# - Update and sign CertificatSigningeRequests referencing cert-manager.io Issuers and ClusterIssuers
+# - Perform SubjectAccessReviews to test whether users are able to reference Namespaced Issuers
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: {{ template "cert-manager.fullname" . }}-controller-certificatesigningrequests
+  labels:
+    app: {{ include "cert-manager.name" . }}
+    app.kubernetes.io/name: {{ include "cert-manager.name" . }}
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    app.kubernetes.io/managed-by: {{ .Release.Service }}
+    app.kubernetes.io/component: "cert-manager"
+    helm.sh/chart: {{ include "cert-manager.chart" . }}
+rules:
+  - apiGroups: ["certificates.k8s.io"]
+    resources: ["certificatesigningrequests"]
+    verbs: ["get", "list", "watch", "update"]
+  - apiGroups: ["certificates.k8s.io"]
+    resources: ["certificatesigningrequests/status"]
+    verbs: ["update"]
+  - apiGroups: ["certificates.k8s.io"]
+    resources: ["signers"]
+    resourceNames: ["issuers.cert-manager.io/*", "clusterissuers.cert-manager.io/*"]
+    verbs: ["sign"]
+  - apiGroups: ["authorization.k8s.io"]
+    resources: ["subjectaccessreviews"]
+    verbs: ["create"]
+
+---
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: {{ template "cert-manager.fullname" . }}-controller-certificatesigningrequests
+  labels:
+    app: {{ include "cert-manager.name" . }}
+    app.kubernetes.io/name: {{ include "cert-manager.name" . }}
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    app.kubernetes.io/managed-by: {{ .Release.Service }}
+    app.kubernetes.io/component: "cert-manager"
+    helm.sh/chart: {{ include "cert-manager.chart" . }}
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: {{ template "cert-manager.fullname" . }}-controller-certificatesigningrequests
+subjects:
+  - name: {{ template "cert-manager.serviceAccountName" . }}
+    namespace: {{ .Release.Namespace | quote }}
+    kind: ServiceAccount
+
 {{- end }}
diff --git a/pkg/api/util/BUILD.bazel b/pkg/api/util/BUILD.bazel
index dc14963d5..c0a591829 100644
--- a/pkg/api/util/BUILD.bazel
+++ b/pkg/api/util/BUILD.bazel
@@ -6,6 +6,7 @@ go_library(
         "conditions.go",
         "duration.go",
         "issuers.go",
+        "kube.go",
         "names.go",
         "usages.go",
     ],
@@ -15,6 +16,7 @@ go_library(
         "//pkg/apis/certmanager/v1:go_default_library",
         "//pkg/apis/meta/v1:go_default_library",
         "//pkg/logs:go_default_library",
+        "@io_k8s_api//certificates/v1:go_default_library",
         "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
         "@io_k8s_utils//clock:go_default_library",
     ],
diff --git a/pkg/api/util/kube.go b/pkg/api/util/kube.go
new file mode 100644
index 000000000..67414f395
--- /dev/null
+++ b/pkg/api/util/kube.go
@@ -0,0 +1,64 @@
+/*
+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 util
+
+import (
+	"crypto/x509"
+
+	certificatesv1 "k8s.io/api/certificates/v1"
+)
+
+var keyUsagesKube = map[certificatesv1.KeyUsage]x509.KeyUsage{
+	certificatesv1.UsageSigning:           x509.KeyUsageDigitalSignature,
+	certificatesv1.UsageDigitalSignature:  x509.KeyUsageDigitalSignature,
+	certificatesv1.UsageContentCommitment: x509.KeyUsageContentCommitment,
+	certificatesv1.UsageKeyEncipherment:   x509.KeyUsageKeyEncipherment,
+	certificatesv1.UsageKeyAgreement:      x509.KeyUsageKeyAgreement,
+	certificatesv1.UsageDataEncipherment:  x509.KeyUsageDataEncipherment,
+	certificatesv1.UsageCertSign:          x509.KeyUsageCertSign,
+	certificatesv1.UsageCRLSign:           x509.KeyUsageCRLSign,
+	certificatesv1.UsageEncipherOnly:      x509.KeyUsageEncipherOnly,
+	certificatesv1.UsageDecipherOnly:      x509.KeyUsageDecipherOnly,
+}
+
+var extKeyUsagesKube = map[certificatesv1.KeyUsage]x509.ExtKeyUsage{
+	certificatesv1.UsageAny:             x509.ExtKeyUsageAny,
+	certificatesv1.UsageServerAuth:      x509.ExtKeyUsageServerAuth,
+	certificatesv1.UsageClientAuth:      x509.ExtKeyUsageClientAuth,
+	certificatesv1.UsageCodeSigning:     x509.ExtKeyUsageCodeSigning,
+	certificatesv1.UsageEmailProtection: x509.ExtKeyUsageEmailProtection,
+	certificatesv1.UsageSMIME:           x509.ExtKeyUsageEmailProtection,
+	certificatesv1.UsageIPsecEndSystem:  x509.ExtKeyUsageIPSECEndSystem,
+	certificatesv1.UsageIPsecTunnel:     x509.ExtKeyUsageIPSECTunnel,
+	certificatesv1.UsageIPsecUser:       x509.ExtKeyUsageIPSECUser,
+	certificatesv1.UsageTimestamping:    x509.ExtKeyUsageTimeStamping,
+	certificatesv1.UsageOCSPSigning:     x509.ExtKeyUsageOCSPSigning,
+	certificatesv1.UsageMicrosoftSGC:    x509.ExtKeyUsageMicrosoftServerGatedCrypto,
+	certificatesv1.UsageNetscapeSGC:     x509.ExtKeyUsageNetscapeServerGatedCrypto,
+}
+
+// KeyUsageTypeKube returns the relevant x509.KeyUsage or false if not found
+func KeyUsageTypeKube(usage certificatesv1.KeyUsage) (x509.KeyUsage, bool) {
+	u, ok := keyUsagesKube[usage]
+	return u, ok
+}
+
+// ExtKeyUsageTypeKube returns the relevant x509.KeyUsage or false if not found
+func ExtKeyUsageTypeKube(usage certificatesv1.KeyUsage) (x509.ExtKeyUsage, bool) {
+	eu, ok := extKeyUsagesKube[usage]
+	return eu, ok
+}
diff --git a/pkg/apis/BUILD.bazel b/pkg/apis/BUILD.bazel
index 3f8faaffb..f045e68a8 100644
--- a/pkg/apis/BUILD.bazel
+++ b/pkg/apis/BUILD.bazel
@@ -20,6 +20,7 @@ filegroup(
         ":package-srcs",
         "//pkg/apis/acme:all-srcs",
         "//pkg/apis/certmanager:all-srcs",
+        "//pkg/apis/experimental:all-srcs",
         "//pkg/apis/meta:all-srcs",
     ],
     tags = ["automanaged"],
diff --git a/pkg/apis/experimental/BUILD.bazel b/pkg/apis/experimental/BUILD.bazel
new file mode 100644
index 000000000..074f339c1
--- /dev/null
+++ b/pkg/apis/experimental/BUILD.bazel
@@ -0,0 +1,25 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["doc.go"],
+    importpath = "github.com/jetstack/cert-manager/pkg/apis/experimental",
+    visibility = ["//visibility:public"],
+)
+
+filegroup(
+    name = "package-srcs",
+    srcs = glob(["**"]),
+    tags = ["automanaged"],
+    visibility = ["//visibility:private"],
+)
+
+filegroup(
+    name = "all-srcs",
+    srcs = [
+        ":package-srcs",
+        "//pkg/apis/experimental/v1alpha1:all-srcs",
+    ],
+    tags = ["automanaged"],
+    visibility = ["//visibility:public"],
+)
diff --git a/pkg/apis/experimental/doc.go b/pkg/apis/experimental/doc.go
new file mode 100644
index 000000000..bb35d8d32
--- /dev/null
+++ b/pkg/apis/experimental/doc.go
@@ -0,0 +1,23 @@
+/*
+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.
+*/
+
+// +groupName=experimental.cert-manager.io
+// +groupGoName=Experimental
+
+// Package experimental contains the group containing experimental APIs.
+package experimental
+
+const GroupName = "experimental.cert-manager.io"
diff --git a/pkg/apis/experimental/v1alpha1/BUILD.bazel b/pkg/apis/experimental/v1alpha1/BUILD.bazel
new file mode 100644
index 000000000..f08b32214
--- /dev/null
+++ b/pkg/apis/experimental/v1alpha1/BUILD.bazel
@@ -0,0 +1,22 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["types.go"],
+    importpath = "github.com/jetstack/cert-manager/pkg/apis/experimental/v1alpha1",
+    visibility = ["//visibility:public"],
+)
+
+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/pkg/apis/experimental/v1alpha1/types.go b/pkg/apis/experimental/v1alpha1/types.go
new file mode 100644
index 000000000..d4ec0bb0c
--- /dev/null
+++ b/pkg/apis/experimental/v1alpha1/types.go
@@ -0,0 +1,33 @@
+/*
+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 v1alpha1
+
+// CertificateSigningRequest specific Annotations
+const (
+	// CertificateSigningRequestDurationAnnotationKey is the
+	// annotation key used to request a particular duration
+	// represented as a Go Duration.
+	CertificateSigningRequestDurationAnnotationKey = "experimental.cert-manager.io/request-duration"
+
+	// CertificateSigningRequestIsCAAnnotationKey is the annotation key used to
+	// request whether the certificate should be marked as CA.
+	CertificateSigningRequestIsCAAnnotationKey = "experimental.cert-manager.io/request-is-ca"
+
+	// CertificateSigningRequestCAAnnotationKey is the annotation key which will
+	// contain the base 64 encoded resulting CA certificate which signed the CSR.
+	CertificateSigningRequestCAAnnotationKey = "experimental.cert-manager.io/ca"
+)
diff --git a/pkg/controller/BUILD.bazel b/pkg/controller/BUILD.bazel
index f4d75cd6d..e4c431c12 100644
--- a/pkg/controller/BUILD.bazel
+++ b/pkg/controller/BUILD.bazel
@@ -52,6 +52,7 @@ filegroup(
         "//pkg/controller/cainjector:all-srcs",
         "//pkg/controller/certificaterequests:all-srcs",
         "//pkg/controller/certificates:all-srcs",
+        "//pkg/controller/certificatesigningrequests:all-srcs",
         "//pkg/controller/clusterissuers:all-srcs",
         "//pkg/controller/ingress-shim:all-srcs",
         "//pkg/controller/issuers:all-srcs",
diff --git a/pkg/controller/certificatesigningrequests/BUILD.bazel b/pkg/controller/certificatesigningrequests/BUILD.bazel
new file mode 100644
index 000000000..652db98b6
--- /dev/null
+++ b/pkg/controller/certificatesigningrequests/BUILD.bazel
@@ -0,0 +1,78 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        "checks.go",
+        "controller.go",
+        "sync.go",
+    ],
+    importpath = "github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//pkg/api/util:go_default_library",
+        "//pkg/apis/certmanager:go_default_library",
+        "//pkg/apis/certmanager/v1:go_default_library",
+        "//pkg/apis/meta/v1:go_default_library",
+        "//pkg/controller:go_default_library",
+        "//pkg/controller/certificatesigningrequests/util:go_default_library",
+        "//pkg/issuer:go_default_library",
+        "//pkg/logs:go_default_library",
+        "@com_github_go_logr_logr//:go_default_library",
+        "@io_k8s_api//authorization/v1:go_default_library",
+        "@io_k8s_api//certificates/v1:go_default_library",
+        "@io_k8s_api//core/v1:go_default_library",
+        "@io_k8s_apimachinery//pkg/api/errors:go_default_library",
+        "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
+        "@io_k8s_apimachinery//pkg/labels:go_default_library",
+        "@io_k8s_client_go//kubernetes/typed/authorization/v1:go_default_library",
+        "@io_k8s_client_go//kubernetes/typed/certificates/v1:go_default_library",
+        "@io_k8s_client_go//listers/certificates/v1:go_default_library",
+        "@io_k8s_client_go//tools/cache:go_default_library",
+        "@io_k8s_client_go//tools/record:go_default_library",
+        "@io_k8s_client_go//util/workqueue:go_default_library",
+        "@io_k8s_utils//clock:go_default_library",
+    ],
+)
+
+go_test(
+    name = "go_default_test",
+    srcs = ["controller_test.go"],
+    embed = [":go_default_library"],
+    deps = [
+        "//pkg/api/util:go_default_library",
+        "//pkg/apis/certmanager/v1:go_default_library",
+        "//pkg/apis/meta/v1:go_default_library",
+        "//pkg/controller:go_default_library",
+        "//pkg/controller/certificatesigningrequests/fake:go_default_library",
+        "//pkg/controller/certificatesigningrequests/util:go_default_library",
+        "//pkg/controller/test:go_default_library",
+        "//test/unit/gen:go_default_library",
+        "@io_k8s_api//authorization/v1:go_default_library",
+        "@io_k8s_api//certificates/v1:go_default_library",
+        "@io_k8s_api//core/v1:go_default_library",
+        "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
+        "@io_k8s_apimachinery//pkg/runtime:go_default_library",
+        "@io_k8s_client_go//testing:go_default_library",
+        "@io_k8s_utils//clock/testing:go_default_library",
+    ],
+)
+
+filegroup(
+    name = "package-srcs",
+    srcs = glob(["**"]),
+    tags = ["automanaged"],
+    visibility = ["//visibility:private"],
+)
+
+filegroup(
+    name = "all-srcs",
+    srcs = [
+        ":package-srcs",
+        "//pkg/controller/certificatesigningrequests/ca:all-srcs",
+        "//pkg/controller/certificatesigningrequests/fake:all-srcs",
+        "//pkg/controller/certificatesigningrequests/util:all-srcs",
+    ],
+    tags = ["automanaged"],
+    visibility = ["//visibility:public"],
+)
diff --git a/pkg/controller/certificatesigningrequests/ca/BUILD.bazel b/pkg/controller/certificatesigningrequests/ca/BUILD.bazel
new file mode 100644
index 000000000..7b2d35867
--- /dev/null
+++ b/pkg/controller/certificatesigningrequests/ca/BUILD.bazel
@@ -0,0 +1,70 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["ca.go"],
+    importpath = "github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests/ca",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//pkg/api/util:go_default_library",
+        "//pkg/apis/certmanager/v1:go_default_library",
+        "//pkg/apis/experimental/v1alpha1:go_default_library",
+        "//pkg/controller:go_default_library",
+        "//pkg/controller/certificatesigningrequests:go_default_library",
+        "//pkg/controller/certificatesigningrequests/util:go_default_library",
+        "//pkg/logs:go_default_library",
+        "//pkg/util/errors:go_default_library",
+        "//pkg/util/kube:go_default_library",
+        "//pkg/util/pki:go_default_library",
+        "@io_k8s_api//certificates/v1:go_default_library",
+        "@io_k8s_api//core/v1:go_default_library",
+        "@io_k8s_apimachinery//pkg/api/errors:go_default_library",
+        "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
+        "@io_k8s_client_go//kubernetes/typed/certificates/v1:go_default_library",
+        "@io_k8s_client_go//listers/core/v1:go_default_library",
+        "@io_k8s_client_go//tools/record:go_default_library",
+    ],
+)
+
+go_test(
+    name = "go_default_test",
+    srcs = ["ca_test.go"],
+    embed = [":go_default_library"],
+    deps = [
+        "//pkg/api/util:go_default_library",
+        "//pkg/apis/certmanager:go_default_library",
+        "//pkg/apis/certmanager/v1:go_default_library",
+        "//pkg/apis/meta/v1:go_default_library",
+        "//pkg/controller:go_default_library",
+        "//pkg/controller/certificatesigningrequests:go_default_library",
+        "//pkg/controller/certificatesigningrequests/util:go_default_library",
+        "//pkg/controller/test:go_default_library",
+        "//pkg/util/pki:go_default_library",
+        "//test/unit/gen:go_default_library",
+        "//test/unit/listers:go_default_library",
+        "@com_github_stretchr_testify//assert:go_default_library",
+        "@com_github_stretchr_testify//require:go_default_library",
+        "@io_k8s_api//authorization/v1:go_default_library",
+        "@io_k8s_api//certificates/v1:go_default_library",
+        "@io_k8s_api//core/v1:go_default_library",
+        "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
+        "@io_k8s_apimachinery//pkg/runtime:go_default_library",
+        "@io_k8s_client_go//listers/core/v1:go_default_library",
+        "@io_k8s_client_go//testing:go_default_library",
+        "@io_k8s_utils//clock/testing: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/pkg/controller/certificatesigningrequests/ca/ca.go b/pkg/controller/certificatesigningrequests/ca/ca.go
new file mode 100644
index 000000000..ed8d9ba4b
--- /dev/null
+++ b/pkg/controller/certificatesigningrequests/ca/ca.go
@@ -0,0 +1,161 @@
+/*
+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 ca
+
+import (
+	"context"
+	"crypto"
+	"crypto/x509"
+	"encoding/base64"
+	"fmt"
+
+	certificatesv1 "k8s.io/api/certificates/v1"
+	corev1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	certificatesclient "k8s.io/client-go/kubernetes/typed/certificates/v1"
+	corelisters "k8s.io/client-go/listers/core/v1"
+	"k8s.io/client-go/tools/record"
+
+	apiutil "github.com/jetstack/cert-manager/pkg/api/util"
+	cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
+	experimentalapi "github.com/jetstack/cert-manager/pkg/apis/experimental/v1alpha1"
+	controllerpkg "github.com/jetstack/cert-manager/pkg/controller"
+	"github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests"
+	"github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests/util"
+	logf "github.com/jetstack/cert-manager/pkg/logs"
+	cmerrors "github.com/jetstack/cert-manager/pkg/util/errors"
+	"github.com/jetstack/cert-manager/pkg/util/kube"
+	"github.com/jetstack/cert-manager/pkg/util/pki"
+)
+
+const (
+	CSRControllerName = "certificatesigningrequests-issuer-ca"
+)
+
+type templateGenerator func(*certificatesv1.CertificateSigningRequest) (*x509.Certificate, error)
+type signingFn func([]*x509.Certificate, crypto.Signer, *x509.Certificate) (pki.PEMBundle, error)
+
+// CA is a Kubernetes CertificateSigningRequest controller, responsible for
+// signing CertificateSigningRequests that reference a cert-manager CA Issuer
+// or ClusterIssuer
+type CA struct {
+	issuerOptions controllerpkg.IssuerOptions
+	secretsLister corelisters.SecretLister
+
+	certClient certificatesclient.CertificateSigningRequestInterface
+
+	recorder record.EventRecorder
+
+	// Used for testing to get reproducible resulting certificates
+	templateGenerator templateGenerator
+	signingFn         signingFn
+}
+
+func init() {
+	// create certificate request controller for ca issuer
+	controllerpkg.Register(CSRControllerName, func(ctx *controllerpkg.Context) (controllerpkg.Interface, error) {
+		return controllerpkg.NewBuilder(ctx, CSRControllerName).
+			For(certificatesigningrequests.New(apiutil.IssuerCA, NewCA(ctx))).
+			Complete()
+	})
+}
+
+func NewCA(ctx *controllerpkg.Context) *CA {
+	return &CA{
+		issuerOptions:     ctx.IssuerOptions,
+		secretsLister:     ctx.KubeSharedInformerFactory.Core().V1().Secrets().Lister(),
+		certClient:        ctx.Client.CertificatesV1().CertificateSigningRequests(),
+		recorder:          ctx.Recorder,
+		templateGenerator: pki.GenerateTemplateFromCertificateSigningRequest,
+		signingFn:         pki.SignCSRTemplate,
+	}
+}
+
+// Sign attempts to sign the given CertificateSigningRequest based on the
+// provided CA Issuer or ClusterIssuer. This function will update the resource
+// if signing was successful. Returns an error which, if not nil, should
+// trigger a retry.
+func (c *CA) Sign(ctx context.Context, csr *certificatesv1.CertificateSigningRequest, issuerObj cmapi.GenericIssuer) error {
+	log := logf.FromContext(ctx, "sign")
+
+	secretName := issuerObj.GetSpec().CA.SecretName
+	resourceNamespace := c.issuerOptions.ResourceNamespace(issuerObj)
+
+	// get a copy of the CA certificate named on the Issuer
+	caCerts, caKey, err := kube.SecretTLSKeyPairAndCA(ctx, c.secretsLister, resourceNamespace, issuerObj.GetSpec().CA.SecretName)
+	if apierrors.IsNotFound(err) {
+		message := fmt.Sprintf("Referenced secret %s/%s not found", resourceNamespace, secretName)
+		c.recorder.Event(csr, corev1.EventTypeWarning, "SecretMissing", message)
+		return nil
+	}
+
+	if cmerrors.IsInvalidData(err) {
+		message := fmt.Sprintf("Failed to parse signing CA keypair from secret %s/%s", resourceNamespace, secretName)
+		c.recorder.Eventf(csr, corev1.EventTypeWarning, "SecretInvalidData", "%s: %s", message, err)
+		return nil
+	}
+
+	if err != nil {
+		// We are probably in a network error here so we should backoff and retry
+		message := fmt.Sprintf("Failed to get certificate key pair from secret %s/%s", resourceNamespace, secretName)
+		c.recorder.Eventf(csr, corev1.EventTypeWarning, "SecretGetError", "%s: %s", message, err)
+		return err
+	}
+
+	template, err := c.templateGenerator(csr)
+	if err != nil {
+		message := fmt.Sprintf("Error generating certificate template: %s", err)
+		c.recorder.Event(csr, corev1.EventTypeWarning, "SigningError", message)
+		util.CertificateSigningRequestSetFailed(csr, "SigningError", message)
+		_, err = c.certClient.UpdateStatus(ctx, csr, metav1.UpdateOptions{})
+		return nil
+	}
+
+	template.CRLDistributionPoints = issuerObj.GetSpec().CA.CRLDistributionPoints
+	template.OCSPServer = issuerObj.GetSpec().CA.OCSPServers
+
+	bundle, err := c.signingFn(caCerts, caKey, template)
+	if err != nil {
+		message := fmt.Sprintf("Error signing certificate: %s", err)
+		c.recorder.Event(csr, corev1.EventTypeWarning, "SigningError", message)
+		util.CertificateSigningRequestSetFailed(csr, "SigningError", message)
+		_, err = c.certClient.UpdateStatus(ctx, csr, metav1.UpdateOptions{})
+		return nil
+	}
+
+	csr.Status.Certificate = bundle.ChainPEM
+	csr, err = c.certClient.UpdateStatus(ctx, csr, metav1.UpdateOptions{})
+	if err != nil {
+		message := "Error updating certificate"
+		c.recorder.Eventf(csr, corev1.EventTypeWarning, "SigningError", "%s: %s", message, err)
+		return err
+	}
+
+	csr.Annotations[experimentalapi.CertificateSigningRequestCAAnnotationKey] = base64.StdEncoding.EncodeToString(bundle.CAPEM)
+	_, err = c.certClient.Update(ctx, csr, metav1.UpdateOptions{})
+	if err != nil {
+		message := fmt.Sprintf("Error setting %q", experimentalapi.CertificateSigningRequestCAAnnotationKey)
+		c.recorder.Eventf(csr, corev1.EventTypeWarning, "SigningError", "%s: %s", message, err)
+		return err
+	}
+
+	log.V(logf.DebugLevel).Info("certificate issued")
+	c.recorder.Event(csr, corev1.EventTypeNormal, "CertificateIssued", "Certificate fetched from issuer successfully")
+
+	return nil
+}
diff --git a/pkg/controller/certificatesigningrequests/checks.go b/pkg/controller/certificatesigningrequests/checks.go
new file mode 100644
index 000000000..4e9cd7c60
--- /dev/null
+++ b/pkg/controller/certificatesigningrequests/checks.go
@@ -0,0 +1,83 @@
+/*
+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 certificatesigningrequests
+
+import (
+	"fmt"
+
+	certificatesv1 "k8s.io/api/certificates/v1"
+	"k8s.io/apimachinery/pkg/labels"
+
+	"github.com/jetstack/cert-manager/pkg/apis/certmanager"
+	cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
+	"github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests/util"
+	logf "github.com/jetstack/cert-manager/pkg/logs"
+)
+
+func (c *Controller) handleGenericIssuer(obj interface{}) {
+	log := c.log.WithName("handleGenericIssuer")
+
+	iss, ok := obj.(cmapi.GenericIssuer)
+	if !ok {
+		log.Error(nil, "object does not implement GenericIssuer")
+		return
+	}
+
+	log = logf.WithResource(log, iss)
+	crs, err := c.certificateSigningRequestsForGenericIssuer(iss)
+	if err != nil {
+		log.Error(err, "error looking up certificate signing requests observing issuer or clusterissuer")
+		return
+	}
+	for _, cr := range crs {
+		log := logf.WithRelatedResource(log, cr)
+		key, err := keyFunc(cr)
+		if err != nil {
+			log.Error(err, "error computing key for resource")
+			continue
+		}
+		c.queue.Add(key)
+	}
+}
+
+func (c *Controller) certificateSigningRequestsForGenericIssuer(iss cmapi.GenericIssuer) ([]*certificatesv1.CertificateSigningRequest, error) {
+	csrs, err := c.csrLister.List(labels.NewSelector())
+	if err != nil {
+		return nil, fmt.Errorf("error listing certificates signing requests: %s", err.Error())
+	}
+
+	_, isClusterIssuer := iss.(*cmapi.ClusterIssuer)
+
+	var affected []*certificatesv1.CertificateSigningRequest
+	for _, csr := range csrs {
+		ref, ok := util.SignerIssuerRefFromSignerName(csr.Spec.SignerName)
+
+		switch {
+		case !ok,
+			ref.Group != certmanager.GroupName,
+			iss.GetNamespace() != ref.Namespace,
+			iss.GetName() != ref.Name,
+			isClusterIssuer && ref.Type != "clusterissuers",
+			!isClusterIssuer && ref.Type != "issuers":
+			continue
+		}
+
+		affected = append(affected, csr)
+	}
+
+	return affected, nil
+}
diff --git a/pkg/controller/certificatesigningrequests/controller.go b/pkg/controller/certificatesigningrequests/controller.go
new file mode 100644
index 000000000..28a513138
--- /dev/null
+++ b/pkg/controller/certificatesigningrequests/controller.go
@@ -0,0 +1,163 @@
+/*
+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 certificatesigningrequests
+
+import (
+	"context"
+
+	"github.com/go-logr/logr"
+	certificatesv1 "k8s.io/api/certificates/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	authzclient "k8s.io/client-go/kubernetes/typed/authorization/v1"
+	certificatesclient "k8s.io/client-go/kubernetes/typed/certificates/v1"
+	certificateslisters "k8s.io/client-go/listers/certificates/v1"
+	"k8s.io/client-go/tools/cache"
+	"k8s.io/client-go/tools/record"
+	"k8s.io/client-go/util/workqueue"
+	"k8s.io/utils/clock"
+
+	cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
+	controllerpkg "github.com/jetstack/cert-manager/pkg/controller"
+	"github.com/jetstack/cert-manager/pkg/issuer"
+	logf "github.com/jetstack/cert-manager/pkg/logs"
+)
+
+const (
+	ControllerName = "certificatesigningrequests"
+)
+
+var keyFunc = controllerpkg.KeyFunc
+
+// Signer is an implementation of a Kubernetes CertificateSigningRequest
+// signer, backed by a cert-manager Issuer.
+type Signer interface {
+	Sign(context.Context, *certificatesv1.CertificateSigningRequest, cmapi.GenericIssuer) error
+}
+
+// Controller is a base Kubernetes CertificateSigningRequest controller. It is
+// responsible for orchestrating and performing shared operations that all
+// CertificateSigningRequest controllers do, before passing the
+// CertificateSigningRequest to a Singer implementation who instantiated this
+// controller.
+type Controller struct {
+	helper issuer.Helper
+
+	// clientset used to update CertificateSigningRequest API resources
+	certClient certificatesclient.CertificateSigningRequestInterface
+	csrLister  certificateslisters.CertificateSigningRequestLister
+	sarClient  authzclient.SubjectAccessReviewInterface
+
+	queue workqueue.RateLimitingInterface
+
+	// logger to be used by this controller
+	log logr.Logger
+
+	// used to record Events about resources to the API
+	recorder record.EventRecorder
+
+	// Signer to call sign function
+	signer Signer
+
+	// the signer kind to react to when a certificate signing request is synced
+	signerType string
+
+	// used for testing
+	clock clock.Clock
+}
+
+func New(signerType string, signer Signer) *Controller {
+	return &Controller{
+		signerType: signerType,
+		signer:     signer,
+	}
+}
+
+func (c *Controller) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) {
+	// construct a new named logger to be reused throughout the controller
+	c.log = logf.FromContext(ctx.RootContext, ControllerName)
+
+	// create a queue used to queue up items to be processed
+	c.queue = workqueue.NewNamedRateLimitingQueue(controllerpkg.DefaultItemBasedRateLimiter(), ControllerName)
+
+	c.sarClient = ctx.Client.AuthorizationV1().SubjectAccessReviews()
+
+	issuerInformer := ctx.SharedInformerFactory.Certmanager().V1().Issuers()
+
+	// obtain references to all the informers used by this controller
+	csrInformer := ctx.KubeSharedInformerFactory.Certificates().V1().CertificateSigningRequests()
+
+	// build a list of InformerSynced functions that will be returned by the Register method.
+	// the controller will only begin processing items once all of these informers have synced.
+	mustSync := []cache.InformerSynced{
+		csrInformer.Informer().HasSynced,
+		issuerInformer.Informer().HasSynced,
+	}
+
+	// if scoped to a single namespace
+	// if we are running in non-namespaced mode (i.e. --namespace=""), we also
+	// register event handlers and obtain a lister for clusterissuers.
+	clusterIssuerInformer := ctx.SharedInformerFactory.Certmanager().V1().ClusterIssuers()
+	if ctx.Namespace == "" {
+		// register handler function for clusterissuer resources
+		clusterIssuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer})
+		mustSync = append(mustSync, clusterIssuerInformer.Informer().HasSynced)
+	}
+
+	// set all the references to the listers for used by the Sync function
+	c.csrLister = csrInformer.Lister()
+
+	// register handler functions
+	csrInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: c.queue})
+	issuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer})
+
+	// create an issuer helper for reading generic issuers
+	c.helper = issuer.NewHelper(issuerInformer.Lister(), clusterIssuerInformer.Lister())
+
+	c.clock = ctx.Clock
+	// recorder records events about resources to the Kubernetes api
+	c.recorder = ctx.Recorder
+	c.certClient = ctx.Client.CertificatesV1().CertificateSigningRequests()
+
+	c.log.V(logf.DebugLevel).Info("new certificate signing request controller registered",
+		"type", c.signerType)
+
+	return c.queue, mustSync, nil
+}
+
+func (c *Controller) ProcessItem(ctx context.Context, key string) error {
+	log := logf.FromContext(ctx)
+	dbg := log.V(logf.DebugLevel)
+
+	_, name, err := cache.SplitMetaNamespaceKey(key)
+	if err != nil {
+		log.Error(err, "invalid resource key")
+		return nil
+	}
+
+	csr, err := c.csrLister.Get(name)
+	if apierrors.IsNotFound(err) {
+		dbg.Info("certificate signing request in work queue no longer exists", "error", err.Error())
+		return nil
+	}
+
+	if err != nil {
+		return err
+	}
+
+	ctx = logf.NewContext(ctx, logf.WithResource(log, csr))
+	return c.Sync(ctx, csr)
+}
diff --git a/pkg/controller/certificatesigningrequests/fake/BUILD.bazel b/pkg/controller/certificatesigningrequests/fake/BUILD.bazel
new file mode 100644
index 000000000..56f52abcf
--- /dev/null
+++ b/pkg/controller/certificatesigningrequests/fake/BUILD.bazel
@@ -0,0 +1,26 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["fake.go"],
+    importpath = "github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests/fake",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//pkg/apis/certmanager/v1:go_default_library",
+        "@io_k8s_api//certificates/v1: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/pkg/controller/certificatesigningrequests/fake/fake.go b/pkg/controller/certificatesigningrequests/fake/fake.go
new file mode 100644
index 000000000..acf524ed0
--- /dev/null
+++ b/pkg/controller/certificatesigningrequests/fake/fake.go
@@ -0,0 +1,33 @@
+/*
+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 fake
+
+import (
+	"context"
+
+	certificatesv1 "k8s.io/api/certificates/v1"
+
+	cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
+)
+
+type Signer struct {
+	FakeSign func(context.Context, *certificatesv1.CertificateSigningRequest, cmapi.GenericIssuer) error
+}
+
+func (s *Signer) Sign(ctx context.Context, csr *certificatesv1.CertificateSigningRequest, issuerObj cmapi.GenericIssuer) error {
+	return s.FakeSign(ctx, csr, issuerObj)
+}
diff --git a/pkg/controller/certificatesigningrequests/sync.go b/pkg/controller/certificatesigningrequests/sync.go
new file mode 100644
index 000000000..1edb87927
--- /dev/null
+++ b/pkg/controller/certificatesigningrequests/sync.go
@@ -0,0 +1,184 @@
+/*
+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 certificatesigningrequests
+
+import (
+	"context"
+	"fmt"
+
+	authzv1 "k8s.io/api/authorization/v1"
+	certificatesv1 "k8s.io/api/certificates/v1"
+	corev1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	apiutil "github.com/jetstack/cert-manager/pkg/api/util"
+	"github.com/jetstack/cert-manager/pkg/apis/certmanager"
+	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/controller/certificatesigningrequests/util"
+	logf "github.com/jetstack/cert-manager/pkg/logs"
+)
+
+func (c *Controller) Sync(ctx context.Context, csr *certificatesv1.CertificateSigningRequest) error {
+	log := logf.WithResource(logf.FromContext(ctx), csr).WithValues("signerName", csr.Spec.SignerName)
+	dbg := log.V(logf.DebugLevel)
+
+	ref, ok := util.SignerIssuerRefFromSignerName(csr.Spec.SignerName)
+	if !ok {
+		dbg.Info("certificate signing request has malformed signer name,", "signerName", csr.Spec.SignerName)
+		return nil
+	}
+
+	if ref.Group != certmanager.GroupName {
+		dbg.Info("certificate signing request signerName group does not match 'cert-manager.io' group so skipping processing")
+		return nil
+	}
+
+	if util.CertificateSigningRequestIsFailed(csr) {
+		dbg.Info("certificate signing request has failed so skipping processing")
+		return nil
+	}
+	if !util.CertificateSigningRequestIsApproved(csr) {
+		dbg.Info("certificate signing request is not approved so skipping processing")
+		return nil
+	}
+
+	if len(csr.Status.Certificate) > 0 {
+		dbg.Info("certificate field is already set in status so skipping processing")
+		return nil
+	}
+
+	var kind string
+	switch ref.Type {
+	case "issuers":
+		kind = cmapi.IssuerKind
+
+	case "clusterissuers":
+		kind = cmapi.ClusterIssuerKind
+
+	default:
+		dbg.Info("certificate signing request signerName type does not match 'issuers' or 'clusterissuers' so skipping processing")
+		return nil
+	}
+
+	issuerObj, err := c.helper.GetGenericIssuer(cmmeta.ObjectReference{
+		Name:  ref.Name,
+		Kind:  kind,
+		Group: ref.Group,
+	}, ref.Namespace)
+	if apierrors.IsNotFound(err) {
+		c.recorder.Eventf(csr, corev1.EventTypeWarning, "IssuerNotFound", "Referenced %s %s/%s not found", kind, ref.Namespace, ref.Name)
+		return nil
+	}
+
+	if err != nil {
+		log.Error(err, "failed to get issuer")
+		return err
+	}
+
+	log = logf.WithRelatedResource(log, issuerObj)
+	dbg.Info("ensuring issuer type matches this controller")
+
+	signerType, err := apiutil.NameForIssuer(issuerObj)
+	if err != nil {
+		c.recorder.Eventf(csr, corev1.EventTypeWarning, "IssuerTypeMissing", "Referenced %s %s/%s is missing type", kind, ref.Namespace, ref.Name)
+		return nil
+	}
+
+	// This CertificateSigningRequest is not meant for us, ignore
+	if signerType != c.signerType {
+		dbg.WithValues(logf.RelatedResourceKindKey, signerType).Info("signer reference type does not match controller resource kind, ignoring")
+		return nil
+	}
+
+	if kind == cmapi.IssuerKind {
+		ok, err := c.userCanReferenceSigner(ctx, csr, ref.Namespace, ref.Name)
+		if err != nil {
+			return err
+		}
+
+		if !ok {
+			message := fmt.Sprintf("Requester may not reference Namespaced Issuer %s/%s", ref.Namespace, ref.Name)
+			c.recorder.Event(csr, corev1.EventTypeWarning, "DeniedReference", message)
+			util.CertificateSigningRequestSetFailed(csr, "DeniedReference", message)
+			if _, err := c.certClient.UpdateStatus(ctx, csr, metav1.UpdateOptions{}); err != nil {
+				return err
+			}
+
+			return nil
+		}
+	}
+
+	// check ready condition
+	if !apiutil.IssuerHasCondition(issuerObj, cmapi.IssuerCondition{
+		Type:   cmapi.IssuerConditionReady,
+		Status: cmmeta.ConditionTrue,
+	}) {
+		c.recorder.Eventf(csr, corev1.EventTypeWarning, "IssuerNotReady", "Referenced %s %s/%s does not have a Ready status condition",
+			kind, issuerObj.GetNamespace(), issuerObj.GetName())
+		return nil
+	}
+
+	dbg.Info("invoking sign function as existing certificate does not exist")
+
+	return c.signer.Sign(ctx, csr, issuerObj)
+}
+
+// userCanReferenceSigner will return true if the CSR requester has a bound
+// role that allows them to reference a given Namespaced signer. The user must
+// have the permissions:
+// group: cert-manager.io
+// resource: signers
+// verb: reference
+// namespace: <referenced signer namespace>
+// name: <either the name of the signer or '*' for all signer names in that namespace>
+func (c *Controller) userCanReferenceSigner(ctx context.Context, csr *certificatesv1.CertificateSigningRequest, issuerNamespace, issuerName string) (bool, error) {
+	extra := make(map[string]authzv1.ExtraValue)
+	for k, v := range csr.Spec.Extra {
+		extra[k] = authzv1.ExtraValue(v)
+	}
+
+	for _, name := range []string{issuerName, "*"} {
+		resp, err := c.sarClient.Create(ctx, &authzv1.SubjectAccessReview{
+			Spec: authzv1.SubjectAccessReviewSpec{
+				User:   csr.Spec.Username,
+				Groups: csr.Spec.Groups,
+				Extra:  extra,
+				UID:    csr.Spec.UID,
+
+				ResourceAttributes: &authzv1.ResourceAttributes{
+					Group:     certmanager.GroupName,
+					Resource:  "signers",
+					Verb:      "reference",
+					Namespace: issuerNamespace,
+					Name:      name,
+					Version:   "*",
+				},
+			},
+		}, metav1.CreateOptions{})
+		if err != nil {
+			return false, err
+		}
+
+		if resp.Status.Allowed {
+			return true, nil
+		}
+	}
+
+	return false, nil
+}
diff --git a/pkg/controller/certificatesigningrequests/util/BUILD.bazel b/pkg/controller/certificatesigningrequests/util/BUILD.bazel
new file mode 100644
index 000000000..f520c30d0
--- /dev/null
+++ b/pkg/controller/certificatesigningrequests/util/BUILD.bazel
@@ -0,0 +1,38 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        "conditions.go",
+        "signername.go",
+    ],
+    importpath = "github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests/util",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//pkg/logs:go_default_library",
+        "@io_k8s_api//certificates/v1:go_default_library",
+        "@io_k8s_api//core/v1:go_default_library",
+        "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
+        "@io_k8s_utils//clock:go_default_library",
+    ],
+)
+
+go_test(
+    name = "go_default_test",
+    srcs = ["signername_test.go"],
+    embed = [":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/pkg/controller/certificatesigningrequests/util/conditions.go b/pkg/controller/certificatesigningrequests/util/conditions.go
new file mode 100644
index 000000000..0ed5cfb40
--- /dev/null
+++ b/pkg/controller/certificatesigningrequests/util/conditions.go
@@ -0,0 +1,65 @@
+/*
+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 util
+
+import (
+	certificatesv1 "k8s.io/api/certificates/v1"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/utils/clock"
+
+	logf "github.com/jetstack/cert-manager/pkg/logs"
+)
+
+// Clock is defined as a package var so it can be stubbed out during tests.
+var Clock clock.Clock = clock.RealClock{}
+
+func CertificateSigningRequestIsApproved(csr *certificatesv1.CertificateSigningRequest) bool {
+	for _, cond := range csr.Status.Conditions {
+		if cond.Type == certificatesv1.CertificateApproved {
+			return true
+		}
+	}
+	return false
+}
+
+func CertificateSigningRequestIsFailed(csr *certificatesv1.CertificateSigningRequest) bool {
+	for _, cond := range csr.Status.Conditions {
+		if cond.Type == certificatesv1.CertificateFailed {
+			return true
+		}
+	}
+	return false
+}
+
+func CertificateSigningRequestSetFailed(csr *certificatesv1.CertificateSigningRequest, reason, message string) {
+	nowTime := metav1.NewTime(Clock.Now())
+
+	// Since we only ever set this condition once (enforced by the API), we
+	// needn't need to check whether the condition is already set.
+	csr.Status.Conditions = append(csr.Status.Conditions, certificatesv1.CertificateSigningRequestCondition{
+		Type:               certificatesv1.CertificateFailed,
+		Status:             corev1.ConditionTrue,
+		Reason:             reason,
+		Message:            message,
+		LastTransitionTime: nowTime,
+		LastUpdateTime:     nowTime,
+	})
+
+	logf.V(logf.InfoLevel).Infof("Setting lastTransitionTime for CertificateSigningRequest %s/%s condition Failed to %v",
+		csr.Namespace, csr.Name, nowTime.Time)
+}
diff --git a/pkg/controller/certificatesigningrequests/util/signername.go b/pkg/controller/certificatesigningrequests/util/signername.go
new file mode 100644
index 000000000..8ed572e35
--- /dev/null
+++ b/pkg/controller/certificatesigningrequests/util/signername.go
@@ -0,0 +1,69 @@
+/*
+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 util
+
+import (
+	"strings"
+)
+
+type SignerIssuerRef struct {
+	Namespace, Name string
+	Type, Group     string
+}
+
+// SignerIssuerRefFromSignerName will return a SignerIssuerRef from a
+// CertificateSigningRequests.SignerName
+func SignerIssuerRefFromSignerName(name string) (SignerIssuerRef, bool) {
+	split := strings.Split(name, "/")
+	if len(split) != 2 {
+		return SignerIssuerRef{}, false
+	}
+
+	signerTypeSplit := strings.SplitN(split[0], ".", 2)
+	signerNameSplit := strings.Split(split[1], ".")
+
+	if len(signerTypeSplit) < 2 || signerNameSplit[0] == "" {
+		return SignerIssuerRef{}, false
+	}
+
+	if len(signerNameSplit) == 1 {
+		return SignerIssuerRef{
+			Namespace: "",
+			Name:      signerNameSplit[0],
+			Type:      signerTypeSplit[0],
+			Group:     signerTypeSplit[1],
+		}, true
+	}
+
+	// ClusterIssuers do not have Namespaces
+	if signerTypeSplit[0] == "clusterissuers" {
+		return SignerIssuerRef{
+			Namespace: "",
+			Name:      strings.Join(signerNameSplit[0:], "."),
+			Type:      signerTypeSplit[0],
+			Group:     signerTypeSplit[1],
+		}, true
+	}
+
+	// Non Cluster Scoped issuers always have Namespaces
+	return SignerIssuerRef{
+		Namespace: signerNameSplit[0],
+		Name:      strings.Join(signerNameSplit[1:], "."),
+		Type:      signerTypeSplit[0],
+		Group:     signerTypeSplit[1],
+	}, true
+}
diff --git a/pkg/feature/features.go b/pkg/feature/features.go
index 21462aca9..7acb5dd52 100644
--- a/pkg/feature/features.go
+++ b/pkg/feature/features.go
@@ -28,6 +28,12 @@ const (
 	//
 	// ValidateCAA enables CAA checking when issuing certificates
 	ValidateCAA featuregate.Feature = "ValidateCAA"
+
+	// alpha: v1.4.0
+	//
+	// ExperimentalCertificateSigningRequestControllers enables all CertificateSigningRequest
+	// controllers that sign Kubernetes CertificateSigningRequest resources
+	ExperimentalCertificateSigningRequestControllers featuregate.Feature = "ExperimentalCertificateSigningRequestControllers"
 )
 
 func init() {
@@ -39,4 +45,5 @@ func init() {
 // available throughout Kubernetes binaries.
 var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
 	ValidateCAA: {Default: false, PreRelease: featuregate.Alpha},
+	ExperimentalCertificateSigningRequestControllers: {Default: false, PreRelease: featuregate.Alpha},
 }
diff --git a/pkg/util/pki/BUILD.bazel b/pkg/util/pki/BUILD.bazel
index 9007275a6..b116ac17e 100644
--- a/pkg/util/pki/BUILD.bazel
+++ b/pkg/util/pki/BUILD.bazel
@@ -6,6 +6,7 @@ go_library(
         "csr.go",
         "generate.go",
         "keyusage.go",
+        "kube.go",
         "parse.go",
     ],
     importpath = "github.com/jetstack/cert-manager/pkg/util/pki",
@@ -13,7 +14,9 @@ go_library(
     deps = [
         "//pkg/api/util:go_default_library",
         "//pkg/apis/certmanager/v1:go_default_library",
+        "//pkg/apis/experimental/v1alpha1:go_default_library",
         "//pkg/util/errors:go_default_library",
+        "@io_k8s_api//certificates/v1:go_default_library",
     ],
 )
 
@@ -22,14 +25,17 @@ go_test(
     srcs = [
         "csr_test.go",
         "generate_test.go",
+        "kube_test.go",
         "parse_test.go",
     ],
     embed = [":go_default_library"],
     deps = [
         "//pkg/apis/certmanager/v1:go_default_library",
         "//pkg/util:go_default_library",
+        "//test/unit/gen:go_default_library",
         "@com_github_stretchr_testify//assert:go_default_library",
         "@com_github_stretchr_testify//require:go_default_library",
+        "@io_k8s_api//certificates/v1:go_default_library",
     ],
 )
 
diff --git a/pkg/util/pki/kube.go b/pkg/util/pki/kube.go
new file mode 100644
index 000000000..8658e9814
--- /dev/null
+++ b/pkg/util/pki/kube.go
@@ -0,0 +1,81 @@
+/*
+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 pki
+
+import (
+	"crypto/x509"
+	"fmt"
+	"time"
+
+	certificatesv1 "k8s.io/api/certificates/v1"
+
+	apiutil "github.com/jetstack/cert-manager/pkg/api/util"
+	cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
+	experimentalapi "github.com/jetstack/cert-manager/pkg/apis/experimental/v1alpha1"
+)
+
+// GenerateTemplateFromCertificateSigningRequest will create an
+// *x509.Certificate from the given CertificateSigningRequest resource
+func GenerateTemplateFromCertificateSigningRequest(csr *certificatesv1.CertificateSigningRequest) (*x509.Certificate, error) {
+	duration := cmapi.DefaultCertificateDuration
+	requestedDuration, ok := csr.Annotations[experimentalapi.CertificateSigningRequestDurationAnnotationKey]
+	if ok {
+		dur, err := time.ParseDuration(requestedDuration)
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse requested duration on annotation %q: %w",
+				experimentalapi.CertificateSigningRequestDurationAnnotationKey, err)
+		}
+		duration = dur
+	}
+
+	ku, eku, err := BuildKeyUsagesKube(csr.Spec.Usages)
+	if err != nil {
+		return nil, err
+	}
+
+	isCA := csr.Annotations[experimentalapi.CertificateSigningRequestIsCAAnnotationKey] == "true"
+
+	return GenerateTemplateFromCSRPEMWithUsages(csr.Spec.Request, duration, isCA, ku, eku)
+}
+
+func BuildKeyUsagesKube(usages []certificatesv1.KeyUsage) (x509.KeyUsage, []x509.ExtKeyUsage, error) {
+	var unk []certificatesv1.KeyUsage
+	if len(usages) == 0 {
+		usages = []certificatesv1.KeyUsage{certificatesv1.UsageDigitalSignature, certificatesv1.UsageKeyEncipherment}
+	}
+
+	var (
+		ku  x509.KeyUsage
+		eku []x509.ExtKeyUsage
+	)
+
+	for _, u := range usages {
+		if kuse, ok := apiutil.KeyUsageTypeKube(u); ok {
+			ku |= kuse
+		} else if ekuse, ok := apiutil.ExtKeyUsageTypeKube(u); ok {
+			eku = append(eku, ekuse)
+		} else {
+			unk = append(unk, u)
+		}
+	}
+
+	if len(unk) > 0 {
+		return -1, nil, fmt.Errorf("unknown key usages: %v", unk)
+	}
+
+	return ku, eku, nil
+}


Test patch:
diff --git a/cmd/controller/app/options/options_test.go b/cmd/controller/app/options/options_test.go
index 3eb04b4f5..3a0ca1439 100644
--- a/cmd/controller/app/options/options_test.go
+++ b/cmd/controller/app/options/options_test.go
@@ -39,13 +39,13 @@ func TestEnabledControllers(t *testing.T) {
 			controllers: []string{"foo", "bar", "-foo"},
 			expEnabled:  sets.NewString("bar"),
 		},
-		"if all controllers enabled, return all controllers": {
+		"if all default controllers enabled, return all default controllers": {
 			controllers: []string{"*"},
-			expEnabled:  sets.NewString(allControllers...),
+			expEnabled:  sets.NewString(defaultEnabledControllers...),
 		},
 		"if all controllers enabled, some diabled, return all controllers with disabled": {
-			controllers: []string{"*", "-clusrerissuers", "-issuer"},
-			expEnabled:  sets.NewString(allControllers...).Delete("-clusterissuers", "-issuers"),
+			controllers: []string{"*", "-clusterissuers", "-issuers"},
+			expEnabled:  sets.NewString(defaultEnabledControllers...).Delete("clusterissuers", "issuers"),
 		},
 	}
 
diff --git a/pkg/controller/certificatesigningrequests/ca/ca_test.go b/pkg/controller/certificatesigningrequests/ca/ca_test.go
new file mode 100644
index 000000000..2ab480ee7
--- /dev/null
+++ b/pkg/controller/certificatesigningrequests/ca/ca_test.go
@@ -0,0 +1,756 @@
+/*
+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 ca
+
+import (
+	"context"
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/rand"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/pem"
+	"errors"
+	"math"
+	"math/big"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	authzv1 "k8s.io/api/authorization/v1"
+	certificatesv1 "k8s.io/api/certificates/v1"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	clientcorev1 "k8s.io/client-go/listers/core/v1"
+	coretesting "k8s.io/client-go/testing"
+	fakeclock "k8s.io/utils/clock/testing"
+
+	apiutil "github.com/jetstack/cert-manager/pkg/api/util"
+	"github.com/jetstack/cert-manager/pkg/apis/certmanager"
+	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/controller"
+	"github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests"
+	"github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests/util"
+	testpkg "github.com/jetstack/cert-manager/pkg/controller/test"
+	"github.com/jetstack/cert-manager/pkg/util/pki"
+	"github.com/jetstack/cert-manager/test/unit/gen"
+	testlisters "github.com/jetstack/cert-manager/test/unit/listers"
+)
+
+var (
+	fixedClockStart = time.Now()
+	fixedClock      = fakeclock.NewFakeClock(fixedClockStart)
+)
+
+func generateCSR(t *testing.T, secretKey crypto.Signer, sigAlg x509.SignatureAlgorithm) []byte {
+	template := x509.CertificateRequest{
+		Subject: pkix.Name{
+			CommonName: "test",
+		},
+		SignatureAlgorithm: sigAlg,
+	}
+
+	csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &template, secretKey)
+	if err != nil {
+		t.Fatal(err)
+	}
+	csr := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes})
+
+	return csr
+}
+
+func generateSelfSignedCACert(t *testing.T, key crypto.Signer, name string) (*x509.Certificate, []byte) {
+	tmpl := &x509.Certificate{
+		Version:               3,
+		BasicConstraintsValid: true,
+		SerialNumber:          big.NewInt(0),
+		Subject: pkix.Name{
+			CommonName: name,
+		},
+		NotBefore: time.Now(),
+		NotAfter:  time.Now().Add(time.Minute),
+		KeyUsage:  x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
+		PublicKey: key.Public(),
+		IsCA:      true,
+	}
+
+	pem, cert, err := pki.SignCertificate(tmpl, tmpl, key.Public(), key)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	return cert, pem
+}
+
+func TestSign(t *testing.T) {
+	metaFixedClockStart := metav1.NewTime(fixedClockStart)
+	util.Clock = fixedClock
+
+	baseIssuer := gen.Issuer("test-issuer",
+		gen.SetIssuerCA(cmapi.CAIssuer{SecretName: "root-ca-secret"}),
+		gen.AddIssuerCondition(cmapi.IssuerCondition{
+			Type:   cmapi.IssuerConditionReady,
+			Status: cmmeta.ConditionTrue,
+		}),
+	)
+
+	rootPK, err := pki.GenerateECPrivateKey(256)
+	if err != nil {
+		t.Fatal(err)
+	}
+	rootPKPEM, err := pki.EncodeECPrivateKey(rootPK)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	testpk, err := pki.GenerateECPrivateKey(256)
+	if err != nil {
+		t.Fatal(err)
+	}
+	testCSR := generateCSR(t, testpk, x509.ECDSAWithSHA256)
+
+	baseCSRNotApproved := gen.CertificateSigningRequest("test-cr",
+		gen.SetCertificateSigningRequestIsCA(true),
+		gen.SetCertificateSigningRequestRequest(testCSR),
+		gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/"+gen.DefaultTestNamespace+"."+baseIssuer.Name),
+		gen.SetCertificateSigningRequestDuration("1440h"),
+		gen.SetCertificateSigningRequestUsername("user-1"),
+		gen.SetCertificateSigningRequestGroups([]string{"group-1", "group-2"}),
+		gen.SetCertificateSigningRequestUID("uid-1"),
+		gen.SetCertificateSigningRequestExtra(map[string]certificatesv1.ExtraValue{
+			"extra": []string{"1", "2"},
+		}),
+	)
+	baseCSRDenied := gen.CertificateSigningRequestFrom(baseCSRNotApproved,
+		gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
+			Type:               certificatesv1.CertificateDenied,
+			Status:             corev1.ConditionTrue,
+			Reason:             "Foo",
+			Message:            "Certificate request has been denied by cert-manager.io",
+			LastTransitionTime: metaFixedClockStart,
+		}),
+	)
+	baseCSR := gen.CertificateSigningRequestFrom(baseCSRNotApproved,
+		gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
+			Type:               certificatesv1.CertificateApproved,
+			Status:             corev1.ConditionTrue,
+			Reason:             "cert-manager.io",
+			Message:            "Certificate request has been approved by cert-manager.io",
+			LastTransitionTime: metaFixedClockStart,
+		}),
+	)
+
+	// generate a self signed root ca valid for 60d
+	rootCert, rootCertPEM := generateSelfSignedCACert(t, rootPK, "root")
+	ecCASecret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "root-ca-secret",
+			Namespace: gen.DefaultTestNamespace,
+		},
+		Data: map[string][]byte{
+			corev1.TLSPrivateKeyKey: rootPKPEM,
+			corev1.TLSCertKey:       rootCertPEM,
+		},
+	}
+
+	badDataSecret := ecCASecret.DeepCopy()
+	badDataSecret.Data[corev1.TLSPrivateKeyKey] = []byte("bad key")
+
+	template, err := pki.GenerateTemplateFromCertificateSigningRequest(baseCSR)
+	if err != nil {
+		t.Fatal(err)
+	}
+	certBundle, err := pki.SignCSRTemplate([]*x509.Certificate{rootCert}, rootPK, template)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	tests := map[string]testT{
+		"a CertificateSigningRequest without an approved condition should do nothing": {
+			csr: baseCSRNotApproved.DeepCopy(),
+			builder: &testpkg.Builder{
+				KubeObjects:        []runtime.Object{baseCSRNotApproved.DeepCopy()},
+				CertManagerObjects: []runtime.Object{baseIssuer.DeepCopy()},
+			},
+		},
+		"a CertificateSigningRequest with a denied condition should do nothing": {
+			csr: baseCSRDenied.DeepCopy(),
+			builder: &testpkg.Builder{
+				KubeObjects:        []runtime.Object{baseCSRDenied.DeepCopy()},
+				CertManagerObjects: []runtime.Object{baseIssuer.DeepCopy()},
+				ExpectedEvents:     []string{},
+				ExpectedActions:    nil,
+			},
+		},
+		"an approved CSR with missing CA key pair should send event and wait for a re-sync": {
+			csr: baseCSR.DeepCopy(),
+			builder: &testpkg.Builder{
+				KubeObjects:        []runtime.Object{baseCSR.DeepCopy()},
+				CertManagerObjects: []runtime.Object{baseIssuer.DeepCopy()},
+				ExpectedEvents: []string{
+					`Warning SecretMissing Referenced secret default-unit-test-ns/root-ca-secret not found`,
+				},
+
+				ExpectedActions: []testpkg.Action{
+					testpkg.NewAction(coretesting.NewCreateAction(
+						authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews"),
+						"",
+						&authzv1.SubjectAccessReview{
+							Spec: authzv1.SubjectAccessReviewSpec{
+								User:   "user-1",
+								Groups: []string{"group-1", "group-2"},
+								Extra: map[string]authzv1.ExtraValue{
+									"extra": []string{"1", "2"},
+								},
+								UID: "uid-1",
+
+								ResourceAttributes: &authzv1.ResourceAttributes{
+									Group:     certmanager.GroupName,
+									Resource:  "signers",
+									Verb:      "reference",
+									Namespace: baseIssuer.Namespace,
+									Name:      baseIssuer.Name,
+									Version:   "*",
+								},
+							},
+						},
+					)),
+				},
+			},
+		},
+		"an approved CSR but secret with invalid data should send event and wait for re-sync": {
+			csr: baseCSR.DeepCopy(),
+			builder: &testpkg.Builder{
+				KubeObjects: []runtime.Object{badDataSecret, baseCSR.DeepCopy()},
+				CertManagerObjects: []runtime.Object{gen.IssuerFrom(baseIssuer.DeepCopy(),
+					gen.SetIssuerCA(cmapi.CAIssuer{SecretName: badDataSecret.Name}),
+				),
+				},
+				ExpectedEvents: []string{
+					"Warning SecretInvalidData Failed to parse signing CA keypair from secret default-unit-test-ns/root-ca-secret: error decoding private key PEM block",
+				},
+
+				ExpectedActions: []testpkg.Action{
+					testpkg.NewAction(coretesting.NewCreateAction(
+						authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews"),
+						"",
+						&authzv1.SubjectAccessReview{
+							Spec: authzv1.SubjectAccessReviewSpec{
+								User:   "user-1",
+								Groups: []string{"group-1", "group-2"},
+								Extra: map[string]authzv1.ExtraValue{
+									"extra": []string{"1", "2"},
+								},
+								UID: "uid-1",
+
+								ResourceAttributes: &authzv1.ResourceAttributes{
+									Group:     certmanager.GroupName,
+									Resource:  "signers",
+									Verb:      "reference",
+									Namespace: baseIssuer.Namespace,
+									Name:      baseIssuer.Name,
+									Version:   "*",
+								},
+							},
+						},
+					)),
+				},
+			},
+		},
+		"an approved CSR that transiently fails a secret lookup should backoff error to retry": {
+			csr: baseCSR.DeepCopy(),
+			builder: &testpkg.Builder{
+				KubeObjects:        []runtime.Object{ecCASecret, baseCSR.DeepCopy()},
+				CertManagerObjects: []runtime.Object{baseIssuer.DeepCopy()},
+				ExpectedEvents: []string{
+					`Warning SecretGetError Failed to get certificate key pair from secret default-unit-test-ns/root-ca-secret: this is a network error`,
+				},
+				ExpectedActions: []testpkg.Action{
+					testpkg.NewAction(coretesting.NewCreateAction(
+						authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews"),
+						"",
+						&authzv1.SubjectAccessReview{
+							Spec: authzv1.SubjectAccessReviewSpec{
+								User:   "user-1",
+								Groups: []string{"group-1", "group-2"},
+								Extra: map[string]authzv1.ExtraValue{
+									"extra": []string{"1", "2"},
+								},
+								UID: "uid-1",
+
+								ResourceAttributes: &authzv1.ResourceAttributes{
+									Group:     certmanager.GroupName,
+									Resource:  "signers",
+									Verb:      "reference",
+									Namespace: baseIssuer.Namespace,
+									Name:      baseIssuer.Name,
+									Version:   "*",
+								},
+							},
+						},
+					)),
+				},
+			},
+			fakeLister: &testlisters.FakeSecretLister{
+				SecretsFn: func(namespace string) clientcorev1.SecretNamespaceLister {
+					return &testlisters.FakeSecretNamespaceLister{
+						GetFn: func(name string) (ret *corev1.Secret, err error) {
+							return nil, errors.New("this is a network error")
+						},
+					}
+				},
+			},
+			expectedErr: true,
+		},
+		"an approved CSR should exit nil and send event if referenced issuer is not ready": {
+			csr: baseCSR.DeepCopy(),
+			builder: &testpkg.Builder{
+				KubeObjects: []runtime.Object{ecCASecret, baseCSR.DeepCopy()},
+				CertManagerObjects: []runtime.Object{gen.Issuer(baseIssuer.DeepCopy().Name,
+					gen.SetIssuerCA(cmapi.CAIssuer{}),
+				)},
+				ExpectedEvents: []string{
+					"Warning IssuerNotReady Referenced Issuer default-unit-test-ns/test-issuer does not have a Ready status condition",
+				},
+				ExpectedActions: []testpkg.Action{
+					testpkg.NewAction(coretesting.NewCreateAction(
+						authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews"),
+						"",
+						&authzv1.SubjectAccessReview{
+							Spec: authzv1.SubjectAccessReviewSpec{
+								User:   "user-1",
+								Groups: []string{"group-1", "group-2"},
+								Extra: map[string]authzv1.ExtraValue{
+									"extra": []string{"1", "2"},
+								},
+								UID: "uid-1",
+
+								ResourceAttributes: &authzv1.ResourceAttributes{
+									Group:     certmanager.GroupName,
+									Resource:  "signers",
+									Verb:      "reference",
+									Namespace: baseIssuer.Namespace,
+									Name:      baseIssuer.Name,
+									Version:   "*",
+								},
+							},
+						},
+					)),
+				},
+			},
+		},
+		"a secret that fails to sign due to failing to generate the certificate template should set condition to failed": {
+			csr: baseCSR.DeepCopy(),
+			templateGenerator: func(*certificatesv1.CertificateSigningRequest) (*x509.Certificate, error) {
+				return nil, errors.New("this is a template generate error")
+			},
+			builder: &testpkg.Builder{
+				KubeObjects:        []runtime.Object{ecCASecret, baseCSR.DeepCopy()},
+				CertManagerObjects: []runtime.Object{baseIssuer.DeepCopy()},
+				ExpectedEvents: []string{
+					"Warning SigningError Error generating certificate template: this is a template generate error",
+				},
+				ExpectedActions: []testpkg.Action{
+					testpkg.NewAction(coretesting.NewCreateAction(
+						authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews"),
+						"",
+						&authzv1.SubjectAccessReview{
+							Spec: authzv1.SubjectAccessReviewSpec{
+								User:   "user-1",
+								Groups: []string{"group-1", "group-2"},
+								Extra: map[string]authzv1.ExtraValue{
+									"extra": []string{"1", "2"},
+								},
+								UID: "uid-1",
+
+								ResourceAttributes: &authzv1.ResourceAttributes{
+									Group:     certmanager.GroupName,
+									Resource:  "signers",
+									Verb:      "reference",
+									Namespace: baseIssuer.Namespace,
+									Name:      baseIssuer.Name,
+									Version:   "*",
+								},
+							},
+						},
+					)),
+					testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
+						certificatesv1.SchemeGroupVersion.WithResource("certificatesigningrequests"),
+						"status",
+						"",
+						gen.CertificateSigningRequestFrom(baseCSR.DeepCopy(),
+							gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
+								Type:               certificatesv1.CertificateFailed,
+								Status:             corev1.ConditionTrue,
+								Reason:             "SigningError",
+								Message:            "Error generating certificate template: this is a template generate error",
+								LastTransitionTime: metaFixedClockStart,
+								LastUpdateTime:     metaFixedClockStart,
+							}),
+						),
+					)),
+				},
+			},
+			expectedErr: false,
+		},
+		"if signing fails then the CertificateSigningRequest should be updated as Failed": {
+			csr: baseCSR.DeepCopy(),
+			signingFn: func(_ []*x509.Certificate, _ crypto.Signer, _ *x509.Certificate) (pki.PEMBundle, error) {
+				return pki.PEMBundle{}, errors.New("this is a signing error")
+			},
+			builder: &testpkg.Builder{
+				KubeObjects:        []runtime.Object{ecCASecret, baseCSR.DeepCopy()},
+				CertManagerObjects: []runtime.Object{baseIssuer.DeepCopy()},
+				ExpectedEvents: []string{
+					"Warning SigningError Error signing certificate: this is a signing error",
+				},
+				ExpectedActions: []testpkg.Action{
+					testpkg.NewAction(coretesting.NewCreateAction(
+						authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews"),
+						"",
+						&authzv1.SubjectAccessReview{
+							Spec: authzv1.SubjectAccessReviewSpec{
+								User:   "user-1",
+								Groups: []string{"group-1", "group-2"},
+								Extra: map[string]authzv1.ExtraValue{
+									"extra": []string{"1", "2"},
+								},
+								UID: "uid-1",
+
+								ResourceAttributes: &authzv1.ResourceAttributes{
+									Group:     certmanager.GroupName,
+									Resource:  "signers",
+									Verb:      "reference",
+									Namespace: baseIssuer.Namespace,
+									Name:      baseIssuer.Name,
+									Version:   "*",
+								},
+							},
+						},
+					)),
+					testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
+						certificatesv1.SchemeGroupVersion.WithResource("certificatesigningrequests"),
+						"status",
+						"",
+						gen.CertificateSigningRequestFrom(baseCSR.DeepCopy(),
+							gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
+								Type:               certificatesv1.CertificateFailed,
+								Status:             corev1.ConditionTrue,
+								Reason:             "SigningError",
+								Message:            "Error signing certificate: this is a signing error",
+								LastTransitionTime: metaFixedClockStart,
+								LastUpdateTime:     metaFixedClockStart,
+							}),
+						),
+					)),
+				},
+			},
+			expectedErr: false,
+		},
+		"a successful signing should update CertificateSigningRequest Certificate and CA annotation": {
+			csr: baseCSR.DeepCopy(),
+			templateGenerator: func(csr *certificatesv1.CertificateSigningRequest) (*x509.Certificate, error) {
+				// Pass the given CSR to a "real" template generator to ensure that it
+				// doesn't err. Return the pre-generated template.
+				_, err := pki.GenerateTemplateFromCertificateSigningRequest(csr)
+				if err != nil {
+					return nil, err
+				}
+
+				return template, nil
+			},
+			signingFn: func(_ []*x509.Certificate, _ crypto.Signer, _ *x509.Certificate) (pki.PEMBundle, error) {
+				return pki.PEMBundle{CAPEM: certBundle.CAPEM, ChainPEM: certBundle.ChainPEM}, nil
+			},
+			builder: &testpkg.Builder{
+				KubeObjects:        []runtime.Object{ecCASecret, baseCSR.DeepCopy()},
+				CertManagerObjects: []runtime.Object{baseIssuer.DeepCopy()},
+				ExpectedEvents: []string{
+					"Normal CertificateIssued Certificate fetched from issuer successfully",
+				},
+				ExpectedActions: []testpkg.Action{
+					testpkg.NewAction(coretesting.NewCreateAction(
+						authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews"),
+						"",
+						&authzv1.SubjectAccessReview{
+							Spec: authzv1.SubjectAccessReviewSpec{
+								User:   "user-1",
+								Groups: []string{"group-1", "group-2"},
+								Extra: map[string]authzv1.ExtraValue{
+									"extra": []string{"1", "2"},
+								},
+								UID: "uid-1",
+
+								ResourceAttributes: &authzv1.ResourceAttributes{
+									Group:     certmanager.GroupName,
+									Resource:  "signers",
+									Verb:      "reference",
+									Namespace: baseIssuer.Namespace,
+									Name:      baseIssuer.Name,
+									Version:   "*",
+								},
+							},
+						},
+					)),
+					testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
+						certificatesv1.SchemeGroupVersion.WithResource("certificatesigningrequests"),
+						"status",
+						"",
+						gen.CertificateSigningRequestFrom(baseCSR,
+							gen.SetCertificateSigningRequestCertificate(certBundle.ChainPEM),
+						),
+					)),
+					testpkg.NewAction(coretesting.NewUpdateAction(
+						certificatesv1.SchemeGroupVersion.WithResource("certificatesigningrequests"),
+						"",
+						gen.CertificateSigningRequestFrom(baseCSR,
+							gen.SetCertificateSigningRequestCertificate(certBundle.ChainPEM),
+							gen.SetCertificateSigningRequestCA(certBundle.CAPEM),
+						),
+					)),
+				},
+			},
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			fixedClock.SetTime(fixedClockStart)
+			test.builder.Clock = fixedClock
+			runTest(t, test)
+		})
+	}
+}
+
+type testT struct {
+	builder           *testpkg.Builder
+	csr               *certificatesv1.CertificateSigningRequest
+	templateGenerator templateGenerator
+	signingFn         signingFn
+
+	expectedErr bool
+
+	fakeLister *testlisters.FakeSecretLister
+}
+
+func runTest(t *testing.T, test testT) {
+	test.builder.T = t
+	test.builder.Init()
+
+	// Always return true for SubjectAccessReviews in tests
+	test.builder.FakeKubeClient().PrependReactor("create", "*", func(action coretesting.Action) (bool, runtime.Object, error) {
+		if action.GetResource() != authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews") {
+			return false, nil, nil
+		}
+		return true, &authzv1.SubjectAccessReview{
+			Status: authzv1.SubjectAccessReviewStatus{
+				Allowed: true,
+			},
+		}, nil
+	})
+
+	defer test.builder.Stop()
+
+	ca := NewCA(test.builder.Context)
+
+	if test.fakeLister != nil {
+		ca.secretsLister = test.fakeLister
+	}
+
+	if test.templateGenerator != nil {
+		ca.templateGenerator = test.templateGenerator
+	}
+	if test.signingFn != nil {
+		ca.signingFn = test.signingFn
+	}
+
+	controller := certificatesigningrequests.New(apiutil.IssuerCA, ca)
+	controller.Register(test.builder.Context)
+	test.builder.Start()
+
+	err := controller.ProcessItem(context.Background(), test.csr.Name)
+	if err != nil && !test.expectedErr {
+		t.Errorf("expected to not get an error, but got: %v", err)
+	}
+	if err == nil && test.expectedErr {
+		t.Errorf("expected to get an error but did not get one")
+	}
+
+	test.builder.CheckAndFinish(err)
+}
+
+func TestCA_Sign(t *testing.T) {
+	rootPK, err := pki.GenerateECPrivateKey(256)
+	if err != nil {
+		t.Fatal(err)
+	}
+	rootCert, _ := generateSelfSignedCACert(t, rootPK, "root")
+
+	// Build test CSR
+	testpk, err := pki.GenerateECPrivateKey(256)
+	if err != nil {
+		t.Fatal(err)
+	}
+	testCSR := generateCSR(t, testpk, x509.ECDSAWithSHA256)
+
+	tests := map[string]struct {
+		givenCASecret    *corev1.Secret
+		givenCAIssuer    cmapi.GenericIssuer
+		givenCSR         *certificatesv1.CertificateSigningRequest
+		assertSignedCert func(t *testing.T, got *x509.Certificate)
+	}{
+		"when the CertificateSigningRequest has the duration field set, it should appear as notAfter on the signed ca": {
+			givenCASecret: gen.SecretFrom(gen.Secret("secret-1"), gen.SetSecretNamespace("default"), gen.SetSecretData(secretDataFor(t, rootPK, rootCert))),
+			givenCAIssuer: gen.Issuer("issuer-1", gen.SetIssuerCA(cmapi.CAIssuer{
+				SecretName: "secret-1",
+			})),
+			givenCSR: gen.CertificateSigningRequest("csr-1",
+				gen.SetCertificateSigningRequestRequest(testCSR),
+				gen.SetCertificateSigningRequestSignerName("issers.cert-manager.io/"+gen.DefaultTestNamespace+".issuer-1"),
+				gen.SetCertificateSigningRequestDuration("30m"),
+			),
+			assertSignedCert: func(t *testing.T, got *x509.Certificate) {
+				// Although there is less than 1µs between the time.Now
+				// call made by the certificate template func (in the "pki"
+				// package) and the time.Now below, rounding or truncating
+				// will always end up with a flaky test. This is due to the
+				// rounding made to the notAfter value when serializing the
+				// certificate to ASN.1 [1].
+				//
+				//  [1]: https://tools.ietf.org/html/rfc5280#section-4.1.2.5.1
+				//
+				// So instead of using a truncation or rounding in order to
+				// check the time, we use a delta of 2 seconds. One entire
+				// second is totally overkill since, as detailed above, the
+				// delay is probably less than a microsecond. But that will
+				// do for now!
+				//
+				// Note that we do have a plan to fix this. We want to be
+				// injecting a time (instead of time.Now) to the template
+				// functions. This work is being tracked in this issue:
+				// https://github.com/jetstack/cert-manager/issues/3738
+				expectNotAfter := time.Now().UTC().Add(30 * time.Minute)
+				deltaSec := math.Abs(expectNotAfter.Sub(got.NotAfter).Seconds())
+				assert.LessOrEqualf(t, deltaSec, 2., "expected a time delta lower than 2 second. Time expected='%s', got='%s'", expectNotAfter.String(), got.NotAfter.String())
+			},
+		},
+		"when the CertificateSigningRequest has the isCA field set, it should appear on the signed ca": {
+			givenCASecret: gen.SecretFrom(gen.Secret("secret-1"), gen.SetSecretNamespace("default"), gen.SetSecretData(secretDataFor(t, rootPK, rootCert))),
+			givenCAIssuer: gen.Issuer("issuer-1", gen.SetIssuerCA(cmapi.CAIssuer{
+				SecretName: "secret-1",
+			})),
+			givenCSR: gen.CertificateSigningRequest("csr-1",
+				gen.SetCertificateSigningRequestRequest(testCSR),
+				gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/"+gen.DefaultTestNamespace+".issuer-1"),
+				gen.SetCertificateSigningRequestIsCA(true),
+			),
+			assertSignedCert: func(t *testing.T, got *x509.Certificate) {
+				assert.Equal(t, true, got.IsCA)
+			},
+		},
+		"when the Issuer has ocspServers set, it should appear on the signed ca": {
+			givenCASecret: gen.SecretFrom(gen.Secret("secret-1"), gen.SetSecretNamespace("default"), gen.SetSecretData(secretDataFor(t, rootPK, rootCert))),
+			givenCAIssuer: gen.Issuer("issuer-1", gen.SetIssuerCA(cmapi.CAIssuer{
+				SecretName:  "secret-1",
+				OCSPServers: []string{"http://ocsp-v3.example.org"},
+			})),
+			givenCSR: gen.CertificateSigningRequest("cr-1",
+				gen.SetCertificateSigningRequestRequest(testCSR),
+				gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/"+gen.DefaultTestNamespace+".issuer-1"),
+			),
+			assertSignedCert: func(t *testing.T, got *x509.Certificate) {
+				assert.Equal(t, []string{"http://ocsp-v3.example.org"}, got.OCSPServer)
+			},
+		},
+		"when the Issuer has crlDistributionPoints set, it should appear on the signed ca ": {
+			givenCASecret: gen.SecretFrom(gen.Secret("secret-1"), gen.SetSecretNamespace("default"), gen.SetSecretData(secretDataFor(t, rootPK, rootCert))),
+			givenCAIssuer: gen.Issuer("issuer-1", gen.SetIssuerCA(cmapi.CAIssuer{
+				SecretName:            "secret-1",
+				CRLDistributionPoints: []string{"http://www.example.com/crl/test.crl"},
+			})),
+			givenCSR: gen.CertificateSigningRequest("cr-1",
+				gen.SetCertificateSigningRequestIsCA(true),
+				gen.SetCertificateSigningRequestRequest(testCSR),
+				gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/"+gen.DefaultTestNamespace+".issuer-1"),
+			),
+			assertSignedCert: func(t *testing.T, gotCA *x509.Certificate) {
+				assert.Equal(t, []string{"http://www.example.com/crl/test.crl"}, gotCA.CRLDistributionPoints)
+			},
+		},
+	}
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			builder := &testpkg.Builder{
+				KubeObjects:        []runtime.Object{test.givenCSR, test.givenCASecret},
+				CertManagerObjects: []runtime.Object{test.givenCAIssuer},
+			}
+			builder.T = t
+			builder.Init()
+			defer builder.Stop()
+			builder.Start()
+
+			c := &CA{
+				issuerOptions: controller.IssuerOptions{
+					ClusterResourceNamespace:        "",
+					ClusterIssuerAmbientCredentials: false,
+					IssuerAmbientCredentials:        false,
+				},
+				certClient: builder.Client.CertificatesV1().CertificateSigningRequests(),
+				recorder:   new(testpkg.FakeRecorder),
+				secretsLister: testlisters.FakeSecretListerFrom(testlisters.NewFakeSecretLister(),
+					testlisters.SetFakeSecretNamespaceListerGet(test.givenCASecret, nil),
+				),
+				templateGenerator: pki.GenerateTemplateFromCertificateSigningRequest,
+				signingFn:         pki.SignCSRTemplate,
+			}
+
+			gotErr := c.Sign(context.Background(), test.givenCSR, test.givenCAIssuer)
+			require.NoError(t, gotErr)
+			builder.Sync()
+
+			csr, err := builder.Client.CertificatesV1().CertificateSigningRequests().Get(context.TODO(), test.givenCSR.Name, metav1.GetOptions{})
+			require.NoError(t, err)
+
+			require.NotEmpty(t, csr.Status.Certificate)
+			gotCert, err := pki.DecodeX509CertificateBytes(csr.Status.Certificate)
+			require.NoError(t, err)
+
+			test.assertSignedCert(t, gotCert)
+		})
+	}
+}
+
+// Returns a map that is meant to be used for creating a certificate Secret
+// that contains the fields "tls.crt" and "tls.key".
+func secretDataFor(t *testing.T, caKey *ecdsa.PrivateKey, caCrt *x509.Certificate) (secretData map[string][]byte) {
+	rootCADER, err := x509.CreateCertificate(rand.Reader, caCrt, caCrt, caKey.Public(), caKey)
+	require.NoError(t, err)
+	caCrt, err = x509.ParseCertificate(rootCADER)
+	require.NoError(t, err)
+	caKeyPEM, err := pki.EncodeECPrivateKey(caKey)
+	require.NoError(t, err)
+	caCrtPEM, err := pki.EncodeX509(caCrt)
+	require.NoError(t, err)
+
+	return map[string][]byte{
+		"tls.key": caKeyPEM,
+		"tls.crt": caCrtPEM,
+	}
+}
diff --git a/pkg/controller/certificatesigningrequests/controller_test.go b/pkg/controller/certificatesigningrequests/controller_test.go
new file mode 100644
index 000000000..5dd39e3ac
--- /dev/null
+++ b/pkg/controller/certificatesigningrequests/controller_test.go
@@ -0,0 +1,638 @@
+/*
+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 certificatesigningrequests
+
+import (
+	"context"
+	"errors"
+	"testing"
+	"time"
+
+	authzv1 "k8s.io/api/authorization/v1"
+	certificatesv1 "k8s.io/api/certificates/v1"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	coretesting "k8s.io/client-go/testing"
+	fakeclock "k8s.io/utils/clock/testing"
+
+	apiutil "github.com/jetstack/cert-manager/pkg/api/util"
+	cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
+	cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1"
+	controllerpkg "github.com/jetstack/cert-manager/pkg/controller"
+	"github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests/fake"
+	"github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests/util"
+	testpkg "github.com/jetstack/cert-manager/pkg/controller/test"
+	"github.com/jetstack/cert-manager/test/unit/gen"
+)
+
+func TestController(t *testing.T) {
+	fixedClockStart := time.Now()
+	fixedClock := fakeclock.NewFakeClock(fixedClockStart)
+	metaFixedClockStart := metav1.NewTime(fixedClockStart)
+
+	signerExpectNoCall := func(t *testing.T) Signer {
+		return &fake.Signer{
+			FakeSign: func(context.Context, *certificatesv1.CertificateSigningRequest, cmapi.GenericIssuer) error {
+				t.Fatal("unexpected sign call")
+				return nil
+			},
+		}
+	}
+
+	sarReactionExpectNoCall := func(t *testing.T) coretesting.ReactionFunc {
+		return func(_ coretesting.Action) (bool, runtime.Object, error) {
+			t.Fatal("unexpected call")
+			return true, nil, nil
+		}
+	}
+	sarReactionAllow := func(t *testing.T) coretesting.ReactionFunc {
+		return func(_ coretesting.Action) (bool, runtime.Object, error) {
+			return true, &authzv1.SubjectAccessReview{
+				Status: authzv1.SubjectAccessReviewStatus{
+					Allowed: true,
+				},
+			}, nil
+		}
+	}
+
+	tests := map[string]struct {
+		// key that should be passed to ProcessItem. If not set, the
+		// 'namespace/name' of the 'CertificateSigningRequest' field will be used.
+		// If neither is set, the key will be "".
+		key string
+
+		// CertificateSigningRequest to be synced for the test. If not set, the
+		// 'key' will be passed to ProcessItem instead.
+		existingCSR *certificatesv1.CertificateSigningRequest
+
+		// If not nil, generic issuer object will be made available for the test
+		existingIssuer runtime.Object
+
+		signerType string
+
+		signerImpl      func(t *testing.T) Signer
+		sarReaction     func(t *testing.T) coretesting.ReactionFunc
+		wantSARCreation []*authzv1.SubjectAccessReview
+
+		// wantEvent, if set, is an 'event string' that is expected to be fired.
+		wantEvent string
+
+		// wantConditions is the expected set of conditions on the
+		// CertificateSigningRequest resource if an Update is made.
+		// If nil, no update is expected.
+		// If empty, an update to the empty set/nil is expected.
+		wantConditions []certificatesv1.CertificateSigningRequestCondition
+
+		wantErr bool
+	}{
+		"do nothing if an empty 'key' is used": {
+			signerType:  apiutil.IssuerCA,
+			signerImpl:  signerExpectNoCall,
+			sarReaction: sarReactionExpectNoCall,
+		},
+		"do nothing if an invalid 'key' is used": {
+			key:         "abc/def/ghi",
+			signerType:  apiutil.IssuerCA,
+			signerImpl:  signerExpectNoCall,
+			sarReaction: sarReactionExpectNoCall,
+		},
+		"do nothing if a key references a CertificateSigningRequest that does not exist": {
+			key:         "namespace/name",
+			signerType:  apiutil.IssuerCA,
+			signerImpl:  signerExpectNoCall,
+			sarReaction: sarReactionExpectNoCall,
+		},
+		"do nothing if a key references a CertificateSigningRequest that has a malformed SignerName for cert-manager.io": {
+			signerType: apiutil.IssuerCA,
+			existingCSR: gen.CertificateSigningRequest("csr-1",
+				gen.SetCertificateSigningRequestSignerName("malformed.signer.name/"),
+			),
+			signerImpl:  signerExpectNoCall,
+			sarReaction: sarReactionExpectNoCall,
+		},
+		"if CertificateSigningRequest references the cert-manager.io signer group but the type is not recognised, should ignore": {
+			signerType: apiutil.IssuerCA,
+			existingCSR: gen.CertificateSigningRequest("csr-1",
+				gen.SetCertificateSigningRequestSignerName("foo.cert-manager.io/hello.world"),
+				gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
+					Type:    certificatesv1.CertificateApproved,
+					Status:  corev1.ConditionTrue,
+					Reason:  "ApprovedReason",
+					Message: "Approved message",
+				}),
+			),
+			signerImpl:  signerExpectNoCall,
+			sarReaction: sarReactionExpectNoCall,
+		},
+		"do nothing if CertificateSigningRequest has a SignerName not for cert-manager.io": {
+			signerType: apiutil.IssuerCA,
+			existingCSR: gen.CertificateSigningRequest("csr-1",
+				gen.SetCertificateSigningRequestSignerName("issuers.my-group.io/hello.world"),
+			),
+			signerImpl:  signerExpectNoCall,
+			sarReaction: sarReactionExpectNoCall,
+		},
+		"do nothing if CertificateSigningRequest is marked as Failed": {
+			signerType: apiutil.IssuerCA,
+			existingCSR: gen.CertificateSigningRequest("csr-1",
+				gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"),
+				gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
+					Type:    certificatesv1.CertificateFailed,
+					Status:  corev1.ConditionTrue,
+					Reason:  "FailedReason",
+					Message: "Failed message",
+				}),
+			),
+			signerImpl:  signerExpectNoCall,
+			sarReaction: sarReactionExpectNoCall,
+		},
+		"do nothing if CertificateSigningRequest is no yet approved": {
+			signerType: apiutil.IssuerCA,
+			existingCSR: gen.CertificateSigningRequest("csr-1",
+				gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"),
+			),
+			signerImpl:  signerExpectNoCall,
+			sarReaction: sarReactionExpectNoCall,
+		},
+		"do nothing if CertificateSigningRequest already has a non empty Certificate present": {
+			signerType: apiutil.IssuerCA,
+			existingCSR: gen.CertificateSigningRequest("csr-1",
+				gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"),
+				gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
+					Type:    certificatesv1.CertificateApproved,
+					Status:  corev1.ConditionTrue,
+					Reason:  "ApprovedReason",
+					Message: "Approved message",
+				}),
+				gen.SetCertificateSigningRequestCertificate([]byte("non-empty-certificate")),
+			),
+			signerImpl:  signerExpectNoCall,
+			sarReaction: sarReactionExpectNoCall,
+		},
+		"if CertificateSigningRequest references an Issuer that does not exist, should fire an event that it can't be found": {
+			signerType: apiutil.IssuerCA,
+			existingCSR: gen.CertificateSigningRequest("csr-1",
+				gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"),
+				gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
+					Type:    certificatesv1.CertificateApproved,
+					Status:  corev1.ConditionTrue,
+					Reason:  "ApprovedReason",
+					Message: "Approved message",
+				}),
+			),
+			signerImpl:     signerExpectNoCall,
+			sarReaction:    sarReactionExpectNoCall,
+			existingIssuer: nil,
+			wantEvent:      "Warning IssuerNotFound Referenced Issuer hello/world not found",
+		},
+		"if CertificateSigningRequest references an Issuer that does not yet have a type, should fire an event it doesn't have a type": {
+			signerType: apiutil.IssuerCA,
+			existingCSR: gen.CertificateSigningRequest("csr-1",
+				gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"),
+				gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
+					Type:    certificatesv1.CertificateApproved,
+					Status:  corev1.ConditionTrue,
+					Reason:  "ApprovedReason",
+					Message: "Approved message",
+				}),
+			),
+			signerImpl:     signerExpectNoCall,
+			sarReaction:    sarReactionExpectNoCall,
+			existingIssuer: gen.Issuer("world", gen.SetIssuerNamespace("hello")),
+			wantEvent:      "Warning IssuerTypeMissing Referenced Issuer hello/world is missing type",
+		},
+		"if CertificateSigningRequest references an Issuer which does not match the same signer type, should ignore": {
+			signerType: apiutil.IssuerSelfSigned,
+			existingCSR: gen.CertificateSigningRequest("csr-1",
+				gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"),
+				gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
+					Type:    certificatesv1.CertificateApproved,
+					Status:  corev1.ConditionTrue,
+					Reason:  "ApprovedReason",
+					Message: "Approved message",
+				}),
+			),
+			signerImpl:  signerExpectNoCall,
+			sarReaction: sarReactionExpectNoCall,
+			existingIssuer: gen.Issuer("world", gen.SetIssuerNamespace("hello"),
+				gen.SetIssuerCA(cmapi.CAIssuer{
+					SecretName: "tls",
+				}),
+			),
+		},
+		"do nothing if CertificateSigningRequest references a signer that is not 'issuers' or 'clusterissuers'": {
+			signerType: apiutil.IssuerCA,
+			existingCSR: gen.CertificateSigningRequest("csr-1",
+				gen.SetCertificateSigningRequestSignerName("not-issuers.cert-manager.io/hello.world"),
+				gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
+					Type:    certificatesv1.CertificateApproved,
+					Status:  corev1.ConditionTrue,
+					Reason:  "ApprovedReason",
+					Message: "Approved message",
+				}),
+			),
+			signerImpl:  signerExpectNoCall,
+			sarReaction: sarReactionExpectNoCall,
+		},
+		"if CertificateSigningRequest references a issuers signer but the SubjectAccessReview errors, should error": {
+			signerType: apiutil.IssuerCA,
+			existingCSR: gen.CertificateSigningRequest("csr-1",
+				gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"),
+				gen.SetCertificateSigningRequestUsername("user-1"),
+				gen.SetCertificateSigningRequestGroups([]string{"group-1", "group-2"}),
+				gen.SetCertificateSigningRequestUID("uid-1"),
+				gen.SetCertificateSigningRequestExtra(map[string]certificatesv1.ExtraValue{
+					"extra": []string{"1", "2"},
+				}),
+				gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
+					Type:    certificatesv1.CertificateApproved,
+					Status:  corev1.ConditionTrue,
+					Reason:  "ApprovedReason",
+					Message: "Approved message",
+				}),
+			),
+			signerImpl: signerExpectNoCall,
+			sarReaction: func(t *testing.T) coretesting.ReactionFunc {
+				return func(_ coretesting.Action) (bool, runtime.Object, error) {
+					return true, nil, errors.New("this is a simulated error")
+				}
+			},
+			wantSARCreation: []*authzv1.SubjectAccessReview{
+				{
+					Spec: authzv1.SubjectAccessReviewSpec{
+						User:   "user-1",
+						Groups: []string{"group-1", "group-2"},
+						Extra: map[string]authzv1.ExtraValue{
+							"extra": []string{"1", "2"},
+						},
+						UID: "uid-1",
+
+						ResourceAttributes: &authzv1.ResourceAttributes{
+							Group:     "cert-manager.io",
+							Resource:  "signers",
+							Verb:      "reference",
+							Namespace: "hello",
+							Name:      "world",
+							Version:   "*",
+						},
+					},
+				},
+			},
+			existingIssuer: gen.Issuer("world", gen.SetIssuerNamespace("hello"),
+				gen.SetIssuerCA(cmapi.CAIssuer{
+					SecretName: "tls",
+				}),
+			),
+			wantErr: true,
+		},
+		"if CertificateSigningRequest references a issuers signer but the requesting user does not have permissions, should update Failed": {
+			signerType: apiutil.IssuerCA,
+			existingCSR: gen.CertificateSigningRequest("csr-1",
+				gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"),
+				gen.SetCertificateSigningRequestUsername("user-1"),
+				gen.SetCertificateSigningRequestGroups([]string{"group-1", "group-2"}),
+				gen.SetCertificateSigningRequestUID("uid-1"),
+				gen.SetCertificateSigningRequestExtra(map[string]certificatesv1.ExtraValue{
+					"extra": []string{"1", "2"},
+				}),
+				gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
+					Type:    certificatesv1.CertificateApproved,
+					Status:  corev1.ConditionTrue,
+					Reason:  "ApprovedReason",
+					Message: "Approved message",
+				}),
+			),
+			signerImpl: signerExpectNoCall,
+			sarReaction: func(t *testing.T) coretesting.ReactionFunc {
+				return func(_ coretesting.Action) (bool, runtime.Object, error) {
+					return true, &authzv1.SubjectAccessReview{
+						Status: authzv1.SubjectAccessReviewStatus{
+							Allowed: false,
+						},
+					}, nil
+				}
+			},
+			wantSARCreation: []*authzv1.SubjectAccessReview{
+				{
+					Spec: authzv1.SubjectAccessReviewSpec{
+						User:   "user-1",
+						Groups: []string{"group-1", "group-2"},
+						Extra: map[string]authzv1.ExtraValue{
+							"extra": []string{"1", "2"},
+						},
+						UID: "uid-1",
+
+						ResourceAttributes: &authzv1.ResourceAttributes{
+							Group:     "cert-manager.io",
+							Resource:  "signers",
+							Verb:      "reference",
+							Namespace: "hello",
+							Name:      "world",
+							Version:   "*",
+						},
+					},
+				},
+				{
+					Spec: authzv1.SubjectAccessReviewSpec{
+						User:   "user-1",
+						Groups: []string{"group-1", "group-2"},
+						Extra: map[string]authzv1.ExtraValue{
+							"extra": []string{"1", "2"},
+						},
+						UID: "uid-1",
+
+						ResourceAttributes: &authzv1.ResourceAttributes{
+							Group:     "cert-manager.io",
+							Resource:  "signers",
+							Verb:      "reference",
+							Namespace: "hello",
+							Name:      "*",
+							Version:   "*",
+						},
+					},
+				},
+			},
+			existingIssuer: gen.Issuer("world", gen.SetIssuerNamespace("hello"),
+				gen.SetIssuerCA(cmapi.CAIssuer{
+					SecretName: "tls",
+				}),
+			),
+			wantEvent: "Warning DeniedReference Requester may not reference Namespaced Issuer hello/world",
+			wantConditions: []certificatesv1.CertificateSigningRequestCondition{
+				{
+					Type:    certificatesv1.CertificateApproved,
+					Status:  corev1.ConditionTrue,
+					Reason:  "ApprovedReason",
+					Message: "Approved message",
+				},
+				{
+					Type:               certificatesv1.CertificateFailed,
+					Status:             corev1.ConditionTrue,
+					Reason:             "DeniedReference",
+					Message:            "Requester may not reference Namespaced Issuer hello/world",
+					LastTransitionTime: metaFixedClockStart,
+					LastUpdateTime:     metaFixedClockStart,
+				},
+			},
+		},
+		"if CertificateSigningRequest references a issuers signer but the Issuer is not ready, fire event not Ready": {
+			signerType: apiutil.IssuerCA,
+			existingCSR: gen.CertificateSigningRequest("csr-1",
+				gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"),
+				gen.SetCertificateSigningRequestUsername("user-1"),
+				gen.SetCertificateSigningRequestGroups([]string{"group-1", "group-2"}),
+				gen.SetCertificateSigningRequestUID("uid-1"),
+				gen.SetCertificateSigningRequestExtra(map[string]certificatesv1.ExtraValue{
+					"extra": []string{"1", "2"},
+				}),
+				gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
+					Type:    certificatesv1.CertificateApproved,
+					Status:  corev1.ConditionTrue,
+					Reason:  "ApprovedReason",
+					Message: "Approved message",
+				}),
+			),
+			signerImpl:  signerExpectNoCall,
+			sarReaction: sarReactionAllow,
+			wantSARCreation: []*authzv1.SubjectAccessReview{
+				{
+					Spec: authzv1.SubjectAccessReviewSpec{
+						User:   "user-1",
+						Groups: []string{"group-1", "group-2"},
+						Extra: map[string]authzv1.ExtraValue{
+							"extra": []string{"1", "2"},
+						},
+						UID: "uid-1",
+
+						ResourceAttributes: &authzv1.ResourceAttributes{
+							Group:     "cert-manager.io",
+							Resource:  "signers",
+							Verb:      "reference",
+							Namespace: "hello",
+							Name:      "world",
+							Version:   "*",
+						},
+					},
+				},
+			},
+			wantEvent: "Warning IssuerNotReady Referenced Issuer hello/world does not have a Ready status condition",
+			existingIssuer: gen.Issuer("world", gen.SetIssuerNamespace("hello"),
+				gen.SetIssuerCA(cmapi.CAIssuer{
+					SecretName: "tls",
+				}),
+			),
+		},
+		"if CertificateSigningRequest called invoked sign but it errors, should return error": {
+			signerType: apiutil.IssuerCA,
+			existingCSR: gen.CertificateSigningRequest("csr-1",
+				gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"),
+				gen.SetCertificateSigningRequestUsername("user-1"),
+				gen.SetCertificateSigningRequestGroups([]string{"group-1", "group-2"}),
+				gen.SetCertificateSigningRequestUID("uid-1"),
+				gen.SetCertificateSigningRequestExtra(map[string]certificatesv1.ExtraValue{
+					"extra": []string{"1", "2"},
+				}),
+				gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
+					Type:    certificatesv1.CertificateApproved,
+					Status:  corev1.ConditionTrue,
+					Reason:  "ApprovedReason",
+					Message: "Approved message",
+				}),
+			),
+			signerImpl: func(t *testing.T) Signer {
+				return &fake.Signer{
+					FakeSign: func(context.Context, *certificatesv1.CertificateSigningRequest, cmapi.GenericIssuer) error {
+						return errors.New("this is a simulated error")
+					},
+				}
+			},
+			sarReaction: sarReactionAllow,
+			wantSARCreation: []*authzv1.SubjectAccessReview{
+				{
+					Spec: authzv1.SubjectAccessReviewSpec{
+						User:   "user-1",
+						Groups: []string{"group-1", "group-2"},
+						Extra: map[string]authzv1.ExtraValue{
+							"extra": []string{"1", "2"},
+						},
+						UID: "uid-1",
+
+						ResourceAttributes: &authzv1.ResourceAttributes{
+							Group:     "cert-manager.io",
+							Resource:  "signers",
+							Verb:      "reference",
+							Namespace: "hello",
+							Name:      "world",
+							Version:   "*",
+						},
+					},
+				},
+			},
+			existingIssuer: gen.Issuer("world", gen.SetIssuerNamespace("hello"),
+				gen.SetIssuerCA(cmapi.CAIssuer{
+					SecretName: "tls",
+				}),
+				gen.AddIssuerCondition(cmapi.IssuerCondition{
+					Type:    cmapi.IssuerConditionReady,
+					Status:  cmmeta.ConditionTrue,
+					Reason:  "IssuerReady",
+					Message: "Issuer ready message",
+				}),
+			),
+			wantErr: true,
+		},
+		"if CertificateSigningRequest called invoked sign and doesn't error, should return no error": {
+			signerType: apiutil.IssuerCA,
+			existingCSR: gen.CertificateSigningRequest("csr-1",
+				gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"),
+				gen.SetCertificateSigningRequestUsername("user-1"),
+				gen.SetCertificateSigningRequestGroups([]string{"group-1", "group-2"}),
+				gen.SetCertificateSigningRequestUID("uid-1"),
+				gen.SetCertificateSigningRequestExtra(map[string]certificatesv1.ExtraValue{
+					"extra": []string{"1", "2"},
+				}),
+				gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
+					Type:    certificatesv1.CertificateApproved,
+					Status:  corev1.ConditionTrue,
+					Reason:  "ApprovedReason",
+					Message: "Approved message",
+				}),
+			),
+			signerImpl: func(t *testing.T) Signer {
+				return &fake.Signer{
+					FakeSign: func(context.Context, *certificatesv1.CertificateSigningRequest, cmapi.GenericIssuer) error {
+						return nil
+					},
+				}
+			},
+			sarReaction: sarReactionAllow,
+			wantSARCreation: []*authzv1.SubjectAccessReview{
+				{
+					Spec: authzv1.SubjectAccessReviewSpec{
+						User:   "user-1",
+						Groups: []string{"group-1", "group-2"},
+						Extra: map[string]authzv1.ExtraValue{
+							"extra": []string{"1", "2"},
+						},
+						UID: "uid-1",
+
+						ResourceAttributes: &authzv1.ResourceAttributes{
+							Group:     "cert-manager.io",
+							Resource:  "signers",
+							Verb:      "reference",
+							Namespace: "hello",
+							Name:      "world",
+							Version:   "*",
+						},
+					},
+				},
+			},
+			existingIssuer: gen.Issuer("world", gen.SetIssuerNamespace("hello"),
+				gen.SetIssuerCA(cmapi.CAIssuer{
+					SecretName: "tls",
+				}),
+				gen.AddIssuerCondition(cmapi.IssuerCondition{
+					Type:    cmapi.IssuerConditionReady,
+					Status:  cmmeta.ConditionTrue,
+					Reason:  "IssuerReady",
+					Message: "Issuer ready message",
+				}),
+			),
+			wantErr: false,
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			util.Clock = fixedClock
+			builder := &testpkg.Builder{
+				T:     t,
+				Clock: fixedClock,
+			}
+			if test.existingIssuer != nil {
+				builder.CertManagerObjects = append(builder.CertManagerObjects, test.existingIssuer)
+			}
+			if test.existingCSR != nil {
+				builder.KubeObjects = append(builder.KubeObjects, test.existingCSR)
+			}
+
+			for i := range test.wantSARCreation {
+				builder.ExpectedActions = append(builder.ExpectedActions,
+					testpkg.NewAction(coretesting.NewCreateAction(
+						authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews"),
+						"",
+						test.wantSARCreation[i],
+					)),
+				)
+			}
+
+			builder.Init()
+
+			builder.FakeKubeClient().PrependReactor("create", "*", func(action coretesting.Action) (bool, runtime.Object, error) {
+				if action.GetResource() != authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews") {
+					return false, nil, nil
+				}
+				return test.sarReaction(t)(action)
+			})
+
+			controller := New(test.signerType, test.signerImpl(t))
+			_, _, err := controller.Register(builder.Context)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			if test.wantConditions != nil {
+				if test.existingCSR == nil {
+					t.Fatal("cannot expect an Update operation if test.existingCSR is nil")
+				}
+				expectedCSR := test.existingCSR.DeepCopy()
+				expectedCSR.Status.Conditions = test.wantConditions
+				builder.ExpectedActions = append(builder.ExpectedActions,
+					testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
+						certificatesv1.SchemeGroupVersion.WithResource("certificatesigningrequests"),
+						"status",
+						"",
+						expectedCSR,
+					)),
+				)
+			}
+			if test.wantEvent != "" {
+				builder.ExpectedEvents = []string{test.wantEvent}
+			}
+
+			builder.Start()
+			defer builder.Stop()
+
+			key := test.key
+			if key == "" && test.existingCSR != nil {
+				key, err = controllerpkg.KeyFunc(test.existingCSR)
+				if err != nil {
+					t.Fatal(err)
+				}
+			}
+
+			gotErr := controller.ProcessItem(context.Background(), key)
+			if test.wantErr != (gotErr != nil) {
+				t.Errorf("got unexpected error, exp=%t got=%v",
+					test.wantErr, gotErr)
+			}
+
+			builder.CheckAndFinish()
+		})
+	}
+}
diff --git a/pkg/controller/certificatesigningrequests/util/signername_test.go b/pkg/controller/certificatesigningrequests/util/signername_test.go
new file mode 100644
index 000000000..b7a12255d
--- /dev/null
+++ b/pkg/controller/certificatesigningrequests/util/signername_test.go
@@ -0,0 +1,131 @@
+/*
+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 util
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestIssuerRefFromSignerName(t *testing.T) {
+	tests := map[string]struct {
+		inpName            string
+		expSignerIssuerRef SignerIssuerRef
+		expOK              bool
+	}{
+		"an empty name should return false": {
+			inpName:            "",
+			expSignerIssuerRef: SignerIssuerRef{},
+			expOK:              false,
+		},
+		"a reference without a name should return false": {
+			inpName:            "foo.bar",
+			expSignerIssuerRef: SignerIssuerRef{},
+			expOK:              false,
+		},
+		"a reference with a '/' but no name should return false": {
+			inpName:            "foo.bar/",
+			expSignerIssuerRef: SignerIssuerRef{},
+			expOK:              false,
+		},
+		"a reference with no host should return false": {
+			inpName:            "/foo.bar",
+			expSignerIssuerRef: SignerIssuerRef{},
+			expOK:              false,
+		},
+		"a reference with only one domain should return false": {
+			inpName:            "abc/hello-world",
+			expSignerIssuerRef: SignerIssuerRef{},
+			expOK:              false,
+		},
+		"a reference with multiple dots in the path should return a name with multiple dots": {
+			inpName: "foo.bar/hello.world.123",
+			expSignerIssuerRef: SignerIssuerRef{
+				Namespace: "hello",
+				Name:      "world.123",
+				Type:      "foo",
+				Group:     "bar",
+			},
+			expOK: true,
+		},
+		"a reference with 2 domains and 2 names should return namespaced issuer": {
+			inpName: "foo.bar/hello.world",
+			expSignerIssuerRef: SignerIssuerRef{
+				Namespace: "hello",
+				Name:      "world",
+				Type:      "foo",
+				Group:     "bar",
+			},
+			expOK: true,
+		},
+		"a reference with 4 domains and 4 names should return namespaced issuer": {
+			inpName: "foo.bar.abc.dbc/hello.world.123.456",
+			expSignerIssuerRef: SignerIssuerRef{
+				Namespace: "hello",
+				Name:      "world.123.456",
+				Type:      "foo",
+				Group:     "bar.abc.dbc",
+			},
+			expOK: true,
+		},
+		"a reference with 2 domains and one name should return cluster issuer": {
+			inpName: "foo.bar/hello-world",
+			expSignerIssuerRef: SignerIssuerRef{
+				Namespace: "",
+				Name:      "hello-world",
+				Type:      "foo",
+				Group:     "bar",
+			},
+			expOK: true,
+		},
+		"a reference with 4 domains and 1 name should return cluster issuer": {
+			inpName: "foo.bar.abc.dbc/hello-world",
+			expSignerIssuerRef: SignerIssuerRef{
+				Namespace: "",
+				Name:      "hello-world",
+				Type:      "foo",
+				Group:     "bar.abc.dbc",
+			},
+			expOK: true,
+		},
+		"a clusterissuers reference with 4 domains and multiple names should return no Namespace and multiple domain name": {
+			inpName: "clusterissuers.bar.abc.dbc/hello.world.123.456",
+			expSignerIssuerRef: SignerIssuerRef{
+				Namespace: "",
+				Name:      "hello.world.123.456",
+				Type:      "clusterissuers",
+				Group:     "bar.abc.dbc",
+			},
+			expOK: true,
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			ref, ok := SignerIssuerRefFromSignerName(test.inpName)
+			if ok != test.expOK {
+				t.Errorf("unexpected ok, exp=%t got=%t",
+					test.expOK, ok)
+			}
+
+			if !reflect.DeepEqual(ref, test.expSignerIssuerRef) {
+				t.Errorf("unexpected SignerIssuerRef, exp=%v got=%v",
+					test.expSignerIssuerRef, ref)
+			}
+		})
+	}
+}
diff --git a/pkg/util/pki/kube_test.go b/pkg/util/pki/kube_test.go
new file mode 100644
index 000000000..3fb59a0e8
--- /dev/null
+++ b/pkg/util/pki/kube_test.go
@@ -0,0 +1,205 @@
+/*
+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 pki_test
+
+import (
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"math"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	certificatesv1 "k8s.io/api/certificates/v1"
+
+	"github.com/jetstack/cert-manager/pkg/util/pki"
+	"github.com/jetstack/cert-manager/test/unit/gen"
+)
+
+func TestGenerateTemplateFromCertificateSigningRequest(t *testing.T) {
+	csr, pk, err := gen.CSR(x509.RSA, gen.SetCSRDNSNames("example.com", "foo.example.com"))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	tests := map[string]struct {
+		csr            *certificatesv1.CertificateSigningRequest
+		expCertificate *x509.Certificate
+		expErr         bool
+	}{
+		"a CSR that contains an invalid duration should return an error": {
+			csr: gen.CertificateSigningRequest("",
+				gen.SetCertificateSigningRequestDuration("bad-duration"),
+				gen.SetCertificateSigningRequestUsages([]certificatesv1.KeyUsage{
+					certificatesv1.UsageKeyEncipherment,
+					certificatesv1.UsageDigitalSignature,
+				}),
+				gen.SetCertificateSigningRequestRequest(csr),
+			),
+			expCertificate: nil,
+			expErr:         true,
+		},
+		"a CSR that contains invalid usages should return an error": {
+			csr: gen.CertificateSigningRequest("",
+				gen.SetCertificateSigningRequestDuration("10m"),
+				gen.SetCertificateSigningRequestUsages([]certificatesv1.KeyUsage{
+					certificatesv1.UsageKeyEncipherment,
+					certificatesv1.KeyUsage("bad-usage"),
+				}),
+				gen.SetCertificateSigningRequestRequest(csr),
+			),
+			expCertificate: nil,
+			expErr:         true,
+		},
+		"a CSR with isCA=true that is valid should return a valid *x509.Certificate": {
+			csr: gen.CertificateSigningRequest("",
+				gen.SetCertificateSigningRequestDuration("10m"),
+				gen.SetCertificateSigningRequestUsages([]certificatesv1.KeyUsage{
+					certificatesv1.UsageAny,
+					certificatesv1.UsageDigitalSignature,
+					certificatesv1.UsageCRLSign,
+					certificatesv1.UsageCodeSigning,
+					certificatesv1.UsageContentCommitment,
+				}),
+				gen.SetCertificateSigningRequestIsCA(true),
+				gen.SetCertificateSigningRequestRequest(csr),
+			),
+			expCertificate: &x509.Certificate{
+				Version:               0,
+				BasicConstraintsValid: true,
+				SerialNumber:          nil,
+				PublicKeyAlgorithm:    x509.RSA,
+				PublicKey:             pk.Public(),
+				IsCA:                  true,
+				Subject: pkix.Name{
+					CommonName: "example.com",
+				},
+				NotBefore: time.Now(),
+				NotAfter:  time.Now().Add(10 * time.Minute),
+				KeyUsage:  x509.KeyUsageDigitalSignature | x509.KeyUsageCRLSign | x509.KeyUsageContentCommitment,
+				ExtKeyUsage: []x509.ExtKeyUsage{
+					x509.ExtKeyUsageAny,
+					x509.ExtKeyUsageCodeSigning,
+				},
+				DNSNames: []string{"example.com", "foo.example.com"},
+			},
+		},
+		"a CSR with isCA=false that is valid should return a valid *x509.Certificate": {
+			csr: gen.CertificateSigningRequest("",
+				gen.SetCertificateSigningRequestDuration("10m"),
+				gen.SetCertificateSigningRequestUsages([]certificatesv1.KeyUsage{
+					certificatesv1.UsageAny,
+					certificatesv1.UsageDigitalSignature,
+					certificatesv1.UsageCRLSign,
+					certificatesv1.UsageCodeSigning,
+					certificatesv1.UsageContentCommitment,
+				}),
+				gen.SetCertificateSigningRequestIsCA(false),
+				gen.SetCertificateSigningRequestRequest(csr),
+			),
+			expCertificate: &x509.Certificate{
+				Version:               0,
+				BasicConstraintsValid: true,
+				SerialNumber:          nil,
+				PublicKeyAlgorithm:    x509.RSA,
+				PublicKey:             pk.Public(),
+				IsCA:                  false,
+				Subject: pkix.Name{
+					CommonName: "example.com",
+				},
+				NotBefore: time.Now(),
+				NotAfter:  time.Now().Add(10 * time.Minute),
+				KeyUsage:  x509.KeyUsageDigitalSignature | x509.KeyUsageCRLSign | x509.KeyUsageContentCommitment,
+				ExtKeyUsage: []x509.ExtKeyUsage{
+					x509.ExtKeyUsageAny,
+					x509.ExtKeyUsageCodeSigning,
+				},
+				DNSNames: []string{"example.com", "foo.example.com"},
+			},
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			templ, err := pki.GenerateTemplateFromCertificateSigningRequest(test.csr)
+			assert.Equal(t, test.expErr, err != nil)
+
+			if err == nil {
+				deltaSec := math.Abs(test.expCertificate.NotAfter.Sub(templ.NotAfter).Seconds())
+				assert.LessOrEqualf(t, deltaSec, 1., "expected a time delta lower than 1 second. Time expected='%s', got='%s'",
+					test.expCertificate.NotAfter.String(),
+					templ.NotAfter.String(),
+				)
+
+				// Null out field which contain values with randomised fields or are
+				// not worth checking.
+				test.expCertificate.NotAfter = time.Time{}
+				test.expCertificate.NotBefore = time.Time{}
+				templ.NotAfter = time.Time{}
+				templ.NotBefore = time.Time{}
+				templ.SerialNumber = nil
+				templ.Subject.Names = nil
+
+				assert.Equal(t, test.expCertificate, templ)
+			}
+		})
+	}
+}
+
+func TestBuildKeyUsagesKube(t *testing.T) {
+	tests := map[string]struct {
+		usages []certificatesv1.KeyUsage
+		expKU  x509.KeyUsage
+		expEKU []x509.ExtKeyUsage
+		expErr bool
+	}{
+		"no input usages should return defaults": {
+			usages: nil,
+			expKU:  x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
+			expErr: false,
+		},
+		"unknown usages should return an error": {
+			usages: []certificatesv1.KeyUsage{certificatesv1.UsageAny, certificatesv1.KeyUsage("unknown-")},
+			expKU:  -1,
+			expErr: true,
+		},
+		"multiple valid usages should return those usages": {
+			usages: []certificatesv1.KeyUsage{
+				certificatesv1.UsageAny,
+				certificatesv1.UsageDigitalSignature,
+				certificatesv1.UsageCRLSign,
+				certificatesv1.UsageCodeSigning,
+				certificatesv1.UsageContentCommitment,
+			},
+			expKU: x509.KeyUsageDigitalSignature | x509.KeyUsageCRLSign | x509.KeyUsageContentCommitment,
+			expEKU: []x509.ExtKeyUsage{
+				x509.ExtKeyUsageAny,
+				x509.ExtKeyUsageCodeSigning,
+			},
+			expErr: false,
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			ku, eku, err := pki.BuildKeyUsagesKube(test.usages)
+			assert.Equal(t, test.expKU, ku)
+			assert.Equal(t, test.expEKU, eku)
+			assert.Equal(t, test.expErr, err != nil)
+		})
+	}
+}
diff --git a/test/unit/gen/BUILD.bazel b/test/unit/gen/BUILD.bazel
index 2f8c6d9bd..2943a94d3 100644
--- a/test/unit/gen/BUILD.bazel
+++ b/test/unit/gen/BUILD.bazel
@@ -5,6 +5,7 @@ go_library(
     srcs = [
         "certificate.go",
         "certificaterequest.go",
+        "certificatesigningrequest.go",
         "challenge.go",
         "conditions.go",
         "csr.go",
@@ -19,8 +20,10 @@ go_library(
     deps = [
         "//pkg/apis/acme/v1:go_default_library",
         "//pkg/apis/certmanager/v1:go_default_library",
+        "//pkg/apis/experimental/v1alpha1:go_default_library",
         "//pkg/apis/meta/v1:go_default_library",
         "//pkg/util/pki:go_default_library",
+        "@io_k8s_api//certificates/v1:go_default_library",
         "@io_k8s_api//core/v1:go_default_library",
         "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
         "@io_k8s_apimachinery//pkg/types:go_default_library",
diff --git a/test/unit/gen/certificatesigningrequest.go b/test/unit/gen/certificatesigningrequest.go
new file mode 100644
index 000000000..89561dcd5
--- /dev/null
+++ b/test/unit/gen/certificatesigningrequest.go
@@ -0,0 +1,148 @@
+/*
+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 gen
+
+import (
+	"encoding/base64"
+	"strconv"
+
+	certificatesv1 "k8s.io/api/certificates/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	experimentalapi "github.com/jetstack/cert-manager/pkg/apis/experimental/v1alpha1"
+)
+
+type CertificateSigningRequestModifier func(*certificatesv1.CertificateSigningRequest)
+
+func CertificateSigningRequest(name string, mods ...CertificateSigningRequestModifier) *certificatesv1.CertificateSigningRequest {
+	c := &certificatesv1.CertificateSigningRequest{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:        name,
+			Annotations: make(map[string]string),
+			Labels:      make(map[string]string),
+		},
+	}
+	for _, mod := range mods {
+		mod(c)
+	}
+	return c
+}
+
+func CertificateSigningRequestFrom(cr *certificatesv1.CertificateSigningRequest, mods ...CertificateSigningRequestModifier) *certificatesv1.CertificateSigningRequest {
+	cr = cr.DeepCopy()
+	for _, mod := range mods {
+		mod(cr)
+	}
+	return cr
+}
+
+func SetCertificateSigningRequestIsCA(isCA bool) CertificateSigningRequestModifier {
+	return AddCertificateSigningRequestAnnotations(map[string]string{
+		experimentalapi.CertificateSigningRequestIsCAAnnotationKey: strconv.FormatBool(isCA),
+	})
+}
+
+func SetCertificateSigningRequestRequest(request []byte) CertificateSigningRequestModifier {
+	return func(csr *certificatesv1.CertificateSigningRequest) {
+		csr.Spec.Request = request
+	}
+}
+
+func AddCertificateSigningRequestAnnotations(annotations map[string]string) CertificateSigningRequestModifier {
+	return func(csr *certificatesv1.CertificateSigningRequest) {
+		// Make sure to do a merge here with new annotations overriding.
+		annotationsNew := csr.GetAnnotations()
+		if annotationsNew == nil {
+			annotationsNew = make(map[string]string)
+		}
+		for k, v := range annotations {
+			annotationsNew[k] = v
+		}
+		csr.SetAnnotations(annotationsNew)
+	}
+}
+
+func SetCertificateSigningRequestSignerName(signerName string) CertificateSigningRequestModifier {
+	return func(csr *certificatesv1.CertificateSigningRequest) {
+		csr.Spec.SignerName = signerName
+	}
+}
+
+func SetCertificateSigningRequestDuration(duration string) CertificateSigningRequestModifier {
+	return AddCertificateSigningRequestAnnotations(map[string]string{
+		experimentalapi.CertificateSigningRequestDurationAnnotationKey: duration,
+	})
+}
+
+func SetCertificateSigningRequestCertificate(cert []byte) CertificateSigningRequestModifier {
+	return func(csr *certificatesv1.CertificateSigningRequest) {
+		csr.Status.Certificate = cert
+	}
+}
+
+func SetCertificateSigningRequestCA(ca []byte) CertificateSigningRequestModifier {
+	return AddCertificateSigningRequestAnnotations(map[string]string{
+		experimentalapi.CertificateSigningRequestCAAnnotationKey: base64.StdEncoding.EncodeToString(ca),
+	})
+}
+
+func SetCertificateSigningRequestStatusCondition(c certificatesv1.CertificateSigningRequestCondition) CertificateSigningRequestModifier {
+	return func(csr *certificatesv1.CertificateSigningRequest) {
+		if len(csr.Status.Conditions) == 0 {
+			csr.Status.Conditions = []certificatesv1.CertificateSigningRequestCondition{c}
+			return
+		}
+
+		for i, existingC := range csr.Status.Conditions {
+			if existingC.Type == c.Type {
+				csr.Status.Conditions[i] = c
+				return
+			}
+		}
+		csr.Status.Conditions = append(csr.Status.Conditions, c)
+	}
+}
+
+func SetCertificateSigningRequestUsername(username string) CertificateSigningRequestModifier {
+	return func(csr *certificatesv1.CertificateSigningRequest) {
+		csr.Spec.Username = username
+	}
+}
+
+func SetCertificateSigningRequestGroups(groups []string) CertificateSigningRequestModifier {
+	return func(csr *certificatesv1.CertificateSigningRequest) {
+		csr.Spec.Groups = groups
+	}
+}
+
+func SetCertificateSigningRequestUID(uid string) CertificateSigningRequestModifier {
+	return func(csr *certificatesv1.CertificateSigningRequest) {
+		csr.Spec.UID = uid
+	}
+}
+
+func SetCertificateSigningRequestExtra(extra map[string]certificatesv1.ExtraValue) CertificateSigningRequestModifier {
+	return func(csr *certificatesv1.CertificateSigningRequest) {
+		csr.Spec.Extra = extra
+	}
+}
+
+func SetCertificateSigningRequestUsages(usages []certificatesv1.KeyUsage) CertificateSigningRequestModifier {
+	return func(csr *certificatesv1.CertificateSigningRequest) {
+		csr.Spec.Usages = usages
+	}
+}