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

Issue:
SELinux permissive policies (alternative to LOG action of seccomp)
<!-- Please only use this template for submitting feature requests -->

#### What would you like to be added:
A way to mark a SELinux policy as permissive without resorting to `RawSelinuxProfile`.

#### Why is this needed:
In case the recording misses some calls or in general if the admin is not sure that the policy can be put into production

#### User story covered
As an application SRE, Rajesh needs to be able to do long runs of a security profile in “complain-mode” to easily identify the impact of a profile being enforced without impacting the application.

<!-- Please add a reference to the user story that this feature would cover

The existing user stories exist in docs/user-stores.md

If the user story doesn't exist yet, please add it to the main document
to help people understand what the feature is trying to solve and how it's
meant to help the target personas.

If this would help a target persona that's not currently listed, please add it
to the document in docs/personas.md
 -->

#### Implementation notes
Probably just need to set `typepermissive` based on a flag.

#### Acceptance criteria:
 - the profile will be "not enforced", but will log the actions to audit.log. This can be tested by removing parts of the profile and making sure the app still works, but the actions are logged

Pull request description:
selinux: Add a permissive boolean
<!--  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
/kind api-change

<!--
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 deprecation
/kind failing-test
/kind flake
/kind regression
-->

#### What this PR does / why we need it:
Similarly to how seccomp profiles allow setting the default action to
LOG, let's allow the selinux profiles to be set to permissive mode. This
way, all calls would have still be allowed, but would appear in
audit.log.

This is useful in case the user is just iterating on a policy and wants
to deploy a first version of the policy in the wild without causing AVC
denials.


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

<!--
*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?
Yes

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

#### Special notes for your reviewer:
N/A

#### 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
SELinux profiles gained a new attribute .spec.permissive which defaults to false. When set to true, the profile will
run in a permissive mode, that means that all actions would be allowed, but logged. This allows for a more iterative
approach for profile development.
```


Patch:
diff --git a/Dockerfile.build-image b/Dockerfile.build-image
index 5c7ac3f04..507742957 100644
--- a/Dockerfile.build-image
+++ b/Dockerfile.build-image
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 # hash below relates to tag: bullseye-v1.4.0
-FROM k8s.gcr.io/build-image/debian-base@sha256:e6e8c911b1566556ae105d3600df04c600241f43b06d22c73592ebb446d60c49
+FROM registry.k8s.io/build-image/debian-base@sha256:e6e8c911b1566556ae105d3600df04c600241f43b06d22c73592ebb446d60c49
 WORKDIR /work
 
 RUN apt-get update && apt-get install -y wget xz-utils libapparmor-dev
diff --git a/api/selinuxprofile/v1alpha2/selinuxprofile_types.go b/api/selinuxprofile/v1alpha2/selinuxprofile_types.go
index 2ca1be67a..a93f0a7e7 100644
--- a/api/selinuxprofile/v1alpha2/selinuxprofile_types.go
+++ b/api/selinuxprofile/v1alpha2/selinuxprofile_types.go
@@ -57,6 +57,11 @@ type SelinuxProfileSpec struct {
 	// +optional
 	// +kubebuilder:default={{kind:"System",name:"container"}}
 	Inherit []PolicyRef `json:"inherit,omitempty"`
+	// Permissive, when true will cause the SELinux profile to only
+	// log violations instead of enforcing them.
+	// +optional
+	// +kubebuilder:default=false
+	Permissive bool `json:"permissive,omitempty"`
 	// Defines the allow policy for the profile
 	Allow Allow `json:"allow,omitempty"`
 }
diff --git a/bundle/manifests/security-profiles-operator.x-k8s.io_selinuxprofiles.yaml b/bundle/manifests/security-profiles-operator.x-k8s.io_selinuxprofiles.yaml
index ed4be7730..28ba6c057 100644
--- a/bundle/manifests/security-profiles-operator.x-k8s.io_selinuxprofiles.yaml
+++ b/bundle/manifests/security-profiles-operator.x-k8s.io_selinuxprofiles.yaml
@@ -77,6 +77,11 @@ spec:
                   - name
                   type: object
                 type: array
+              permissive:
+                default: false
+                description: Permissive, when true will cause the SELinux profile
+                  to only log violations instead of enforcing them.
+                type: boolean
             type: object
           status:
             description: SelinuxProfileStatus defines the observed state of SelinuxProfile.
diff --git a/dependencies.yaml b/dependencies.yaml
index 94b90e88f..5163738ae 100644
--- a/dependencies.yaml
+++ b/dependencies.yaml
@@ -101,7 +101,7 @@ dependencies:
     version: sha256:e6e8c911b1566556ae105d3600df04c600241f43b06d22c73592ebb446d60c49
     refPaths:
     - path: Dockerfile.build-image
-      match: k8s.gcr.io/build-image/debian-base
+      match: registry.k8s.io/build-image/debian-base
 
   - name: nix
     version: 2.9.2
diff --git a/deploy/base-crds/crds/selinuxpolicy.yaml b/deploy/base-crds/crds/selinuxpolicy.yaml
index 2a2efe56b..cab8efeeb 100644
--- a/deploy/base-crds/crds/selinuxpolicy.yaml
+++ b/deploy/base-crds/crds/selinuxpolicy.yaml
@@ -178,6 +178,11 @@ spec:
                   - name
                   type: object
                 type: array
+              permissive:
+                default: false
+                description: Permissive, when true will cause the SELinux profile
+                  to only log violations instead of enforcing them.
+                type: boolean
             type: object
           status:
             description: SelinuxProfileStatus defines the observed state of SelinuxProfile.
diff --git a/deploy/helm/crds/crds.yaml b/deploy/helm/crds/crds.yaml
index 342539858..b1a7fd0ea 100644
--- a/deploy/helm/crds/crds.yaml
+++ b/deploy/helm/crds/crds.yaml
@@ -1854,6 +1854,11 @@ spec:
                   - name
                   type: object
                 type: array
+              permissive:
+                default: false
+                description: Permissive, when true will cause the SELinux profile
+                  to only log violations instead of enforcing them.
+                type: boolean
             type: object
           status:
             description: SelinuxProfileStatus defines the observed state of SelinuxProfile.
diff --git a/deploy/kustomize-deployment/kustomization.yaml b/deploy/kustomize-deployment/kustomization.yaml
index 23cf10f7f..addd40545 100644
--- a/deploy/kustomize-deployment/kustomization.yaml
+++ b/deploy/kustomize-deployment/kustomization.yaml
@@ -7,7 +7,7 @@ images:
     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
+    # newName: registry.k8s.io/security-profiles-operator/security-profiles-operator
     # newTag: v0.5.1
 
 commonLabels:
diff --git a/deploy/namespace-operator.yaml b/deploy/namespace-operator.yaml
index ff31ccc43..74e5589d3 100644
--- a/deploy/namespace-operator.yaml
+++ b/deploy/namespace-operator.yaml
@@ -1854,6 +1854,11 @@ spec:
                   - name
                   type: object
                 type: array
+              permissive:
+                default: false
+                description: Permissive, when true will cause the SELinux profile
+                  to only log violations instead of enforcing them.
+                type: boolean
             type: object
           status:
             description: SelinuxProfileStatus defines the observed state of SelinuxProfile.
diff --git a/deploy/openshift-dev.yaml b/deploy/openshift-dev.yaml
index 9c8ff07ef..a686ff515 100644
--- a/deploy/openshift-dev.yaml
+++ b/deploy/openshift-dev.yaml
@@ -1854,6 +1854,11 @@ spec:
                   - name
                   type: object
                 type: array
+              permissive:
+                default: false
+                description: Permissive, when true will cause the SELinux profile
+                  to only log violations instead of enforcing them.
+                type: boolean
             type: object
           status:
             description: SelinuxProfileStatus defines the observed state of SelinuxProfile.
diff --git a/deploy/operator.yaml b/deploy/operator.yaml
index fd76d6817..2759b30fc 100644
--- a/deploy/operator.yaml
+++ b/deploy/operator.yaml
@@ -1854,6 +1854,11 @@ spec:
                   - name
                   type: object
                 type: array
+              permissive:
+                default: false
+                description: Permissive, when true will cause the SELinux profile
+                  to only log violations instead of enforcing them.
+                type: boolean
             type: object
           status:
             description: SelinuxProfileStatus defines the observed state of SelinuxProfile.
diff --git a/deploy/webhook-operator.yaml b/deploy/webhook-operator.yaml
index adef0f16e..8d8904f3b 100644
--- a/deploy/webhook-operator.yaml
+++ b/deploy/webhook-operator.yaml
@@ -1925,6 +1925,11 @@ spec:
                   - name
                   type: object
                 type: array
+              permissive:
+                default: false
+                description: Permissive, when true will cause the SELinux profile
+                  to only log violations instead of enforcing them.
+                type: boolean
             type: object
           status:
             description: SelinuxProfileStatus defines the observed state of SelinuxProfile.
diff --git a/installation-usage.md b/installation-usage.md
index 226859468..767667302 100644
--- a/installation-usage.md
+++ b/installation-usage.md
@@ -26,6 +26,7 @@
     - [Merging per-container profile instances](#merging-per-container-profile-instances)
 - [Create a SELinux Profile](#create-a-selinux-profile)
   - [Apply a SELinux profile to a pod](#apply-a-selinux-profile-to-a-pod)
+  - [Make a SELinux profile permissive](#make-a-selinux-profile-permissive)
   - [Record a SELinux profile](#record-a-selinux-profile)
 - [Restricting to a Single Namespace](#restricting-to-a-single-namespace)
   - [Restricting to a Single Namespace with upstream deployment manifests](#restricting-to-a-single-namespace-with-upstream-deployment-manifests)
@@ -850,6 +851,15 @@ spec:
 
 Note that the SELinux type must exist before creating the workload.
 
+### Make a SELinux profile permissive
+
+Similarly to how a `SeccompProfile` might have a default action `SCMP_ACT_LOG`
+which would merely log violations of the policy, but not actually block the
+container from executing, a `SelinuxProfile` can be marked as "permissive"
+by setting `.spec.permissive` to `true`. This mode might be useful e.g. when
+the policy is known or suspected to be incomplete and you'd prefer to just
+watch for subsequent AVC denials after deploying the policy.
+
 ### Record a SELinux profile
 
 Please refer to the seccomp recording documentation, recording a SELinux
diff --git a/internal/pkg/translator/obj2cil.go b/internal/pkg/translator/obj2cil.go
index 374e1af4e..0dea33b54 100644
--- a/internal/pkg/translator/obj2cil.go
+++ b/internal/pkg/translator/obj2cil.go
@@ -26,6 +26,10 @@ import (
 	selxv1alpha2 "sigs.k8s.io/security-profiles-operator/api/selinuxprofile/v1alpha2"
 )
 
+const (
+	typePermissive = "(typepermissive process)"
+)
+
 func Object2CIL(
 	systemInherits []string,
 	objInherits []selxv1alpha2.SelinuxProfileObject,
@@ -39,6 +43,10 @@ func Object2CIL(
 	for _, inherit := range objInherits {
 		cilbuilder.WriteString(getCILInheritline(inherit.GetPolicyUsage()))
 	}
+	if sp.Spec.Permissive {
+		cilbuilder.WriteString(typePermissive)
+		cilbuilder.WriteString("\n")
+	}
 	for ttype, tclassMap := range sp.Spec.Allow {
 		for tclass, perms := range tclassMap {
 			cilbuilder.WriteString(getCILAllowLine(sp, ttype, tclass, perms))
diff --git a/release.md b/release.md
index c45542bed..c3e32ae93 100644
--- a/release.md
+++ b/release.md
@@ -15,13 +15,13 @@ The first PR targets this repository and:
 - changes the `images` `newName`/`newTag` fields of
   [./deploy/kustomize-deployment/kustomization.yaml](deploy/kustomize-deployment/kustomization.yaml)
   from `gcr.io/k8s-staging-sp-operator/security-profiles-operator` to
-  `k8s.gcr.io/security-profiles-operator/security-profiles-operator` (`newName`) and the
+  `registry.k8s.io/security-profiles-operator/security-profiles-operator` (`newName`) and the
   corresponding tag (`newTag`). After that the make target `make bundle`
   has to be run and the changes have to be committed.
 - changes the `image` in the `CatalogSource` in the same way at
   [./examples/olm/install-resources.yaml](/examples/olm/install-resources.yaml)
 - changes [`hack/ci/e2e-olm.sh`](/hack/ci/e2e-olm.sh) to sed
-  `"s#k8s.gcr.io/security-profiles-operator/security-profiles-operator-catalog:v0.0.0#${CATALOG_IMG}#g"`
+  `"s#registry.k8s.io/security-profiles-operator/security-profiles-operator-catalog:v0.0.0#${CATALOG_IMG}#g"`
   instead of
   `"s#gcr.io/k8s-staging-sp-operator/security-profiles-operator-catalog:latest#${CATALOG_IMG}#g"`
   (please note to change the version `v0.0.0` to the upcoming release)


Test patch:
diff --git a/hack/ci/e2e-olm.sh b/hack/ci/e2e-olm.sh
index 27400039a..cbd1bb13d 100755
--- a/hack/ci/e2e-olm.sh
+++ b/hack/ci/e2e-olm.sh
@@ -32,7 +32,7 @@ function build_and_push_packages() {
 
     # Create a manifest with local image
     cp deploy/operator.yaml ${OPERATOR_MANIFEST}
-    sed -i "s#k8s.gcr.io/security-profiles-operator/security-profiles-operator.*\$#${IMG}#" ${OPERATOR_MANIFEST}
+    sed -i "s#registry.k8s.io/security-profiles-operator/security-profiles-operator.*\$#${IMG}#" ${OPERATOR_MANIFEST}
 
     # this is a kludge, we need to make sure kustomize can be overwritten
     rm -f build/kustomize
diff --git a/internal/pkg/translator/obj2cil_test.go b/internal/pkg/translator/obj2cil_test.go
index 5af5cdc7f..21578c558 100644
--- a/internal/pkg/translator/obj2cil_test.go
+++ b/internal/pkg/translator/obj2cil_test.go
@@ -190,6 +190,75 @@ func TestObject2CIL(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "Test errorlogger translation with permissive mode",
+			profile: &selxv1alpha2.SelinuxProfile{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "foo-permissive",
+					Namespace: "bar",
+				},
+				Spec: selxv1alpha2.SelinuxProfileSpec{
+					Permissive: true,
+					Inherit: []selxv1alpha2.PolicyRef{
+						{
+							Name: "container",
+						},
+					},
+					Allow: selxv1alpha2.Allow{
+						"var_log_t": {
+							"dir": []string{
+								"open",
+								"read",
+								"getattr",
+								"lock",
+								"search",
+								"ioctl",
+								"add_name",
+								"remove_name",
+								"write",
+							},
+							"file": []string{
+								"getattr",
+								"read",
+								"write",
+								"append",
+								"ioctl",
+								"lock",
+								"map",
+								"open",
+								"create",
+							},
+							"sock_file": []string{
+								"getattr",
+								"read",
+								"write",
+								"append",
+								"open",
+							},
+						},
+					},
+				},
+			},
+			wantMatches: []string{
+				"\\(block foo-permissive_bar",
+				"\\(blockinherit container\\)",
+				"\\(typepermissive process\\)",
+				// We match on several lines since we don't care about the order
+				"\\(allow process var_log_t \\( dir \\(.*open.*\\)\\)\\)\n",
+				"\\(allow process var_log_t \\( dir \\(.*read.*\\)\\)\\)\n",
+				"\\(allow process var_log_t \\( dir \\(.*remove_name.*\\)\\)\\)\n",
+				"\\(allow process var_log_t \\( dir \\(.*write.*\\)\\)\\)\n",
+				"\\(allow process var_log_t \\( file \\(.*getattr.*\\)\\)\\)\n",
+				"\\(allow process var_log_t \\( file \\(.*map.*\\)\\)\\)\n",
+				"\\(allow process var_log_t \\( file \\(.*create.*\\)\\)\\)\n",
+				"\\(allow process var_log_t \\( sock_file \\(.*getattr.*\\)\\)\\)\n",
+				"\\(allow process var_log_t \\( sock_file \\(.*append.*\\)\\)\\)\n",
+				"\\(allow process var_log_t \\( sock_file \\(.*open.*\\)\\)\\)\n",
+			},
+			inheritsys: []string{
+				"container",
+			},
+		},
 	}
 	for _, tt := range tests {
 		tt := tt
diff --git a/test/e2e_test.go b/test/e2e_test.go
index 87edde57b..e26e28df8 100644
--- a/test/e2e_test.go
+++ b/test/e2e_test.go
@@ -133,6 +133,11 @@ func (e *e2e) TestSecurityProfilesOperator() {
 		e.testCaseSelinuxProfileBindingNsNotEnabled()
 	})
 
+	e.Run("cluster-wide: Selinux: Verify the policy can be marked as permissive", func() {
+		e.testCaseSelinuxIncompletePolicy()
+		e.testCaseSelinuxIncompletePermissivePolicy()
+	})
+
 	e.Run("cluster-wide: Selinux: Verify SELinux profile recording logs", func() {
 		e.testCaseProfileRecordingStaticPodSELinuxLogs()
 		e.testCaseProfileRecordingMultiContainerSELinuxLogs()
diff --git a/test/tc_selinux_base_usage_test.go b/test/tc_selinux_base_usage_test.go
index 22c08197e..61f41caab 100644
--- a/test/tc_selinux_base_usage_test.go
+++ b/test/tc_selinux_base_usage_test.go
@@ -61,6 +61,42 @@ spec:
         - open
 `
 
+	// this is the equivalent of errorloggerPolicy but with several calls removed. The idea is to
+	// ensure that the workload will fail if the policy in incomplete. Allows setting the permissive
+	// flag as needed.
+	errorloggerIncompletePolFmt = `
+apiVersion: security-profiles-operator.x-k8s.io/v1alpha2
+kind: SelinuxProfile
+metadata:
+  name: errorlogger-incomplete-%s
+spec:
+  permissive: %s
+  allow:
+    var_log_t:
+      dir:
+        - getattr
+        - lock
+        - search
+        - ioctl
+        - add_name
+        - remove_name
+        - write
+      file:
+        - getattr
+        - read
+        - ioctl
+        - lock
+        - map
+        - open
+        - create
+      sock_file:
+        - getattr
+        - read
+        - write
+        - append
+        - open
+`
+
 	//nolint:lll // full yaml
 	podWithPolicyFmt = `
 apiVersion: v1
@@ -131,6 +167,80 @@ func (e *e2e) testCaseSelinuxBaseUsage(nodes []string) {
 	e.assertSelinuxPolicyIsRemoved(nodes, rawPolicyName, maxNodeIterations, sleepBetweenIterations)
 }
 
+func (e *e2e) testCaseSelinuxIncompletePolicy() {
+	e.selinuxOnlyTestCase()
+	enforcingProfileName := "errorlogger-incomplete-enforcing"
+
+	e.logf("The 'errorlogger' workload should error out with a wrong policy")
+
+	e.logf("creating incomplete policy")
+	removeFn := e.writeAndCreate(
+		fmt.Sprintf(errorloggerIncompletePolFmt, "enforcing", "false"),
+		"errorlogger-policy-incomplete-enforcing.yml")
+	defer removeFn()
+
+	// Let's wait for the policy to be processed
+	e.kubectl("wait", "--timeout", defaultSelinuxOpTimeout,
+		"--for", "condition=ready", "selinuxprofile", enforcingProfileName)
+
+	e.logf("creating workload - it should become ready, but fail")
+	podWithPolicy := fmt.Sprintf(podWithPolicyFmt, e.getSELinuxPolicyUsage(enforcingProfileName))
+	e.writeAndCreate(podWithPolicy, "pod-w-incomplete-policy.yml")
+
+	// note: this would have been much nicer with kubectl wait --jsonpath, but I found it racy incase the status
+	// doesn't exist yet. So we're using a loop instead.
+	var exitCode string
+	for i := 0; i < 10; i++ {
+		exitCode = e.kubectl("get", "pods", "errorlogger",
+			"-o", "jsonpath={.status.containerStatuses[0].state.terminated.exitCode}")
+		if exitCode == "1" {
+			break
+		}
+		time.Sleep(2 * time.Second)
+	}
+	if exitCode != "1" {
+		e.Fail("The pod should have failed, but it didn't")
+	}
+
+	log := e.kubectl("logs", "errorlogger", "-c", "errorlogger")
+	e.Contains(log, "Permission denied")
+
+	e.logf("removing workload")
+	e.kubectl("delete", "pod", "errorlogger")
+
+	e.logf("removing policy")
+	e.kubectl("delete", "selinuxprofile", enforcingProfileName)
+}
+
+func (e *e2e) testCaseSelinuxIncompletePermissivePolicy() {
+	e.selinuxOnlyTestCase()
+	permissiveProfileName := "errorlogger-incomplete-permissive"
+
+	e.logf("The 'errorlogger' workload should run fine with a wrong policy in permissive mode")
+
+	e.logf("creating incomplete policy")
+	removeFn := e.writeAndCreate(
+		fmt.Sprintf(errorloggerIncompletePolFmt, "permissive", "true"),
+		"errorlogger-policy-incomplete-permissive.yml")
+	defer removeFn()
+
+	// Let's wait for the policy to be processed
+	e.kubectl("wait", "--timeout", defaultSelinuxOpTimeout,
+		"--for", "condition=ready", "selinuxprofile", permissiveProfileName)
+
+	e.logf("creating workload - it should become ready, but fail")
+	podWithPolicy := fmt.Sprintf(podWithPolicyFmt, e.getSELinuxPolicyUsage(permissiveProfileName))
+	e.writeAndCreate(podWithPolicy, "pod-w-incomplete-permissive-policy.yml")
+
+	e.waitFor("condition=ready", "pod", "errorlogger")
+
+	e.logf("removing workload")
+	e.kubectl("delete", "pod", "errorlogger")
+
+	e.logf("removing policy")
+	e.kubectl("delete", "selinuxprofile", permissiveProfileName)
+}
+
 func (e *e2e) assertSelinuxPolicyIsInstalled(nodes []string, policy string, nodeIterations int, sleep time.Duration) {
 	for i := 0; i < nodeIterations; i++ {
 		var missingPolName string