Repository: kubernetes-sigs/security-profiles-operator (c)

Issue:
Automatic selinuxd image selection
#### What would you like to be added:
SPO to be able to automatically select the appropriate selinuxd image for the nodes where the SPOD DS is running on.

#### Why is this needed:
Different OSs have different libsemanage versions and the version of libsemanage inside the selinuxd container must match the version of libsemanage on the host.

#### User story covered
As an infrastructure SRE, Kirsti wants to make sure that no security profiles are in an errored state

#### High level design
First, selinuxd images  must exist for the target OS. The list of possible (approved) selinuxd images must be available to SPO.

Next, because the image must be known before the DS is created, the selection must be performed by the operator, probably by the spod controller. A new workload that examines the target nodes should probably be created. 

#### More detailed design proposal

1. the list of approved images would be created as a deploy time manifest, similarly to how we create the seccomp profile for the SPO. The format will be a configMap where the key is the `CPE_NAME` key in the `/etc/os-release` file and the value is a pullspec of selinuxd.
2. The spod controller will be adjusted so that if selinuxd is enabled, a `Job` will be created with the configMap mentioned above. This job will be privileged, but the only thing it will do is to read `/etc/os-release` and write the appropriate selinuxd pullspec into the spod status.
3. this pullspec will be then used by the DS

#### Questions

- What should the semantic of `RELATED_IMAGE_SELINUXD` be? IMO if that variable is set, then the selection above should not be performed and the related image should be used.
- What if the selection fails? Is there another option than failure? (I don't think so)

Pull request:
Auto-select selinuxd image based on nodeInfo.OSImage
<!--  Thanks for sending a pull request!  Here are some tips for you:

- If this is your first time, please read our contributor guidelines: https://git.k8s.io/community/contributors/guide#your-first-contribution and developer guide https://git.k8s.io/community/contributors/devel/development.md#development-guide
- Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request. For reference on required PR/issue labels, read here:
https://git.k8s.io/community/contributors/devel/sig-release/release.md#issuepr-kind-label
- If you want *faster* PR reviews, read how: https://git.k8s.io/community/contributors/guide/pull-requests.md#best-practices-for-faster-reviews
- If the PR is unfinished, see how to mark it: https://git.k8s.io/community/contributors/guide/pull-requests.md#marking-unfinished-pull-requests
-->

#### What type of PR is this?
/kind feature

<!--
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind design

Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->

#### What this PR does / why we need it:
Cluster nodes might be upgraded between major OS releases where the
libselinux major version is incompatible. This means that the selinuxd
image must be based on images matching the OS release and the libselinux
major version.

But SPO must also be able to select the images based on what OS are the
cluster nodes running.

To implement that, this patch adds a new key to the
security-profiles-operator-profile configmap that maps regexes matching
nodeInfo.OSImage to selinuxd images. The operator then selects the right
selinuxd image, falling back to using the `RELATED_IMAGE_SELINUXD` environment
variable.


#### Which issue(s) this PR fixes:
Fixes #1576

<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.


or

None
-->

#### Does this PR have test?
some unit tests

<!--
If tests aren't applicable just write N/A.
-->

#### Special notes for your reviewer:
I'll hold this patch before I can install a RHEL-9 based cluster - I've
been having issues with that. But I wanted to get some early feedback.

#### 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
SPO now autoselects the appropriate selinuxd image based on mapping in the
security-profiles-operator-profile. If none of the entries match, SPO falls back
to the image provided by `RELATED_IMAGE_SELINUXD`.
```


Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 726daf4ac..7d9d7c8a5 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -7,6 +7,7 @@ on:
 env:
   GO_VERSION: '1.20'
   NIX_VERSION: '2.13.3'
+  BOM_VERSION: v0.5.1
 
 permissions: {}
 
@@ -56,6 +57,10 @@ jobs:
           name: security-profiles-operator
           authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
           pushFilter: security-profiles-operator
+      - run: |
+          sudo curl -sSfL --retry 5 --retry-delay 3 -o /usr/bin/bom \
+            https://github.com/kubernetes-sigs/bom/releases/download/${{ env.BOM_VERSION }}/bom-amd64-linux
+          sudo chmod +x /usr/bin/bom
       - run: make nix-spoc
       - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
         with:
diff --git a/Makefile b/Makefile
index 625e10d17..d5b4101fe 100644
--- a/Makefile
+++ b/Makefile
@@ -222,10 +222,21 @@ define nix-build-sign-spoc-to
 		$(BUILD_DIR)/spoc.$(1) \
 		--output-signature $(BUILD_DIR)/spoc.$(1).sig \
 		--output-certificate $(BUILD_DIR)/spoc.$(1).cert
+	cd $(BUILD_DIR) && sha512sum spoc.$(1) > spoc.$(1).sha512
 endef
 
 .PHONY: nix-spoc
 nix-spoc: nix-spoc-amd64 nix-spoc-arm64 ## Build all spoc binaries via nix.
+	bom version
+	bom generate \
+		-l Apache-2.0 \
+		--name spoc \
+		-d $(BUILD_DIR) \
+		-o $(BUILD_DIR)/spoc.spdx
+	cosign sign-blob -y \
+		$(BUILD_DIR)/spoc.spdx \
+		--output-signature $(BUILD_DIR)/spoc.spdx.sig \
+		--output-certificate $(BUILD_DIR)/spoc.spdx.cert
 
 .PHONY: nix-spoc-amd64
 nix-spoc-amd64: $(BUILD_DIR) ## Build and sign the spoc binary via nix for amd64
diff --git a/bundle/manifests/security-profiles-operator-profile_v1_configmap.yaml b/bundle/manifests/security-profiles-operator-profile_v1_configmap.yaml
index a59945d56..d125aa80d 100644
--- a/bundle/manifests/security-profiles-operator-profile_v1_configmap.yaml
+++ b/bundle/manifests/security-profiles-operator-profile_v1_configmap.yaml
@@ -98,6 +98,17 @@ data:
         }
       ]
     }
+  selinuxd-image-mapping.json: |
+    [
+        {
+            "regex":"(.*)(CoreOS).*([\\d+])\\.8[\\d+]\\.(.*)",
+            "image":"quay.io/security-profiles-operator/selinuxd-el8:latest"
+        },
+        {
+            "regex":"(.*)(CoreOS).*([\\d+])\\.9[\\d+]\\.(.*)",
+            "image":"quay.io/security-profiles-operator/selinuxd-el9:latest"
+        }
+    ]
   selinuxd.cil: |
     (block selinuxd
         (blockinherit container)
diff --git a/bundle/manifests/security-profiles-operator.clusterserviceversion.yaml b/bundle/manifests/security-profiles-operator.clusterserviceversion.yaml
index 256a2fe37..0836f582f 100644
--- a/bundle/manifests/security-profiles-operator.clusterserviceversion.yaml
+++ b/bundle/manifests/security-profiles-operator.clusterserviceversion.yaml
@@ -306,6 +306,14 @@ spec:
     spec:
       clusterPermissions:
       - rules:
+        - apiGroups:
+          - ""
+          resourceNames:
+          - security-profiles-operator-profile
+          resources:
+          - configmaps
+          verbs:
+          - get
         - apiGroups:
           - ""
           resources:
diff --git a/dependencies.yaml b/dependencies.yaml
index dadbc5252..66867f2fc 100644
--- a/dependencies.yaml
+++ b/dependencies.yaml
@@ -200,10 +200,12 @@ dependencies:
       match: COSIGN_VERSION
 
   - name: bom
-    version: v0.4.1
+    version: v0.5.1
     refPaths:
     - path: hack/ci/install-cri-o.sh
       match: BOM_VERSION
+    - path: .github/workflows/build.yml
+      match: BOM_VERSION
 
   - name: spo-current
     version: 0.7.2-dev
diff --git a/deploy/base/kustomization.yaml b/deploy/base/kustomization.yaml
index 3261fadf9..0ca49b41b 100644
--- a/deploy/base/kustomization.yaml
+++ b/deploy/base/kustomization.yaml
@@ -15,6 +15,7 @@ configMapGenerator:
   - profiles/security-profiles-operator.json
   - profiles/selinuxd.cil
   - profiles/selinuxrecording.cil
+  - profiles/selinuxd-image-mapping.json
   name: security-profiles-operator-profile
 
 generatorOptions:
diff --git a/deploy/base/profiles/selinuxd-image-mapping.json b/deploy/base/profiles/selinuxd-image-mapping.json
new file mode 100644
index 000000000..900be5241
--- /dev/null
+++ b/deploy/base/profiles/selinuxd-image-mapping.json
@@ -0,0 +1,10 @@
+[
+    {
+        "regex":"(.*)(CoreOS).*([\\d+])\\.8[\\d+]\\.(.*)",
+        "image":"quay.io/security-profiles-operator/selinuxd-el8:latest"
+    },
+    {
+        "regex":"(.*)(CoreOS).*([\\d+])\\.9[\\d+]\\.(.*)",
+        "image":"quay.io/security-profiles-operator/selinuxd-el9:latest"
+    }
+]
diff --git a/deploy/base/role.yaml b/deploy/base/role.yaml
index f26df8c1a..54d4a5bd0 100644
--- a/deploy/base/role.yaml
+++ b/deploy/base/role.yaml
@@ -5,6 +5,14 @@ metadata:
   creationTimestamp: null
   name: security-profiles-operator
 rules:
+- apiGroups:
+  - ""
+  resourceNames:
+  - security-profiles-operator-profile
+  resources:
+  - configmaps
+  verbs:
+  - get
 - apiGroups:
   - ""
   resources:
diff --git a/deploy/helm/templates/static-resources.yaml b/deploy/helm/templates/static-resources.yaml
index d7c3ee9e7..b8d95f7ec 100644
--- a/deploy/helm/templates/static-resources.yaml
+++ b/deploy/helm/templates/static-resources.yaml
@@ -50,6 +50,14 @@ metadata:
     helm.sh/chart: security-profiles-operator
   name: security-profiles-operator
 rules:
+- apiGroups:
+  - ""
+  resourceNames:
+  - security-profiles-operator-profile
+  resources:
+  - configmaps
+  verbs:
+  - get
 - apiGroups:
   - ""
   resources:
@@ -966,6 +974,17 @@ data:
         }
       ]
     }
+  selinuxd-image-mapping.json: |
+    [
+        {
+            "regex":"(.*)(CoreOS).*([\\d+])\\.8[\\d+]\\.(.*)",
+            "image":"quay.io/security-profiles-operator/selinuxd-el8:latest"
+        },
+        {
+            "regex":"(.*)(CoreOS).*([\\d+])\\.9[\\d+]\\.(.*)",
+            "image":"quay.io/security-profiles-operator/selinuxd-el9:latest"
+        }
+    ]
   selinuxd.cil: |
     (block selinuxd
         (blockinherit container)
diff --git a/deploy/namespace-operator.yaml b/deploy/namespace-operator.yaml
index 30ec3d6d4..10ff10f5f 100644
--- a/deploy/namespace-operator.yaml
+++ b/deploy/namespace-operator.yaml
@@ -2082,6 +2082,14 @@ metadata:
     app: security-profiles-operator
   name: security-profiles-operator
 rules:
+- apiGroups:
+  - ""
+  resourceNames:
+  - security-profiles-operator-profile
+  resources:
+  - configmaps
+  verbs:
+  - get
 - apiGroups:
   - ""
   resources:
@@ -2922,6 +2930,17 @@ data:
         }
       ]
     }
+  selinuxd-image-mapping.json: |
+    [
+        {
+            "regex":"(.*)(CoreOS).*([\\d+])\\.8[\\d+]\\.(.*)",
+            "image":"quay.io/security-profiles-operator/selinuxd-el8:latest"
+        },
+        {
+            "regex":"(.*)(CoreOS).*([\\d+])\\.9[\\d+]\\.(.*)",
+            "image":"quay.io/security-profiles-operator/selinuxd-el9:latest"
+        }
+    ]
   selinuxd.cil: |
     (block selinuxd
         (blockinherit container)
diff --git a/deploy/openshift-dev.yaml b/deploy/openshift-dev.yaml
index 0f9979f1b..5d61634cc 100644
--- a/deploy/openshift-dev.yaml
+++ b/deploy/openshift-dev.yaml
@@ -2154,6 +2154,14 @@ metadata:
     app: security-profiles-operator
   name: security-profiles-operator
 rules:
+- apiGroups:
+  - ""
+  resourceNames:
+  - security-profiles-operator-profile
+  resources:
+  - configmaps
+  verbs:
+  - get
 - apiGroups:
   - ""
   resources:
@@ -2904,6 +2912,17 @@ data:
         }
       ]
     }
+  selinuxd-image-mapping.json: |
+    [
+        {
+            "regex":"(.*)(CoreOS).*([\\d+])\\.8[\\d+]\\.(.*)",
+            "image":"quay.io/security-profiles-operator/selinuxd-el8:latest"
+        },
+        {
+            "regex":"(.*)(CoreOS).*([\\d+])\\.9[\\d+]\\.(.*)",
+            "image":"quay.io/security-profiles-operator/selinuxd-el9:latest"
+        }
+    ]
   selinuxd.cil: |
     (block selinuxd
         (blockinherit container)
diff --git a/deploy/openshift-downstream.yaml b/deploy/openshift-downstream.yaml
index c2483ea3a..c56a8e663 100644
--- a/deploy/openshift-downstream.yaml
+++ b/deploy/openshift-downstream.yaml
@@ -2082,6 +2082,14 @@ metadata:
     app: security-profiles-operator
   name: security-profiles-operator
 rules:
+- apiGroups:
+  - ""
+  resourceNames:
+  - security-profiles-operator-profile
+  resources:
+  - configmaps
+  verbs:
+  - get
 - apiGroups:
   - ""
   resources:
@@ -2935,6 +2943,17 @@ data:
         }
       ]
     }
+  selinuxd-image-mapping.json: |
+    [
+        {
+            "regex":"(.*)(CoreOS).*([\\d+])\\.8[\\d+]\\.(.*)",
+            "image":"quay.io/security-profiles-operator/selinuxd-el8:latest"
+        },
+        {
+            "regex":"(.*)(CoreOS).*([\\d+])\\.9[\\d+]\\.(.*)",
+            "image":"quay.io/security-profiles-operator/selinuxd-el9:latest"
+        }
+    ]
   selinuxd.cil: |
     (block selinuxd
         (blockinherit container)
diff --git a/deploy/operator.yaml b/deploy/operator.yaml
index acae863fc..184fbf879 100644
--- a/deploy/operator.yaml
+++ b/deploy/operator.yaml
@@ -2082,6 +2082,14 @@ metadata:
     app: security-profiles-operator
   name: security-profiles-operator
 rules:
+- apiGroups:
+  - ""
+  resourceNames:
+  - security-profiles-operator-profile
+  resources:
+  - configmaps
+  verbs:
+  - get
 - apiGroups:
   - ""
   resources:
@@ -2922,6 +2930,17 @@ data:
         }
       ]
     }
+  selinuxd-image-mapping.json: |
+    [
+        {
+            "regex":"(.*)(CoreOS).*([\\d+])\\.8[\\d+]\\.(.*)",
+            "image":"quay.io/security-profiles-operator/selinuxd-el8:latest"
+        },
+        {
+            "regex":"(.*)(CoreOS).*([\\d+])\\.9[\\d+]\\.(.*)",
+            "image":"quay.io/security-profiles-operator/selinuxd-el9:latest"
+        }
+    ]
   selinuxd.cil: |
     (block selinuxd
         (blockinherit container)
diff --git a/deploy/overlays/webhook/role.yaml b/deploy/overlays/webhook/role.yaml
index f5c1444a2..7833d90a2 100644
--- a/deploy/overlays/webhook/role.yaml
+++ b/deploy/overlays/webhook/role.yaml
@@ -1,3 +1,3 @@
 ---
-- path: /rules/2
+- path: /rules/3
   op: remove
diff --git a/deploy/webhook-operator.yaml b/deploy/webhook-operator.yaml
index eecb690a1..cad32631d 100644
--- a/deploy/webhook-operator.yaml
+++ b/deploy/webhook-operator.yaml
@@ -2154,6 +2154,14 @@ metadata:
     app: security-profiles-operator
   name: security-profiles-operator
 rules:
+- apiGroups:
+  - ""
+  resourceNames:
+  - security-profiles-operator-profile
+  resources:
+  - configmaps
+  verbs:
+  - get
 - apiGroups:
   - ""
   resources:
@@ -2893,6 +2901,17 @@ data:
         }
       ]
     }
+  selinuxd-image-mapping.json: |
+    [
+        {
+            "regex":"(.*)(CoreOS).*([\\d+])\\.8[\\d+]\\.(.*)",
+            "image":"quay.io/security-profiles-operator/selinuxd-el8:latest"
+        },
+        {
+            "regex":"(.*)(CoreOS).*([\\d+])\\.9[\\d+]\\.(.*)",
+            "image":"quay.io/security-profiles-operator/selinuxd-el9:latest"
+        }
+    ]
   selinuxd.cil: |
     (block selinuxd
         (blockinherit container)
diff --git a/go.mod b/go.mod
index 78c02b957..cdaa24745 100644
--- a/go.mod
+++ b/go.mod
@@ -112,7 +112,7 @@ require (
 	github.com/dimchansky/utfbom v1.1.1 // indirect
 	github.com/docker/cli v23.0.1+incompatible // indirect
 	github.com/docker/distribution v2.8.1+incompatible // indirect
-	github.com/docker/docker v23.0.1+incompatible // indirect
+	github.com/docker/docker v23.0.3+incompatible // indirect
 	github.com/docker/docker-credential-helpers v0.7.0 // indirect
 	github.com/emicklei/go-restful/v3 v3.9.0 // indirect
 	github.com/emicklei/proto v1.6.15 // indirect
diff --git a/go.sum b/go.sum
index 6aac6696a..541e29c67 100644
--- a/go.sum
+++ b/go.sum
@@ -294,8 +294,8 @@ github.com/docker/cli v23.0.1+incompatible h1:LRyWITpGzl2C9e9uGxzisptnxAn1zfZKXy
 github.com/docker/cli v23.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
 github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/docker v23.0.1+incompatible h1:vjgvJZxprTTE1A37nm+CLNAdwu6xZekyoiVlUZEINcY=
-github.com/docker/docker v23.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker v23.0.3+incompatible h1:9GhVsShNWz1hO//9BNg/dpMnZW25KydO4wtVxWAIbho=
+github.com/docker/docker v23.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
 github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
 github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
diff --git a/hack/ci/install-cri-o.sh b/hack/ci/install-cri-o.sh
index 1690dcc6b..e38e76974 100755
--- a/hack/ci/install-cri-o.sh
+++ b/hack/ci/install-cri-o.sh
@@ -30,9 +30,9 @@ curl_retry -o /usr/bin/cosign \
 chmod +x /usr/bin/cosign
 cosign version
 
-BOM_VERSION=v0.4.1
+BOM_VERSION=v0.5.1
 curl_retry -o /usr/bin/bom \
-    https://github.com/kubernetes-sigs/bom/releases/download/$BOM_VERSION/bom-linux-amd64
+    https://github.com/kubernetes-sigs/bom/releases/download/$BOM_VERSION/bom-amd64-linux
 chmod +x /usr/bin/bom
 bom version
 
diff --git a/internal/pkg/manager/spod/setup.go b/internal/pkg/manager/spod/setup.go
index 9c09c1a6f..d8a9ee735 100644
--- a/internal/pkg/manager/spod/setup.go
+++ b/internal/pkg/manager/spod/setup.go
@@ -26,6 +26,7 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	k8serrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/event"
@@ -120,15 +121,11 @@ func isStaticWebhook(ctx context.Context) bool {
 }
 
 func (r *ReconcileSPOd) getTunables(ctx context.Context) (*daemonTunables, error) {
+	var err error
+
 	dt := &daemonTunables{}
 	dt.watchNamespace = os.Getenv(config.RestrictNamespaceEnvKey)
 
-	selinuxdImage := os.Getenv(selinuxdImageKey)
-	if selinuxdImage == "" {
-		return dt, errors.New("invalid selinuxd image")
-	}
-	dt.selinuxdImage = selinuxdImage
-
 	rbacProxyImage := os.Getenv(rbacProxyImageKey)
 	if rbacProxyImage == "" {
 		return dt, errors.New("invalid rbac proxy image")
@@ -146,10 +143,45 @@ func (r *ReconcileSPOd) getTunables(ctx context.Context) (*daemonTunables, error
 	}
 	dt.seccompLocalhostProfile = util.GetSeccompLocalhostProfilePath(node)
 	dt.containerRuntime = util.GetContainerRuntime(node)
+	dt.selinuxdImage, err = r.getSelinuxdImage(ctx, node)
+	if err != nil {
+		return dt, fmt.Errorf("could not determine selinuxd image: %w", err)
+	}
 
 	return dt, nil
 }
 
+func (r *ReconcileSPOd) getSelinuxdImage(ctx context.Context, node *corev1.Node) (string, error) {
+	var operatorCm corev1.ConfigMap
+	operatorCmName := types.NamespacedName{
+		Namespace: config.GetOperatorNamespace(),
+		Name:      util.OperatorConfigMap,
+	}
+
+	if err := r.clientReader.Get(ctx, operatorCmName, &operatorCm); err != nil {
+		return "", err
+	}
+	selinuxdImageMapping := operatorCm.Data[util.SelinuxdImageMappingKey]
+
+	selinuxdImage, err := util.MatchSelinuxdImageJSONMapping(node, []byte(selinuxdImageMapping))
+	if err != nil {
+		return "", fmt.Errorf("matching selinuxd image: %w", err)
+	}
+
+	if selinuxdImage != "" {
+		r.log.Info("matched selinuxd image against nodeInfo", "image", selinuxdImage)
+		return selinuxdImage, nil
+	}
+
+	selinuxdImage = os.Getenv(selinuxdImageKey)
+	if selinuxdImage != "" {
+		r.log.Info("using selinuxd image from envVar", "image", selinuxdImage)
+		return selinuxdImage, nil
+	}
+
+	return "", errors.New("invalid selinuxd image")
+}
+
 func getEffectiveSPOd(dt *daemonTunables) *appsv1.DaemonSet {
 	refSPOd := bindata.Manifest.DeepCopy()
 	refSPOd.SetNamespace(config.GetOperatorNamespace())
diff --git a/internal/pkg/manager/spod/spod_controller.go b/internal/pkg/manager/spod/spod_controller.go
index 9197d4fa5..c6e88c2b9 100644
--- a/internal/pkg/manager/spod/spod_controller.go
+++ b/internal/pkg/manager/spod/spod_controller.go
@@ -127,6 +127,9 @@ func (r *ReconcileSPOd) Healthz(*http.Request) error {
 //
 // Needed to detect which runtime is active
 // +kubebuilder:rbac:groups="",resources=nodes,verbs=get;list;get
+//
+// Needed to detect the proper selinux image
+// +kubebuilder:rbac:groups="",resources=configmaps,resourceNames=security-profiles-operator-profile,verbs=get
 
 // Reconcile reads that state of the cluster for a SPOD object and makes changes based on the state read
 // and what is in the `ConfigMap.Spec`.
diff --git a/internal/pkg/util/constants.go b/internal/pkg/util/constants.go
index 53d20975d..6e920cc09 100644
--- a/internal/pkg/util/constants.go
+++ b/internal/pkg/util/constants.go
@@ -28,3 +28,11 @@ const (
 	EventTypeNormal  string = "Normal"
 	EventTypeWarning string = "Warning"
 )
+
+const (
+	// OperatorConfigMap corresponds to the configMap created from deploy/base/profiles.
+	OperatorConfigMap = "security-profiles-operator-profile"
+	// SelinuxdImageMappingKey is the key in the configMap that contains the mapping
+	// between the selinuxd image and the OS version.
+	SelinuxdImageMappingKey = "selinuxd-image-mapping.json"
+)
diff --git a/internal/pkg/util/kubernetes.go b/internal/pkg/util/kubernetes.go
index 757651781..722e9c3b0 100644
--- a/internal/pkg/util/kubernetes.go
+++ b/internal/pkg/util/kubernetes.go
@@ -18,9 +18,11 @@ package util
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
 	"os"
 	"path"
+	"regexp"
 	"strings"
 
 	"golang.org/x/mod/semver"
@@ -92,3 +94,35 @@ func GetKubeletDirFromNodeLabel(ctx context.Context, c client.Reader) (string, e
 	}
 	return "", fmt.Errorf("no %s label found on node %s", config.KubeletDirNodeLabelKey, nodeName)
 }
+
+type selinuxdImageMap struct {
+	Regex string `json:"regex"`
+	Image string `json:"image"`
+}
+
+func MatchSelinuxdImageJSONMapping(node *corev1.Node, mappingObj []byte) (string, error) {
+	var mapping []selinuxdImageMap
+
+	if err := json.Unmarshal(mappingObj, &mapping); err != nil {
+		return "", err
+	}
+
+	return matchSelinuxdImage(node, mapping), nil
+}
+
+func matchSelinuxdImage(node *corev1.Node, mapping []selinuxdImageMap) string {
+	if node == nil {
+		return ""
+	}
+
+	for _, m := range mapping {
+		if m.Regex == "" {
+			continue
+		}
+		if matched, err := regexp.MatchString(m.Regex, node.Status.NodeInfo.OSImage); err == nil && matched {
+			return m.Image
+		}
+	}
+
+	return ""
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index d61f8e210..bbe2e1bae 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -360,7 +360,7 @@ github.com/docker/cli/cli/config/types
 # github.com/docker/distribution v2.8.1+incompatible
 ## explicit
 github.com/docker/distribution/registry/client/auth/challenge
-# github.com/docker/docker v23.0.1+incompatible
+# github.com/docker/docker v23.0.3+incompatible
 ## explicit
 github.com/docker/docker/pkg/homedir
 # github.com/docker/docker-credential-helpers v0.7.0


Test patch:
diff --git a/internal/pkg/util/kubernetes_test.go b/internal/pkg/util/kubernetes_test.go
index e383b3d23..697caadd2 100644
--- a/internal/pkg/util/kubernetes_test.go
+++ b/internal/pkg/util/kubernetes_test.go
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package util_test
+package util
 
 import (
 	"path"
@@ -25,7 +25,6 @@ import (
 	corev1 "k8s.io/api/core/v1"
 
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/manager/spod/bindata"
-	"sigs.k8s.io/security-profiles-operator/internal/pkg/util"
 )
 
 func TestGetSeccompLocalhostProfilePath(t *testing.T) {
@@ -86,7 +85,7 @@ func TestGetSeccompLocalhostProfilePath(t *testing.T) {
 		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
-			got := util.GetSeccompLocalhostProfilePath(tt.node)
+			got := GetSeccompLocalhostProfilePath(tt.node)
 			require.Equal(t, tt.want, got)
 		})
 	}
@@ -131,7 +130,7 @@ func TestGetContainerRuntime(t *testing.T) {
 		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
-			got := util.GetContainerRuntime(tt.node)
+			got := GetContainerRuntime(tt.node)
 			require.Equal(t, tt.want, got)
 		})
 	}
@@ -160,9 +159,74 @@ func TestGetVersion(t *testing.T) {
 		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
-			got := util.GetVersion(tt.node)
+			got := GetVersion(tt.node)
 			require.True(t, semver.IsValid(got), "should return a valid version")
 			require.Equal(t, tt.want, got)
 		})
 	}
 }
+
+func TestMatchSelinuxdImageVersion(t *testing.T) {
+	t.Parallel()
+
+	mappingJSON := `[
+		{
+			"regex":"(.*)(CoreOS).*([\\d+])\\.8[\\d+]\\.(.*)",
+			"image":"quay.io/security-profiles-operator/selinuxd:el8"
+		},
+		{
+			"regex":"(.*)(CoreOS).*([\\d+])\\.9[\\d+]\\.(.*)",
+			"image":"quay.io/security-profiles-operator/selinuxd:el9"
+		}
+	]`
+
+	tests := []struct {
+		name string
+		node *corev1.Node
+		want string
+	}{
+		{
+			name: "Should return el8",
+			node: &corev1.Node{
+				Status: corev1.NodeStatus{
+					NodeInfo: corev1.NodeSystemInfo{
+						OSImage: "Red Hat Enterprise Linux CoreOS 411.86.202212072103-0 (Ootpa)",
+					},
+				},
+			},
+			want: "quay.io/security-profiles-operator/selinuxd:el8",
+		},
+		{
+			name: "Should return el9",
+			node: &corev1.Node{
+				Status: corev1.NodeStatus{
+					NodeInfo: corev1.NodeSystemInfo{
+						OSImage: "CentOS Stream CoreOS 413.92.202303061740-0 (Plow)",
+					},
+				},
+			},
+			want: "quay.io/security-profiles-operator/selinuxd:el9",
+		},
+		{
+			name: "Does not match anything",
+			node: &corev1.Node{
+				Status: corev1.NodeStatus{
+					NodeInfo: corev1.NodeSystemInfo{
+						OSImage: "Some other OS",
+					},
+				},
+			},
+			want: "",
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			got, err := MatchSelinuxdImageJSONMapping(tt.node, []byte(mappingJSON))
+			require.NoError(t, err)
+			require.Equal(t, tt.want, got)
+		})
+	}
+}