Repository: kubernetes-sigs/security-profiles-operator
Pull request #287 (2021-02-10 15:00:26)

Add arm64 binary cross compilation support

#### What type of PR is this?

/kind feature

#### What this PR does / why we need it:
We now utilize [nix](https://nixos.org) to be able to build the operator
binary in a reproducible environment. This way we're able to cross-build
the binary to any supported target architecture.

For example the new `make nix` target now builds the locally available
system binary, which should be usually x86-64:

```
> make nix
…
file result/*
result/security-profiles-operator: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, for GNU/Linux 2.6.32, Go BuildID=JPRHwsjKv1rgegLFOzeZ/MbtM6EjbKLpv2qaZ1bUS/OLZzjQg7BJIUB1yQ5x7L/aXXV7z2TB5tXSZBtwwvB, stripped
```

Cross compilation works via the `make nix-arm64` target, too:

```

> make nix-arm64
…
file result/*
result/security-profiles-operator: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, for GNU/Linux 2.6.32, Go BuildID=Vq3MhhbkzqJwPo6-4IUP/wZumN1S4dVZPxXo1vz6F/VFKjirA5ExnyxY3Nll47/JreX-K6WRUgMk3chrXSn, stripped
```

Target is to replace the container image build in a next step to utilize
both nix targets to create multi-arch container images.
#### 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
-->
Refers to https://github.com/kubernetes-sigs/security-profiles-operator/issues/282
#### Does this PR have test?

<!--
If tests aren't applicable just write N/A.
-->
None
#### Special notes for your reviewer:
None
#### Does this PR introduce a user-facing change?

<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".

For more information on release notes see: https://git.k8s.io/community/contributors/guide/release-notes.md
-->

```release-note
None
```


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

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/security-profiles-operator/pull/287#" title="Author self-approved">saschagrunert</a>*

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

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

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

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

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":[]} -->

Patch:
diff --git a/.gitignore b/.gitignore
index d8073b22c..b74770193 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@
 .vagrant
 Vagrantfile
 build
+result
diff --git a/Makefile b/Makefile
index fa1ecf957..eb222c5fc 100644
--- a/Makefile
+++ b/Makefile
@@ -40,6 +40,7 @@ else
 LINT_BUILDTAGS := e2e,netgo,osusergo,-tools
 endif
 
+export CGO_ENABLED=1
 
 BUILD_FILES := $(shell find . -type f -name '*.go' -or -name '*.mod' -or -name '*.sum' -not -name '*_test.go')
 export GOFLAGS?=-mod=vendor
@@ -56,7 +57,7 @@ else
   LDFLAGS := -s -w -extldflags "-static" $(LDVARS)
 endif
 
-export CONTAINER_RUNTIME ?= $(if $(shell which podman),podman,docker)
+export CONTAINER_RUNTIME ?= $(if $(shell which podman 2>/dev/null),podman,docker)
 
 ifeq ($(CONTAINER_RUNTIME), podman)
     LOGIN_PUSH_OPTS="--tls-verify=false"
@@ -126,6 +127,19 @@ deployments: manifests ## Generate the deployment files with kustomize
 image: ## Build the container image
 	$(CONTAINER_RUNTIME) build -f $(DOCKERFILE) --build-arg version=$(VERSION) -t $(IMAGE) .
 
+.PHONY: nix
+nix: ## Build the binary via nix for the current system
+	nix-build nix
+
+.PHONY: nix-arm64
+nix-arm64: ## Build the binary via nix for arm64
+	nix-build nix/default-arm64.nix
+
+.PHONY: update-nixpkgs
+update-nixpkgs: ## Update the pinned nixpkgs to the latest master
+	@nix run -f channel:nixpkgs-unstable nix-prefetch-git -c nix-prefetch-git \
+		--no-deepClone https://github.com/nixos/nixpkgs > nix/nixpkgs.json
+
 # Verification targets
 
 .PHONY: verify
diff --git a/deploy/base/kustomization.yaml b/deploy/base/kustomization.yaml
index c1a90b4e2..5526d0f1e 100644
--- a/deploy/base/kustomization.yaml
+++ b/deploy/base/kustomization.yaml
@@ -24,7 +24,7 @@ patchesStrategicMerge:
           - name: security-profiles-operator
             env:
               - name: RELATED_IMAGE_NON_ROOT_ENABLER
-                value: bash:5.0
+                value: docker.io/bash:5.0
               - name: RELATED_IMAGE_SELINUXD
                 value: quay.io/jaosorior/selinuxd
 
diff --git a/deploy/namespace-operator.yaml b/deploy/namespace-operator.yaml
index e13450de6..dc167050b 100644
--- a/deploy/namespace-operator.yaml
+++ b/deploy/namespace-operator.yaml
@@ -533,7 +533,7 @@ spec:
         - name: RESTRICT_TO_NAMESPACE
           value: NS_REPLACE
         - name: RELATED_IMAGE_NON_ROOT_ENABLER
-          value: bash:5.0
+          value: docker.io/bash:5.0
         - name: RELATED_IMAGE_SELINUXD
           value: quay.io/jaosorior/selinuxd
         image: gcr.io/k8s-staging-sp-operator/security-profiles-operator:latest
diff --git a/deploy/operator.yaml b/deploy/operator.yaml
index 72690c796..0561265f8 100644
--- a/deploy/operator.yaml
+++ b/deploy/operator.yaml
@@ -530,7 +530,7 @@ spec:
         - manager
         env:
         - name: RELATED_IMAGE_NON_ROOT_ENABLER
-          value: bash:5.0
+          value: docker.io/bash:5.0
         - name: RELATED_IMAGE_SELINUXD
           value: quay.io/jaosorior/selinuxd
         image: gcr.io/k8s-staging-sp-operator/security-profiles-operator:latest
diff --git a/examples/baseprofile-crun.yaml b/examples/baseprofile-crun.yaml
new file mode 100644
index 000000000..dc1aedb74
--- /dev/null
+++ b/examples/baseprofile-crun.yaml
@@ -0,0 +1,27 @@
+---
+apiVersion: security-profiles-operator.x-k8s.io/v1alpha1
+kind: SeccompProfile
+metadata:
+  name: crun-v0.17
+spec:
+  defaultAction: SCMP_ACT_ERRNO
+  targetWorkload: base
+  architectures:
+  - SCMP_ARCH_X86_64
+  syscalls:
+  - action: SCMP_ACT_ALLOW
+    names:
+    - arch_prctl
+    - capset
+    - close
+    - execve
+    - exit_group
+    - prctl
+    - read
+    - rt_sigaction
+    - rt_sigprocmask
+    - select
+    - set_tid_address
+    - setresgid
+    - setresuid
+    - write
diff --git a/examples/baseprofile.yaml b/examples/baseprofile-runc.yaml
similarity index 100%
rename from examples/baseprofile.yaml
rename to examples/baseprofile-runc.yaml
diff --git a/hack/ci/Vagrantfile b/hack/ci/Vagrantfile
index 04f731d12..572a0370d 100644
--- a/hack/ci/Vagrantfile
+++ b/hack/ci/Vagrantfile
@@ -4,7 +4,7 @@
 # Vagrant box for testing
 Vagrant.configure("2") do |config|
   config.vm.box = "fedora/33-cloud-base"
-  memory = 4096
+  memory = 6144
   cpus = 4
 
   config.vm.provider :virtualbox do |v|
@@ -27,7 +27,9 @@ Vagrant.configure("2") do |config|
         golang-go \
         iptables \
         make \
-        openssl
+        oci-seccomp-bpf-hook \
+        openssl \
+        podman
     SHELL
   end
 
diff --git a/hack/ci/install-cri-o.sh b/hack/ci/install-cri-o.sh
index 0bff6328c..9a3033b10 100755
--- a/hack/ci/install-cri-o.sh
+++ b/hack/ci/install-cri-o.sh
@@ -47,12 +47,9 @@ chcon -R -u system_u -r object_r -t var_lib_t /var/lib/kubelet
 
 mkdir -p /etc/crio/crio.conf.d
 
-cat <<EOT >>/etc/crio/crio.conf.d/20-crun.conf
+cat <<EOT >>/etc/crio/crio.conf.d/30-selinux.conf
 [crio.runtime]
-default_runtime = "crun"
-
-[crio.runtime.runtimes.crun]
-runtime_path = "/usr/local/bin/crun"
+selinux = true
 EOT
 
 cat <<EOT >>/etc/crio/crio.conf.d/30-cgroup-manager.conf
@@ -61,6 +58,14 @@ conmon_cgroup = "pod"
 cgroup_manager = "cgroupfs"
 EOT
 
+cat <<EOT >>/etc/crio/crio.conf.d/20-crun.conf
+[crio.runtime]
+default_runtime = "crun"
+
+[crio.runtime.runtimes.crun]
+runtime_path = "/usr/local/bin/crun"
+EOT
+
 chcon -R -u system_u -r object_r -t container_config_t \
     /etc/crio \
     /etc/crio/crio.conf \
diff --git a/hack/ci/run.sh b/hack/ci/run.sh
index 2b246a7ae..6967eb20b 100755
--- a/hack/ci/run.sh
+++ b/hack/ci/run.sh
@@ -15,4 +15,4 @@
 
 set -euo pipefail
 
-exec vagrant ssh -- sudo "bash -c '. /vagrant/hack/ci/env.sh && $*'"
+exec vagrant ssh -- sudo "bash -c 'cd /vagrant && . hack/ci/env.sh && $*'"
diff --git a/installation-usage.md b/installation-usage.md
index c691c3ab5..610d0c69d 100644
--- a/installation-usage.md
+++ b/installation-usage.md
@@ -114,10 +114,11 @@ $ kubectl --namespace my-namespace get deployment myapp --output=jsonpath='{.spe
 
 #### Base syscalls for a container runtime
 
-An example of the minimum required syscalls for a runtime such as runc (tested
-on version 1.0.0-rc92) to launch a container can be found in [the
-examples](./examples/baseprofile.yaml). You can use this example as a starting
-point for creating custom profiles for your application. You can also
+An example of the minimum required syscalls for a runtime such as
+[runc](https://github.com/opencontainers/runc) (tested on version 1.0.0-rc92) to
+launch a container can be found in [the
+examples](./examples/baseprofile-runc.yaml). You can use this example as a
+starting point for creating custom profiles for your application. You can also
 programmatically combine it with your custom profiles in order to build
 application-specific profiles that only specify syscalls that are required on
 top of the base calls needed for the container runtime. For example:
@@ -138,6 +139,11 @@ spec:
         - exit_group
 ```
 
+If you're not using runc but the alternative
+[crun](https://github.com/containers/crun), then you can do the same by using
+the [corresponding example profile](./examples/baseprofile-crun.yaml) (tested
+with version 0.17).
+
 #### Bind workloads to profiles with ProfileBindings
 
 If you do not want to directly modify the SecurityContext of a Pod, for instance
diff --git a/internal/pkg/controllers/spod/bindata/default_profiles.go b/internal/pkg/controllers/spod/bindata/default_profiles.go
index f6809bac8..df6abf378 100644
--- a/internal/pkg/controllers/spod/bindata/default_profiles.go
+++ b/internal/pkg/controllers/spod/bindata/default_profiles.go
@@ -90,8 +90,8 @@ func DefaultProfiles() []*v1alpha1.SeccompProfile {
 							"getppid",
 							"getrlimit",
 							"getuid",
-							"ioctl",
 							"io_setup",
+							"ioctl",
 							"listen",
 							"lseek",
 							"mkdir",
@@ -117,14 +117,17 @@ func DefaultProfiles() []*v1alpha1.SeccompProfile {
 							"rt_sigsuspend",
 							"sched_getaffinity",
 							"seccomp",
+							"select",
 							"sendfile",
 							"sendmsg",
+							"set_robust_list",
+							"set_tid_address",
 							"setgid",
 							"setgroups",
 							"setitimer",
-							"set_robust_list",
+							"setresgid",
+							"setresuid",
 							"setsockopt",
-							"set_tid_address",
 							"setuid",
 							"sigaltstack",
 							"socket",
diff --git a/nix/default-arm64.nix b/nix/default-arm64.nix
new file mode 100644
index 000000000..edb5c734c
--- /dev/null
+++ b/nix/default-arm64.nix
@@ -0,0 +1,6 @@
+(import ./nixpkgs.nix {
+  crossSystem = {
+    config = "aarch64-unknown-linux-gnu";
+  };
+}).callPackage ./derivation.nix
+{ }
diff --git a/nix/default.nix b/nix/default.nix
new file mode 100644
index 000000000..3bf7c70fa
--- /dev/null
+++ b/nix/default.nix
@@ -0,0 +1,1 @@
+(import ./nixpkgs.nix { }).callPackage ./derivation.nix { }
diff --git a/nix/derivation.nix b/nix/derivation.nix
new file mode 100644
index 000000000..de9bd6883
--- /dev/null
+++ b/nix/derivation.nix
@@ -0,0 +1,24 @@
+{ pkgs, buildGoModule }:
+with pkgs; buildGoModule rec {
+  name = "security-profiles-operator";
+  src = ./..;
+  vendorSha256 = null;
+  doCheck = false;
+  outputs = [ "out" ];
+  nativeBuildInputs = with buildPackages; [
+    git
+    pkg-config
+    which
+  ];
+  buildInputs = [
+    glibc
+    glibc.static
+    (libseccomp.overrideAttrs (x: { dontDisableStatic = true; }))
+  ];
+  buildPhase = ''
+    make
+  '';
+  installPhase = ''
+    install -Dm755 -t $out build/security-profiles-operator
+  '';
+}
diff --git a/nix/nixpkgs.json b/nix/nixpkgs.json
new file mode 100644
index 000000000..84a2c1a31
--- /dev/null
+++ b/nix/nixpkgs.json
@@ -0,0 +1,10 @@
+{
+  "url": "https://github.com/nixos/nixpkgs",
+  "rev": "cc0da25d94f300f9098d3c8bae2106c7997805e4",
+  "date": "2021-02-10T15:00:01+01:00",
+  "path": "/nix/store/f7xw50gii6j3f88lfb7s7crzs6sdxwxi-nixpkgs",
+  "sha256": "0xarfhl988s5npnaqjgw6291pqcpa0pswaqfp05k8gvzrixs52q0",
+  "fetchSubmodules": false,
+  "deepClone": false,
+  "leaveDotGit": false
+}
diff --git a/nix/nixpkgs.nix b/nix/nixpkgs.nix
new file mode 100644
index 000000000..21e7f17a2
--- /dev/null
+++ b/nix/nixpkgs.nix
@@ -0,0 +1,8 @@
+let
+  json = builtins.fromJSON (builtins.readFile ./nixpkgs.json);
+  nixpkgs = import (builtins.fetchTarball {
+    name = "nixos-unstable";
+    url = "${json.url}/archive/${json.rev}.tar.gz";
+    inherit (json) sha256;
+  });
+in nixpkgs


Test patch:
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index 332166ae1..7fd7a4889 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -7,7 +7,7 @@ on:
 jobs:
   test:
     runs-on: macos-10.15
-    timeout-minutes: 30
+    timeout-minutes: 40
     env:
       RUN: ./hack/ci/run.sh
     steps:
@@ -17,5 +17,6 @@ jobs:
           ln -sf hack/ci/Vagrantfile .
           vagrant up
       - name: Show environment information
-        run: |
-          $RUN kubectl get nodes -o wide
+        run: $RUN kubectl get nodes -o wide
+      - name: Run E2E tests
+        run: $RUN hack/ci/e2e.sh
diff --git a/hack/ci/e2e.sh b/hack/ci/e2e.sh
new file mode 100755
index 000000000..1a705c039
--- /dev/null
+++ b/hack/ci/e2e.sh
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+# Copyright 2021 The Kubernetes 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.
+
+set -euo pipefail
+
+export E2E_CLUSTER_TYPE=vanilla
+export E2E_TEST_SELINUX=true
+
+# These are already tested in the standard e2e test.
+# No need to test them here.
+export E2E_TEST_SECCOMP=false
+export E2E_TEST_PROFILE_BINDING=false
+
+make test-e2e
diff --git a/test/e2e_test.go b/test/e2e_test.go
index f63920b44..89f20fdb8 100644
--- a/test/e2e_test.go
+++ b/test/e2e_test.go
@@ -37,6 +37,7 @@ const (
 	// migrate to a single daemonset-based implementation for the
 	// SELinux pieces.
 	defaultSelinuxOpTimeout = "360s"
+	defaultWaitTimeout      = "60s"
 	defaultWaitTime         = 5 * time.Second
 )
 
@@ -143,6 +144,10 @@ func (e *e2e) deployOperator(manifest string) {
 		"sed", "-i", fmt.Sprintf("s;value: .*gcr.io/.*;value: %s;g", e.testImage),
 		manifest,
 	)
+	e.run(
+		"sed", "-i", fmt.Sprintf("s;value: .*quay.io/.*/selinuxd.*;value: %s;g", e.selinuxdImage),
+		manifest,
+	)
 
 	if e.selinuxEnabled {
 		e.run(
@@ -158,12 +163,12 @@ func (e *e2e) deployOperator(manifest string) {
 	// Wait for the operator to be ready
 	e.logf("Waiting for operator to be ready")
 	// Wait for deployment
-	e.kubectlOperatorNS("wait", "--for", "condition=available", "deployment", "-l", "app=security-profiles-operator")
+	e.waitInOperatorNSFor("condition=available", "deployment", "-l", "app=security-profiles-operator")
 	// Wait for all pods in deployment
-	e.kubectlOperatorNS("wait", "--for", "condition=ready", "pod", "-l", "app=security-profiles-operator")
+	e.waitInOperatorNSFor("condition=ready", "pod", "-l", "app=security-profiles-operator")
 	// Wait for all pods in DaemonSet
 	time.Sleep(defaultWaitTime)
-	e.kubectlOperatorNS("wait", "--for", "condition=ready", "pod", "-l", "app=spod")
+	e.waitInOperatorNSFor("condition=ready", "pod", "-l", "app=spod")
 }
 
 func (e *e2e) cleanupOperator(manifest string) {
diff --git a/test/suite_test.go b/test/suite_test.go
index 1956718ce..1c42f7e74 100644
--- a/test/suite_test.go
+++ b/test/suite_test.go
@@ -44,21 +44,26 @@ const (
 )
 
 var (
-	clusterType        = os.Getenv("E2E_CLUSTER_TYPE")
-	envSkipBuildImages = os.Getenv("E2E_SKIP_BUILD_IMAGES")
-	envTestImage       = os.Getenv("E2E_SPO_IMAGE")
-	envSelinuxEnabled  = os.Getenv("E2E_ENABLE_SELINUX")
-	containerRuntime   = os.Getenv("CONTAINER_RUNTIME")
+	clusterType                   = os.Getenv("E2E_CLUSTER_TYPE")
+	envSkipBuildImages            = os.Getenv("E2E_SKIP_BUILD_IMAGES")
+	envTestImage                  = os.Getenv("E2E_SPO_IMAGE")
+	envSelinuxTestsEnabled        = os.Getenv("E2E_TEST_SELINUX")
+	envSeccompTestsEnabled        = os.Getenv("E2E_TEST_SECCOMP")
+	envProfileBindingTestsEnabled = os.Getenv("E2E_TEST_PROFILE_BINDING")
+	containerRuntime              = os.Getenv("CONTAINER_RUNTIME")
 )
 
 type e2e struct {
 	suite.Suite
-	kubectlPath    string
-	testImage      string
-	pullPolicy     string
-	selinuxEnabled bool
-	logger         logr.Logger
-	execNode       func(node string, args ...string) string
+	kubectlPath        string
+	testImage          string
+	selinuxdImage      string
+	pullPolicy         string
+	selinuxEnabled     bool
+	testSeccomp        bool
+	testProfileBinding bool
+	logger             logr.Logger
+	execNode           func(node string, args ...string) string
 }
 
 type kinde2e struct {
@@ -73,6 +78,10 @@ type openShifte2e struct {
 	skipPushImages  bool
 }
 
+type vanilla struct {
+	e2e
+}
+
 // We're unable to use parallel tests because of our usage of testify/suite.
 // See https://github.com/stretchr/testify/issues/187
 //
@@ -82,10 +91,19 @@ func TestSuite(t *testing.T) {
 	fmt.Printf("container-runtime: %s\n", containerRuntime)
 
 	testImage := envTestImage
-	selinuxEnabled, err := strconv.ParseBool(envSelinuxEnabled)
+	selinuxEnabled, err := strconv.ParseBool(envSelinuxTestsEnabled)
 	if err != nil {
 		selinuxEnabled = false
 	}
+	testSeccomp, err := strconv.ParseBool(envSeccompTestsEnabled)
+	if err != nil {
+		testSeccomp = true
+	}
+	testProfileBinding, err := strconv.ParseBool(envProfileBindingTestsEnabled)
+	if err != nil {
+		testProfileBinding = true
+	}
+	selinuxdImage := "quay.io/jaosorior/selinuxd"
 	switch {
 	case clusterType == "" || strings.EqualFold(clusterType, "kind"):
 		if testImage == "" {
@@ -93,10 +111,13 @@ func TestSuite(t *testing.T) {
 		}
 		suite.Run(t, &kinde2e{
 			e2e{
-				logger:         klogr.New(),
-				pullPolicy:     "Never",
-				testImage:      testImage,
-				selinuxEnabled: selinuxEnabled,
+				logger:             klogr.New(),
+				pullPolicy:         "Never",
+				testImage:          testImage,
+				selinuxEnabled:     selinuxEnabled,
+				testSeccomp:        testSeccomp,
+				testProfileBinding: testProfileBinding,
+				selinuxdImage:      selinuxdImage,
 			},
 			"", "",
 		})
@@ -114,13 +135,32 @@ 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",
-				testImage:      testImage,
-				selinuxEnabled: selinuxEnabled,
+				pullPolicy:         "Always",
+				testImage:          testImage,
+				selinuxEnabled:     selinuxEnabled,
+				testSeccomp:        testSeccomp,
+				testProfileBinding: testProfileBinding,
+				selinuxdImage:      selinuxdImage,
 			},
 			skipBuildImages,
 			skipPushImages,
 		})
+	case strings.EqualFold(clusterType, "vanilla"):
+		if testImage == "" {
+			testImage = "localhost/" + config.OperatorName + ":latest"
+		}
+		selinuxdImage = "quay.io/jaosorior/selinuxd-fedora"
+		suite.Run(t, &vanilla{
+			e2e{
+				logger:             klogr.New(),
+				pullPolicy:         "Never",
+				testImage:          testImage,
+				selinuxEnabled:     selinuxEnabled,
+				testSeccomp:        testSeccomp,
+				testProfileBinding: testProfileBinding,
+				selinuxdImage:      selinuxdImage,
+			},
+		})
 	default:
 		t.Fatalf("Unknown cluster type.")
 	}
@@ -132,11 +172,7 @@ func (e *kinde2e) SetupSuite() {
 	command.SetGlobalVerbose(true)
 	// Override execNode function
 	e.e2e.execNode = e.execNodeKind
-	cwd, err := os.Getwd()
-	e.Nil(err)
-
-	parentCwd := filepath.Dir(cwd)
-	e.Nil(os.Chdir(parentCwd))
+	parentCwd := e.setWorkDir()
 	buildDir := filepath.Join(parentCwd, "build")
 	e.Nil(os.MkdirAll(buildDir, 0o755))
 
@@ -155,6 +191,7 @@ func (e *kinde2e) SetupSuite() {
 	e.downloadAndVerify(fmt.Sprintf("https://github.com/kubernetes-sigs/kind/releases/download/%s/%s",
 		kindVersion, kindOS), e.kindPath, SHA512)
 
+	var err error
 	e.kubectlPath, err = exec.LookPath("kubectl")
 	e.Nil(err)
 }
@@ -188,7 +225,7 @@ func (e *kinde2e) SetupTest() {
 		e.kindPath, "load", "docker-image", "--name="+e.clusterName, e.testImage,
 	)
 	e.run(
-		"docker", "pull", "quay.io/jaosorior/selinuxd",
+		containerRuntime, "pull", e.selinuxdImage,
 	)
 	e.run(
 		e.kindPath, "load", "docker-image", "--name="+e.clusterName, "quay.io/jaosorior/selinuxd",
@@ -217,17 +254,12 @@ func (e *kinde2e) execNodeKind(node string, args ...string) string {
 }
 
 func (e *openShifte2e) SetupSuite() {
+	var err error
 	e.logf("Setting up suite")
 	command.SetGlobalVerbose(true)
 	// Override execNode function
 	e.e2e.execNode = e.execNodeOCP
-	cwd, err := os.Getwd()
-	e.Nil(err)
-
-	parentCwd := filepath.Dir(cwd)
-	e.Nil(os.Chdir(parentCwd))
-	buildDir := filepath.Join(parentCwd, "build")
-	e.Nil(os.MkdirAll(buildDir, 0o755))
+	e.setWorkDir()
 
 	e.kubectlPath, err = exec.LookPath("oc")
 	e.Nil(err)
@@ -235,7 +267,7 @@ func (e *openShifte2e) SetupSuite() {
 	e.logf("Using deployed OpenShift cluster")
 
 	e.logf("Waiting for cluster to be ready")
-	e.kubectl("wait", "--for", "condition=ready", "nodes", "--all")
+	e.waitFor("condition=ready", "nodes", "--all")
 
 	if !e.skipPushImages {
 		e.logf("pushing SPO image to openshift registry")
@@ -309,6 +341,41 @@ func (e *openShifte2e) execNodeOCP(node string, args ...string) string {
 	)
 }
 
+func (e *vanilla) SetupSuite() {
+	var err error
+	e.logf("Setting up suite")
+	e.setWorkDir()
+
+	e.e2e.execNode = e.execNodeVanilla
+	e.kubectlPath, err = exec.LookPath("kubectl")
+	e.Nil(err)
+}
+
+func (e *vanilla) SetupTest() {
+	// Build and load the test image
+	e.logf("Building operator container image")
+	e.run("make", "image", "IMAGE="+e.testImage)
+	e.run(
+		containerRuntime, "pull", e.selinuxdImage,
+	)
+}
+
+func (e *vanilla) TearDownTest() {
+}
+
+func (e *vanilla) execNodeVanilla(node string, args ...string) string {
+	return e.run(args[0], args[1:]...)
+}
+
+func (e *e2e) setWorkDir() string {
+	cwd, err := os.Getwd()
+	e.Nil(err)
+
+	parentCwd := filepath.Dir(cwd)
+	e.Nil(os.Chdir(parentCwd))
+	return parentCwd
+}
+
 func (e *e2e) run(cmd string, args ...string) string {
 	output, err := command.New(cmd, args...).RunSuccessOutput()
 	e.Nil(err)
@@ -352,12 +419,36 @@ func (e *e2e) kubectlOperatorNS(args ...string) {
 	)
 }
 
+func (e *e2e) waitFor(args ...string) {
+	e.kubectl(
+		append([]string{"wait", "--timeout", defaultWaitTimeout, "--for"}, args...)...,
+	)
+}
+
+func (e *e2e) waitInOperatorNSFor(args ...string) {
+	e.kubectlOperatorNS(
+		append([]string{"wait", "--timeout", defaultWaitTimeout, "--for"}, args...)...,
+	)
+}
+
 func (e *e2e) logf(format string, a ...interface{}) {
 	e.logger.Info(fmt.Sprintf(format, a...))
 }
 
 func (e *e2e) selinuxtOnlyTestCase() {
 	if !e.selinuxEnabled {
-		e.T().Skip("Skipping SELinux-related test from non-OpenShift cluster")
+		e.T().Skip("Skipping SELinux-related test")
+	}
+}
+
+func (e *e2e) seccompOnlyTestCase() {
+	if !e.testSeccomp {
+		e.T().Skip("Skipping Seccomp-related test")
+	}
+}
+
+func (e *e2e) profileBindingTestCase() {
+	if !e.testProfileBinding {
+		e.T().Skip("Skipping Profile-Binding-related test")
 	}
 }
diff --git a/test/tc_base_profiles_test.go b/test/tc_base_profiles_test.go
index 3250037d9..f1019987d 100644
--- a/test/tc_base_profiles_test.go
+++ b/test/tc_base_profiles_test.go
@@ -24,7 +24,8 @@ import (
 )
 
 func (e *e2e) testCaseBaseProfile([]string) {
-	const baseProfilePath = "examples/baseprofile.yaml"
+	e.seccompOnlyTestCase()
+	const baseProfilePath = "examples/baseprofile-runc.yaml"
 	const helloProfile = `
 apiVersion: security-profiles-operator.x-k8s.io/v1alpha1
 kind: SeccompProfile
@@ -85,7 +86,7 @@ spec:
 	defer e.kubectl("delete", "pod", "hello")
 
 	e.logf("Waiting for test pod to be initialized")
-	e.kubectl("wait", "--for", "condition=initialized", "pod", "hello")
+	e.waitFor("condition=initialized", "pod", "hello")
 
 	output := e.kubectl("get", "pod", "hello")
 	for strings.Contains(output, "ContainerCreating") {
diff --git a/test/tc_default_profiles_test.go b/test/tc_default_profiles_test.go
index 114bc078f..81aa15bfc 100644
--- a/test/tc_default_profiles_test.go
+++ b/test/tc_default_profiles_test.go
@@ -28,6 +28,7 @@ import (
 )
 
 func (e *e2e) testCaseDefaultAndExampleProfiles(nodes []string) {
+	e.seccompOnlyTestCase()
 	const exampleProfilePath = "examples/seccompprofile.yaml"
 	exampleProfileNames := [3]string{"profile-allow-unsafe", "profile-complain-unsafe", "profile-block-all"}
 	defaultProfileNames := [1]string{"nginx-1.19.1"}
diff --git a/test/tc_delete_profiles_test.go b/test/tc_delete_profiles_test.go
index db74ff815..f8050e1af 100644
--- a/test/tc_delete_profiles_test.go
+++ b/test/tc_delete_profiles_test.go
@@ -24,6 +24,7 @@ import (
 )
 
 func (e *e2e) testCaseDeleteProfiles(nodes []string) {
+	e.seccompOnlyTestCase()
 	const (
 		deleteProfile = `
 apiVersion: security-profiles-operator.x-k8s.io/v1alpha1
@@ -142,7 +143,7 @@ spec:
 		defer profileCleanup()
 		podCleanup := e.writeAndCreate(fmt.Sprintf(testCase.podManifest, namespace), "delete-pod*.yaml")
 		defer podCleanup()
-		e.kubectl("wait", "--for", "condition=ready", "pod", deletePodName)
+		e.waitFor("condition=ready", "pod", deletePodName)
 		e.logf("Ensuring profile cannot be deleted while pod is active")
 		e.kubectl("delete", "seccompprofile", deleteProfileName, "--wait=0")
 		output := e.kubectl("get", "seccompprofile", deleteProfileName)
diff --git a/test/tc_profilebindings_test.go b/test/tc_profilebindings_test.go
index 46615ce5d..6de36e201 100644
--- a/test/tc_profilebindings_test.go
+++ b/test/tc_profilebindings_test.go
@@ -24,6 +24,7 @@ import (
 )
 
 func (e *e2e) testCaseProfileBinding([]string) {
+	e.profileBindingTestCase()
 	const exampleProfilePath = "examples/seccompprofile.yaml"
 	const testBinding = `
 apiVersion: security-profiles-operator.x-k8s.io/v1alpha1
@@ -81,7 +82,7 @@ spec:
 	defer e.kubectl("delete", "pod", "hello")
 
 	e.logf("Waiting for test pod to be initialized")
-	e.kubectl("wait", "--for", "condition=initialized", "pod", "hello")
+	e.waitFor("condition=initialized", "pod", "hello")
 
 	output := e.kubectl("get", "pod", "hello")
 	for strings.Contains(output, "ContainerCreating") {
@@ -126,9 +127,9 @@ func (e *e2e) deployWebhook(manifest string) {
 	// Deploy prerequisites
 	e.logf("Deploying cert-manager")
 	e.kubectl("apply", "-f", certmanager)
-	e.kubectl(
+	e.waitFor(
+		"condition=ready",
 		"--namespace", "cert-manager",
-		"wait", "--for", "condition=ready",
 		"pod", "-l", "app.kubernetes.io/instance=cert-manager",
 	)
 	e.run(
@@ -142,8 +143,8 @@ func (e *e2e) deployWebhook(manifest string) {
 	)
 	e.logf("Deploying webhook")
 	e.kubectl("create", "-f", manifest)
-	e.kubectlOperatorNS("wait", "--for", "condition=ready", "pod", "-l", "name=security-profiles-operator-webhook")
-	e.kubectlOperatorNS("wait", "--for", "condition=ready", "certificate", "webhook-cert")
+	e.waitInOperatorNSFor("condition=ready", "pod", "-l", "name=security-profiles-operator-webhook")
+	e.waitInOperatorNSFor("condition=ready", "certificate", "webhook-cert")
 }
 
 func (e *e2e) cleanupWebhook(manifest string) {
diff --git a/test/tc_run_pod_test.go b/test/tc_run_pod_test.go
index 4af700f72..278ed90af 100644
--- a/test/tc_run_pod_test.go
+++ b/test/tc_run_pod_test.go
@@ -19,6 +19,7 @@ package e2e_test
 import "fmt"
 
 func (e *e2e) testCaseRunPod([]string) {
+	e.seccompOnlyTestCase()
 	const (
 		examplePodPath = "examples/pod.yaml"
 		examplePodName = "test-pod"
@@ -38,7 +39,7 @@ func (e *e2e) testCaseRunPod([]string) {
 	defer e.kubectl("delete", "-f", examplePodPath)
 
 	e.logf("Waiting for test pod to be ready")
-	e.kubectl("wait", "--for", "condition=ready", "pod", "--all")
+	e.waitFor("condition=ready", "pod", "--all")
 
 	e.logf("Testing that `rmdir` is not possible inside the pod")
 	failureOutput := e.kubectlFailure(
diff --git a/test/tc_selinux_base_usage_test.go b/test/tc_selinux_base_usage_test.go
index 57172e5db..a5b262c9d 100644
--- a/test/tc_selinux_base_usage_test.go
+++ b/test/tc_selinux_base_usage_test.go
@@ -108,7 +108,7 @@ spec:
 	podWithPolicy := fmt.Sprintf(podWithPolicyFmt, e.getSELinuxPolicyUsage("errorlogger"))
 	e.writeAndCreate(podWithPolicy, "pod-w-policy.yml")
 
-	e.kubectl("wait", "--for", "condition=ready", "pod", "errorlogger")
+	e.waitFor("condition=ready", "pod", "errorlogger")
 
 	e.logf("the workload should be running")
 	podWithPolicyPhase := e.kubectl(
diff --git a/test/tc_selinux_sanity_check_test.go b/test/tc_selinux_sanity_check_test.go
index 1497eb5b0..b6e1ac937 100644
--- a/test/tc_selinux_sanity_check_test.go
+++ b/test/tc_selinux_sanity_check_test.go
@@ -48,12 +48,12 @@ spec:
 	e.logf("creating workload")
 	e.writeAndCreate(podWithoutPolicy, "pod-wo-policy.yml")
 
-	e.kubectl("wait", "--for", "condition=ready", "pod", "el-no-policy")
+	e.waitFor("condition=ready", "pod", "el-no-policy")
 
 	e.logf("the workload should have errored")
 	expectedLog := "/bin/bash: /var/log/test.log: Permission denied"
 	log := e.kubectl("logs", "el-no-policy", "-c", "errorlogger")
-	e.Equalf(log, expectedLog, "container should have returned a 'Permissions Denied' error")
+	e.Equalf(expectedLog, log, "container should have returned a 'Permissions Denied' error")
 
 	e.kubectl("delete", "pod", "el-no-policy")
 }