Repository: kubernetes-sigs/security-profiles-operator
Pull request #237 (2021-01-26 22:22:26)

Split out ProfileBinding webhook
#### What type of PR is this?

/kind bug
/kind flake

#### What this PR does / why we need it:

The ProfileBinding CRD isn't required for the minimum functionality of
the operator, and it requires installing cert-manager additional
dependency which may not be desireable to all users. Furthermore,
cert-manager is causing intermittent problems in the e2e tests that need
to be worked out. This change splits the webhook into its own command
and its own deployment, and skips deploying and testing it in the e2e
tests. The webhook e2e test can still be run manually by setting the
environment variable E2E_RUN_EXPERIMENTAL=Y.

#### Which issue(s) this PR fixes:

<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.

Fixes #

or

None
-->
Relates to #220 

#### Does this PR have test?

<!--
If tests aren't applicable just write N/A.
-->
N/A

#### Special notes for your reviewer:

#### Does this PR introduce a user-facing change?

```release-note
NONE
```


k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/security-profiles-operator/pull/237#" title="Author self-approved">cmurphy</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **saschagrunert** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @saschagrunert` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fsecurity-profiles-operator).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes-sigs/security-profiles-operator/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["saschagrunert"]} -->
k8s-ci-robot: @cmurphy: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-verify | 0b328d55bdcf5a78c5644ea4e8c3c99a82ffadce | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/237/pull-security-profiles-operator-verify/1354193006348996608) | `/test pull-security-profiles-operator-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=237). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Acmurphy). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->

Patch:
diff --git a/.codecov.yml b/.codecov.yml
new file mode 100644
index 000000000..df2db6cb5
--- /dev/null
+++ b/.codecov.yml
@@ -0,0 +1,33 @@
+---
+codecov:
+  notify:
+    after_n_builds: 1
+    require_ci_to_pass: false
+
+coverage:
+  precision: 2
+  round: down
+  range: 50..75
+
+  status:
+    project:
+      default:
+        enabled: true
+        # allowed to drop coverage and still result in a "success" commit status
+        threshold: null
+        if_not_found: success
+        if_no_uploads: success
+        if_ci_failed: error
+    patch:
+      default:
+        enabled: true
+        threshold: null
+        if_not_found: success
+        if_no_uploads: success
+        if_ci_failed: error
+    changes: false
+
+comment:
+  layout: "header, diff"
+  behavior: default
+  require_changes: false
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 32d65f323..dee7fcf9a 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,4 +1,4 @@
-name: build
+name: macosbuild
 on: [push, pull_request]
 jobs:
   validate:
@@ -15,3 +15,5 @@ jobs:
           go-version: '^1.15'
       - name: unit test
         run: make test-unit
+      - name: verify
+        run: make verify-go-lint
diff --git a/.golangci.yml b/.golangci.yml
index c3a24f1e1..5b0f8270f 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,10 +1,5 @@
 ---
 run:
-  build-tags:
-    - e2e
-    - netgo
-    - osusergo
-    - seccomp
   concurrency: 6
   deadline: 5m
 linters:
diff --git a/Makefile b/Makefile
index ac117b3ba..116931118 100644
--- a/Makefile
+++ b/Makefile
@@ -33,6 +33,14 @@ BUILDTAGS := netgo osusergo seccomp
 else
 BUILDTAGS := netgo osusergo
 endif
+
+ifneq ($(shell uname -s), Darwin)
+LINT_BUILDTAGS := e2e,netgo,osusergo,seccomp
+else
+LINT_BUILDTAGS := e2e,netgo,osusergo
+endif
+
+
 BUILD_FILES := $(shell find . -type f -name '*.go' -or -name '*.mod' -or -name '*.sum' -not -name '*_test.go')
 export GOFLAGS?=-mod=mod
 GO_PROJECT := sigs.k8s.io/$(PROJECT)
@@ -110,6 +118,7 @@ deployments: manifests ## Generate the deployment files with kustomize
 	kustomize build --reorder=none deploy/profiles/base -o deploy/profiles/default-profiles.yaml
 	kustomize build --reorder=none deploy/overlays/namespaced -o deploy/namespace-operator.yaml
 	kustomize build --reorder=none deploy/profiles/overlays/namespaced -o deploy/profiles/namespace-default-profiles.yaml
+	kustomize build --reorder=none deploy/base/webhook -o deploy/webhook.yaml
 
 .PHONY: image
 image: ## Build the container image
@@ -139,7 +148,7 @@ verify-deployments: deployments ## Verify the generated deployments
 
 .PHONY: verify-go-lint
 verify-go-lint: $(BUILD_DIR)/golangci-lint ## Verify the golang code by linting
-	$(BUILD_DIR)/golangci-lint run
+	$(BUILD_DIR)/golangci-lint run --build-tags $(LINT_BUILDTAGS)
 
 $(BUILD_DIR)/golangci-lint:
 	export \
@@ -154,6 +163,9 @@ $(BUILD_DIR)/golangci-lint:
 
 .PHONY: test-unit
 test-unit: $(BUILD_DIR) ## Run the unit tests
+	# remove all coverage files if exists
+	rm -rf *.out
+	# run the go tests and gen the file coverage-all used to do the integration with coverrals.io
 	$(GO) test -ldflags '$(LDVARS)' -tags '$(BUILDTAGS)' -race -v -test.coverprofile=$(BUILD_DIR)/coverage.out ./internal/...
 	$(GO) tool cover -html $(BUILD_DIR)/coverage.out -o $(BUILD_DIR)/coverage.html
 
@@ -165,7 +177,7 @@ test-e2e: ## Run the end-to-end tests
 manifests:
 	$(GO) run -tags generate sigs.k8s.io/controller-tools/cmd/controller-gen $(CRD_OPTIONS) paths="./api/seccompprofile/..." output:crd:stdout > deploy/base/crd.yaml
 	$(GO) run -tags generate sigs.k8s.io/controller-tools/cmd/controller-gen $(CRD_OPTIONS) paths="./api/selinuxpolicy/..." output:crd:stdout >> deploy/base/crd.yaml
-	$(GO) run -tags generate sigs.k8s.io/controller-tools/cmd/controller-gen $(CRD_OPTIONS) paths="./api/profilebinding/..." output:crd:stdout >> deploy/base/crd.yaml
+	$(GO) run -tags generate sigs.k8s.io/controller-tools/cmd/controller-gen $(CRD_OPTIONS) paths="./api/profilebinding/..." output:crd:stdout > deploy/base/webhook/crd.yaml
 
 # Generate deepcopy code
 generate:
diff --git a/cmd/security-profiles-operator/main.go b/cmd/security-profiles-operator/main.go
index cabf9a29c..30c6a786d 100644
--- a/cmd/security-profiles-operator/main.go
+++ b/cmd/security-profiles-operator/main.go
@@ -94,15 +94,7 @@ func main() {
 			Name:    "manager",
 			Aliases: []string{"m"},
 			Usage:   "run the security-profiles-operator manager",
-			Flags: []cli.Flag{
-				&cli.IntFlag{
-					Name:    "port",
-					Aliases: []string{"p"},
-					Value:   defaultPort,
-					Usage:   "the port on which to expose the webhook service (default 9443)",
-				},
-			},
-			Action: runManager,
+			Action:  runManager,
 		},
 		&cli.Command{
 			Name:    "daemon",
@@ -117,6 +109,20 @@ func main() {
 				},
 			},
 		},
+		&cli.Command{
+			Name:    "webhook",
+			Aliases: []string{"w"},
+			Usage:   "run the security-profiles-operator webhook",
+			Action:  runWebhook,
+			Flags: []cli.Flag{
+				&cli.IntFlag{
+					Name:    "port",
+					Aliases: []string{"p"},
+					Value:   defaultPort,
+					Usage:   "the port on which to expose the webhook service (default 9443)",
+				},
+			},
+		},
 	}
 
 	if err := app.Run(os.Args); err != nil {
@@ -148,12 +154,10 @@ func runManager(ctx *cli.Context) error {
 
 	sigHandler := ctrl.SetupSignalHandler()
 
-	port := ctx.Int("port")
 	ctrlOpts := manager.Options{
 		SyncPeriod:       &sync,
 		LeaderElection:   true,
 		LeaderElectionID: "security-profiles-operator-lock",
-		Port:             port,
 	}
 
 	dt, err := getTunables()
@@ -181,10 +185,6 @@ func runManager(ctx *cli.Context) error {
 		return errors.Wrap(err, "setup profile controller")
 	}
 
-	setupLog.Info("registering webhook")
-	hookserver := mgr.GetWebhookServer()
-	webhook.RegisterWebhook(hookserver, mgr.GetClient())
-
 	setupLog.Info("starting manager")
 	if err := mgr.Start(sigHandler); err != nil {
 		return errors.Wrap(err, "controller manager error")
@@ -291,6 +291,45 @@ func runDaemon(ctx *cli.Context) error {
 	return nil
 }
 
+func runWebhook(ctx *cli.Context) error {
+	printInfo("security-profiles-operator-webhook")
+	cfg, err := ctrl.GetConfig()
+	if err != nil {
+		return errors.Wrap(err, "get config")
+	}
+
+	port := ctx.Int("port")
+	ctrlOpts := manager.Options{
+		SyncPeriod:       &sync,
+		LeaderElection:   true,
+		LeaderElectionID: "security-profiles-operator-webhook-lock",
+		Port:             port,
+	}
+
+	mgr, err := ctrl.NewManager(cfg, ctrlOpts)
+	if err != nil {
+		return errors.Wrap(err, "create cluster webhook")
+	}
+
+	if err := profilebindingv1alpha1.AddToScheme(mgr.GetScheme()); err != nil {
+		return errors.Wrap(err, "add profilebinding API to scheme")
+	}
+	if err := seccompprofilev1alpha1.AddToScheme(mgr.GetScheme()); err != nil {
+		return errors.Wrap(err, "add seccompprofile API to scheme")
+	}
+
+	setupLog.Info("registering webhook")
+	hookserver := mgr.GetWebhookServer()
+	webhook.RegisterWebhook(hookserver, mgr.GetClient())
+
+	sigHandler := ctrl.SetupSignalHandler()
+	setupLog.Info("starting webhook")
+	if err := mgr.Start(sigHandler); err != nil {
+		return errors.Wrap(err, "controller manager error")
+	}
+	return nil
+}
+
 func setupEnabledControllers(ctx context.Context, enabledControllers []*controllerSettings, mgr ctrl.Manager) error {
 	for _, enableCtrl := range enabledControllers {
 		if err := enableCtrl.schemaBuilder.AddToScheme(mgr.GetScheme()); err != nil {
diff --git a/deploy/base/crd.yaml b/deploy/base/crd.yaml
index 06e65a21a..feb4841e1 100644
--- a/deploy/base/crd.yaml
+++ b/deploy/base/crd.yaml
@@ -291,78 +291,3 @@ status:
     plural: ""
   conditions: []
   storedVersions: []
-
----
-apiVersion: apiextensions.k8s.io/v1
-kind: CustomResourceDefinition
-metadata:
-  annotations:
-    controller-gen.kubebuilder.io/version: v0.2.4
-  creationTimestamp: null
-  name: profilebindings.security-profiles-operator.x-k8s.io
-spec:
-  group: security-profiles-operator.x-k8s.io
-  names:
-    kind: ProfileBinding
-    listKind: ProfileBindingList
-    plural: profilebindings
-    singular: profilebinding
-  scope: Namespaced
-  versions:
-  - name: v1alpha1
-    schema:
-      openAPIV3Schema:
-        description: ProfileBinding is the Schema for the profilebindings API.
-        properties:
-          apiVersion:
-            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
-            type: string
-          kind:
-            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
-            type: string
-          metadata:
-            type: object
-          spec:
-            description: ProfileBindingSpec defines the desired state of ProfileBinding.
-            properties:
-              image:
-                description: the image name within pod containers to match to the profile
-                type: string
-              profileRef:
-                description: ProfileRef references a SeccompProfile or other profile type in the current namespace
-                properties:
-                  kind:
-                    description: 'the kind of object to be bound, valid values are: SeccompProfile TODO(cmurphy) add SelinuxPolicy'
-                    enum:
-                    - SeccompProfile
-                    type: string
-                  name:
-                    description: the name of the profile within the current namespace to which to bind the selected pods
-                    type: string
-                required:
-                - kind
-                - name
-                type: object
-            required:
-            - image
-            - profileRef
-            type: object
-          status:
-            description: ProfileBindingStatus contains status of the Profilebinding.
-            properties:
-              activeWorkloads:
-                items:
-                  type: string
-                type: array
-            type: object
-        type: object
-    served: true
-    storage: true
-    subresources:
-      status: {}
-status:
-  acceptedNames:
-    kind: ""
-    plural: ""
-  conditions: []
-  storedVersions: []
diff --git a/deploy/base/kustomization.yaml b/deploy/base/kustomization.yaml
index 32109c1b0..9bc67205c 100644
--- a/deploy/base/kustomization.yaml
+++ b/deploy/base/kustomization.yaml
@@ -38,9 +38,6 @@ resources:
   - rbac.yaml
   - operator.yaml
   - config.yaml
-  - certificate.yaml
-  - webhookconfig.yaml
-  - service.yaml
 
 configMapGenerator:
   - name: security-profiles-operator-profile
diff --git a/deploy/base/operator.yaml b/deploy/base/operator.yaml
index d934d1c4c..4eb618568 100644
--- a/deploy/base/operator.yaml
+++ b/deploy/base/operator.yaml
@@ -33,19 +33,6 @@ spec:
               value: non-root-enabler
             - name: RELATED_IMAGE_SELINUXD
               value: quay.io/jaosorior/selinuxd
-          volumeMounts:
-          - name: cert
-            mountPath: /tmp/k8s-webhook-server/serving-certs
-            readOnly: true
-          ports:
-          - containerPort: 9443
-            name: webhook
-            protocol: TCP
-      volumes:
-      - name: cert
-        secret:
-          defaultMode: 420
-          secretName: webhook-server-cert
       nodeSelector:
         node-role.kubernetes.io/master: ""
       tolerations:
diff --git a/deploy/base/rbac.yaml b/deploy/base/rbac.yaml
index 4f53765c6..5a2f3693a 100644
--- a/deploy/base/rbac.yaml
+++ b/deploy/base/rbac.yaml
@@ -170,56 +170,3 @@ roleRef:
   kind: Role
   name: security-profiles-operator
   apiGroup: rbac.authorization.k8s.io
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  name: profilebindings-manager
-rules:
-- apiGroups: ["security-profiles-operator.x-k8s.io"]
-  resources: ["profilebindings"]
-  verbs: ["get", "list", "patch", "update", "watch"]
-- apiGroups: ["security-profiles-operator.x-k8s.io"]
-  resources: ["profilebindings/status"]
-  verbs: ["get", "patch", "update"]
-- apiGroups: ["security-profiles-operator.x-k8s.io"]
-  resources: ["seccompprofiles"]
-  verbs: ["get", "list", "watch"]
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRoleBinding
-metadata:
-  name: profilebindings-manager-binding
-  namespace: security-profiles-operator
-subjects:
-- kind: ServiceAccount
-  name: security-profiles-operator
-  namespace: security-profiles-operator
-roleRef:
-  kind: ClusterRole
-  name: profilebindings-manager
-  apiGroup: rbac.authorization.k8s.io
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: leader-elector
-  namespace: security-profiles-operator
-rules:
-- apiGroups: [""]
-  resources: ["configmaps"]
-  verbs: ["create", "get", "update"]
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: leader-elector-binding
-  namespace: security-profiles-operator
-subjects:
-- kind: ServiceAccount
-  name: security-profiles-operator
-  namespace: security-profiles-operator
-roleRef:
-  kind: Role
-  name: leader-elector
-  apiGroup: rbac.authorization.k8s.io
diff --git a/deploy/base/certificate.yaml b/deploy/base/webhook/certificate.yaml
similarity index 100%
rename from deploy/base/certificate.yaml
rename to deploy/base/webhook/certificate.yaml
diff --git a/deploy/base/webhookconfig.yaml b/deploy/base/webhook/config.yaml
similarity index 94%
rename from deploy/base/webhookconfig.yaml
rename to deploy/base/webhook/config.yaml
index 19bd551b6..44c395ab5 100644
--- a/deploy/base/webhookconfig.yaml
+++ b/deploy/base/webhook/config.yaml
@@ -20,6 +20,7 @@ webhooks:
       operator: NotIn
       values:
       - security-profiles-operator
+      - security-profiles-operator-webhook
   rules:
   - apiGroups:
     - ""
diff --git a/deploy/base/webhook/crd.yaml b/deploy/base/webhook/crd.yaml
new file mode 100644
index 000000000..6472e3428
--- /dev/null
+++ b/deploy/base/webhook/crd.yaml
@@ -0,0 +1,75 @@
+
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.2.4
+  creationTimestamp: null
+  name: profilebindings.security-profiles-operator.x-k8s.io
+spec:
+  group: security-profiles-operator.x-k8s.io
+  names:
+    kind: ProfileBinding
+    listKind: ProfileBindingList
+    plural: profilebindings
+    singular: profilebinding
+  scope: Namespaced
+  versions:
+  - name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: ProfileBinding is the Schema for the profilebindings API.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: ProfileBindingSpec defines the desired state of ProfileBinding.
+            properties:
+              image:
+                description: the image name within pod containers to match to the profile
+                type: string
+              profileRef:
+                description: ProfileRef references a SeccompProfile or other profile type in the current namespace
+                properties:
+                  kind:
+                    description: 'the kind of object to be bound, valid values are: SeccompProfile TODO(cmurphy) add SelinuxPolicy'
+                    enum:
+                    - SeccompProfile
+                    type: string
+                  name:
+                    description: the name of the profile within the current namespace to which to bind the selected pods
+                    type: string
+                required:
+                - kind
+                - name
+                type: object
+            required:
+            - image
+            - profileRef
+            type: object
+          status:
+            description: ProfileBindingStatus contains status of the Profilebinding.
+            properties:
+              activeWorkloads:
+                items:
+                  type: string
+                type: array
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
diff --git a/deploy/base/webhook/deployment.yaml b/deploy/base/webhook/deployment.yaml
new file mode 100644
index 000000000..54fc9c123
--- /dev/null
+++ b/deploy/base/webhook/deployment.yaml
@@ -0,0 +1,53 @@
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: security-profiles-operator-webhook
+  namespace: security-profiles-operator
+spec:
+  replicas: 3
+  selector:
+    matchLabels:
+      name: security-profiles-operator-webhook
+  template:
+    metadata:
+      annotations:
+        seccomp.security.alpha.kubernetes.io/pod: runtime/default
+      labels:
+        name: security-profiles-operator-webhook
+    spec:
+      serviceAccountName: security-profiles-operator
+      containers:
+        - name: security-profiles-operator
+          image: security-profiles-operator
+          args:
+          - webhook
+          imagePullPolicy: Always
+          securityContext:
+            readOnlyRootFilesystem: true
+            allowPrivilegeEscalation: false
+          volumeMounts:
+          - name: cert
+            mountPath: /tmp/k8s-webhook-server/serving-certs
+            readOnly: true
+          ports:
+          - containerPort: 9443
+            name: webhook
+            protocol: TCP
+      volumes:
+      - name: cert
+        secret:
+          defaultMode: 420
+          secretName: webhook-server-cert
+      nodeSelector:
+        node-role.kubernetes.io/master: ""
+      tolerations:
+        - key: "node-role.kubernetes.io/master"
+          operator: "Exists"
+          effect: "NoSchedule"
+        - key: "node-role.kubernetes.io/control-plane"
+          operator: "Exists"
+          effect: "NoSchedule"
+        - key: "node.kubernetes.io/not-ready"
+          operator: "Exists"
+          effect: "NoExecute"
diff --git a/deploy/base/webhook/kustomization.yaml b/deploy/base/webhook/kustomization.yaml
new file mode 100644
index 000000000..b3387104e
--- /dev/null
+++ b/deploy/base/webhook/kustomization.yaml
@@ -0,0 +1,25 @@
+---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+images:
+  - name: security-profiles-operator
+    newName: gcr.io/k8s-staging-sp-operator/security-profiles-operator
+    newTag: latest
+    # For images to be released:
+    # newName: k8s.gcr.io/security-profiles-operator/security-profiles-operator
+    # newTag: v0.3.0
+
+commonLabels:
+  app: security-profiles-operator
+
+resources:
+  - crd.yaml
+  - rbac.yaml
+  - deployment.yaml
+  - config.yaml
+  - certificate.yaml
+  - service.yaml
+
+generatorOptions:
+  disableNameSuffixHash: true
diff --git a/deploy/base/webhook/rbac.yaml b/deploy/base/webhook/rbac.yaml
new file mode 100644
index 000000000..d2127aa83
--- /dev/null
+++ b/deploy/base/webhook/rbac.yaml
@@ -0,0 +1,53 @@
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: profilebindings-manager
+rules:
+- apiGroups: ["security-profiles-operator.x-k8s.io"]
+  resources: ["profilebindings"]
+  verbs: ["get", "list", "patch", "update", "watch"]
+- apiGroups: ["security-profiles-operator.x-k8s.io"]
+  resources: ["profilebindings/status"]
+  verbs: ["get", "patch", "update"]
+- apiGroups: ["security-profiles-operator.x-k8s.io"]
+  resources: ["seccompprofiles"]
+  verbs: ["get", "list", "watch"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: profilebindings-manager-binding
+  namespace: security-profiles-operator
+subjects:
+- kind: ServiceAccount
+  name: security-profiles-operator
+  namespace: security-profiles-operator
+roleRef:
+  kind: ClusterRole
+  name: profilebindings-manager
+  apiGroup: rbac.authorization.k8s.io
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: leader-elector
+  namespace: security-profiles-operator
+rules:
+- apiGroups: [""]
+  resources: ["configmaps"]
+  verbs: ["create", "get", "update"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: leader-elector-binding
+  namespace: security-profiles-operator
+subjects:
+- kind: ServiceAccount
+  name: security-profiles-operator
+  namespace: security-profiles-operator
+roleRef:
+  kind: Role
+  name: leader-elector
+  apiGroup: rbac.authorization.k8s.io
diff --git a/deploy/base/service.yaml b/deploy/base/webhook/service.yaml
similarity index 80%
rename from deploy/base/service.yaml
rename to deploy/base/webhook/service.yaml
index 01356bfb6..2fb4bf27e 100644
--- a/deploy/base/service.yaml
+++ b/deploy/base/webhook/service.yaml
@@ -9,4 +9,4 @@ spec:
     - port: 443
       targetPort: 9443
   selector:
-    name: security-profiles-operator
+    name: security-profiles-operator-webhook
diff --git a/deploy/namespace-operator.yaml b/deploy/namespace-operator.yaml
index 7002ca07e..af597ecb9 100644
--- a/deploy/namespace-operator.yaml
+++ b/deploy/namespace-operator.yaml
@@ -293,82 +293,6 @@ status:
   conditions: []
   storedVersions: []
 ---
-apiVersion: apiextensions.k8s.io/v1
-kind: CustomResourceDefinition
-metadata:
-  annotations:
-    controller-gen.kubebuilder.io/version: v0.2.4
-  creationTimestamp: null
-  labels:
-    app: security-profiles-operator
-  name: profilebindings.security-profiles-operator.x-k8s.io
-spec:
-  group: security-profiles-operator.x-k8s.io
-  names:
-    kind: ProfileBinding
-    listKind: ProfileBindingList
-    plural: profilebindings
-    singular: profilebinding
-  scope: Namespaced
-  versions:
-  - name: v1alpha1
-    schema:
-      openAPIV3Schema:
-        description: ProfileBinding is the Schema for the profilebindings API.
-        properties:
-          apiVersion:
-            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
-            type: string
-          kind:
-            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
-            type: string
-          metadata:
-            type: object
-          spec:
-            description: ProfileBindingSpec defines the desired state of ProfileBinding.
-            properties:
-              image:
-                description: the image name within pod containers to match to the profile
-                type: string
-              profileRef:
-                description: ProfileRef references a SeccompProfile or other profile type in the current namespace
-                properties:
-                  kind:
-                    description: 'the kind of object to be bound, valid values are: SeccompProfile TODO(cmurphy) add SelinuxPolicy'
-                    enum:
-                    - SeccompProfile
-                    type: string
-                  name:
-                    description: the name of the profile within the current namespace to which to bind the selected pods
-                    type: string
-                required:
-                - kind
-                - name
-                type: object
-            required:
-            - image
-            - profileRef
-            type: object
-          status:
-            description: ProfileBindingStatus contains status of the Profilebinding.
-            properties:
-              activeWorkloads:
-                items:
-                  type: string
-                type: array
-            type: object
-        type: object
-    served: true
-    storage: true
-    subresources:
-      status: {}
-status:
-  acceptedNames:
-    kind: ""
-    plural: ""
-  conditions: []
-  storedVersions: []
----
 apiVersion: v1
 kind: Namespace
 metadata:
@@ -550,89 +474,6 @@ subjects:
   name: security-profiles-operator
   namespace: security-profiles-operator
 ---
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app: security-profiles-operator
-  name: profilebindings-manager
-rules:
-- apiGroups:
-  - security-profiles-operator.x-k8s.io
-  resources:
-  - profilebindings
-  verbs:
-  - get
-  - list
-  - patch
-  - update
-  - watch
-- apiGroups:
-  - security-profiles-operator.x-k8s.io
-  resources:
-  - profilebindings/status
-  verbs:
-  - get
-  - patch
-  - update
-- apiGroups:
-  - security-profiles-operator.x-k8s.io
-  resources:
-  - seccompprofiles
-  verbs:
-  - get
-  - list
-  - watch
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRoleBinding
-metadata:
-  labels:
-    app: security-profiles-operator
-  name: profilebindings-manager-binding
-  namespace: security-profiles-operator
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: ClusterRole
-  name: profilebindings-manager
-subjects:
-- kind: ServiceAccount
-  name: security-profiles-operator
-  namespace: security-profiles-operator
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  labels:
-    app: security-profiles-operator
-  name: leader-elector
-  namespace: security-profiles-operator
-rules:
-- apiGroups:
-  - ""
-  resources:
-  - configmaps
-  verbs:
-  - create
-  - get
-  - update
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  labels:
-    app: security-profiles-operator
-  name: leader-elector-binding
-  namespace: security-profiles-operator
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: Role
-  name: leader-elector
-subjects:
-- kind: ServiceAccount
-  name: security-profiles-operator
-  namespace: security-profiles-operator
----
 apiVersion: apps/v1
 kind: Deployment
 metadata:
@@ -669,17 +510,9 @@ spec:
         image: gcr.io/k8s-staging-sp-operator/security-profiles-operator:latest
         imagePullPolicy: Always
         name: security-profiles-operator
-        ports:
-        - containerPort: 9443
-          name: webhook
-          protocol: TCP
         securityContext:
           allowPrivilegeEscalation: false
           readOnlyRootFilesystem: true
-        volumeMounts:
-        - mountPath: /tmp/k8s-webhook-server/serving-certs
-          name: cert
-          readOnly: true
       nodeSelector:
         node-role.kubernetes.io/master: ""
       serviceAccountName: security-profiles-operator
@@ -693,11 +526,6 @@ spec:
       - effect: NoExecute
         key: node.kubernetes.io/not-ready
         operator: Exists
-      volumes:
-      - name: cert
-        secret:
-          defaultMode: 420
-          secretName: webhook-server-cert
 ---
 apiVersion: v1
 data:
@@ -711,85 +539,6 @@ metadata:
   name: config
   namespace: security-profiles-operator
 ---
-apiVersion: cert-manager.io/v1
-kind: Issuer
-metadata:
-  labels:
-    app: security-profiles-operator
-  name: selfsigned-issuer
-  namespace: security-profiles-operator
-spec:
-  selfSigned: {}
----
-apiVersion: cert-manager.io/v1
-kind: Certificate
-metadata:
-  labels:
-    app: security-profiles-operator
-  name: webhook-cert
-  namespace: security-profiles-operator
-spec:
-  dnsNames:
-  - profilebinding-webhook-service.security-profiles-operator.svc
-  - profilebinding-webhook-service.security-profiles-operator.svc.cluster.local
-  issuerRef:
-    kind: Issuer
-    name: selfsigned-issuer
-  secretName: webhook-server-cert
----
-apiVersion: admissionregistration.k8s.io/v1
-kind: MutatingWebhookConfiguration
-metadata:
-  annotations:
-    cert-manager.io/inject-ca-from: security-profiles-operator/webhook-cert
-  labels:
-    app: security-profiles-operator
-  name: spo-mutating-webhook-configuration
-webhooks:
-- admissionReviewVersions:
-  - v1beta1
-  clientConfig:
-    caBundle: Cg==
-    service:
-      name: profilebinding-webhook-service
-      namespace: security-profiles-operator
-      path: /mutate-v1-pod
-  failurePolicy: Fail
-  name: mpod.kb.io
-  objectSelector:
-    matchExpressions:
-    - key: name
-      operator: NotIn
-      values:
-      - security-profiles-operator
-  rules:
-  - apiGroups:
-    - ""
-    apiVersions:
-    - v1
-    operations:
-    - CREATE
-    - UPDATE
-    - DELETE
-    resources:
-    - pods
-  sideEffects: None
----
-apiVersion: v1
-kind: Service
-metadata:
-  labels:
-    app: security-profiles-operator
-  name: profilebinding-webhook-service
-  namespace: security-profiles-operator
-spec:
-  ports:
-  - port: 443
-    targetPort: 9443
-  selector:
-    app: security-profiles-operator
-    name: security-profiles-operator
----
 apiVersion: v1
 data:
   security-profiles-operator.json: |
diff --git a/deploy/operator.yaml b/deploy/operator.yaml
index 5d17eeb78..52de58173 100644
--- a/deploy/operator.yaml
+++ b/deploy/operator.yaml
@@ -293,82 +293,6 @@ status:
   conditions: []
   storedVersions: []
 ---
-apiVersion: apiextensions.k8s.io/v1
-kind: CustomResourceDefinition
-metadata:
-  annotations:
-    controller-gen.kubebuilder.io/version: v0.2.4
-  creationTimestamp: null
-  labels:
-    app: security-profiles-operator
-  name: profilebindings.security-profiles-operator.x-k8s.io
-spec:
-  group: security-profiles-operator.x-k8s.io
-  names:
-    kind: ProfileBinding
-    listKind: ProfileBindingList
-    plural: profilebindings
-    singular: profilebinding
-  scope: Namespaced
-  versions:
-  - name: v1alpha1
-    schema:
-      openAPIV3Schema:
-        description: ProfileBinding is the Schema for the profilebindings API.
-        properties:
-          apiVersion:
-            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
-            type: string
-          kind:
-            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
-            type: string
-          metadata:
-            type: object
-          spec:
-            description: ProfileBindingSpec defines the desired state of ProfileBinding.
-            properties:
-              image:
-                description: the image name within pod containers to match to the profile
-                type: string
-              profileRef:
-                description: ProfileRef references a SeccompProfile or other profile type in the current namespace
-                properties:
-                  kind:
-                    description: 'the kind of object to be bound, valid values are: SeccompProfile TODO(cmurphy) add SelinuxPolicy'
-                    enum:
-                    - SeccompProfile
-                    type: string
-                  name:
-                    description: the name of the profile within the current namespace to which to bind the selected pods
-                    type: string
-                required:
-                - kind
-                - name
-                type: object
-            required:
-            - image
-            - profileRef
-            type: object
-          status:
-            description: ProfileBindingStatus contains status of the Profilebinding.
-            properties:
-              activeWorkloads:
-                items:
-                  type: string
-                type: array
-            type: object
-        type: object
-    served: true
-    storage: true
-    subresources:
-      status: {}
-status:
-  acceptedNames:
-    kind: ""
-    plural: ""
-  conditions: []
-  storedVersions: []
----
 apiVersion: v1
 kind: Namespace
 metadata:
@@ -550,89 +474,6 @@ subjects:
   name: security-profiles-operator
   namespace: security-profiles-operator
 ---
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app: security-profiles-operator
-  name: profilebindings-manager
-rules:
-- apiGroups:
-  - security-profiles-operator.x-k8s.io
-  resources:
-  - profilebindings
-  verbs:
-  - get
-  - list
-  - patch
-  - update
-  - watch
-- apiGroups:
-  - security-profiles-operator.x-k8s.io
-  resources:
-  - profilebindings/status
-  verbs:
-  - get
-  - patch
-  - update
-- apiGroups:
-  - security-profiles-operator.x-k8s.io
-  resources:
-  - seccompprofiles
-  verbs:
-  - get
-  - list
-  - watch
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRoleBinding
-metadata:
-  labels:
-    app: security-profiles-operator
-  name: profilebindings-manager-binding
-  namespace: security-profiles-operator
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: ClusterRole
-  name: profilebindings-manager
-subjects:
-- kind: ServiceAccount
-  name: security-profiles-operator
-  namespace: security-profiles-operator
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  labels:
-    app: security-profiles-operator
-  name: leader-elector
-  namespace: security-profiles-operator
-rules:
-- apiGroups:
-  - ""
-  resources:
-  - configmaps
-  verbs:
-  - create
-  - get
-  - update
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  labels:
-    app: security-profiles-operator
-  name: leader-elector-binding
-  namespace: security-profiles-operator
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: Role
-  name: leader-elector
-subjects:
-- kind: ServiceAccount
-  name: security-profiles-operator
-  namespace: security-profiles-operator
----
 apiVersion: apps/v1
 kind: Deployment
 metadata:
@@ -667,17 +508,9 @@ spec:
         image: gcr.io/k8s-staging-sp-operator/security-profiles-operator:latest
         imagePullPolicy: Always
         name: security-profiles-operator
-        ports:
-        - containerPort: 9443
-          name: webhook
-          protocol: TCP
         securityContext:
           allowPrivilegeEscalation: false
           readOnlyRootFilesystem: true
-        volumeMounts:
-        - mountPath: /tmp/k8s-webhook-server/serving-certs
-          name: cert
-          readOnly: true
       nodeSelector:
         node-role.kubernetes.io/master: ""
       serviceAccountName: security-profiles-operator
@@ -691,11 +524,6 @@ spec:
       - effect: NoExecute
         key: node.kubernetes.io/not-ready
         operator: Exists
-      volumes:
-      - name: cert
-        secret:
-          defaultMode: 420
-          secretName: webhook-server-cert
 ---
 apiVersion: v1
 data:
@@ -709,85 +537,6 @@ metadata:
   name: config
   namespace: security-profiles-operator
 ---
-apiVersion: cert-manager.io/v1
-kind: Issuer
-metadata:
-  labels:
-    app: security-profiles-operator
-  name: selfsigned-issuer
-  namespace: security-profiles-operator
-spec:
-  selfSigned: {}
----
-apiVersion: cert-manager.io/v1
-kind: Certificate
-metadata:
-  labels:
-    app: security-profiles-operator
-  name: webhook-cert
-  namespace: security-profiles-operator
-spec:
-  dnsNames:
-  - profilebinding-webhook-service.security-profiles-operator.svc
-  - profilebinding-webhook-service.security-profiles-operator.svc.cluster.local
-  issuerRef:
-    kind: Issuer
-    name: selfsigned-issuer
-  secretName: webhook-server-cert
----
-apiVersion: admissionregistration.k8s.io/v1
-kind: MutatingWebhookConfiguration
-metadata:
-  annotations:
-    cert-manager.io/inject-ca-from: security-profiles-operator/webhook-cert
-  labels:
-    app: security-profiles-operator
-  name: spo-mutating-webhook-configuration
-webhooks:
-- admissionReviewVersions:
-  - v1beta1
-  clientConfig:
-    caBundle: Cg==
-    service:
-      name: profilebinding-webhook-service
-      namespace: security-profiles-operator
-      path: /mutate-v1-pod
-  failurePolicy: Fail
-  name: mpod.kb.io
-  objectSelector:
-    matchExpressions:
-    - key: name
-      operator: NotIn
-      values:
-      - security-profiles-operator
-  rules:
-  - apiGroups:
-    - ""
-    apiVersions:
-    - v1
-    operations:
-    - CREATE
-    - UPDATE
-    - DELETE
-    resources:
-    - pods
-  sideEffects: None
----
-apiVersion: v1
-kind: Service
-metadata:
-  labels:
-    app: security-profiles-operator
-  name: profilebinding-webhook-service
-  namespace: security-profiles-operator
-spec:
-  ports:
-  - port: 443
-    targetPort: 9443
-  selector:
-    app: security-profiles-operator
-    name: security-profiles-operator
----
 apiVersion: v1
 data:
   security-profiles-operator.json: |
diff --git a/deploy/webhook.yaml b/deploy/webhook.yaml
new file mode 100644
index 000000000..83b58cb51
--- /dev/null
+++ b/deploy/webhook.yaml
@@ -0,0 +1,295 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.2.4
+  creationTimestamp: null
+  labels:
+    app: security-profiles-operator
+  name: profilebindings.security-profiles-operator.x-k8s.io
+spec:
+  group: security-profiles-operator.x-k8s.io
+  names:
+    kind: ProfileBinding
+    listKind: ProfileBindingList
+    plural: profilebindings
+    singular: profilebinding
+  scope: Namespaced
+  versions:
+  - name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: ProfileBinding is the Schema for the profilebindings API.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: ProfileBindingSpec defines the desired state of ProfileBinding.
+            properties:
+              image:
+                description: the image name within pod containers to match to the profile
+                type: string
+              profileRef:
+                description: ProfileRef references a SeccompProfile or other profile type in the current namespace
+                properties:
+                  kind:
+                    description: 'the kind of object to be bound, valid values are: SeccompProfile TODO(cmurphy) add SelinuxPolicy'
+                    enum:
+                    - SeccompProfile
+                    type: string
+                  name:
+                    description: the name of the profile within the current namespace to which to bind the selected pods
+                    type: string
+                required:
+                - kind
+                - name
+                type: object
+            required:
+            - image
+            - profileRef
+            type: object
+          status:
+            description: ProfileBindingStatus contains status of the Profilebinding.
+            properties:
+              activeWorkloads:
+                items:
+                  type: string
+                type: array
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  labels:
+    app: security-profiles-operator
+  name: profilebindings-manager
+rules:
+- apiGroups:
+  - security-profiles-operator.x-k8s.io
+  resources:
+  - profilebindings
+  verbs:
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - security-profiles-operator.x-k8s.io
+  resources:
+  - profilebindings/status
+  verbs:
+  - get
+  - patch
+  - update
+- apiGroups:
+  - security-profiles-operator.x-k8s.io
+  resources:
+  - seccompprofiles
+  verbs:
+  - get
+  - list
+  - watch
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  labels:
+    app: security-profiles-operator
+  name: profilebindings-manager-binding
+  namespace: security-profiles-operator
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: profilebindings-manager
+subjects:
+- kind: ServiceAccount
+  name: security-profiles-operator
+  namespace: security-profiles-operator
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  labels:
+    app: security-profiles-operator
+  name: leader-elector
+  namespace: security-profiles-operator
+rules:
+- apiGroups:
+  - ""
+  resources:
+  - configmaps
+  verbs:
+  - create
+  - get
+  - update
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  labels:
+    app: security-profiles-operator
+  name: leader-elector-binding
+  namespace: security-profiles-operator
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: leader-elector
+subjects:
+- kind: ServiceAccount
+  name: security-profiles-operator
+  namespace: security-profiles-operator
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  labels:
+    app: security-profiles-operator
+  name: security-profiles-operator-webhook
+  namespace: security-profiles-operator
+spec:
+  replicas: 3
+  selector:
+    matchLabels:
+      app: security-profiles-operator
+      name: security-profiles-operator-webhook
+  template:
+    metadata:
+      annotations:
+        seccomp.security.alpha.kubernetes.io/pod: runtime/default
+      labels:
+        app: security-profiles-operator
+        name: security-profiles-operator-webhook
+    spec:
+      containers:
+      - args:
+        - webhook
+        image: gcr.io/k8s-staging-sp-operator/security-profiles-operator:latest
+        imagePullPolicy: Always
+        name: security-profiles-operator
+        ports:
+        - containerPort: 9443
+          name: webhook
+          protocol: TCP
+        securityContext:
+          allowPrivilegeEscalation: false
+          readOnlyRootFilesystem: true
+        volumeMounts:
+        - mountPath: /tmp/k8s-webhook-server/serving-certs
+          name: cert
+          readOnly: true
+      nodeSelector:
+        node-role.kubernetes.io/master: ""
+      serviceAccountName: security-profiles-operator
+      tolerations:
+      - effect: NoSchedule
+        key: node-role.kubernetes.io/master
+        operator: Exists
+      - effect: NoSchedule
+        key: node-role.kubernetes.io/control-plane
+        operator: Exists
+      - effect: NoExecute
+        key: node.kubernetes.io/not-ready
+        operator: Exists
+      volumes:
+      - name: cert
+        secret:
+          defaultMode: 420
+          secretName: webhook-server-cert
+---
+apiVersion: admissionregistration.k8s.io/v1
+kind: MutatingWebhookConfiguration
+metadata:
+  annotations:
+    cert-manager.io/inject-ca-from: security-profiles-operator/webhook-cert
+  labels:
+    app: security-profiles-operator
+  name: spo-mutating-webhook-configuration
+webhooks:
+- admissionReviewVersions:
+  - v1beta1
+  clientConfig:
+    caBundle: Cg==
+    service:
+      name: profilebinding-webhook-service
+      namespace: security-profiles-operator
+      path: /mutate-v1-pod
+  failurePolicy: Fail
+  name: mpod.kb.io
+  objectSelector:
+    matchExpressions:
+    - key: name
+      operator: NotIn
+      values:
+      - security-profiles-operator
+      - security-profiles-operator-webhook
+  rules:
+  - apiGroups:
+    - ""
+    apiVersions:
+    - v1
+    operations:
+    - CREATE
+    - UPDATE
+    - DELETE
+    resources:
+    - pods
+  sideEffects: None
+---
+apiVersion: cert-manager.io/v1
+kind: Issuer
+metadata:
+  labels:
+    app: security-profiles-operator
+  name: selfsigned-issuer
+  namespace: security-profiles-operator
+spec:
+  selfSigned: {}
+---
+apiVersion: cert-manager.io/v1
+kind: Certificate
+metadata:
+  labels:
+    app: security-profiles-operator
+  name: webhook-cert
+  namespace: security-profiles-operator
+spec:
+  dnsNames:
+  - profilebinding-webhook-service.security-profiles-operator.svc
+  - profilebinding-webhook-service.security-profiles-operator.svc.cluster.local
+  issuerRef:
+    kind: Issuer
+    name: selfsigned-issuer
+  secretName: webhook-server-cert
+---
+apiVersion: v1
+kind: Service
+metadata:
+  labels:
+    app: security-profiles-operator
+  name: profilebinding-webhook-service
+  namespace: security-profiles-operator
+spec:
+  ports:
+  - port: 443
+    targetPort: 9443
+  selector:
+    app: security-profiles-operator
+    name: security-profiles-operator-webhook
diff --git a/go.mod b/go.mod
index c5c59e075..edcbb77f3 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.15
 require (
 	github.com/containers/common v0.32.0
 	github.com/crossplane/crossplane-runtime v0.12.0
-	github.com/go-logr/logr v0.3.0
+	github.com/go-logr/logr v0.4.0
 	github.com/pkg/errors v0.9.1
 	github.com/stretchr/testify v1.7.0
 	github.com/urfave/cli/v2 v2.3.0
diff --git a/go.sum b/go.sum
index 0f5e88ac5..c2a882868 100644
--- a/go.sum
+++ b/go.sum
@@ -226,8 +226,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
 github.com/go-logr/logr v0.1.0 h1:M1Tv3VzNlEHg6uyACnRdtrploV2P7wZqH8BoQMtz0cg=
 github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
 github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
-github.com/go-logr/logr v0.3.0 h1:q4c+kbcR0d5rSurhBR8dIgieOaYpXtsdTYfx22Cu6rs=
-github.com/go-logr/logr v0.3.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
+github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc=
+github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
 github.com/go-logr/zapr v0.1.0 h1:h+WVe9j6HAA01niTJPA/kKH0i7e0rLZBCwauQFcRE54=
 github.com/go-logr/zapr v0.1.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk=
 github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
diff --git a/installation-usage.md b/installation-usage.md
index 732f77cf2..9ad2dc230 100644
--- a/installation-usage.md
+++ b/installation-usage.md
@@ -19,7 +19,6 @@ The feature scope of the security-profiles-operator is right now limited to:
 ### 1. Install operator
 
 ```sh
-$ kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.1.0/cert-manager.yaml
 $ kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/security-profiles-operator/master/deploy/operator.yaml
 $ kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/security-profiles-operator/master/deploy/profiles/default-profiles.yaml
 ```
@@ -146,9 +145,16 @@ if you are deploying a public application, you can use the ProfileBinding
 resource to bind a security profile to a container's securityContext. Currently,
 the ProfileBinding resource can only refer to a SeccompProfile.
 
-For example, to bind a Pod that uses an 'nginx:1.19.1' image to the
-'profile-complain' example seccomp profile, create a ProfileBinding in the same
-namespace as both the Pod and the SeccompProfile:
+To enable ProfileBindings, install cert-manager and the webhook:
+
+```sh
+$ kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.1.0/cert-manager.yaml
+$ kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/security-profiles-operator/master/deploy/webhook.yaml
+```
+
+To bind a Pod that uses an 'nginx:1.19.1' image to the 'profile-complain'
+example seccomp profile, create a ProfileBinding in the same namespace as both
+the Pod and the SeccompProfile:
 
 ```
 apiVersion: security-profiles-operator.x-k8s.io/v1alpha1


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 000000000..c90a77ce7
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,28 @@
+# Inlcuding the tests with ubuntu to get actual test coverage. The OSX builds skip seccomp tests.
+name: test-coverage
+on:
+  push:
+    branches:
+      - master
+  pull_request:
+jobs:
+  validate:
+    name: Validate 
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - name: Setup Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: '1.15'
+      - name: Install libseccomp-dev
+        run: |
+          sudo apt update
+          sudo apt install -y libseccomp-dev
+      - name: unit test
+        run: make test-unit
+      - uses: codecov/codecov-action@v1
+        with:
+          files: build/coverage.out
+          flags: unittests 
+          verbose: true 
diff --git a/test/e2e_test.go b/test/e2e_test.go
index aed8518aa..12113c7cc 100644
--- a/test/e2e_test.go
+++ b/test/e2e_test.go
@@ -149,15 +149,6 @@ func (e *e2e) deployOperator(manifest, profiles string) {
 		manifest,
 	)
 
-	// Deploy prerequisites
-	e.logf("Deploying cert-manager")
-	e.kubectl("apply", "-f", certmanager)
-	e.kubectl(
-		"--namespace", "cert-manager",
-		"wait", "--for", "condition=ready",
-		"pod", "-l", "app.kubernetes.io/instance=cert-manager",
-	)
-
 	if e.platformSupportsSelinux() {
 		e.run(
 			"sed", "-i", "s/EnableSelinux: \"false\"/EnableSelinux: \"true\"/",
diff --git a/test/suite_test.go b/test/suite_test.go
index 8d8cc5e36..3b9ba6de1 100644
--- a/test/suite_test.go
+++ b/test/suite_test.go
@@ -47,15 +47,17 @@ var (
 	clusterType        = os.Getenv("E2E_CLUSTER_TYPE")
 	envSkipBuildImages = os.Getenv("E2E_SKIP_BUILD_IMAGES")
 	containerRuntime   = os.Getenv("CONTAINER_RUNTIME")
+	runExperimental    = os.Getenv("E2E_RUN_EXPERIMENTAL")
 )
 
 type e2e struct {
 	suite.Suite
-	kubectlPath string
-	testImage   string
-	pullPolicy  string
-	logger      logr.Logger
-	execNode    func(node string, args ...string) string
+	kubectlPath     string
+	testImage       string
+	pullPolicy      string
+	logger          logr.Logger
+	execNode        func(node string, args ...string) string
+	runExperimental bool
 }
 
 type kinde2e struct {
@@ -77,9 +79,10 @@ func TestSuite(t *testing.T) {
 	case clusterType == "" || strings.EqualFold(clusterType, "kind"):
 		suite.Run(t, &kinde2e{
 			e2e{
-				logger:     klogr.New(),
-				testImage:  config.OperatorName + ":latest",
-				pullPolicy: "Never",
+				logger:          klogr.New(),
+				testImage:       config.OperatorName + ":latest",
+				pullPolicy:      "Never",
+				runExperimental: runExperimental != "",
 			},
 			"", "",
 		})
@@ -94,7 +97,8 @@ func TestSuite(t *testing.T) {
 				logger: klogr.New(),
 				// Need to pull the image as it'll be uploaded to the cluster OCP
 				// image registry and not on the nodes.
-				pullPolicy: "Always",
+				pullPolicy:      "Always",
+				runExperimental: runExperimental != "",
 			},
 			skipBuildImages,
 		})
diff --git a/test/tc_profilebindings_test.go b/test/tc_profilebindings_test.go
index 29833df81..20e3a6dab 100644
--- a/test/tc_profilebindings_test.go
+++ b/test/tc_profilebindings_test.go
@@ -24,6 +24,9 @@ import (
 )
 
 func (e *e2e) testCaseProfileBinding([]string) {
+	if !e.runExperimental {
+		e.T().Skip("skipping experimental test")
+	}
 	const exampleProfilePath = "examples/seccompprofile.yaml"
 	const testBinding = `
 apiVersion: security-profiles-operator.x-k8s.io/v1alpha1
@@ -48,6 +51,12 @@ spec:
     resources: {}
   restartPolicy: Never
 `
+	const manifest = "deploy/webhook.yaml"
+
+	e.deployWebhook(manifest)
+	defer e.run("git", "checkout", manifest)
+	defer e.cleanupWebhook(manifest)
+
 	e.kubectl("create", "-f", exampleProfilePath)
 	defer e.kubectl("delete", "-f", exampleProfilePath)
 
@@ -111,3 +120,32 @@ spec:
 	output = e.kubectl("get", "seccompprofile", "profile-allow", "--output", "jsonpath={.metadata.finalizers}")
 	e.Contains(output, "in-use-by-active-pods")
 }
+
+func (e *e2e) deployWebhook(manifest string) {
+	// Deploy prerequisites
+	e.logf("Deploying cert-manager")
+	e.kubectl("apply", "-f", certmanager)
+	e.kubectl(
+		"--namespace", "cert-manager",
+		"wait", "--for", "condition=ready",
+		"pod", "-l", "app.kubernetes.io/instance=cert-manager",
+	)
+	e.run(
+		"sed", "-i", fmt.Sprintf("s;image: .*gcr.io/.*;image: %s;g", e.testImage),
+		manifest,
+	)
+	e.run(
+		"sed", "-i",
+		fmt.Sprintf("s;imagePullPolicy: Always;imagePullPolicy: %s;g", e.pullPolicy),
+		manifest,
+	)
+	e.logf("Deploying webhook")
+	e.kubectl("create", "-f", manifest)
+	e.kubectlOperatorNS("wait", "--for", "condition=ready", "pod", "-l", "name=security-profiles-operator-webhook")
+}
+
+func (e *e2e) cleanupWebhook(manifest string) {
+	e.logf("Cleaning up webhook")
+	e.kubectl("delete", "-f", manifest)
+	e.kubectl("delete", "-f", certmanager)
+}