Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
version: 1.1.0
name: k8spspselinuxv2
displayName: SELinux V2
createdAt: "2024-05-20T18:10:16Z"
description: Defines an allow-list of seLinuxOptions configurations for pod containers. Corresponds to a PodSecurityPolicy requiring SELinux configs. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#selinux
digest: d493a5cbaf488e226d1c1133c917d1e2b0eac47b60618a3714ab8c69b07ae3cb
license: Apache-2.0
homeURL: https://open-policy-agent.github.io/gatekeeper-library/website/selinux
keywords:
- gatekeeper
- open-policy-agent
- policies
readme: |-
# SELinux V2
Defines an allow-list of seLinuxOptions configurations for pod containers. Corresponds to a PodSecurityPolicy requiring SELinux configs. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#selinux
install: |-
### Usage
```shell
kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/artifacthub/library/pod-security-policy/selinux/1.1.0/template.yaml
```
provider:
name: Gatekeeper Library
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
resources:
- template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sPSPSELinuxV2
metadata:
name: psp-selinux-v2
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
allowedSELinuxOptions:
- level: s0:c123,c456
role: object_r
type: svirt_sandbox_file_t
user: system_u
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sPSPSELinuxV2
metadata:
name: psp-selinux-v2
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
allowedSELinuxOptions: []
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: v1
kind: Pod
metadata:
name: nginx-selinux-disallowed
labels:
app: nginx-selinux
spec:
ephemeralContainers:
- name: nginx
image: nginx
securityContext:
seLinuxOptions:
level: s1:c234,c567
user: sysadm_u
role: sysadm_r
type: svirt_lxc_net_t
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: v1
kind: Pod
metadata:
name: nginx-selinux-allowed
labels:
app: nginx-selinux
spec:
containers:
- name: nginx
image: nginx
securityContext:
seLinuxOptions:
level: s0:c123,c456
role: object_r
type: svirt_sandbox_file_t
user: system_u
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: v1
kind: Pod
metadata:
name: nginx-selinux-allowed-without-selinux-opts
labels:
app: nginx-selinux
spec:
containers:
- name: nginx
image: nginx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: v1
kind: Pod
metadata:
name: nginx-selinux-disallowed
labels:
app: nginx-selinux
spec:
containers:
- name: nginx
image: nginx
securityContext:
seLinuxOptions:
level: s1:c234,c567
user: sysadm_u
role: sysadm_r
type: svirt_lxc_net_t
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
kind: AdmissionReview
apiVersion: admission.k8s.io/v1beta1
request:
operation: "UPDATE"
object:
apiVersion: v1
kind: Pod
metadata:
name: nginx-selinux-disallowed
labels:
app: nginx-selinux
spec:
containers:
- name: nginx
image: nginx
securityContext:
seLinuxOptions:
level: s1:c234,c567
user: sysadm_u
role: sysadm_r
type: svirt_lxc_net_t
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
kind: Suite
apiVersion: test.gatekeeper.sh/v1alpha1
metadata:
name: selinux
tests:
- name: require-matching-selinux-options
template: template.yaml
constraint: samples/psp-selinux-v2/constraint.yaml
cases:
- name: example-disallowed
object: samples/psp-selinux-v2/example_disallowed.yaml
assertions:
- violations: yes
- name: example-allowed
object: samples/psp-selinux-v2/example_allowed.yaml
assertions:
- violations: no
- name: example-allowed-wihtout-selinux-opts
object: samples/psp-selinux-v2/example_allowed_without_selinux_opts.yaml
assertions:
- violations: no
- name: disallowed-ephemeral
object: samples/psp-selinux-v2/disallowed_ephemeral.yaml
assertions:
- violations: yes
- name: update
object: samples/psp-selinux-v2/update.yaml
assertions:
- violations: no
- name: deny-all-selinux-options
template: template.yaml
constraint: samples/psp-selinux-v2/constraint2.yaml
cases:
- name: example-disallowed
object: samples/psp-selinux-v2/example_disallowed.yaml
assertions:
- violations: yes
- name: example-allowed
object: samples/psp-selinux-v2/example_allowed.yaml
assertions:
- violations: yes
- name: example-allowed-wihtout-selinux-opts
object: samples/psp-selinux-v2/example_allowed_without_selinux_opts.yaml
assertions:
- violations: no
- name: disallowed-ephemeral
object: samples/psp-selinux-v2/disallowed_ephemeral.yaml
assertions:
- violations: yes
- name: update
object: samples/psp-selinux-v2/update.yaml
assertions:
- violations: no
202 changes: 202 additions & 0 deletions artifacthub/library/pod-security-policy/selinux/1.1.0/template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8spspselinuxv2
annotations:
metadata.gatekeeper.sh/title: "SELinux V2"
metadata.gatekeeper.sh/version: 1.1.0
description: >-
Defines an allow-list of seLinuxOptions configurations for pod
containers. Corresponds to a PodSecurityPolicy requiring SELinux configs.
For more information, see
https://kubernetes.io/docs/concepts/policy/pod-security-policy/#selinux
spec:
crd:
spec:
names:
kind: K8sPSPSELinuxV2
validation:
# Schema for the `parameters` field
openAPIV3Schema:
type: object
description: >-
Defines an allow-list of seLinuxOptions configurations for pod
containers. Corresponds to a PodSecurityPolicy requiring SELinux configs.
For more information, see
https://kubernetes.io/docs/concepts/policy/pod-security-policy/#selinux
properties:
exemptImages:
description: >-
Any container that uses an image that matches an entry in this list will be excluded
from enforcement. Prefix-matching can be signified with `*`. For example: `my-image-*`.

It is recommended that users use the fully-qualified Docker image name (e.g. start with a domain name)
in order to avoid unexpectedly exempting images from an untrusted repository.
type: array
items:
type: string
allowedSELinuxOptions:
type: array
description: "An allow-list of SELinux options configurations."
items:
type: object
description: "An allowed configuration of SELinux options for a pod container."
properties:
level:
type: string
description: "An SELinux level."
role:
type: string
description: "An SELinux role."
type:
type: string
description: "An SELinux type."
user:
type: string
description: "An SELinux user."
targets:
- target: admission.k8s.gatekeeper.sh
code:
- engine: K8sNativeValidation
source:
variables:
- name: usingAllowedSELinuxOptions
expression: |
has(variables.anyObject.spec.securityContext) && has(variables.anyObject.spec.securityContext.seLinuxOptions) ?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test looks wrong. Shouldn't we be assuming an option is disallowed unless specifically allowed by parameters?

As-written, it looks like undefined parameters => allow everything, which would seem to drift from the rego.

Adding gator tests for this would validate the drift.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! I fixed it, it should be in parity with rego now.

(has(variables.params.allowedSELinuxOptions) ?
(
size(variables.params.allowedSELinuxOptions) > 0 && variables.params.allowedSELinuxOptions.all(opts,
(has(opts.level) ? has(variables.anyObject.spec.securityContext.seLinuxOptions.level) && (variables.anyObject.spec.securityContext.seLinuxOptions.level == opts.level) : !has(variables.anyObject.spec.securityContext.seLinuxOptions.level)) &&
(has(opts.role) ? has(variables.anyObject.spec.securityContext.seLinuxOptions.role) && (variables.anyObject.spec.securityContext.seLinuxOptions.role == opts.role) : !has(variables.anyObject.spec.securityContext.seLinuxOptions.role)) &&
(has(opts.type) ? has(variables.anyObject.spec.securityContext.seLinuxOptions.type) && (variables.anyObject.spec.securityContext.seLinuxOptions.type == opts.type) : !has(variables.anyObject.spec.securityContext.seLinuxOptions.type)) &&
(has(opts.user) ? has(variables.anyObject.spec.securityContext.seLinuxOptions.user) && (variables.anyObject.spec.securityContext.seLinuxOptions.user == opts.user) : !has(variables.anyObject.spec.securityContext.seLinuxOptions.user))
)
) :
true)
: true
- name: containers
expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers.filter(c, has(c.securityContext) && has(c.securityContext.seLinuxOptions)) : []'
- name: initContainers
expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers.filter(c, has(c.securityContext) && has(c.securityContext.seLinuxOptions)) : []'
- name: ephemeralContainers
expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers.filter(c, has(c.securityContext) && has(c.securityContext.seLinuxOptions)) : []'
- name: exemptImagePrefixes
expression: |
!has(variables.params.exemptImages) ? [] :
variables.params.exemptImages.filter(image, image.endsWith("*")).map(image, string(image).replace("*", ""))
- name: exemptImageExplicit
expression: |
!has(variables.params.exemptImages) ? [] :
variables.params.exemptImages.filter(image, !image.endsWith("*"))
- name: exemptImages
expression: |
(variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container,
container.image in variables.exemptImageExplicit ||
variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption)))
- name: badContainers
expression: |
(variables.containers + variables.initContainers + variables.ephemeralContainers).filter(c, !(c.image in variables.exemptImages) && (has(c.securityContext.seLinuxOptions) ? (
has(variables.params.allowedSELinuxOptions) ?
(
size(variables.params.allowedSELinuxOptions) == 0 || !variables.params.allowedSELinuxOptions.all(opts,
(has(opts.level) ? has(c.securityContext.seLinuxOptions.level) && (c.securityContext.seLinuxOptions.level == opts.level) : !has(c.securityContext.seLinuxOptions.level)) &&
(has(opts.role) ? has(c.securityContext.seLinuxOptions.role) && (c.securityContext.seLinuxOptions.role == opts.role) : !has(c.securityContext.seLinuxOptions.role)) &&
(has(opts.type) ? has(c.securityContext.seLinuxOptions.type) && (c.securityContext.seLinuxOptions.type == opts.type) : !has(c.securityContext.seLinuxOptions.type)) &&
(has(opts.user) ? has(c.securityContext.seLinuxOptions.user) && (c.securityContext.seLinuxOptions.user == opts.user) : !has(c.securityContext.seLinuxOptions.user))
)
) : false)
: false)
)
validations:
- expression: '(has(request.operation) && request.operation == "UPDATE") || variables.usingAllowedSELinuxOptions'
messageExpression: '"SELinux options is not allowed, pod: " + variables.anyObject.metadata.name + ". Allowed options: [" + variables.params.allowedSELinuxOptions.map(opts, "{ level: " + opts.level + ", role: " + opts.role + ", type: " + opts.type + ", user: " + opts.user + "}").join(", ") + "]"'
- expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.badContainers) == 0'
messageExpression: '"SELinux options is not allowed, pod: " + variables.anyObject.metadata.name + ", container: " + variables.badContainers.map(c, c.name).join(", ") + ". Allowed options: [" + variables.params.allowedSELinuxOptions.map(opts, "{ level: " + opts.level + ", role: " + opts.role + ", type: " + opts.type + ", user: " + opts.user + "}").join(", ") + "]"'
- engine: Rego
source:
rego: |
package k8spspselinux

import data.lib.exclude_update.is_update
import data.lib.exempt_container.is_exempt

# Disallow top level custom SELinux options
violation[{"msg": msg, "details": {}}] {
# spec.securityContext.seLinuxOptions field is immutable.
not is_update(input.review)

has_field(input.review.object.spec.securityContext, "seLinuxOptions")
not input_seLinuxOptions_allowed(input.review.object.spec.securityContext.seLinuxOptions)
msg := sprintf("SELinux options is not allowed, pod: %v. Allowed options: %v", [input.review.object.metadata.name, input.parameters.allowedSELinuxOptions])
}
# Disallow container level custom SELinux options
violation[{"msg": msg, "details": {}}] {
# spec.containers.securityContext.seLinuxOptions field is immutable.
not is_update(input.review)

c := input_security_context[_]
not is_exempt(c)
has_field(c.securityContext, "seLinuxOptions")
not input_seLinuxOptions_allowed(c.securityContext.seLinuxOptions)
msg := sprintf("SELinux options is not allowed, pod: %v, container: %v. Allowed options: %v", [input.review.object.metadata.name, c.name, input.parameters.allowedSELinuxOptions])
}

input_seLinuxOptions_allowed(options) {
params := input.parameters.allowedSELinuxOptions[_]
field_allowed("level", options, params)
field_allowed("role", options, params)
field_allowed("type", options, params)
field_allowed("user", options, params)
}

field_allowed(field, options, params) {
params[field] == options[field]
}
field_allowed(field, options, _) {
not has_field(options, field)
}

input_security_context[c] {
c := input.review.object.spec.containers[_]
has_field(c.securityContext, "seLinuxOptions")
}
input_security_context[c] {
c := input.review.object.spec.initContainers[_]
has_field(c.securityContext, "seLinuxOptions")
}
input_security_context[c] {
c := input.review.object.spec.ephemeralContainers[_]
has_field(c.securityContext, "seLinuxOptions")
}

# has_field returns whether an object has a field
has_field(object, field) = true {
object[field]
}
libs:
- |
package lib.exclude_update

is_update(review) {
review.operation == "UPDATE"
}
- |
package lib.exempt_container

is_exempt(container) {
exempt_images := object.get(object.get(input, "parameters", {}), "exemptImages", [])
img := container.image
exemption := exempt_images[_]
_matches_exemption(img, exemption)
}

_matches_exemption(img, exemption) {
not endswith(exemption, "*")
exemption == img
}

_matches_exemption(img, exemption) {
endswith(exemption, "*")
prefix := trim_suffix(exemption, "*")
startswith(img, prefix)
}
Loading