From 3acd611596720a1e23c71208f00f3458c3566928 Mon Sep 17 00:00:00 2001 From: Julian Katz Date: Thu, 29 Aug 2024 13:41:48 -0700 Subject: [PATCH 01/19] chore(k8spspallowprivilegeescalationcontainer): suite test confirming exemptImages (#587) I recently found (#584) that some K8sNativeValidation implementations of certain templates that iterate over and exempt containers by image had a bug preventing the exemption logic from working. The k8spspallowprivilegeescalationcontainer turns out not to have this problem, as proved by the passing tests with the addition of an image exemption suite test. Signed-off-by: juliankatz --- .../constraint.yaml | 2 +- .../example_allowed_exempt.yaml | 12 ++++++++ .../1.1.0/suite.yaml | 4 +++ .../constraint.yaml | 2 +- .../example_allowed_exempt.yaml | 12 ++++++++ .../allow-privilege-escalation/suite.yaml | 4 +++ .../validation/allow-privilege-escalation.md | 28 ++++++++++++++++++- 7 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 artifacthub/library/pod-security-policy/allow-privilege-escalation/1.1.0/samples/psp-allow-privilege-escalation-container/example_allowed_exempt.yaml create mode 100644 library/pod-security-policy/allow-privilege-escalation/samples/psp-allow-privilege-escalation-container/example_allowed_exempt.yaml diff --git a/artifacthub/library/pod-security-policy/allow-privilege-escalation/1.1.0/samples/psp-allow-privilege-escalation-container/constraint.yaml b/artifacthub/library/pod-security-policy/allow-privilege-escalation/1.1.0/samples/psp-allow-privilege-escalation-container/constraint.yaml index 1d4f031c4..cfa512d9d 100644 --- a/artifacthub/library/pod-security-policy/allow-privilege-escalation/1.1.0/samples/psp-allow-privilege-escalation-container/constraint.yaml +++ b/artifacthub/library/pod-security-policy/allow-privilege-escalation/1.1.0/samples/psp-allow-privilege-escalation-container/constraint.yaml @@ -8,4 +8,4 @@ spec: - apiGroups: [""] kinds: ["Pod"] parameters: - exemptImages: ["nonexistent/*"] + exemptImages: ["safeimages.com/*"] diff --git a/artifacthub/library/pod-security-policy/allow-privilege-escalation/1.1.0/samples/psp-allow-privilege-escalation-container/example_allowed_exempt.yaml b/artifacthub/library/pod-security-policy/allow-privilege-escalation/1.1.0/samples/psp-allow-privilege-escalation-container/example_allowed_exempt.yaml new file mode 100644 index 000000000..841e56f89 --- /dev/null +++ b/artifacthub/library/pod-security-policy/allow-privilege-escalation/1.1.0/samples/psp-allow-privilege-escalation-container/example_allowed_exempt.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-privilege-escalation-disallowed + labels: + app: nginx-privilege-escalation +spec: + containers: + - name: nginx + image: "safeimages.com/nginx" + securityContext: + allowPrivilegeEscalation: true diff --git a/artifacthub/library/pod-security-policy/allow-privilege-escalation/1.1.0/suite.yaml b/artifacthub/library/pod-security-policy/allow-privilege-escalation/1.1.0/suite.yaml index cd7531cfa..345a7582c 100644 --- a/artifacthub/library/pod-security-policy/allow-privilege-escalation/1.1.0/suite.yaml +++ b/artifacthub/library/pod-security-policy/allow-privilege-escalation/1.1.0/suite.yaml @@ -23,3 +23,7 @@ tests: object: samples/psp-allow-privilege-escalation-container/update.yaml assertions: - violations: no + - name: exempted-path + object: samples/psp-allow-privilege-escalation-container/example_allowed_exempt.yaml + assertions: + - violations: no diff --git a/library/pod-security-policy/allow-privilege-escalation/samples/psp-allow-privilege-escalation-container/constraint.yaml b/library/pod-security-policy/allow-privilege-escalation/samples/psp-allow-privilege-escalation-container/constraint.yaml index 1d4f031c4..cfa512d9d 100644 --- a/library/pod-security-policy/allow-privilege-escalation/samples/psp-allow-privilege-escalation-container/constraint.yaml +++ b/library/pod-security-policy/allow-privilege-escalation/samples/psp-allow-privilege-escalation-container/constraint.yaml @@ -8,4 +8,4 @@ spec: - apiGroups: [""] kinds: ["Pod"] parameters: - exemptImages: ["nonexistent/*"] + exemptImages: ["safeimages.com/*"] diff --git a/library/pod-security-policy/allow-privilege-escalation/samples/psp-allow-privilege-escalation-container/example_allowed_exempt.yaml b/library/pod-security-policy/allow-privilege-escalation/samples/psp-allow-privilege-escalation-container/example_allowed_exempt.yaml new file mode 100644 index 000000000..841e56f89 --- /dev/null +++ b/library/pod-security-policy/allow-privilege-escalation/samples/psp-allow-privilege-escalation-container/example_allowed_exempt.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-privilege-escalation-disallowed + labels: + app: nginx-privilege-escalation +spec: + containers: + - name: nginx + image: "safeimages.com/nginx" + securityContext: + allowPrivilegeEscalation: true diff --git a/library/pod-security-policy/allow-privilege-escalation/suite.yaml b/library/pod-security-policy/allow-privilege-escalation/suite.yaml index cd7531cfa..345a7582c 100644 --- a/library/pod-security-policy/allow-privilege-escalation/suite.yaml +++ b/library/pod-security-policy/allow-privilege-escalation/suite.yaml @@ -23,3 +23,7 @@ tests: object: samples/psp-allow-privilege-escalation-container/update.yaml assertions: - violations: no + - name: exempted-path + object: samples/psp-allow-privilege-escalation-container/example_allowed_exempt.yaml + assertions: + - violations: no diff --git a/website/docs/validation/allow-privilege-escalation.md b/website/docs/validation/allow-privilege-escalation.md index e36f36688..66d6c037e 100644 --- a/website/docs/validation/allow-privilege-escalation.md +++ b/website/docs/validation/allow-privilege-escalation.md @@ -173,7 +173,7 @@ spec: - apiGroups: [""] kinds: ["Pod"] parameters: - exemptImages: ["nonexistent/*"] + exemptImages: ["safeimages.com/*"] ``` @@ -262,6 +262,32 @@ Usage kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/allow-privilege-escalation/samples/psp-allow-privilege-escalation-container/disallowed_ephemeral.yaml ``` + +
+exempted-path + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-privilege-escalation-disallowed + labels: + app: nginx-privilege-escalation +spec: + containers: + - name: nginx + image: "safeimages.com/nginx" + securityContext: + allowPrivilegeEscalation: true + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/allow-privilege-escalation/samples/psp-allow-privilege-escalation-container/example_allowed_exempt.yaml +``` +
From 9cf3312cc0902b8c3cf940b72a3f198011d37d0a Mon Sep 17 00:00:00 2001 From: Julian Katz Date: Thu, 29 Aug 2024 14:21:41 -0700 Subject: [PATCH 02/19] fix(k8spspreadonlyrootfilesystem): CEL support wildcard in exemptImages (#584) Despite it being non-sensical to put a `*` in exemptImages (functionally disabling the policy), this is supported in the existing rego implementation of the template. Thus, not doing it in the CEL implementation is an inconsistency and a breaking change. This PR upholds the contract by adding support for `*` as an exemptImage. Signed-off-by: juliankatz Co-authored-by: Andrew Peabody --- .../1.1.1/artifacthub-pkg.yml | 22 +++ .../1.1.1/kustomization.yaml | 2 + .../samples/full_wildcard/constraint.yaml | 12 ++ .../constraint.yaml | 12 ++ .../disallowed_ephemeral.yaml | 12 ++ .../example_allowed.yaml | 12 ++ .../example_allowed_exempted.yaml | 12 ++ .../example_disallowed.yaml | 12 ++ .../psp-readonlyrootfilesystem/update.yaml | 17 ++ .../samples/wildcard-prefix/constraint.yaml | 12 ++ .../example_allowed_safe_prefix.yaml | 12 ++ .../example_disallowed_unsafe_prefix.yaml | 12 ++ .../1.1.1/suite.yaml | 51 +++++ .../1.1.1/template.yaml | 140 ++++++++++++++ .../samples/full_wildcard/constraint.yaml | 12 ++ .../constraint.yaml | 3 + .../example_allowed_exempted.yaml | 12 ++ .../samples/wildcard-prefix/constraint.yaml | 12 ++ .../example_allowed_safe_prefix.yaml | 12 ++ .../example_disallowed_unsafe_prefix.yaml | 12 ++ .../read-only-root-filesystem/suite.yaml | 24 +++ .../read-only-root-filesystem/template.yaml | 4 +- .../read-only-root-filesystem/constraint.tmpl | 2 +- .../read-only-root-filesystem/src.cel | 2 +- .../validation/read-only-root-filesystem.md | 175 +++++++++++++++++- 25 files changed, 604 insertions(+), 6 deletions(-) create mode 100644 artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/artifacthub-pkg.yml create mode 100644 artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/kustomization.yaml create mode 100644 artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/full_wildcard/constraint.yaml create mode 100644 artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/psp-readonlyrootfilesystem/constraint.yaml create mode 100644 artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/psp-readonlyrootfilesystem/disallowed_ephemeral.yaml create mode 100644 artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/psp-readonlyrootfilesystem/example_allowed.yaml create mode 100644 artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/psp-readonlyrootfilesystem/example_allowed_exempted.yaml create mode 100644 artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/psp-readonlyrootfilesystem/example_disallowed.yaml create mode 100644 artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/psp-readonlyrootfilesystem/update.yaml create mode 100644 artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/wildcard-prefix/constraint.yaml create mode 100644 artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/wildcard-prefix/example_allowed_safe_prefix.yaml create mode 100644 artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/wildcard-prefix/example_disallowed_unsafe_prefix.yaml create mode 100644 artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/suite.yaml create mode 100644 artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/template.yaml create mode 100644 library/pod-security-policy/read-only-root-filesystem/samples/full_wildcard/constraint.yaml create mode 100644 library/pod-security-policy/read-only-root-filesystem/samples/psp-readonlyrootfilesystem/example_allowed_exempted.yaml create mode 100644 library/pod-security-policy/read-only-root-filesystem/samples/wildcard-prefix/constraint.yaml create mode 100644 library/pod-security-policy/read-only-root-filesystem/samples/wildcard-prefix/example_allowed_safe_prefix.yaml create mode 100644 library/pod-security-policy/read-only-root-filesystem/samples/wildcard-prefix/example_disallowed_unsafe_prefix.yaml diff --git a/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/artifacthub-pkg.yml b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/artifacthub-pkg.yml new file mode 100644 index 000000000..297b50060 --- /dev/null +++ b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/artifacthub-pkg.yml @@ -0,0 +1,22 @@ +version: 1.1.1 +name: k8spspreadonlyrootfilesystem +displayName: Read Only Root Filesystem +createdAt: "2024-08-29T20:26:02Z" +description: Requires the use of a read-only root file system by pod containers. Corresponds to the `readOnlyRootFilesystem` field in a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#volumes-and-file-systems +digest: 9ec2743dce71b385161873179a1d70b6f801f72b077bd178334f75f2ee1023bd +license: Apache-2.0 +homeURL: https://open-policy-agent.github.io/gatekeeper-library/website/read-only-root-filesystem +keywords: + - gatekeeper + - open-policy-agent + - policies +readme: |- + # Read Only Root Filesystem + Requires the use of a read-only root file system by pod containers. Corresponds to the `readOnlyRootFilesystem` field in a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#volumes-and-file-systems +install: |- + ### Usage + ```shell + kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/template.yaml + ``` +provider: + name: Gatekeeper Library diff --git a/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/kustomization.yaml b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/kustomization.yaml new file mode 100644 index 000000000..7d70d11b7 --- /dev/null +++ b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - template.yaml diff --git a/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/full_wildcard/constraint.yaml b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/full_wildcard/constraint.yaml new file mode 100644 index 000000000..b5497e761 --- /dev/null +++ b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/full_wildcard/constraint.yaml @@ -0,0 +1,12 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPReadOnlyRootFilesystem +metadata: + name: psp-readonlyrootfilesystem +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + exemptImages: + - "*" diff --git a/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/psp-readonlyrootfilesystem/constraint.yaml b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/psp-readonlyrootfilesystem/constraint.yaml new file mode 100644 index 000000000..d32dc33ab --- /dev/null +++ b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/psp-readonlyrootfilesystem/constraint.yaml @@ -0,0 +1,12 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPReadOnlyRootFilesystem +metadata: + name: psp-readonlyrootfilesystem +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + exemptImages: + - "specialprogram" diff --git a/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/psp-readonlyrootfilesystem/disallowed_ephemeral.yaml b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/psp-readonlyrootfilesystem/disallowed_ephemeral.yaml new file mode 100644 index 000000000..d0ce2c4dc --- /dev/null +++ b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/psp-readonlyrootfilesystem/disallowed_ephemeral.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-readonlyrootfilesystem-disallowed + labels: + app: nginx-readonlyrootfilesystem +spec: + ephemeralContainers: + - name: nginx + image: nginx + securityContext: + readOnlyRootFilesystem: false diff --git a/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/psp-readonlyrootfilesystem/example_allowed.yaml b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/psp-readonlyrootfilesystem/example_allowed.yaml new file mode 100644 index 000000000..9c96bd18c --- /dev/null +++ b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/psp-readonlyrootfilesystem/example_allowed.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-readonlyrootfilesystem-allowed + labels: + app: nginx-readonlyrootfilesystem +spec: + containers: + - name: nginx + image: nginx + securityContext: + readOnlyRootFilesystem: true diff --git a/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/psp-readonlyrootfilesystem/example_allowed_exempted.yaml b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/psp-readonlyrootfilesystem/example_allowed_exempted.yaml new file mode 100644 index 000000000..1f86b59ae --- /dev/null +++ b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/psp-readonlyrootfilesystem/example_allowed_exempted.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-readonlyrootfilesystem-disallowed + labels: + app: nginx-readonlyrootfilesystem +spec: + containers: + - name: specialprogram + image: specialprogram + securityContext: + readOnlyRootFilesystem: false diff --git a/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/psp-readonlyrootfilesystem/example_disallowed.yaml b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/psp-readonlyrootfilesystem/example_disallowed.yaml new file mode 100644 index 000000000..7571bfd9f --- /dev/null +++ b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/psp-readonlyrootfilesystem/example_disallowed.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-readonlyrootfilesystem-disallowed + labels: + app: nginx-readonlyrootfilesystem +spec: + containers: + - name: nginx + image: nginx + securityContext: + readOnlyRootFilesystem: false diff --git a/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/psp-readonlyrootfilesystem/update.yaml b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/psp-readonlyrootfilesystem/update.yaml new file mode 100644 index 000000000..b31ae5e3a --- /dev/null +++ b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/psp-readonlyrootfilesystem/update.yaml @@ -0,0 +1,17 @@ +kind: AdmissionReview +apiVersion: admission.k8s.io/v1beta1 +request: + operation: "UPDATE" + object: + apiVersion: v1 + kind: Pod + metadata: + name: nginx-readonlyrootfilesystem-disallowed + labels: + app: nginx-readonlyrootfilesystem + spec: + containers: + - name: nginx + image: nginx + securityContext: + readOnlyRootFilesystem: false diff --git a/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/wildcard-prefix/constraint.yaml b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/wildcard-prefix/constraint.yaml new file mode 100644 index 000000000..8bac93140 --- /dev/null +++ b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/wildcard-prefix/constraint.yaml @@ -0,0 +1,12 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPReadOnlyRootFilesystem +metadata: + name: psp-readonlyrootfilesystem +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + exemptImages: + - "safe-images.com/*" diff --git a/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/wildcard-prefix/example_allowed_safe_prefix.yaml b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/wildcard-prefix/example_allowed_safe_prefix.yaml new file mode 100644 index 000000000..6c4c60d10 --- /dev/null +++ b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/wildcard-prefix/example_allowed_safe_prefix.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-readonlyrootfilesystem-allowed + labels: + app: nginx-readonlyrootfilesystem +spec: + containers: + - name: nginx + image: "safe-images.com/nginx" + securityContext: + readOnlyRootFilesystem: false diff --git a/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/wildcard-prefix/example_disallowed_unsafe_prefix.yaml b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/wildcard-prefix/example_disallowed_unsafe_prefix.yaml new file mode 100644 index 000000000..563abb05f --- /dev/null +++ b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/samples/wildcard-prefix/example_disallowed_unsafe_prefix.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-readonlyrootfilesystem-allowed + labels: + app: nginx-readonlyrootfilesystem +spec: + containers: + - name: nginx + image: "unsafe-images.com/nginx" + securityContext: + readOnlyRootFilesystem: false diff --git a/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/suite.yaml b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/suite.yaml new file mode 100644 index 000000000..bb064b179 --- /dev/null +++ b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/suite.yaml @@ -0,0 +1,51 @@ +kind: Suite +apiVersion: test.gatekeeper.sh/v1alpha1 +metadata: + name: read-only-root-filesystem +tests: +- name: require-read-only-root-filesystem + template: template.yaml + constraint: samples/psp-readonlyrootfilesystem/constraint.yaml + cases: + - name: example-disallowed + object: samples/psp-readonlyrootfilesystem/example_disallowed.yaml + assertions: + - violations: yes + message: "only read-only root filesystem container is allowed: nginx" + - name: example-allowed + object: samples/psp-readonlyrootfilesystem/example_allowed.yaml + assertions: + - violations: no + - name: disallowed-ephemeral + object: samples/psp-readonlyrootfilesystem/disallowed_ephemeral.yaml + assertions: + - violations: yes + message: "only read-only root filesystem container is allowed: nginx" + - name: update + object: samples/psp-readonlyrootfilesystem/update.yaml + assertions: + - violations: no + - name: exact-exemption + object: samples/psp-readonlyrootfilesystem/example_allowed_exempted.yaml + assertions: + - violations: no +- name: full-wildcard + template: template.yaml + constraint: samples/full_wildcard/constraint.yaml + cases: + - name: allow-normally-disallowed + object: samples/psp-readonlyrootfilesystem/example_disallowed.yaml + assertions: + - violations: no +- name: wildcard-prefix + template: template.yaml + constraint: samples/wildcard-prefix/constraint.yaml + cases: + - name: image-with-exempt-prefix-readOnlyRootFilesystem-not-required + object: samples/wildcard-prefix/example_allowed_safe_prefix.yaml + assertions: + - violations: no + - name: image-with-different-prefix-must-set-readOnlyRootFilesystem + object: samples/wildcard-prefix/example_disallowed_unsafe_prefix.yaml + assertions: + - violations: yes diff --git a/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/template.yaml b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/template.yaml new file mode 100644 index 000000000..7f3a402e8 --- /dev/null +++ b/artifacthub/library/pod-security-policy/read-only-root-filesystem/1.1.1/template.yaml @@ -0,0 +1,140 @@ +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: k8spspreadonlyrootfilesystem + annotations: + metadata.gatekeeper.sh/title: "Read Only Root Filesystem" + metadata.gatekeeper.sh/version: 1.1.1 + description: >- + Requires the use of a read-only root file system by pod containers. + Corresponds to the `readOnlyRootFilesystem` field in a + PodSecurityPolicy. For more information, see + https://kubernetes.io/docs/concepts/policy/pod-security-policy/#volumes-and-file-systems +spec: + crd: + spec: + names: + kind: K8sPSPReadOnlyRootFilesystem + validation: + # Schema for the `parameters` field + openAPIV3Schema: + type: object + description: >- + Requires the use of a read-only root file system by pod containers. + Corresponds to the `readOnlyRootFilesystem` field in a + PodSecurityPolicy. For more information, see + https://kubernetes.io/docs/concepts/policy/pod-security-policy/#volumes-and-file-systems + 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 + targets: + - target: admission.k8s.gatekeeper.sh + code: + - engine: K8sNativeValidation + source: + variables: + - name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' + - name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' + - name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' + - 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))).map(container, container.image) + - name: badContainers + expression: | + (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, + !(container.image in variables.exemptImages) && + (!has(container.securityContext) || + !has(container.securityContext.readOnlyRootFilesystem) || + container.securityContext.readOnlyRootFilesystem != true) + ).map(container, container.name) + validations: + - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.badContainers) == 0' + messageExpression: '"only read-only root filesystem container is allowed: " + variables.badContainers.join(", ")' + + - engine: Rego + source: + rego: | + package k8spspreadonlyrootfilesystem + + import data.lib.exclude_update.is_update + import data.lib.exempt_container.is_exempt + + violation[{"msg": msg, "details": {}}] { + # spec.containers.readOnlyRootFilesystem field is immutable. + not is_update(input.review) + + c := input_containers[_] + not is_exempt(c) + input_read_only_root_fs(c) + msg := sprintf("only read-only root filesystem container is allowed: %v", [c.name]) + } + + input_read_only_root_fs(c) { + not has_field(c, "securityContext") + } + input_read_only_root_fs(c) { + not c.securityContext.readOnlyRootFilesystem == true + } + + input_containers[c] { + c := input.review.object.spec.containers[_] + } + input_containers[c] { + c := input.review.object.spec.initContainers[_] + } + input_containers[c] { + c := input.review.object.spec.ephemeralContainers[_] + } + + # 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) + } diff --git a/library/pod-security-policy/read-only-root-filesystem/samples/full_wildcard/constraint.yaml b/library/pod-security-policy/read-only-root-filesystem/samples/full_wildcard/constraint.yaml new file mode 100644 index 000000000..b5497e761 --- /dev/null +++ b/library/pod-security-policy/read-only-root-filesystem/samples/full_wildcard/constraint.yaml @@ -0,0 +1,12 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPReadOnlyRootFilesystem +metadata: + name: psp-readonlyrootfilesystem +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + exemptImages: + - "*" diff --git a/library/pod-security-policy/read-only-root-filesystem/samples/psp-readonlyrootfilesystem/constraint.yaml b/library/pod-security-policy/read-only-root-filesystem/samples/psp-readonlyrootfilesystem/constraint.yaml index 66d6bdabe..d32dc33ab 100644 --- a/library/pod-security-policy/read-only-root-filesystem/samples/psp-readonlyrootfilesystem/constraint.yaml +++ b/library/pod-security-policy/read-only-root-filesystem/samples/psp-readonlyrootfilesystem/constraint.yaml @@ -7,3 +7,6 @@ spec: kinds: - apiGroups: [""] kinds: ["Pod"] + parameters: + exemptImages: + - "specialprogram" diff --git a/library/pod-security-policy/read-only-root-filesystem/samples/psp-readonlyrootfilesystem/example_allowed_exempted.yaml b/library/pod-security-policy/read-only-root-filesystem/samples/psp-readonlyrootfilesystem/example_allowed_exempted.yaml new file mode 100644 index 000000000..1f86b59ae --- /dev/null +++ b/library/pod-security-policy/read-only-root-filesystem/samples/psp-readonlyrootfilesystem/example_allowed_exempted.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-readonlyrootfilesystem-disallowed + labels: + app: nginx-readonlyrootfilesystem +spec: + containers: + - name: specialprogram + image: specialprogram + securityContext: + readOnlyRootFilesystem: false diff --git a/library/pod-security-policy/read-only-root-filesystem/samples/wildcard-prefix/constraint.yaml b/library/pod-security-policy/read-only-root-filesystem/samples/wildcard-prefix/constraint.yaml new file mode 100644 index 000000000..8bac93140 --- /dev/null +++ b/library/pod-security-policy/read-only-root-filesystem/samples/wildcard-prefix/constraint.yaml @@ -0,0 +1,12 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPReadOnlyRootFilesystem +metadata: + name: psp-readonlyrootfilesystem +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + exemptImages: + - "safe-images.com/*" diff --git a/library/pod-security-policy/read-only-root-filesystem/samples/wildcard-prefix/example_allowed_safe_prefix.yaml b/library/pod-security-policy/read-only-root-filesystem/samples/wildcard-prefix/example_allowed_safe_prefix.yaml new file mode 100644 index 000000000..6c4c60d10 --- /dev/null +++ b/library/pod-security-policy/read-only-root-filesystem/samples/wildcard-prefix/example_allowed_safe_prefix.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-readonlyrootfilesystem-allowed + labels: + app: nginx-readonlyrootfilesystem +spec: + containers: + - name: nginx + image: "safe-images.com/nginx" + securityContext: + readOnlyRootFilesystem: false diff --git a/library/pod-security-policy/read-only-root-filesystem/samples/wildcard-prefix/example_disallowed_unsafe_prefix.yaml b/library/pod-security-policy/read-only-root-filesystem/samples/wildcard-prefix/example_disallowed_unsafe_prefix.yaml new file mode 100644 index 000000000..563abb05f --- /dev/null +++ b/library/pod-security-policy/read-only-root-filesystem/samples/wildcard-prefix/example_disallowed_unsafe_prefix.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-readonlyrootfilesystem-allowed + labels: + app: nginx-readonlyrootfilesystem +spec: + containers: + - name: nginx + image: "unsafe-images.com/nginx" + securityContext: + readOnlyRootFilesystem: false diff --git a/library/pod-security-policy/read-only-root-filesystem/suite.yaml b/library/pod-security-policy/read-only-root-filesystem/suite.yaml index fb52faa00..bb064b179 100644 --- a/library/pod-security-policy/read-only-root-filesystem/suite.yaml +++ b/library/pod-security-policy/read-only-root-filesystem/suite.yaml @@ -25,3 +25,27 @@ tests: object: samples/psp-readonlyrootfilesystem/update.yaml assertions: - violations: no + - name: exact-exemption + object: samples/psp-readonlyrootfilesystem/example_allowed_exempted.yaml + assertions: + - violations: no +- name: full-wildcard + template: template.yaml + constraint: samples/full_wildcard/constraint.yaml + cases: + - name: allow-normally-disallowed + object: samples/psp-readonlyrootfilesystem/example_disallowed.yaml + assertions: + - violations: no +- name: wildcard-prefix + template: template.yaml + constraint: samples/wildcard-prefix/constraint.yaml + cases: + - name: image-with-exempt-prefix-readOnlyRootFilesystem-not-required + object: samples/wildcard-prefix/example_allowed_safe_prefix.yaml + assertions: + - violations: no + - name: image-with-different-prefix-must-set-readOnlyRootFilesystem + object: samples/wildcard-prefix/example_disallowed_unsafe_prefix.yaml + assertions: + - violations: yes diff --git a/library/pod-security-policy/read-only-root-filesystem/template.yaml b/library/pod-security-policy/read-only-root-filesystem/template.yaml index 361ff999d..7f3a402e8 100644 --- a/library/pod-security-policy/read-only-root-filesystem/template.yaml +++ b/library/pod-security-policy/read-only-root-filesystem/template.yaml @@ -4,7 +4,7 @@ metadata: name: k8spspreadonlyrootfilesystem annotations: metadata.gatekeeper.sh/title: "Read Only Root Filesystem" - metadata.gatekeeper.sh/version: 1.1.0 + metadata.gatekeeper.sh/version: 1.1.1 description: >- Requires the use of a read-only root file system by pod containers. Corresponds to the `readOnlyRootFilesystem` field in a @@ -59,7 +59,7 @@ spec: expression: | (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, container.image in variables.exemptImageExplicit || - variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption))) + variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption))).map(container, container.image) - name: badContainers expression: | (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, diff --git a/src/pod-security-policy/read-only-root-filesystem/constraint.tmpl b/src/pod-security-policy/read-only-root-filesystem/constraint.tmpl index 862dfa51c..57516f3d8 100644 --- a/src/pod-security-policy/read-only-root-filesystem/constraint.tmpl +++ b/src/pod-security-policy/read-only-root-filesystem/constraint.tmpl @@ -4,7 +4,7 @@ metadata: name: k8spspreadonlyrootfilesystem annotations: metadata.gatekeeper.sh/title: "Read Only Root Filesystem" - metadata.gatekeeper.sh/version: 1.1.0 + metadata.gatekeeper.sh/version: 1.1.1 description: >- Requires the use of a read-only root file system by pod containers. Corresponds to the `readOnlyRootFilesystem` field in a diff --git a/src/pod-security-policy/read-only-root-filesystem/src.cel b/src/pod-security-policy/read-only-root-filesystem/src.cel index 0c4db172b..0be247c12 100644 --- a/src/pod-security-policy/read-only-root-filesystem/src.cel +++ b/src/pod-security-policy/read-only-root-filesystem/src.cel @@ -17,7 +17,7 @@ variables: expression: | (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, container.image in variables.exemptImageExplicit || - variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption))) + variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption))).map(container, container.image) - name: badContainers expression: | (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, diff --git a/website/docs/validation/read-only-root-filesystem.md b/website/docs/validation/read-only-root-filesystem.md index 038698b2a..4a841832f 100644 --- a/website/docs/validation/read-only-root-filesystem.md +++ b/website/docs/validation/read-only-root-filesystem.md @@ -16,7 +16,7 @@ metadata: name: k8spspreadonlyrootfilesystem annotations: metadata.gatekeeper.sh/title: "Read Only Root Filesystem" - metadata.gatekeeper.sh/version: 1.1.0 + metadata.gatekeeper.sh/version: 1.1.1 description: >- Requires the use of a read-only root file system by pod containers. Corresponds to the `readOnlyRootFilesystem` field in a @@ -71,7 +71,7 @@ spec: expression: | (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, container.image in variables.exemptImageExplicit || - variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption))) + variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption))).map(container, container.image) - name: badContainers expression: | (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, @@ -174,6 +174,9 @@ spec: kinds: - apiGroups: [""] kinds: ["Pod"] + parameters: + exemptImages: + - "specialprogram" ``` @@ -262,6 +265,174 @@ Usage kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/read-only-root-filesystem/samples/psp-readonlyrootfilesystem/disallowed_ephemeral.yaml ``` + +
+exact-exemption + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-readonlyrootfilesystem-disallowed + labels: + app: nginx-readonlyrootfilesystem +spec: + containers: + - name: specialprogram + image: specialprogram + securityContext: + readOnlyRootFilesystem: false + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/read-only-root-filesystem/samples/psp-readonlyrootfilesystem/example_allowed_exempted.yaml +``` + +
+ + +
+full-wildcard + +
+constraint + +```yaml +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPReadOnlyRootFilesystem +metadata: + name: psp-readonlyrootfilesystem +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + exemptImages: + - "*" + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/read-only-root-filesystem/samples/full_wildcard/constraint.yaml +``` + +
+ +
+allow-normally-disallowed + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-readonlyrootfilesystem-disallowed + labels: + app: nginx-readonlyrootfilesystem +spec: + containers: + - name: nginx + image: nginx + securityContext: + readOnlyRootFilesystem: false + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/read-only-root-filesystem/samples/psp-readonlyrootfilesystem/example_disallowed.yaml +``` + +
+ + +
+wildcard-prefix + +
+constraint + +```yaml +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPReadOnlyRootFilesystem +metadata: + name: psp-readonlyrootfilesystem +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + exemptImages: + - "safe-images.com/*" + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/read-only-root-filesystem/samples/wildcard-prefix/constraint.yaml +``` + +
+ +
+image-with-exempt-prefix-readOnlyRootFilesystem-not-required + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-readonlyrootfilesystem-allowed + labels: + app: nginx-readonlyrootfilesystem +spec: + containers: + - name: nginx + image: "safe-images.com/nginx" + securityContext: + readOnlyRootFilesystem: false + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/read-only-root-filesystem/samples/wildcard-prefix/example_allowed_safe_prefix.yaml +``` + +
+
+image-with-different-prefix-must-set-readOnlyRootFilesystem + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-readonlyrootfilesystem-allowed + labels: + app: nginx-readonlyrootfilesystem +spec: + containers: + - name: nginx + image: "unsafe-images.com/nginx" + securityContext: + readOnlyRootFilesystem: false + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/read-only-root-filesystem/samples/wildcard-prefix/example_disallowed_unsafe_prefix.yaml +``` +
From 888da7b46eca34eb7b66b771fdd3172f07a9fc38 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:26:09 -0700 Subject: [PATCH 03/19] chore: bump golang from 1.22 to 1.23 in /build/gomplate (#576) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps golang from 1.22 to 1.23. --- updated-dependencies: - dependency-name: golang dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sertaç Özercan <852750+sozercan@users.noreply.github.com> --- build/gomplate/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/gomplate/Dockerfile b/build/gomplate/Dockerfile index 21e0df975..9b3fde0df 100644 --- a/build/gomplate/Dockerfile +++ b/build/gomplate/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.22@sha256:2bd56f00ff47baf33e64eae7996b65846c7cb5e0a46e0a882ef179fd89654afa +FROM golang:1.23@sha256:613a108a4a4b1dfb6923305db791a19d088f77632317cfc3446825c54fb862cd ARG GOMPLATE_VERSION From 4f6d2f54c2e628db232730af1eac93733ea982d6 Mon Sep 17 00:00:00 2001 From: Julian Katz Date: Fri, 30 Aug 2024 13:10:48 -0700 Subject: [PATCH 04/19] fix(k8spspprocmount): fix exemptImages support (#588) I recently found (#584) that some K8sNativeValidation implementations of certain templates that iterate over and exempt containers by image had a bug preventing the exemption logic from working. I've fixed that bug here by mapping from `container` struct to `container.image` string. I've also added a suite test to verify this. That case fails without the change to the CEL logic. Signed-off-by: juliankatz --- .../proc-mount/1.1.1/README.md | 12 ++ .../proc-mount/1.1.1/artifacthub-pkg.yml | 22 +++ .../proc-mount/1.1.1/kustomization.yaml | 2 + .../samples/psp-proc-mount/constraint.yaml | 13 ++ .../psp-proc-mount/disallowed_ephemeral.yaml | 13 ++ .../psp-proc-mount/example_allowed.yaml | 13 ++ .../example_allowed_exempt_image.yaml | 13 ++ .../psp-proc-mount/example_disallowed.yaml | 13 ++ .../1.1.1/samples/psp-proc-mount/update.yaml | 17 ++ .../proc-mount/1.1.1/suite.yaml | 29 +++ .../proc-mount/1.1.1/template.yaml | 185 ++++++++++++++++++ .../samples/psp-proc-mount/constraint.yaml | 2 + .../example_allowed_exempt_image.yaml | 13 ++ .../pod-security-policy/proc-mount/suite.yaml | 4 + .../proc-mount/template.yaml | 11 +- .../proc-mount/constraint.tmpl | 2 +- src/pod-security-policy/proc-mount/src.cel | 11 +- website/docs/validation/proc-mount.md | 40 +++- 18 files changed, 405 insertions(+), 10 deletions(-) create mode 100644 artifacthub/library/pod-security-policy/proc-mount/1.1.1/README.md create mode 100644 artifacthub/library/pod-security-policy/proc-mount/1.1.1/artifacthub-pkg.yml create mode 100644 artifacthub/library/pod-security-policy/proc-mount/1.1.1/kustomization.yaml create mode 100644 artifacthub/library/pod-security-policy/proc-mount/1.1.1/samples/psp-proc-mount/constraint.yaml create mode 100644 artifacthub/library/pod-security-policy/proc-mount/1.1.1/samples/psp-proc-mount/disallowed_ephemeral.yaml create mode 100644 artifacthub/library/pod-security-policy/proc-mount/1.1.1/samples/psp-proc-mount/example_allowed.yaml create mode 100644 artifacthub/library/pod-security-policy/proc-mount/1.1.1/samples/psp-proc-mount/example_allowed_exempt_image.yaml create mode 100644 artifacthub/library/pod-security-policy/proc-mount/1.1.1/samples/psp-proc-mount/example_disallowed.yaml create mode 100644 artifacthub/library/pod-security-policy/proc-mount/1.1.1/samples/psp-proc-mount/update.yaml create mode 100644 artifacthub/library/pod-security-policy/proc-mount/1.1.1/suite.yaml create mode 100644 artifacthub/library/pod-security-policy/proc-mount/1.1.1/template.yaml create mode 100644 library/pod-security-policy/proc-mount/samples/psp-proc-mount/example_allowed_exempt_image.yaml diff --git a/artifacthub/library/pod-security-policy/proc-mount/1.1.1/README.md b/artifacthub/library/pod-security-policy/proc-mount/1.1.1/README.md new file mode 100644 index 000000000..9e45b7207 --- /dev/null +++ b/artifacthub/library/pod-security-policy/proc-mount/1.1.1/README.md @@ -0,0 +1,12 @@ +# ProcMount security context policy + +`procMount` denotes the type of proc mount to use for the containers. The default is `DefaultProcMount` which uses the container runtime defaults for readonly paths and masked paths. + +Types of proc mount are: + +- `DefaultProcMount` uses the container runtime default ProcType. Most container runtimes mask certain paths in /proc to avoid accidental security exposure of special devices or information. + +- `UnmaskedProcMount` bypasses the default masking behavior of the container runtime and ensures the newly created /proc the container stays in tact with no modifications. + +This requires the `ProcMountType` feature flag to be enabled. Set `--feature-gates=ProcMountType=true` in Kubernetes API Server to be able to use `Unmasked` procMount type (requires v1.12 and above). For more information, see +https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/#options and https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/. diff --git a/artifacthub/library/pod-security-policy/proc-mount/1.1.1/artifacthub-pkg.yml b/artifacthub/library/pod-security-policy/proc-mount/1.1.1/artifacthub-pkg.yml new file mode 100644 index 000000000..5ae52d10c --- /dev/null +++ b/artifacthub/library/pod-security-policy/proc-mount/1.1.1/artifacthub-pkg.yml @@ -0,0 +1,22 @@ +version: 1.1.1 +name: k8spspprocmount +displayName: Proc Mount +createdAt: "2024-08-29T21:24:52Z" +description: Controls the allowed `procMount` types for the container. Corresponds to the `allowedProcMountTypes` field in a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#allowedprocmounttypes +digest: e3a4bd2b1bf1c2401a90292cac4a479fe102f0a78dbeb6ac2bf8808f14f4ea2d +license: Apache-2.0 +homeURL: https://open-policy-agent.github.io/gatekeeper-library/website/proc-mount +keywords: + - gatekeeper + - open-policy-agent + - policies +readme: |- + # Proc Mount + Controls the allowed `procMount` types for the container. Corresponds to the `allowedProcMountTypes` field in a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#allowedprocmounttypes +install: |- + ### Usage + ```shell + kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/artifacthub/library/pod-security-policy/proc-mount/1.1.1/template.yaml + ``` +provider: + name: Gatekeeper Library diff --git a/artifacthub/library/pod-security-policy/proc-mount/1.1.1/kustomization.yaml b/artifacthub/library/pod-security-policy/proc-mount/1.1.1/kustomization.yaml new file mode 100644 index 000000000..7d70d11b7 --- /dev/null +++ b/artifacthub/library/pod-security-policy/proc-mount/1.1.1/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - template.yaml diff --git a/artifacthub/library/pod-security-policy/proc-mount/1.1.1/samples/psp-proc-mount/constraint.yaml b/artifacthub/library/pod-security-policy/proc-mount/1.1.1/samples/psp-proc-mount/constraint.yaml new file mode 100644 index 000000000..79ad221ab --- /dev/null +++ b/artifacthub/library/pod-security-policy/proc-mount/1.1.1/samples/psp-proc-mount/constraint.yaml @@ -0,0 +1,13 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPProcMount +metadata: + name: psp-proc-mount +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + procMount: Default + exemptImages: + - "safeimages.com/*" diff --git a/artifacthub/library/pod-security-policy/proc-mount/1.1.1/samples/psp-proc-mount/disallowed_ephemeral.yaml b/artifacthub/library/pod-security-policy/proc-mount/1.1.1/samples/psp-proc-mount/disallowed_ephemeral.yaml new file mode 100644 index 000000000..74e034148 --- /dev/null +++ b/artifacthub/library/pod-security-policy/proc-mount/1.1.1/samples/psp-proc-mount/disallowed_ephemeral.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-proc-mount-disallowed + labels: + app: nginx-proc-mount +spec: + hostUsers: false + ephemeralContainers: + - name: nginx + image: nginx + securityContext: + procMount: Unmasked #Default diff --git a/artifacthub/library/pod-security-policy/proc-mount/1.1.1/samples/psp-proc-mount/example_allowed.yaml b/artifacthub/library/pod-security-policy/proc-mount/1.1.1/samples/psp-proc-mount/example_allowed.yaml new file mode 100644 index 000000000..cc272bafb --- /dev/null +++ b/artifacthub/library/pod-security-policy/proc-mount/1.1.1/samples/psp-proc-mount/example_allowed.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-proc-mount-allowed + labels: + app: nginx-proc-mount +spec: + hostUsers: false + containers: + - name: nginx + image: nginx + securityContext: + procMount: Default diff --git a/artifacthub/library/pod-security-policy/proc-mount/1.1.1/samples/psp-proc-mount/example_allowed_exempt_image.yaml b/artifacthub/library/pod-security-policy/proc-mount/1.1.1/samples/psp-proc-mount/example_allowed_exempt_image.yaml new file mode 100644 index 000000000..c197740a4 --- /dev/null +++ b/artifacthub/library/pod-security-policy/proc-mount/1.1.1/samples/psp-proc-mount/example_allowed_exempt_image.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-proc-mount-exempt-image + labels: + app: nginx-proc-mount +spec: + hostUsers: false + containers: + - name: nginx + image: safeimages.com/nginx + securityContext: + procMount: Unmasked #Default diff --git a/artifacthub/library/pod-security-policy/proc-mount/1.1.1/samples/psp-proc-mount/example_disallowed.yaml b/artifacthub/library/pod-security-policy/proc-mount/1.1.1/samples/psp-proc-mount/example_disallowed.yaml new file mode 100644 index 000000000..f0c3b030a --- /dev/null +++ b/artifacthub/library/pod-security-policy/proc-mount/1.1.1/samples/psp-proc-mount/example_disallowed.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-proc-mount-disallowed + labels: + app: nginx-proc-mount +spec: + hostUsers: false + containers: + - name: nginx + image: nginx + securityContext: + procMount: Unmasked #Default diff --git a/artifacthub/library/pod-security-policy/proc-mount/1.1.1/samples/psp-proc-mount/update.yaml b/artifacthub/library/pod-security-policy/proc-mount/1.1.1/samples/psp-proc-mount/update.yaml new file mode 100644 index 000000000..dc21b1142 --- /dev/null +++ b/artifacthub/library/pod-security-policy/proc-mount/1.1.1/samples/psp-proc-mount/update.yaml @@ -0,0 +1,17 @@ +kind: AdmissionReview +apiVersion: admission.k8s.io/v1beta1 +request: + operation: "UPDATE" + object: + apiVersion: v1 + kind: Pod + metadata: + name: nginx-proc-mount-disallowed + labels: + app: nginx-proc-mount + spec: + containers: + - name: nginx + image: nginx + securityContext: + procMount: Unmasked #Default diff --git a/artifacthub/library/pod-security-policy/proc-mount/1.1.1/suite.yaml b/artifacthub/library/pod-security-policy/proc-mount/1.1.1/suite.yaml new file mode 100644 index 000000000..282246b5a --- /dev/null +++ b/artifacthub/library/pod-security-policy/proc-mount/1.1.1/suite.yaml @@ -0,0 +1,29 @@ +kind: Suite +apiVersion: test.gatekeeper.sh/v1alpha1 +metadata: + name: proc-mount +tests: +- name: default-proc-mount-required + template: template.yaml + constraint: samples/psp-proc-mount/constraint.yaml + cases: + - name: example-disallowed + object: samples/psp-proc-mount/example_disallowed.yaml + assertions: + - violations: yes + - name: example-allowed + object: samples/psp-proc-mount/example_allowed.yaml + assertions: + - violations: no + - name: disallowed-ephemeral + object: samples/psp-proc-mount/disallowed_ephemeral.yaml + assertions: + - violations: yes + - name: update + object: samples/psp-proc-mount/update.yaml + assertions: + - violations: no + - name: image-exempt-prefix-match + object: samples/psp-proc-mount/example_allowed_exempt_image.yaml + assertions: + - violations: no diff --git a/artifacthub/library/pod-security-policy/proc-mount/1.1.1/template.yaml b/artifacthub/library/pod-security-policy/proc-mount/1.1.1/template.yaml new file mode 100644 index 000000000..ed32cb383 --- /dev/null +++ b/artifacthub/library/pod-security-policy/proc-mount/1.1.1/template.yaml @@ -0,0 +1,185 @@ +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: k8spspprocmount + annotations: + metadata.gatekeeper.sh/title: "Proc Mount" + metadata.gatekeeper.sh/version: 1.1.1 + description: >- + Controls the allowed `procMount` types for the container. Corresponds to + the `allowedProcMountTypes` field in a PodSecurityPolicy. For more + information, see + https://kubernetes.io/docs/concepts/policy/pod-security-policy/#allowedprocmounttypes +spec: + crd: + spec: + names: + kind: K8sPSPProcMount + validation: + # Schema for the `parameters` field + openAPIV3Schema: + type: object + description: >- + Controls the allowed `procMount` types for the container. Corresponds to + the `allowedProcMountTypes` field in a PodSecurityPolicy. For more + information, see + https://kubernetes.io/docs/concepts/policy/pod-security-policy/#allowedprocmounttypes + 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 + procMount: + type: string + description: >- + Defines the strategy for the security exposure of certain paths + in `/proc` by the container runtime. Setting to `Default` uses + the runtime defaults, where `Unmasked` bypasses the default + behavior. + enum: + - Default + - Unmasked + targets: + - target: admission.k8s.gatekeeper.sh + code: + - engine: K8sNativeValidation + source: + variables: + - name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' + - name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' + - name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' + - 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) + ) + ).map(container, container.image) + - name: allowedProcMount + expression: | + !has(variables.params) ? "default" : + !has(variables.params.procMount) ? "default" : + (variables.params.procMount.lowerAscii() == "default" || variables.params.procMount.lowerAscii() == "unmasked") ? variables.params.procMount.lowerAscii() : "default" + - name: badContainers + expression: | + (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, + !(container.image in variables.exemptImages) && + !( + (variables.allowedProcMount == "unmasked") || + (variables.allowedProcMount == "default" && has(container.securityContext) && has(container.securityContext.procMount) && container.securityContext.procMount.lowerAscii() == "default") + ) + ).map(container, "ProcMount type is not allowed, container: " + container.name +". Allowed procMount types: " + variables.allowedProcMount) + validations: + - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.badContainers) == 0' + messageExpression: 'variables.badContainers.join("\n")' + - engine: Rego + source: + rego: | + package k8spspprocmount + + import data.lib.exclude_update.is_update + import data.lib.exempt_container.is_exempt + + violation[{"msg": msg, "details": {}}] { + # spec.containers.securityContext.procMount field is immutable. + not is_update(input.review) + + c := input_containers[_] + not is_exempt(c) + allowedProcMount := get_allowed_proc_mount(input) + not input_proc_mount_type_allowed(allowedProcMount, c) + msg := sprintf("ProcMount type is not allowed, container: %v. Allowed procMount types: %v", [c.name, allowedProcMount]) + } + + input_proc_mount_type_allowed(allowedProcMount, c) { + allowedProcMount == "default" + lower(c.securityContext.procMount) == "default" + } + input_proc_mount_type_allowed(allowedProcMount, _) { + allowedProcMount == "unmasked" + } + + input_containers[c] { + c := input.review.object.spec.containers[_] + c.securityContext.procMount + } + input_containers[c] { + c := input.review.object.spec.initContainers[_] + c.securityContext.procMount + } + input_containers[c] { + c := input.review.object.spec.ephemeralContainers[_] + c.securityContext.procMount + } + + get_allowed_proc_mount(arg) = out { + not arg.parameters + out = "default" + } + get_allowed_proc_mount(arg) = out { + not arg.parameters.procMount + out = "default" + } + get_allowed_proc_mount(arg) = out { + arg.parameters.procMount + not valid_proc_mount(arg.parameters.procMount) + out = "default" + } + get_allowed_proc_mount(arg) = out { + valid_proc_mount(arg.parameters.procMount) + out = lower(arg.parameters.procMount) + } + + valid_proc_mount(str) { + lower(str) == "default" + } + valid_proc_mount(str) { + lower(str) == "unmasked" + } + 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) + } diff --git a/library/pod-security-policy/proc-mount/samples/psp-proc-mount/constraint.yaml b/library/pod-security-policy/proc-mount/samples/psp-proc-mount/constraint.yaml index 1d7434ac0..79ad221ab 100644 --- a/library/pod-security-policy/proc-mount/samples/psp-proc-mount/constraint.yaml +++ b/library/pod-security-policy/proc-mount/samples/psp-proc-mount/constraint.yaml @@ -9,3 +9,5 @@ spec: kinds: ["Pod"] parameters: procMount: Default + exemptImages: + - "safeimages.com/*" diff --git a/library/pod-security-policy/proc-mount/samples/psp-proc-mount/example_allowed_exempt_image.yaml b/library/pod-security-policy/proc-mount/samples/psp-proc-mount/example_allowed_exempt_image.yaml new file mode 100644 index 000000000..c197740a4 --- /dev/null +++ b/library/pod-security-policy/proc-mount/samples/psp-proc-mount/example_allowed_exempt_image.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-proc-mount-exempt-image + labels: + app: nginx-proc-mount +spec: + hostUsers: false + containers: + - name: nginx + image: safeimages.com/nginx + securityContext: + procMount: Unmasked #Default diff --git a/library/pod-security-policy/proc-mount/suite.yaml b/library/pod-security-policy/proc-mount/suite.yaml index 501493e14..282246b5a 100644 --- a/library/pod-security-policy/proc-mount/suite.yaml +++ b/library/pod-security-policy/proc-mount/suite.yaml @@ -23,3 +23,7 @@ tests: object: samples/psp-proc-mount/update.yaml assertions: - violations: no + - name: image-exempt-prefix-match + object: samples/psp-proc-mount/example_allowed_exempt_image.yaml + assertions: + - violations: no diff --git a/library/pod-security-policy/proc-mount/template.yaml b/library/pod-security-policy/proc-mount/template.yaml index 1007de107..ed32cb383 100644 --- a/library/pod-security-policy/proc-mount/template.yaml +++ b/library/pod-security-policy/proc-mount/template.yaml @@ -4,7 +4,7 @@ metadata: name: k8spspprocmount annotations: metadata.gatekeeper.sh/title: "Proc Mount" - metadata.gatekeeper.sh/version: 1.1.0 + metadata.gatekeeper.sh/version: 1.1.1 description: >- Controls the allowed `procMount` types for the container. Corresponds to the `allowedProcMountTypes` field in a PodSecurityPolicy. For more @@ -67,9 +67,14 @@ spec: variables.params.exemptImages.filter(image, !image.endsWith("*")) - name: exemptImages expression: | - (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, + (variables.containers + variables.initContainers + variables.ephemeralContainers).filter( + container, container.image in variables.exemptImageExplicit || - variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption))) + variables.exemptImagePrefixes.exists( + exemption, + string(container.image).startsWith(exemption) + ) + ).map(container, container.image) - name: allowedProcMount expression: | !has(variables.params) ? "default" : diff --git a/src/pod-security-policy/proc-mount/constraint.tmpl b/src/pod-security-policy/proc-mount/constraint.tmpl index 0499c535a..0a43cd845 100644 --- a/src/pod-security-policy/proc-mount/constraint.tmpl +++ b/src/pod-security-policy/proc-mount/constraint.tmpl @@ -4,7 +4,7 @@ metadata: name: k8spspprocmount annotations: metadata.gatekeeper.sh/title: "Proc Mount" - metadata.gatekeeper.sh/version: 1.1.0 + metadata.gatekeeper.sh/version: 1.1.1 description: >- Controls the allowed `procMount` types for the container. Corresponds to the `allowedProcMountTypes` field in a PodSecurityPolicy. For more diff --git a/src/pod-security-policy/proc-mount/src.cel b/src/pod-security-policy/proc-mount/src.cel index b7cef9b98..df4331028 100644 --- a/src/pod-security-policy/proc-mount/src.cel +++ b/src/pod-security-policy/proc-mount/src.cel @@ -15,9 +15,14 @@ variables: variables.params.exemptImages.filter(image, !image.endsWith("*")) - name: exemptImages expression: | - (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, + (variables.containers + variables.initContainers + variables.ephemeralContainers).filter( + container, container.image in variables.exemptImageExplicit || - variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption))) + variables.exemptImagePrefixes.exists( + exemption, + string(container.image).startsWith(exemption) + ) + ).map(container, container.image) - name: allowedProcMount expression: | !has(variables.params) ? "default" : @@ -34,4 +39,4 @@ variables: ).map(container, "ProcMount type is not allowed, container: " + container.name +". Allowed procMount types: " + variables.allowedProcMount) validations: - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.badContainers) == 0' - messageExpression: 'variables.badContainers.join("\n")' \ No newline at end of file + messageExpression: 'variables.badContainers.join("\n")' diff --git a/website/docs/validation/proc-mount.md b/website/docs/validation/proc-mount.md index 17c26d23f..635e0c57e 100644 --- a/website/docs/validation/proc-mount.md +++ b/website/docs/validation/proc-mount.md @@ -16,7 +16,7 @@ metadata: name: k8spspprocmount annotations: metadata.gatekeeper.sh/title: "Proc Mount" - metadata.gatekeeper.sh/version: 1.1.0 + metadata.gatekeeper.sh/version: 1.1.1 description: >- Controls the allowed `procMount` types for the container. Corresponds to the `allowedProcMountTypes` field in a PodSecurityPolicy. For more @@ -79,9 +79,14 @@ spec: variables.params.exemptImages.filter(image, !image.endsWith("*")) - name: exemptImages expression: | - (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, + (variables.containers + variables.initContainers + variables.ephemeralContainers).filter( + container, container.image in variables.exemptImageExplicit || - variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption))) + variables.exemptImagePrefixes.exists( + exemption, + string(container.image).startsWith(exemption) + ) + ).map(container, container.image) - name: allowedProcMount expression: | !has(variables.params) ? "default" : @@ -216,6 +221,8 @@ spec: kinds: ["Pod"] parameters: procMount: Default + exemptImages: + - "safeimages.com/*" ``` @@ -307,6 +314,33 @@ Usage kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/proc-mount/samples/psp-proc-mount/disallowed_ephemeral.yaml ``` +
+
+image-exempt-prefix-match + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-proc-mount-exempt-image + labels: + app: nginx-proc-mount +spec: + hostUsers: false + containers: + - name: nginx + image: safeimages.com/nginx + securityContext: + procMount: Unmasked #Default + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/proc-mount/samples/psp-proc-mount/example_allowed_exempt_image.yaml +``` +
From 525a0050e47678a5500660d205742d7ed070e8e8 Mon Sep 17 00:00:00 2001 From: Julian Katz Date: Fri, 30 Aug 2024 13:32:01 -0700 Subject: [PATCH 05/19] fix(k8spsphostnetworkingports): CEL fixes for hostNetwork variable and (#589) message My updates to the suite.yaml file yielded an expected failure due to an incorrect CEL expression: ``` unexpected number of violations: got 1 violations but want none: got messages [failed expression: (has(request.operation) && request.operation == "UPDATE") || (!has(variables.params.hostNetwork) || !variables.params.hostNetwork ? (has(variables.anyObject.spec.hostNetwork) && !variables.anyObject.spec.hostNetwork) : true)] ``` By contrast, a run of `gator verify -v . --enable-k8s-native-validation=false` yielded a fully passing suite.yaml. This expression was actually failing due to its `messageExpression`, as non-primitive types cannot be combined with strings as in some interpreted languages (like rego). Unfortunately the compiler does not indicate that the messageExpression is the source of the problem. Once the message was fixed, I resolved the incorrect violation expression to fix the bug in the handling of params.hostNetwork. Signed-off-by: juliankatz --- .../1.1.3/artifacthub-pkg.yml | 22 +++ .../1.1.3/kustomization.yaml | 2 + .../block_host_network/constraint.yaml | 2 - .../constraint.yaml | 13 ++ ...llowed_out_of_range_host_network_true.yaml | 0 .../psp-host-network-ports/constraint.yaml | 13 ++ .../disallowed_ephemeral.yaml | 14 ++ .../example_allowed_in_range.yaml | 1 - .../example_allowed_no_ports.yaml | 11 ++ ...e_allowed_no_ports_host_network_false.yaml | 9 + ...le_allowed_no_ports_host_network_true.yaml | 9 + .../psp-host-network-ports/update.yaml | 19 ++ .../host-network-ports/1.1.3/suite.yaml | 62 +++++++ .../host-network-ports/1.1.3/template.yaml | 165 ++++++++++++++++++ .../block_host_network/constraint.yaml | 11 ++ .../constraint.yaml | 13 ++ ...llowed_out_of_range_host_network_true.yaml | 14 ++ .../example_allowed_in_range.yaml | 13 ++ ...e_allowed_no_ports_host_network_false.yaml | 9 + ...le_allowed_no_ports_host_network_true.yaml | 9 + .../host-network-ports/suite.yaml | 34 ++-- .../host-network-ports/template.yaml | 20 ++- .../host-network-ports/constraint.tmpl | 2 +- .../host-network-ports/src.cel | 18 +- website/docs/validation/host-network-ports.md | 126 ++++++++++--- 25 files changed, 561 insertions(+), 50 deletions(-) create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.3/artifacthub-pkg.yml create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.3/kustomization.yaml rename library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/constraint_block_host_network.yaml => artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/block_host_network/constraint.yaml (87%) create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/port_range_block_host_network/constraint.yaml rename library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_disallowed.yaml => artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/port_range_block_host_network/example_disallowed_out_of_range_host_network_true.yaml (100%) create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/constraint.yaml create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/disallowed_ephemeral.yaml rename library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed.yaml => artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/example_allowed_in_range.yaml (91%) create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/example_allowed_no_ports.yaml create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/example_allowed_no_ports_host_network_false.yaml create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/example_allowed_no_ports_host_network_true.yaml create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/update.yaml create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.3/suite.yaml create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.3/template.yaml create mode 100644 library/pod-security-policy/host-network-ports/samples/block_host_network/constraint.yaml create mode 100644 library/pod-security-policy/host-network-ports/samples/port_range_block_host_network/constraint.yaml create mode 100644 library/pod-security-policy/host-network-ports/samples/port_range_block_host_network/example_disallowed_out_of_range_host_network_true.yaml create mode 100644 library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed_in_range.yaml create mode 100644 library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed_no_ports_host_network_false.yaml create mode 100644 library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed_no_ports_host_network_true.yaml diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/artifacthub-pkg.yml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/artifacthub-pkg.yml new file mode 100644 index 000000000..878f809ba --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/artifacthub-pkg.yml @@ -0,0 +1,22 @@ +version: 1.1.3 +name: k8spsphostnetworkingports +displayName: Host Networking Ports +createdAt: "2024-08-29T21:28:18Z" +description: Controls usage of host network namespace by pod containers. HostNetwork verification happens without exception for exemptImages. Specific ports must be specified. Corresponds to the `hostNetwork` and `hostPorts` fields in a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#host-namespaces +digest: 751752950daeb4002a10cad6cbeba6a4afe03b98605f32885ae3fe0179eaff67 +license: Apache-2.0 +homeURL: https://open-policy-agent.github.io/gatekeeper-library/website/host-network-ports +keywords: + - gatekeeper + - open-policy-agent + - policies +readme: |- + # Host Networking Ports + Controls usage of host network namespace by pod containers. HostNetwork verification happens without exception for exemptImages. Specific ports must be specified. Corresponds to the `hostNetwork` and `hostPorts` fields in a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#host-namespaces +install: |- + ### Usage + ```shell + kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/template.yaml + ``` +provider: + name: Gatekeeper Library diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/kustomization.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/kustomization.yaml new file mode 100644 index 000000000..7d70d11b7 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - template.yaml diff --git a/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/constraint_block_host_network.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/block_host_network/constraint.yaml similarity index 87% rename from library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/constraint_block_host_network.yaml rename to artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/block_host_network/constraint.yaml index 7ef87dbb3..b5f2e9f44 100644 --- a/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/constraint_block_host_network.yaml +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/block_host_network/constraint.yaml @@ -9,5 +9,3 @@ spec: kinds: ["Pod"] parameters: hostNetwork: false - exemptImages: - - "nginx" \ No newline at end of file diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/port_range_block_host_network/constraint.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/port_range_block_host_network/constraint.yaml new file mode 100644 index 000000000..46e16454c --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/port_range_block_host_network/constraint.yaml @@ -0,0 +1,13 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPHostNetworkingPorts +metadata: + name: psp-host-network-ports +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + hostNetwork: false + min: 80 + max: 9000 diff --git a/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_disallowed.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/port_range_block_host_network/example_disallowed_out_of_range_host_network_true.yaml similarity index 100% rename from library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_disallowed.yaml rename to artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/port_range_block_host_network/example_disallowed_out_of_range_host_network_true.yaml diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/constraint.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/constraint.yaml new file mode 100644 index 000000000..aba7c24e7 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/constraint.yaml @@ -0,0 +1,13 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPHostNetworkingPorts +metadata: + name: psp-host-network-ports +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + hostNetwork: true + min: 80 + max: 9000 \ No newline at end of file diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/disallowed_ephemeral.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/disallowed_ephemeral.yaml new file mode 100644 index 000000000..7a4fa3114 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/disallowed_ephemeral.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-networking-ports-disallowed + labels: + app: nginx-host-networking-ports +spec: + hostNetwork: true + ephemeralContainers: + - name: nginx + image: nginx + ports: + - containerPort: 9001 + hostPort: 9001 diff --git a/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/example_allowed_in_range.yaml similarity index 91% rename from library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed.yaml rename to artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/example_allowed_in_range.yaml index 08b321fe5..2b4f7c926 100644 --- a/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed.yaml +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/example_allowed_in_range.yaml @@ -5,7 +5,6 @@ metadata: labels: app: nginx-host-networking-ports spec: - hostNetwork: false containers: - name: nginx image: nginx diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/example_allowed_no_ports.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/example_allowed_no_ports.yaml new file mode 100644 index 000000000..e009decf9 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/example_allowed_no_ports.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-networking-ports-disallowed + labels: + app: nginx-host-networking-ports +spec: + hostNetwork: true + containers: + - name: nginx + image: nginx diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/example_allowed_no_ports_host_network_false.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/example_allowed_no_ports_host_network_false.yaml new file mode 100644 index 000000000..8c0b0ef57 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/example_allowed_no_ports_host_network_false.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-network-false +spec: + hostNetwork: false + containers: + - name: nginx + image: nginx diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/example_allowed_no_ports_host_network_true.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/example_allowed_no_ports_host_network_true.yaml new file mode 100644 index 000000000..91cd7f4cd --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/example_allowed_no_ports_host_network_true.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-network-true +spec: + hostNetwork: true + containers: + - name: nginx + image: nginx diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/update.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/update.yaml new file mode 100644 index 000000000..231096430 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/samples/psp-host-network-ports/update.yaml @@ -0,0 +1,19 @@ +kind: AdmissionReview +apiVersion: admission.k8s.io/v1beta1 +request: + operation: "UPDATE" + object: + apiVersion: v1 + kind: Pod + metadata: + name: nginx-host-networking-ports-disallowed + labels: + app: nginx-host-networking-ports + spec: + hostNetwork: true + containers: + - name: nginx + image: nginx + ports: + - containerPort: 9001 + hostPort: 9001 diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/suite.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/suite.yaml new file mode 100644 index 000000000..44bbab1be --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/suite.yaml @@ -0,0 +1,62 @@ +kind: Suite +apiVersion: test.gatekeeper.sh/v1alpha1 +metadata: + name: host-network-ports +tests: +- name: port-range-with-host-network-allowed + template: template.yaml + constraint: samples/psp-host-network-ports/constraint.yaml + cases: + - name: out-of-range + object: samples/port_range_block_host_network/example_disallowed_out_of_range_host_network_true.yaml + assertions: + - violations: yes + - name: example-allowed + object: samples/psp-host-network-ports/example_allowed_in_range.yaml + assertions: + - violations: no + - name: out-of-range-ephemeral + object: samples/psp-host-network-ports/disallowed_ephemeral.yaml + assertions: + - violations: yes + - name: update + object: samples/psp-host-network-ports/update.yaml + assertions: + - violations: no + - name: no-ports-specified + object: samples/psp-host-network-ports/example_allowed_no_ports.yaml + assertions: + - violations: no +- name: host-network-forbidden + template: template.yaml + constraint: samples/block_host_network/constraint.yaml + cases: + - name: hostnetwork-true + object: samples/psp-host-network-ports/example_allowed_no_ports_host_network_true.yaml + assertions: + - violations: yes + - name: hostnetwork-false + object: samples/psp-host-network-ports/example_allowed_no_ports_host_network_false.yaml + assertions: + - violations: no +- name: port-range-with-host-network-forbidden + template: template.yaml + constraint: samples/port_range_block_host_network/constraint.yaml + cases: + - name: out-of-range-and-host-network-true + object: samples/port_range_block_host_network/example_disallowed_out_of_range_host_network_true.yaml + assertions: + - violations: yes + - name: in-range-host-network-false + object: samples/psp-host-network-ports/example_allowed_in_range.yaml + assertions: + - violations: no + - name: disallowed-ephemeral + object: samples/psp-host-network-ports/disallowed_ephemeral.yaml + assertions: + - violations: yes + - name: update + object: samples/psp-host-network-ports/update.yaml + assertions: + - violations: no + diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/template.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/template.yaml new file mode 100644 index 000000000..036a3e045 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.3/template.yaml @@ -0,0 +1,165 @@ +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: k8spsphostnetworkingports + annotations: + metadata.gatekeeper.sh/title: "Host Networking Ports" + metadata.gatekeeper.sh/version: 1.1.3 + description: >- + Controls usage of host network namespace by pod containers. HostNetwork verification happens without exception for exemptImages. Specific + ports must be specified. Corresponds to the `hostNetwork` and + `hostPorts` fields in a PodSecurityPolicy. For more information, see + https://kubernetes.io/docs/concepts/policy/pod-security-policy/#host-namespaces +spec: + crd: + spec: + names: + kind: K8sPSPHostNetworkingPorts + validation: + # Schema for the `parameters` field + openAPIV3Schema: + type: object + description: >- + Controls usage of host network namespace by pod containers. HostNetwork verification happens without exception for exemptImages. Specific + ports must be specified. Corresponds to the `hostNetwork` and + `hostPorts` fields in a PodSecurityPolicy. For more information, see + https://kubernetes.io/docs/concepts/policy/pod-security-policy/#host-namespaces + 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 + hostNetwork: + description: "Determines if the policy allows the use of HostNetwork in the pod spec." + type: boolean + min: + description: "The start of the allowed port range, inclusive." + type: integer + max: + description: "The end of the allowed port range, inclusive." + type: integer + targets: + - target: admission.k8s.gatekeeper.sh + code: + - engine: K8sNativeValidation + source: + variables: + - name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' + - name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' + - name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' + - 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(container, + !(container.image in variables.exemptImages) && has(container.ports) && + ( + (container.ports.all(port, has(port.hostPort) && has(variables.params.min) && port.hostPort < variables.params.min)) || + (container.ports.all(port, has(port.hostPort) && has(variables.params.max) && port.hostPort > variables.params.max)) + ) + ) + - name: isUpdate + expression: has(request.operation) && request.operation == "UPDATE" + - name: hostNetworkAllowed + expression: has(variables.params.hostNetwork) && variables.params.hostNetwork + - name: hostNetworkEnabled + expression: has(variables.anyObject.spec.hostNetwork) && variables.anyObject.spec.hostNetwork + - name: hostNetworkViolation + expression: variables.hostNetworkEnabled && !variables.hostNetworkAllowed + validations: + - expression: 'variables.isUpdate || size(variables.badContainers) == 0' + messageExpression: '"The specified hostNetwork and hostPort are not allowed, pod: " + variables.anyObject.metadata.name' + - expression: variables.isUpdate || !variables.hostNetworkViolation + messageExpression: '"The specified hostNetwork and hostPort are not allowed, pod: " + variables.anyObject.metadata.name' + - engine: Rego + source: + rego: | + package k8spsphostnetworkingports + + import data.lib.exclude_update.is_update + import data.lib.exempt_container.is_exempt + + violation[{"msg": msg, "details": {}}] { + # spec.hostNetwork field is immutable. + not is_update(input.review) + + input_share_hostnetwork(input.review.object) + msg := sprintf("The specified hostNetwork and hostPort are not allowed, pod: %v. Allowed values: %v", [input.review.object.metadata.name, input.parameters]) + } + + input_share_hostnetwork(o) { + not input.parameters.hostNetwork + o.spec.hostNetwork + } + + input_share_hostnetwork(_) { + hostPort := input_containers[_].ports[_].hostPort + hostPort < input.parameters.min + } + + input_share_hostnetwork(_) { + hostPort := input_containers[_].ports[_].hostPort + hostPort > input.parameters.max + } + + input_containers[c] { + c := input.review.object.spec.containers[_] + not is_exempt(c) + } + + input_containers[c] { + c := input.review.object.spec.initContainers[_] + not is_exempt(c) + } + + input_containers[c] { + c := input.review.object.spec.ephemeralContainers[_] + not is_exempt(c) + } + 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) + } diff --git a/library/pod-security-policy/host-network-ports/samples/block_host_network/constraint.yaml b/library/pod-security-policy/host-network-ports/samples/block_host_network/constraint.yaml new file mode 100644 index 000000000..b5f2e9f44 --- /dev/null +++ b/library/pod-security-policy/host-network-ports/samples/block_host_network/constraint.yaml @@ -0,0 +1,11 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPHostNetworkingPorts +metadata: + name: psp-host-network +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + hostNetwork: false diff --git a/library/pod-security-policy/host-network-ports/samples/port_range_block_host_network/constraint.yaml b/library/pod-security-policy/host-network-ports/samples/port_range_block_host_network/constraint.yaml new file mode 100644 index 000000000..46e16454c --- /dev/null +++ b/library/pod-security-policy/host-network-ports/samples/port_range_block_host_network/constraint.yaml @@ -0,0 +1,13 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPHostNetworkingPorts +metadata: + name: psp-host-network-ports +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + hostNetwork: false + min: 80 + max: 9000 diff --git a/library/pod-security-policy/host-network-ports/samples/port_range_block_host_network/example_disallowed_out_of_range_host_network_true.yaml b/library/pod-security-policy/host-network-ports/samples/port_range_block_host_network/example_disallowed_out_of_range_host_network_true.yaml new file mode 100644 index 000000000..9a496cd60 --- /dev/null +++ b/library/pod-security-policy/host-network-ports/samples/port_range_block_host_network/example_disallowed_out_of_range_host_network_true.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-networking-ports-disallowed + labels: + app: nginx-host-networking-ports +spec: + hostNetwork: true + containers: + - name: nginx + image: nginx + ports: + - containerPort: 9001 + hostPort: 9001 diff --git a/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed_in_range.yaml b/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed_in_range.yaml new file mode 100644 index 000000000..2b4f7c926 --- /dev/null +++ b/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed_in_range.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-networking-ports-allowed + labels: + app: nginx-host-networking-ports +spec: + containers: + - name: nginx + image: nginx + ports: + - containerPort: 9000 + hostPort: 80 diff --git a/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed_no_ports_host_network_false.yaml b/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed_no_ports_host_network_false.yaml new file mode 100644 index 000000000..8c0b0ef57 --- /dev/null +++ b/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed_no_ports_host_network_false.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-network-false +spec: + hostNetwork: false + containers: + - name: nginx + image: nginx diff --git a/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed_no_ports_host_network_true.yaml b/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed_no_ports_host_network_true.yaml new file mode 100644 index 000000000..91cd7f4cd --- /dev/null +++ b/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed_no_ports_host_network_true.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-network-true +spec: + hostNetwork: true + containers: + - name: nginx + image: nginx diff --git a/library/pod-security-policy/host-network-ports/suite.yaml b/library/pod-security-policy/host-network-ports/suite.yaml index b0c7f7816..44bbab1be 100644 --- a/library/pod-security-policy/host-network-ports/suite.yaml +++ b/library/pod-security-policy/host-network-ports/suite.yaml @@ -3,19 +3,19 @@ apiVersion: test.gatekeeper.sh/v1alpha1 metadata: name: host-network-ports tests: -- name: use-of-host-networking-ports-blocked +- name: port-range-with-host-network-allowed template: template.yaml constraint: samples/psp-host-network-ports/constraint.yaml cases: - - name: example-disallowed - object: samples/psp-host-network-ports/example_disallowed.yaml + - name: out-of-range + object: samples/port_range_block_host_network/example_disallowed_out_of_range_host_network_true.yaml assertions: - violations: yes - name: example-allowed - object: samples/psp-host-network-ports/example_allowed.yaml + object: samples/psp-host-network-ports/example_allowed_in_range.yaml assertions: - violations: no - - name: disallowed-ephemeral + - name: out-of-range-ephemeral object: samples/psp-host-network-ports/disallowed_ephemeral.yaml assertions: - violations: yes @@ -27,16 +27,28 @@ tests: object: samples/psp-host-network-ports/example_allowed_no_ports.yaml assertions: - violations: no -- name: use-of-host-network-blocked +- name: host-network-forbidden template: template.yaml - constraint: samples/psp-host-network-ports/constraint_block_host_network.yaml + constraint: samples/block_host_network/constraint.yaml cases: - - name: example-disallowed - object: samples/psp-host-network-ports/example_disallowed.yaml + - name: hostnetwork-true + object: samples/psp-host-network-ports/example_allowed_no_ports_host_network_true.yaml assertions: - violations: yes - - name: example-allowed - object: samples/psp-host-network-ports/example_allowed.yaml + - name: hostnetwork-false + object: samples/psp-host-network-ports/example_allowed_no_ports_host_network_false.yaml + assertions: + - violations: no +- name: port-range-with-host-network-forbidden + template: template.yaml + constraint: samples/port_range_block_host_network/constraint.yaml + cases: + - name: out-of-range-and-host-network-true + object: samples/port_range_block_host_network/example_disallowed_out_of_range_host_network_true.yaml + assertions: + - violations: yes + - name: in-range-host-network-false + object: samples/psp-host-network-ports/example_allowed_in_range.yaml assertions: - violations: no - name: disallowed-ephemeral diff --git a/library/pod-security-policy/host-network-ports/template.yaml b/library/pod-security-policy/host-network-ports/template.yaml index 218cb36e7..036a3e045 100644 --- a/library/pod-security-policy/host-network-ports/template.yaml +++ b/library/pod-security-policy/host-network-ports/template.yaml @@ -4,7 +4,7 @@ metadata: name: k8spsphostnetworkingports annotations: metadata.gatekeeper.sh/title: "Host Networking Ports" - metadata.gatekeeper.sh/version: 1.1.2 + metadata.gatekeeper.sh/version: 1.1.3 description: >- Controls usage of host network namespace by pod containers. HostNetwork verification happens without exception for exemptImages. Specific ports must be specified. Corresponds to the `hostNetwork` and @@ -78,13 +78,19 @@ spec: (container.ports.all(port, has(port.hostPort) && has(variables.params.max) && port.hostPort > variables.params.max)) ) ) + - name: isUpdate + expression: has(request.operation) && request.operation == "UPDATE" + - name: hostNetworkAllowed + expression: has(variables.params.hostNetwork) && variables.params.hostNetwork + - name: hostNetworkEnabled + expression: has(variables.anyObject.spec.hostNetwork) && variables.anyObject.spec.hostNetwork + - name: hostNetworkViolation + expression: variables.hostNetworkEnabled && !variables.hostNetworkAllowed validations: - - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.badContainers) == 0' - messageExpression: '"The specified hostNetwork and hostPort are not allowed, pod: " + variables.anyObject.metadata.name + ". Allowed values: " + variables.params' - - expression: | - (has(request.operation) && request.operation == "UPDATE") || - (!has(variables.params.hostNetwork) || !variables.params.hostNetwork ? (has(variables.anyObject.spec.hostNetwork) && !variables.anyObject.spec.hostNetwork) : true) - messageExpression: '"The specified hostNetwork and hostPort are not allowed, pod: " + variables.anyObject.metadata.name + ". Allowed values: " + variables.params' + - expression: 'variables.isUpdate || size(variables.badContainers) == 0' + messageExpression: '"The specified hostNetwork and hostPort are not allowed, pod: " + variables.anyObject.metadata.name' + - expression: variables.isUpdate || !variables.hostNetworkViolation + messageExpression: '"The specified hostNetwork and hostPort are not allowed, pod: " + variables.anyObject.metadata.name' - engine: Rego source: rego: | diff --git a/src/pod-security-policy/host-network-ports/constraint.tmpl b/src/pod-security-policy/host-network-ports/constraint.tmpl index 9f6e90d28..d5f714682 100644 --- a/src/pod-security-policy/host-network-ports/constraint.tmpl +++ b/src/pod-security-policy/host-network-ports/constraint.tmpl @@ -4,7 +4,7 @@ metadata: name: k8spsphostnetworkingports annotations: metadata.gatekeeper.sh/title: "Host Networking Ports" - metadata.gatekeeper.sh/version: 1.1.2 + metadata.gatekeeper.sh/version: 1.1.3 description: >- Controls usage of host network namespace by pod containers. HostNetwork verification happens without exception for exemptImages. Specific ports must be specified. Corresponds to the `hostNetwork` and diff --git a/src/pod-security-policy/host-network-ports/src.cel b/src/pod-security-policy/host-network-ports/src.cel index 1bd9b333c..8d0fe507e 100644 --- a/src/pod-security-policy/host-network-ports/src.cel +++ b/src/pod-security-policy/host-network-ports/src.cel @@ -27,10 +27,16 @@ variables: (container.ports.all(port, has(port.hostPort) && has(variables.params.max) && port.hostPort > variables.params.max)) ) ) +- name: isUpdate + expression: has(request.operation) && request.operation == "UPDATE" +- name: hostNetworkAllowed + expression: has(variables.params.hostNetwork) && variables.params.hostNetwork +- name: hostNetworkEnabled + expression: has(variables.anyObject.spec.hostNetwork) && variables.anyObject.spec.hostNetwork +- name: hostNetworkViolation + expression: variables.hostNetworkEnabled && !variables.hostNetworkAllowed validations: -- expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.badContainers) == 0' - messageExpression: '"The specified hostNetwork and hostPort are not allowed, pod: " + variables.anyObject.metadata.name + ". Allowed values: " + variables.params' -- expression: | - (has(request.operation) && request.operation == "UPDATE") || - (!has(variables.params.hostNetwork) || !variables.params.hostNetwork ? (has(variables.anyObject.spec.hostNetwork) && !variables.anyObject.spec.hostNetwork) : true) - messageExpression: '"The specified hostNetwork and hostPort are not allowed, pod: " + variables.anyObject.metadata.name + ". Allowed values: " + variables.params' +- expression: 'variables.isUpdate || size(variables.badContainers) == 0' + messageExpression: '"The specified hostNetwork and hostPort are not allowed, pod: " + variables.anyObject.metadata.name' +- expression: variables.isUpdate || !variables.hostNetworkViolation + messageExpression: '"The specified hostNetwork and hostPort are not allowed, pod: " + variables.anyObject.metadata.name' diff --git a/website/docs/validation/host-network-ports.md b/website/docs/validation/host-network-ports.md index 17c2b6b6b..549b37380 100644 --- a/website/docs/validation/host-network-ports.md +++ b/website/docs/validation/host-network-ports.md @@ -16,7 +16,7 @@ metadata: name: k8spsphostnetworkingports annotations: metadata.gatekeeper.sh/title: "Host Networking Ports" - metadata.gatekeeper.sh/version: 1.1.2 + metadata.gatekeeper.sh/version: 1.1.3 description: >- Controls usage of host network namespace by pod containers. HostNetwork verification happens without exception for exemptImages. Specific ports must be specified. Corresponds to the `hostNetwork` and @@ -90,13 +90,19 @@ spec: (container.ports.all(port, has(port.hostPort) && has(variables.params.max) && port.hostPort > variables.params.max)) ) ) + - name: isUpdate + expression: has(request.operation) && request.operation == "UPDATE" + - name: hostNetworkAllowed + expression: has(variables.params.hostNetwork) && variables.params.hostNetwork + - name: hostNetworkEnabled + expression: has(variables.anyObject.spec.hostNetwork) && variables.anyObject.spec.hostNetwork + - name: hostNetworkViolation + expression: variables.hostNetworkEnabled && !variables.hostNetworkAllowed validations: - - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.badContainers) == 0' - messageExpression: '"The specified hostNetwork and hostPort are not allowed, pod: " + variables.anyObject.metadata.name + ". Allowed values: " + variables.params' - - expression: | - (has(request.operation) && request.operation == "UPDATE") || - (!has(variables.params.hostNetwork) || !variables.params.hostNetwork ? (has(variables.anyObject.spec.hostNetwork) && !variables.anyObject.spec.hostNetwork) : true) - messageExpression: '"The specified hostNetwork and hostPort are not allowed, pod: " + variables.anyObject.metadata.name + ". Allowed values: " + variables.params' + - expression: 'variables.isUpdate || size(variables.badContainers) == 0' + messageExpression: '"The specified hostNetwork and hostPort are not allowed, pod: " + variables.anyObject.metadata.name' + - expression: variables.isUpdate || !variables.hostNetworkViolation + messageExpression: '"The specified hostNetwork and hostPort are not allowed, pod: " + variables.anyObject.metadata.name' - engine: Rego source: rego: | @@ -178,7 +184,7 @@ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper- ``` ## Examples
-use-of-host-networking-ports-blocked +port-range-with-host-network-allowed
constraint @@ -208,7 +214,7 @@ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-
-example-disallowed +out-of-range ```yaml apiVersion: v1 @@ -231,7 +237,7 @@ spec: Usage ```shell -kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_disallowed.yaml +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/host-network-ports/samples/port_range_block_host_network/example_disallowed_out_of_range_host_network_true.yaml ```
@@ -246,7 +252,6 @@ metadata: labels: app: nginx-host-networking-ports spec: - hostNetwork: false containers: - name: nginx image: nginx @@ -259,12 +264,12 @@ spec: Usage ```shell -kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed.yaml +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed_in_range.yaml ```
-disallowed-ephemeral +out-of-range-ephemeral ```yaml apiVersion: v1 @@ -319,7 +324,7 @@ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-
-use-of-host-network-blocked +host-network-forbidden
constraint @@ -336,20 +341,98 @@ spec: kinds: ["Pod"] parameters: hostNetwork: false - exemptImages: - - "nginx" + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/host-network-ports/samples/block_host_network/constraint.yaml +``` + +
+ +
+hostnetwork-true + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-network-true +spec: + hostNetwork: true + containers: + - name: nginx + image: nginx + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed_no_ports_host_network_true.yaml +``` + +
+
+hostnetwork-false + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-network-false +spec: + hostNetwork: false + containers: + - name: nginx + image: nginx + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed_no_ports_host_network_false.yaml +``` + +
+ + +
+port-range-with-host-network-forbidden + +
+constraint + +```yaml +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPHostNetworkingPorts +metadata: + name: psp-host-network-ports +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + hostNetwork: false + min: 80 + max: 9000 + ``` Usage ```shell -kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/constraint_block_host_network.yaml +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/host-network-ports/samples/port_range_block_host_network/constraint.yaml ```
-example-disallowed +out-of-range-and-host-network-true ```yaml apiVersion: v1 @@ -372,12 +455,12 @@ spec: Usage ```shell -kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_disallowed.yaml +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/host-network-ports/samples/port_range_block_host_network/example_disallowed_out_of_range_host_network_true.yaml ```
-example-allowed +in-range-host-network-false ```yaml apiVersion: v1 @@ -387,7 +470,6 @@ metadata: labels: app: nginx-host-networking-ports spec: - hostNetwork: false containers: - name: nginx image: nginx @@ -400,7 +482,7 @@ spec: Usage ```shell -kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed.yaml +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed_in_range.yaml ```
From 033906e30c07168907e3b4fec5848e1fa4178ff5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Sep 2024 09:04:37 -0700 Subject: [PATCH 06/19] chore: bump the all group with 2 updates (#594) Bumps the all group with 2 updates: [github/codeql-action](https://github.com/github/codeql-action) and [actions/upload-artifact](https://github.com/actions/upload-artifact). Updates `github/codeql-action` from 3.26.5 to 3.26.6 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/2c779ab0d087cd7fe7b826087247c2c81f27bfa6...4dd16135b69a43b6c8efb853346f8437d92d3c93) Updates `actions/upload-artifact` from 4.3.6 to 4.4.0 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/834a144ee995460fba8ed112a2fc961b36a5ec5a...50769540e7f4bd5e21e526ee35c689e35e0d6874) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 6 +++--- .github/workflows/scorecards.yml | 4 ++-- .github/workflows/workflow.yaml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b0267abce..83584c2d2 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -50,7 +50,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 + uses: github/codeql-action/init@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -60,7 +60,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 + uses: github/codeql-action/autobuild@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -73,6 +73,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 + uses: github/codeql-action/analyze@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index fee0d7637..f06e45705 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -63,7 +63,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: SARIF file path: results.sarif @@ -71,6 +71,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 + uses: github/codeql-action/upload-sarif@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 with: sarif_file: results.sarif diff --git a/.github/workflows/workflow.yaml b/.github/workflows/workflow.yaml index 1af1df56f..dd8e93ebc 100644 --- a/.github/workflows/workflow.yaml +++ b/.github/workflows/workflow.yaml @@ -99,7 +99,7 @@ jobs: kubectl logs -n gatekeeper-system -l control-plane=audit-controller --tail=-1 > logs-audit.json - name: Upload artifacts - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 if: ${{ always() }} with: name: logs-int-test-${{ matrix.gatekeeper }}-${{ matrix.engine }} From 8b37aeed632c1ed6f6c1d781bd0795dcf9a60598 Mon Sep 17 00:00:00 2001 From: Julian Katz Date: Tue, 3 Sep 2024 18:51:34 -0700 Subject: [PATCH 07/19] fix(k8spsphostnetworkingports): exemptImages CEL bug (#590) I recently found (#584) that some K8sNativeValidation implementations of certain templates that iterate over and exempt containers by image had a bug preventing the exemption logic from working. I've fixed that bug here by mapping from container struct to container.image string. I've also added a suite test to verify this. That case fails without the change to the CEL logic. Signed-off-by: juliankatz --- .../1.1.4/artifacthub-pkg.yml | 22 +++ .../1.1.4/kustomization.yaml | 2 + .../block_host_network/constraint.yaml | 11 ++ .../constraint.yaml | 15 ++ ...empted_container_host_network_enabled.yaml | 14 ++ ...llowed_out_of_range_host_network_true.yaml | 14 ++ .../psp-host-network-ports/constraint.yaml | 15 ++ .../disallowed_ephemeral.yaml | 14 ++ .../example_allowed_in_range.yaml | 13 ++ .../example_allowed_no_ports.yaml | 11 ++ ...e_allowed_no_ports_host_network_false.yaml | 9 + ...le_allowed_no_ports_host_network_true.yaml | 9 + ...example_allowed_out_of_range_exempted.yaml | 14 ++ .../psp-host-network-ports/update.yaml | 19 ++ .../host-network-ports/1.1.4/suite.yaml | 69 ++++++++ .../host-network-ports/1.1.4/template.yaml | 166 ++++++++++++++++++ .../constraint.yaml | 2 + ...empted_container_host_network_enabled.yaml | 14 ++ .../psp-host-network-ports/constraint.yaml | 4 +- ...example_allowed_out_of_range_exempted.yaml | 14 ++ .../host-network-ports/suite.yaml | 9 +- .../host-network-ports/template.yaml | 5 +- .../host-network-ports/constraint.tmpl | 2 +- .../host-network-ports/src.cel | 3 +- website/docs/validation/host-network-ports.md | 66 ++++++- 25 files changed, 528 insertions(+), 8 deletions(-) create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.4/artifacthub-pkg.yml create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.4/kustomization.yaml create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/block_host_network/constraint.yaml create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/port_range_block_host_network/constraint.yaml create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/port_range_block_host_network/example_disallowed_exempted_container_host_network_enabled.yaml create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/port_range_block_host_network/example_disallowed_out_of_range_host_network_true.yaml create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/constraint.yaml create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/disallowed_ephemeral.yaml create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/example_allowed_in_range.yaml create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/example_allowed_no_ports.yaml create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/example_allowed_no_ports_host_network_false.yaml create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/example_allowed_no_ports_host_network_true.yaml create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/example_allowed_out_of_range_exempted.yaml create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/update.yaml create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.4/suite.yaml create mode 100644 artifacthub/library/pod-security-policy/host-network-ports/1.1.4/template.yaml create mode 100644 library/pod-security-policy/host-network-ports/samples/port_range_block_host_network/example_disallowed_exempted_container_host_network_enabled.yaml create mode 100644 library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed_out_of_range_exempted.yaml diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/artifacthub-pkg.yml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/artifacthub-pkg.yml new file mode 100644 index 000000000..24366190f --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/artifacthub-pkg.yml @@ -0,0 +1,22 @@ +version: 1.1.4 +name: k8spsphostnetworkingports +displayName: Host Networking Ports +createdAt: "2024-08-30T22:03:40Z" +description: Controls usage of host network namespace by pod containers. HostNetwork verification happens without exception for exemptImages. Specific ports must be specified. Corresponds to the `hostNetwork` and `hostPorts` fields in a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#host-namespaces +digest: 5e295f3ee2762996e89f926faae128ca3ae86166aac8fb9e518433ba1300deec +license: Apache-2.0 +homeURL: https://open-policy-agent.github.io/gatekeeper-library/website/host-network-ports +keywords: + - gatekeeper + - open-policy-agent + - policies +readme: |- + # Host Networking Ports + Controls usage of host network namespace by pod containers. HostNetwork verification happens without exception for exemptImages. Specific ports must be specified. Corresponds to the `hostNetwork` and `hostPorts` fields in a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#host-namespaces +install: |- + ### Usage + ```shell + kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/template.yaml + ``` +provider: + name: Gatekeeper Library diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/kustomization.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/kustomization.yaml new file mode 100644 index 000000000..7d70d11b7 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - template.yaml diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/block_host_network/constraint.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/block_host_network/constraint.yaml new file mode 100644 index 000000000..b5f2e9f44 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/block_host_network/constraint.yaml @@ -0,0 +1,11 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPHostNetworkingPorts +metadata: + name: psp-host-network +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + hostNetwork: false diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/port_range_block_host_network/constraint.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/port_range_block_host_network/constraint.yaml new file mode 100644 index 000000000..c315f9e23 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/port_range_block_host_network/constraint.yaml @@ -0,0 +1,15 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPHostNetworkingPorts +metadata: + name: psp-host-network-ports +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + hostNetwork: false + min: 80 + max: 9000 + exemptImages: + - "safeimages.com/*" diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/port_range_block_host_network/example_disallowed_exempted_container_host_network_enabled.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/port_range_block_host_network/example_disallowed_exempted_container_host_network_enabled.yaml new file mode 100644 index 000000000..0056f9b1d --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/port_range_block_host_network/example_disallowed_exempted_container_host_network_enabled.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-networking-hn-ok-bad-port + labels: + app: nginx-host-networking-ports +spec: + hostNetwork: true + containers: + - name: nginx + image: safeimages.com/nginx + ports: + - containerPort: 9001 + hostPort: 9001 diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/port_range_block_host_network/example_disallowed_out_of_range_host_network_true.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/port_range_block_host_network/example_disallowed_out_of_range_host_network_true.yaml new file mode 100644 index 000000000..9a496cd60 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/port_range_block_host_network/example_disallowed_out_of_range_host_network_true.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-networking-ports-disallowed + labels: + app: nginx-host-networking-ports +spec: + hostNetwork: true + containers: + - name: nginx + image: nginx + ports: + - containerPort: 9001 + hostPort: 9001 diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/constraint.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/constraint.yaml new file mode 100644 index 000000000..b6176404c --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/constraint.yaml @@ -0,0 +1,15 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPHostNetworkingPorts +metadata: + name: psp-host-network-ports +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + hostNetwork: true + min: 80 + max: 9000 + exemptImages: + - "safeimages.com/*" diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/disallowed_ephemeral.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/disallowed_ephemeral.yaml new file mode 100644 index 000000000..7a4fa3114 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/disallowed_ephemeral.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-networking-ports-disallowed + labels: + app: nginx-host-networking-ports +spec: + hostNetwork: true + ephemeralContainers: + - name: nginx + image: nginx + ports: + - containerPort: 9001 + hostPort: 9001 diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/example_allowed_in_range.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/example_allowed_in_range.yaml new file mode 100644 index 000000000..2b4f7c926 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/example_allowed_in_range.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-networking-ports-allowed + labels: + app: nginx-host-networking-ports +spec: + containers: + - name: nginx + image: nginx + ports: + - containerPort: 9000 + hostPort: 80 diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/example_allowed_no_ports.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/example_allowed_no_ports.yaml new file mode 100644 index 000000000..e009decf9 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/example_allowed_no_ports.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-networking-ports-disallowed + labels: + app: nginx-host-networking-ports +spec: + hostNetwork: true + containers: + - name: nginx + image: nginx diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/example_allowed_no_ports_host_network_false.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/example_allowed_no_ports_host_network_false.yaml new file mode 100644 index 000000000..8c0b0ef57 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/example_allowed_no_ports_host_network_false.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-network-false +spec: + hostNetwork: false + containers: + - name: nginx + image: nginx diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/example_allowed_no_ports_host_network_true.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/example_allowed_no_ports_host_network_true.yaml new file mode 100644 index 000000000..91cd7f4cd --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/example_allowed_no_ports_host_network_true.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-network-true +spec: + hostNetwork: true + containers: + - name: nginx + image: nginx diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/example_allowed_out_of_range_exempted.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/example_allowed_out_of_range_exempted.yaml new file mode 100644 index 000000000..e4d013447 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/example_allowed_out_of_range_exempted.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-networking-ports-exempted + labels: + app: nginx-host-networking-ports +spec: + hostNetwork: true + containers: + - name: nginx + image: safeimages.com/nginx + ports: + - containerPort: 9001 + hostPort: 9001 diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/update.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/update.yaml new file mode 100644 index 000000000..231096430 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/samples/psp-host-network-ports/update.yaml @@ -0,0 +1,19 @@ +kind: AdmissionReview +apiVersion: admission.k8s.io/v1beta1 +request: + operation: "UPDATE" + object: + apiVersion: v1 + kind: Pod + metadata: + name: nginx-host-networking-ports-disallowed + labels: + app: nginx-host-networking-ports + spec: + hostNetwork: true + containers: + - name: nginx + image: nginx + ports: + - containerPort: 9001 + hostPort: 9001 diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/suite.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/suite.yaml new file mode 100644 index 000000000..8879f7fc9 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/suite.yaml @@ -0,0 +1,69 @@ +kind: Suite +apiVersion: test.gatekeeper.sh/v1alpha1 +metadata: + name: host-network-ports +tests: +- name: port-range-with-host-network-allowed + template: template.yaml + constraint: samples/psp-host-network-ports/constraint.yaml + cases: + - name: out-of-range + object: samples/port_range_block_host_network/example_disallowed_out_of_range_host_network_true.yaml + assertions: + - violations: yes + - name: example-allowed + object: samples/psp-host-network-ports/example_allowed_in_range.yaml + assertions: + - violations: no + - name: out-of-range-ephemeral + object: samples/psp-host-network-ports/disallowed_ephemeral.yaml + assertions: + - violations: yes + - name: update + object: samples/psp-host-network-ports/update.yaml + assertions: + - violations: no + - name: no-ports-specified + object: samples/psp-host-network-ports/example_allowed_no_ports.yaml + assertions: + - violations: no + - name: port-violation-exempted + object: samples/psp-host-network-ports/example_allowed_out_of_range_exempted.yaml + assertions: + - violations: no +- name: host-network-forbidden + template: template.yaml + constraint: samples/block_host_network/constraint.yaml + cases: + - name: hostnetwork-true + object: samples/psp-host-network-ports/example_allowed_no_ports_host_network_true.yaml + assertions: + - violations: yes + - name: hostnetwork-false + object: samples/psp-host-network-ports/example_allowed_no_ports_host_network_false.yaml + assertions: + - violations: no +- name: port-range-with-host-network-forbidden + template: template.yaml + constraint: samples/port_range_block_host_network/constraint.yaml + cases: + - name: out-of-range-and-host-network-true + object: samples/port_range_block_host_network/example_disallowed_out_of_range_host_network_true.yaml + assertions: + - violations: yes + - name: exempted-image-still-violates-on-hostnetwork + object: samples/port_range_block_host_network/example_disallowed_exempted_container_host_network_enabled.yaml + assertions: + - violations: yes + - name: in-range-host-network-false + object: samples/psp-host-network-ports/example_allowed_in_range.yaml + assertions: + - violations: no + - name: disallowed-ephemeral + object: samples/psp-host-network-ports/disallowed_ephemeral.yaml + assertions: + - violations: yes + - name: update + object: samples/psp-host-network-ports/update.yaml + assertions: + - violations: no diff --git a/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/template.yaml b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/template.yaml new file mode 100644 index 000000000..c310ffcbb --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-network-ports/1.1.4/template.yaml @@ -0,0 +1,166 @@ +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: k8spsphostnetworkingports + annotations: + metadata.gatekeeper.sh/title: "Host Networking Ports" + metadata.gatekeeper.sh/version: 1.1.4 + description: >- + Controls usage of host network namespace by pod containers. HostNetwork verification happens without exception for exemptImages. Specific + ports must be specified. Corresponds to the `hostNetwork` and + `hostPorts` fields in a PodSecurityPolicy. For more information, see + https://kubernetes.io/docs/concepts/policy/pod-security-policy/#host-namespaces +spec: + crd: + spec: + names: + kind: K8sPSPHostNetworkingPorts + validation: + # Schema for the `parameters` field + openAPIV3Schema: + type: object + description: >- + Controls usage of host network namespace by pod containers. HostNetwork verification happens without exception for exemptImages. Specific + ports must be specified. Corresponds to the `hostNetwork` and + `hostPorts` fields in a PodSecurityPolicy. For more information, see + https://kubernetes.io/docs/concepts/policy/pod-security-policy/#host-namespaces + 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 + hostNetwork: + description: "Determines if the policy allows the use of HostNetwork in the pod spec." + type: boolean + min: + description: "The start of the allowed port range, inclusive." + type: integer + max: + description: "The end of the allowed port range, inclusive." + type: integer + targets: + - target: admission.k8s.gatekeeper.sh + code: + - engine: K8sNativeValidation + source: + variables: + - name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' + - name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' + - name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' + - 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)) + ).map(container, container.image) + - name: badContainers + expression: | + (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, + !(container.image in variables.exemptImages) && has(container.ports) && + ( + (container.ports.all(port, has(port.hostPort) && has(variables.params.min) && port.hostPort < variables.params.min)) || + (container.ports.all(port, has(port.hostPort) && has(variables.params.max) && port.hostPort > variables.params.max)) + ) + ) + - name: isUpdate + expression: has(request.operation) && request.operation == "UPDATE" + - name: hostNetworkAllowed + expression: has(variables.params.hostNetwork) && variables.params.hostNetwork + - name: hostNetworkEnabled + expression: has(variables.anyObject.spec.hostNetwork) && variables.anyObject.spec.hostNetwork + - name: hostNetworkViolation + expression: variables.hostNetworkEnabled && !variables.hostNetworkAllowed + validations: + - expression: 'variables.isUpdate || size(variables.badContainers) == 0' + messageExpression: '"The specified hostNetwork and hostPort are not allowed, pod: " + variables.anyObject.metadata.name' + - expression: variables.isUpdate || !variables.hostNetworkViolation + messageExpression: '"The specified hostNetwork and hostPort are not allowed, pod: " + variables.anyObject.metadata.name' + - engine: Rego + source: + rego: | + package k8spsphostnetworkingports + + import data.lib.exclude_update.is_update + import data.lib.exempt_container.is_exempt + + violation[{"msg": msg, "details": {}}] { + # spec.hostNetwork field is immutable. + not is_update(input.review) + + input_share_hostnetwork(input.review.object) + msg := sprintf("The specified hostNetwork and hostPort are not allowed, pod: %v. Allowed values: %v", [input.review.object.metadata.name, input.parameters]) + } + + input_share_hostnetwork(o) { + not input.parameters.hostNetwork + o.spec.hostNetwork + } + + input_share_hostnetwork(_) { + hostPort := input_containers[_].ports[_].hostPort + hostPort < input.parameters.min + } + + input_share_hostnetwork(_) { + hostPort := input_containers[_].ports[_].hostPort + hostPort > input.parameters.max + } + + input_containers[c] { + c := input.review.object.spec.containers[_] + not is_exempt(c) + } + + input_containers[c] { + c := input.review.object.spec.initContainers[_] + not is_exempt(c) + } + + input_containers[c] { + c := input.review.object.spec.ephemeralContainers[_] + not is_exempt(c) + } + 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) + } diff --git a/library/pod-security-policy/host-network-ports/samples/port_range_block_host_network/constraint.yaml b/library/pod-security-policy/host-network-ports/samples/port_range_block_host_network/constraint.yaml index 46e16454c..c315f9e23 100644 --- a/library/pod-security-policy/host-network-ports/samples/port_range_block_host_network/constraint.yaml +++ b/library/pod-security-policy/host-network-ports/samples/port_range_block_host_network/constraint.yaml @@ -11,3 +11,5 @@ spec: hostNetwork: false min: 80 max: 9000 + exemptImages: + - "safeimages.com/*" diff --git a/library/pod-security-policy/host-network-ports/samples/port_range_block_host_network/example_disallowed_exempted_container_host_network_enabled.yaml b/library/pod-security-policy/host-network-ports/samples/port_range_block_host_network/example_disallowed_exempted_container_host_network_enabled.yaml new file mode 100644 index 000000000..0056f9b1d --- /dev/null +++ b/library/pod-security-policy/host-network-ports/samples/port_range_block_host_network/example_disallowed_exempted_container_host_network_enabled.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-networking-hn-ok-bad-port + labels: + app: nginx-host-networking-ports +spec: + hostNetwork: true + containers: + - name: nginx + image: safeimages.com/nginx + ports: + - containerPort: 9001 + hostPort: 9001 diff --git a/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/constraint.yaml b/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/constraint.yaml index aba7c24e7..b6176404c 100644 --- a/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/constraint.yaml +++ b/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/constraint.yaml @@ -10,4 +10,6 @@ spec: parameters: hostNetwork: true min: 80 - max: 9000 \ No newline at end of file + max: 9000 + exemptImages: + - "safeimages.com/*" diff --git a/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed_out_of_range_exempted.yaml b/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed_out_of_range_exempted.yaml new file mode 100644 index 000000000..e4d013447 --- /dev/null +++ b/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed_out_of_range_exempted.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-networking-ports-exempted + labels: + app: nginx-host-networking-ports +spec: + hostNetwork: true + containers: + - name: nginx + image: safeimages.com/nginx + ports: + - containerPort: 9001 + hostPort: 9001 diff --git a/library/pod-security-policy/host-network-ports/suite.yaml b/library/pod-security-policy/host-network-ports/suite.yaml index 44bbab1be..8879f7fc9 100644 --- a/library/pod-security-policy/host-network-ports/suite.yaml +++ b/library/pod-security-policy/host-network-ports/suite.yaml @@ -27,6 +27,10 @@ tests: object: samples/psp-host-network-ports/example_allowed_no_ports.yaml assertions: - violations: no + - name: port-violation-exempted + object: samples/psp-host-network-ports/example_allowed_out_of_range_exempted.yaml + assertions: + - violations: no - name: host-network-forbidden template: template.yaml constraint: samples/block_host_network/constraint.yaml @@ -47,6 +51,10 @@ tests: object: samples/port_range_block_host_network/example_disallowed_out_of_range_host_network_true.yaml assertions: - violations: yes + - name: exempted-image-still-violates-on-hostnetwork + object: samples/port_range_block_host_network/example_disallowed_exempted_container_host_network_enabled.yaml + assertions: + - violations: yes - name: in-range-host-network-false object: samples/psp-host-network-ports/example_allowed_in_range.yaml assertions: @@ -59,4 +67,3 @@ tests: object: samples/psp-host-network-ports/update.yaml assertions: - violations: no - diff --git a/library/pod-security-policy/host-network-ports/template.yaml b/library/pod-security-policy/host-network-ports/template.yaml index 036a3e045..c310ffcbb 100644 --- a/library/pod-security-policy/host-network-ports/template.yaml +++ b/library/pod-security-policy/host-network-ports/template.yaml @@ -4,7 +4,7 @@ metadata: name: k8spsphostnetworkingports annotations: metadata.gatekeeper.sh/title: "Host Networking Ports" - metadata.gatekeeper.sh/version: 1.1.3 + metadata.gatekeeper.sh/version: 1.1.4 description: >- Controls usage of host network namespace by pod containers. HostNetwork verification happens without exception for exemptImages. Specific ports must be specified. Corresponds to the `hostNetwork` and @@ -68,7 +68,8 @@ spec: expression: | (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, container.image in variables.exemptImageExplicit || - variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption))) + variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption)) + ).map(container, container.image) - name: badContainers expression: | (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, diff --git a/src/pod-security-policy/host-network-ports/constraint.tmpl b/src/pod-security-policy/host-network-ports/constraint.tmpl index d5f714682..51637ed2b 100644 --- a/src/pod-security-policy/host-network-ports/constraint.tmpl +++ b/src/pod-security-policy/host-network-ports/constraint.tmpl @@ -4,7 +4,7 @@ metadata: name: k8spsphostnetworkingports annotations: metadata.gatekeeper.sh/title: "Host Networking Ports" - metadata.gatekeeper.sh/version: 1.1.3 + metadata.gatekeeper.sh/version: 1.1.4 description: >- Controls usage of host network namespace by pod containers. HostNetwork verification happens without exception for exemptImages. Specific ports must be specified. Corresponds to the `hostNetwork` and diff --git a/src/pod-security-policy/host-network-ports/src.cel b/src/pod-security-policy/host-network-ports/src.cel index 8d0fe507e..a78f54985 100644 --- a/src/pod-security-policy/host-network-ports/src.cel +++ b/src/pod-security-policy/host-network-ports/src.cel @@ -17,7 +17,8 @@ variables: expression: | (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, container.image in variables.exemptImageExplicit || - variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption))) + variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption)) + ).map(container, container.image) - name: badContainers expression: | (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, diff --git a/website/docs/validation/host-network-ports.md b/website/docs/validation/host-network-ports.md index 549b37380..8dd053f9b 100644 --- a/website/docs/validation/host-network-ports.md +++ b/website/docs/validation/host-network-ports.md @@ -16,7 +16,7 @@ metadata: name: k8spsphostnetworkingports annotations: metadata.gatekeeper.sh/title: "Host Networking Ports" - metadata.gatekeeper.sh/version: 1.1.3 + metadata.gatekeeper.sh/version: 1.1.4 description: >- Controls usage of host network namespace by pod containers. HostNetwork verification happens without exception for exemptImages. Specific ports must be specified. Corresponds to the `hostNetwork` and @@ -80,7 +80,8 @@ spec: expression: | (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, container.image in variables.exemptImageExplicit || - variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption))) + variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption)) + ).map(container, container.image) - name: badContainers expression: | (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, @@ -203,6 +204,9 @@ spec: hostNetwork: true min: 80 max: 9000 + exemptImages: + - "safeimages.com/*" + ``` Usage @@ -320,6 +324,34 @@ Usage kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed_no_ports.yaml ``` +
+
+port-violation-exempted + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-networking-ports-exempted + labels: + app: nginx-host-networking-ports +spec: + hostNetwork: true + containers: + - name: nginx + image: safeimages.com/nginx + ports: + - containerPort: 9001 + hostPort: 9001 + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/host-network-ports/samples/psp-host-network-ports/example_allowed_out_of_range_exempted.yaml +``` +
@@ -420,6 +452,8 @@ spec: hostNetwork: false min: 80 max: 9000 + exemptImages: + - "safeimages.com/*" ``` @@ -458,6 +492,34 @@ Usage kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/host-network-ports/samples/port_range_block_host_network/example_disallowed_out_of_range_host_network_true.yaml ``` + +
+exempted-image-still-violates-on-hostnetwork + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-networking-hn-ok-bad-port + labels: + app: nginx-host-networking-ports +spec: + hostNetwork: true + containers: + - name: nginx + image: safeimages.com/nginx + ports: + - containerPort: 9001 + hostPort: 9001 + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/host-network-ports/samples/port_range_block_host_network/example_disallowed_exempted_container_host_network_enabled.yaml +``` +
in-range-host-network-false From cd8f2ecab7402e9635a50c29476a7822324c599a Mon Sep 17 00:00:00 2001 From: Julian Katz Date: Tue, 3 Sep 2024 18:52:28 -0700 Subject: [PATCH 08/19] fix(k8spspprivilegedcontainer): exemptImages CEL bug (#591) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I recently found (#584) that some K8sNativeValidation implementations of certain templates that iterate over and exempt containers by image had a bug preventing the exemption logic from working. I've fixed that bug here by mapping from container struct to container.image string. I've also added a suite test to verify this. That case fails without the change to the CEL logic. Signed-off-by: juliankatz Co-authored-by: Sertaç Özercan <852750+sozercan@users.noreply.github.com> --- .../1.1.1/artifacthub-pkg.yml | 22 +++ .../1.1.1/kustomization.yaml | 2 + .../psp-privileged-container/constraint.yaml | 13 ++ .../disallowed_ephemeral.yaml | 12 ++ .../example_allowed.yaml | 12 ++ .../example_allowed_exempt.yaml | 12 ++ .../example_disallowed.yaml | 12 ++ .../psp-privileged-container/update.yaml | 17 +++ .../privileged-containers/1.1.1/suite.yaml | 29 ++++ .../privileged-containers/1.1.1/template.yaml | 129 ++++++++++++++++++ .../psp-privileged-container/constraint.yaml | 3 + .../example_allowed_exempt.yaml | 12 ++ .../privileged-containers/suite.yaml | 4 + .../privileged-containers/template.yaml | 9 +- .../privileged-containers/constraint.tmpl | 2 +- .../privileged-containers/src.cel | 9 +- .../docs/validation/privileged-containers.md | 38 +++++- 17 files changed, 327 insertions(+), 10 deletions(-) create mode 100644 artifacthub/library/pod-security-policy/privileged-containers/1.1.1/artifacthub-pkg.yml create mode 100644 artifacthub/library/pod-security-policy/privileged-containers/1.1.1/kustomization.yaml create mode 100644 artifacthub/library/pod-security-policy/privileged-containers/1.1.1/samples/psp-privileged-container/constraint.yaml create mode 100644 artifacthub/library/pod-security-policy/privileged-containers/1.1.1/samples/psp-privileged-container/disallowed_ephemeral.yaml create mode 100644 artifacthub/library/pod-security-policy/privileged-containers/1.1.1/samples/psp-privileged-container/example_allowed.yaml create mode 100644 artifacthub/library/pod-security-policy/privileged-containers/1.1.1/samples/psp-privileged-container/example_allowed_exempt.yaml create mode 100644 artifacthub/library/pod-security-policy/privileged-containers/1.1.1/samples/psp-privileged-container/example_disallowed.yaml create mode 100644 artifacthub/library/pod-security-policy/privileged-containers/1.1.1/samples/psp-privileged-container/update.yaml create mode 100644 artifacthub/library/pod-security-policy/privileged-containers/1.1.1/suite.yaml create mode 100644 artifacthub/library/pod-security-policy/privileged-containers/1.1.1/template.yaml create mode 100644 library/pod-security-policy/privileged-containers/samples/psp-privileged-container/example_allowed_exempt.yaml diff --git a/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/artifacthub-pkg.yml b/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/artifacthub-pkg.yml new file mode 100644 index 000000000..5f2dffd89 --- /dev/null +++ b/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/artifacthub-pkg.yml @@ -0,0 +1,22 @@ +version: 1.1.1 +name: k8spspprivilegedcontainer +displayName: Privileged Container +createdAt: "2024-08-30T22:14:08Z" +description: Controls the ability of any container to enable privileged mode. Corresponds to the `privileged` field in a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#privileged +digest: 5d9b2b840bb1f530d3e66cb44d4ab170e7d4b7895d722a51999134a032b61c6f +license: Apache-2.0 +homeURL: https://open-policy-agent.github.io/gatekeeper-library/website/privileged-containers +keywords: + - gatekeeper + - open-policy-agent + - policies +readme: |- + # Privileged Container + Controls the ability of any container to enable privileged mode. Corresponds to the `privileged` field in a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#privileged +install: |- + ### Usage + ```shell + kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/template.yaml + ``` +provider: + name: Gatekeeper Library diff --git a/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/kustomization.yaml b/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/kustomization.yaml new file mode 100644 index 000000000..7d70d11b7 --- /dev/null +++ b/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - template.yaml diff --git a/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/samples/psp-privileged-container/constraint.yaml b/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/samples/psp-privileged-container/constraint.yaml new file mode 100644 index 000000000..bf2fe519d --- /dev/null +++ b/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/samples/psp-privileged-container/constraint.yaml @@ -0,0 +1,13 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPPrivilegedContainer +metadata: + name: psp-privileged-container +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + excludedNamespaces: ["kube-system"] + parameters: + exemptImages: + - "safeimages.com/*" diff --git a/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/samples/psp-privileged-container/disallowed_ephemeral.yaml b/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/samples/psp-privileged-container/disallowed_ephemeral.yaml new file mode 100644 index 000000000..e8c8b9945 --- /dev/null +++ b/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/samples/psp-privileged-container/disallowed_ephemeral.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-privileged-disallowed + labels: + app: nginx-privileged +spec: + ephemeralContainers: + - name: nginx + image: nginx + securityContext: + privileged: true diff --git a/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/samples/psp-privileged-container/example_allowed.yaml b/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/samples/psp-privileged-container/example_allowed.yaml new file mode 100644 index 000000000..bb65a2c0e --- /dev/null +++ b/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/samples/psp-privileged-container/example_allowed.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-privileged-allowed + labels: + app: nginx-privileged +spec: + containers: + - name: nginx + image: nginx + securityContext: + privileged: false diff --git a/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/samples/psp-privileged-container/example_allowed_exempt.yaml b/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/samples/psp-privileged-container/example_allowed_exempt.yaml new file mode 100644 index 000000000..782a6036a --- /dev/null +++ b/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/samples/psp-privileged-container/example_allowed_exempt.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-privileged-allowed-exempt + labels: + app: nginx-privileged +spec: + containers: + - name: nginx + image: safeimages.com/nginx + securityContext: + privileged: true diff --git a/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/samples/psp-privileged-container/example_disallowed.yaml b/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/samples/psp-privileged-container/example_disallowed.yaml new file mode 100644 index 000000000..936a24f8e --- /dev/null +++ b/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/samples/psp-privileged-container/example_disallowed.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-privileged-disallowed + labels: + app: nginx-privileged +spec: + containers: + - name: nginx + image: nginx + securityContext: + privileged: true diff --git a/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/samples/psp-privileged-container/update.yaml b/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/samples/psp-privileged-container/update.yaml new file mode 100644 index 000000000..08f36044c --- /dev/null +++ b/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/samples/psp-privileged-container/update.yaml @@ -0,0 +1,17 @@ +kind: AdmissionReview +apiVersion: admission.k8s.io/v1beta1 +request: + operation: "UPDATE" + object: + apiVersion: v1 + kind: Pod + metadata: + name: nginx-privileged-disallowed + labels: + app: nginx-privileged + spec: + containers: + - name: nginx + image: nginx + securityContext: + privileged: true diff --git a/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/suite.yaml b/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/suite.yaml new file mode 100644 index 000000000..3dd4ff863 --- /dev/null +++ b/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/suite.yaml @@ -0,0 +1,29 @@ +kind: Suite +apiVersion: test.gatekeeper.sh/v1alpha1 +metadata: + name: privileged-containers +tests: +- name: privileged-containers-disallowed + template: template.yaml + constraint: samples/psp-privileged-container/constraint.yaml + cases: + - name: example-disallowed + object: samples/psp-privileged-container/example_disallowed.yaml + assertions: + - violations: yes + - name: example-allowed + object: samples/psp-privileged-container/example_allowed.yaml + assertions: + - violations: no + - name: disallowed-ephemeral + object: samples/psp-privileged-container/disallowed_ephemeral.yaml + assertions: + - violations: yes + - name: update + object: samples/psp-privileged-container/update.yaml + assertions: + - violations: no + - name: exempted-image + object: samples/psp-privileged-container/example_allowed_exempt.yaml + assertions: + - violations: no diff --git a/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/template.yaml b/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/template.yaml new file mode 100644 index 000000000..c5a09674c --- /dev/null +++ b/artifacthub/library/pod-security-policy/privileged-containers/1.1.1/template.yaml @@ -0,0 +1,129 @@ +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: k8spspprivilegedcontainer + annotations: + metadata.gatekeeper.sh/title: "Privileged Container" + metadata.gatekeeper.sh/version: 1.1.1 + description: >- + Controls the ability of any container to enable privileged mode. + Corresponds to the `privileged` field in a PodSecurityPolicy. For more + information, see + https://kubernetes.io/docs/concepts/policy/pod-security-policy/#privileged +spec: + crd: + spec: + names: + kind: K8sPSPPrivilegedContainer + validation: + openAPIV3Schema: + type: object + description: >- + Controls the ability of any container to enable privileged mode. + Corresponds to the `privileged` field in a PodSecurityPolicy. For more + information, see + https://kubernetes.io/docs/concepts/policy/pod-security-policy/#privileged + 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 + targets: + - target: admission.k8s.gatekeeper.sh + code: + - engine: K8sNativeValidation + source: + variables: + - name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' + - name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' + - name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' + - 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)) + ).map(container, container.image) + - name: badContainers + expression: | + (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, + !(container.image in variables.exemptImages) && + (has(container.securityContext) && has(container.securityContext.privileged) && container.securityContext.privileged == true) + ).map(container, "Privileged container is not allowed: " + container.name +", securityContext: " + container.securityContext) + - name: isUpdate + expression: has(request.operation) && request.operation == "UPDATE" + validations: + - expression: variables.isUpdate || size(variables.badContainers) == 0 + messageExpression: 'variables.badContainers.join("\n")' + - engine: Rego + source: + rego: | + package k8spspprivileged + + import data.lib.exclude_update.is_update + import data.lib.exempt_container.is_exempt + + violation[{"msg": msg, "details": {}}] { + # spec.containers.privileged field is immutable. + not is_update(input.review) + + c := input_containers[_] + not is_exempt(c) + c.securityContext.privileged + msg := sprintf("Privileged container is not allowed: %v, securityContext: %v", [c.name, c.securityContext]) + } + + input_containers[c] { + c := input.review.object.spec.containers[_] + } + + input_containers[c] { + c := input.review.object.spec.initContainers[_] + } + + input_containers[c] { + c := input.review.object.spec.ephemeralContainers[_] + } + 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) + } diff --git a/library/pod-security-policy/privileged-containers/samples/psp-privileged-container/constraint.yaml b/library/pod-security-policy/privileged-containers/samples/psp-privileged-container/constraint.yaml index b246b244a..bf2fe519d 100644 --- a/library/pod-security-policy/privileged-containers/samples/psp-privileged-container/constraint.yaml +++ b/library/pod-security-policy/privileged-containers/samples/psp-privileged-container/constraint.yaml @@ -8,3 +8,6 @@ spec: - apiGroups: [""] kinds: ["Pod"] excludedNamespaces: ["kube-system"] + parameters: + exemptImages: + - "safeimages.com/*" diff --git a/library/pod-security-policy/privileged-containers/samples/psp-privileged-container/example_allowed_exempt.yaml b/library/pod-security-policy/privileged-containers/samples/psp-privileged-container/example_allowed_exempt.yaml new file mode 100644 index 000000000..782a6036a --- /dev/null +++ b/library/pod-security-policy/privileged-containers/samples/psp-privileged-container/example_allowed_exempt.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-privileged-allowed-exempt + labels: + app: nginx-privileged +spec: + containers: + - name: nginx + image: safeimages.com/nginx + securityContext: + privileged: true diff --git a/library/pod-security-policy/privileged-containers/suite.yaml b/library/pod-security-policy/privileged-containers/suite.yaml index c2e484fc5..3dd4ff863 100644 --- a/library/pod-security-policy/privileged-containers/suite.yaml +++ b/library/pod-security-policy/privileged-containers/suite.yaml @@ -23,3 +23,7 @@ tests: object: samples/psp-privileged-container/update.yaml assertions: - violations: no + - name: exempted-image + object: samples/psp-privileged-container/example_allowed_exempt.yaml + assertions: + - violations: no diff --git a/library/pod-security-policy/privileged-containers/template.yaml b/library/pod-security-policy/privileged-containers/template.yaml index bd8452e67..c5a09674c 100644 --- a/library/pod-security-policy/privileged-containers/template.yaml +++ b/library/pod-security-policy/privileged-containers/template.yaml @@ -4,7 +4,7 @@ metadata: name: k8spspprivilegedcontainer annotations: metadata.gatekeeper.sh/title: "Privileged Container" - metadata.gatekeeper.sh/version: 1.1.0 + metadata.gatekeeper.sh/version: 1.1.1 description: >- Controls the ability of any container to enable privileged mode. Corresponds to the `privileged` field in a PodSecurityPolicy. For more @@ -58,15 +58,18 @@ spec: expression: | (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, container.image in variables.exemptImageExplicit || - variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption))) + variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption)) + ).map(container, container.image) - name: badContainers expression: | (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, !(container.image in variables.exemptImages) && (has(container.securityContext) && has(container.securityContext.privileged) && container.securityContext.privileged == true) ).map(container, "Privileged container is not allowed: " + container.name +", securityContext: " + container.securityContext) + - name: isUpdate + expression: has(request.operation) && request.operation == "UPDATE" validations: - - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.badContainers) == 0' + - expression: variables.isUpdate || size(variables.badContainers) == 0 messageExpression: 'variables.badContainers.join("\n")' - engine: Rego source: diff --git a/src/pod-security-policy/privileged-containers/constraint.tmpl b/src/pod-security-policy/privileged-containers/constraint.tmpl index d5c87bb9d..4711137dd 100644 --- a/src/pod-security-policy/privileged-containers/constraint.tmpl +++ b/src/pod-security-policy/privileged-containers/constraint.tmpl @@ -4,7 +4,7 @@ metadata: name: k8spspprivilegedcontainer annotations: metadata.gatekeeper.sh/title: "Privileged Container" - metadata.gatekeeper.sh/version: 1.1.0 + metadata.gatekeeper.sh/version: 1.1.1 description: >- Controls the ability of any container to enable privileged mode. Corresponds to the `privileged` field in a PodSecurityPolicy. For more diff --git a/src/pod-security-policy/privileged-containers/src.cel b/src/pod-security-policy/privileged-containers/src.cel index 99c7b4a45..b0c77fbc4 100644 --- a/src/pod-security-policy/privileged-containers/src.cel +++ b/src/pod-security-policy/privileged-containers/src.cel @@ -17,13 +17,16 @@ variables: expression: | (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, container.image in variables.exemptImageExplicit || - variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption))) + variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption)) + ).map(container, container.image) - name: badContainers expression: | (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, !(container.image in variables.exemptImages) && (has(container.securityContext) && has(container.securityContext.privileged) && container.securityContext.privileged == true) ).map(container, "Privileged container is not allowed: " + container.name +", securityContext: " + container.securityContext) +- name: isUpdate + expression: has(request.operation) && request.operation == "UPDATE" validations: -- expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.badContainers) == 0' - messageExpression: 'variables.badContainers.join("\n")' \ No newline at end of file +- expression: variables.isUpdate || size(variables.badContainers) == 0 + messageExpression: 'variables.badContainers.join("\n")' diff --git a/website/docs/validation/privileged-containers.md b/website/docs/validation/privileged-containers.md index bf0d8717f..8354c1976 100644 --- a/website/docs/validation/privileged-containers.md +++ b/website/docs/validation/privileged-containers.md @@ -16,7 +16,7 @@ metadata: name: k8spspprivilegedcontainer annotations: metadata.gatekeeper.sh/title: "Privileged Container" - metadata.gatekeeper.sh/version: 1.1.0 + metadata.gatekeeper.sh/version: 1.1.1 description: >- Controls the ability of any container to enable privileged mode. Corresponds to the `privileged` field in a PodSecurityPolicy. For more @@ -70,15 +70,18 @@ spec: expression: | (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, container.image in variables.exemptImageExplicit || - variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption))) + variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption)) + ).map(container, container.image) - name: badContainers expression: | (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, !(container.image in variables.exemptImages) && (has(container.securityContext) && has(container.securityContext.privileged) && container.securityContext.privileged == true) ).map(container, "Privileged container is not allowed: " + container.name +", securityContext: " + container.securityContext) + - name: isUpdate + expression: has(request.operation) && request.operation == "UPDATE" validations: - - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.badContainers) == 0' + - expression: variables.isUpdate || size(variables.badContainers) == 0 messageExpression: 'variables.badContainers.join("\n")' - engine: Rego source: @@ -161,6 +164,9 @@ spec: - apiGroups: [""] kinds: ["Pod"] excludedNamespaces: ["kube-system"] + parameters: + exemptImages: + - "safeimages.com/*" ``` @@ -249,6 +255,32 @@ Usage kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/privileged-containers/samples/psp-privileged-container/disallowed_ephemeral.yaml ``` +
+
+exempted-image + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-privileged-allowed-exempt + labels: + app: nginx-privileged +spec: + containers: + - name: nginx + image: safeimages.com/nginx + securityContext: + privileged: true + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/privileged-containers/samples/psp-privileged-container/example_allowed_exempt.yaml +``` +
From 7983a1d1706746efe27813555fd7a05938f53482 Mon Sep 17 00:00:00 2001 From: Jaydipkumar Arvindbhai Gabani Date: Tue, 3 Sep 2024 18:52:53 -0700 Subject: [PATCH 09/19] chore: adding CEL for psp-host-filesystem (#547) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: adding CEL for psp-host-filesystem Signed-off-by: Jaydip Gabani * updating cel, updating labels on example Signed-off-by: Jaydip Gabani * removing blank lines Signed-off-by: Jaydip Gabani * fixing CEL error Signed-off-by: Jaydip Gabani --------- Signed-off-by: Jaydip Gabani Co-authored-by: Sertaç Özercan <852750+sozercan@users.noreply.github.com> --- .../disallowed_ephemeral.yaml | 2 - .../psp-host-filesystem/example_allowed.yaml | 2 - .../example_disallowed.yaml | 2 - .../disallowed_ephemeral.yaml | 2 - .../psp-host-filesystem/example_allowed.yaml | 2 - .../example_disallowed.yaml | 2 - .../disallowed_ephemeral.yaml | 2 - .../psp-host-filesystem/example_allowed.yaml | 2 - .../example_disallowed.yaml | 2 - .../host-filesystem/1.1.0/artifacthub-pkg.yml | 22 ++ .../host-filesystem/1.1.0/kustomization.yaml | 2 + .../psp-host-filesystem/constraint.yaml | 13 + .../disallowed_ephemeral.yaml | 16 ++ .../psp-host-filesystem/example_allowed.yaml | 16 ++ .../example_disallowed.yaml | 16 ++ .../samples/psp-host-filesystem/update.yaml | 23 ++ .../host-filesystem/1.1.0/suite.yaml | 25 ++ .../host-filesystem/1.1.0/template.yaml | 181 +++++++++++++ .../disallowed_ephemeral.yaml | 2 - .../psp-host-filesystem/example_allowed.yaml | 2 - .../example_disallowed.yaml | 2 - .../host-filesystem/template.yaml | 249 +++++++++-------- .../host-filesystem/constraint.tmpl | 18 +- .../host-filesystem/src.cel | 28 ++ website/docs/validation/host-filesystem.md | 255 ++++++++++-------- 25 files changed, 636 insertions(+), 252 deletions(-) create mode 100644 artifacthub/library/pod-security-policy/host-filesystem/1.1.0/artifacthub-pkg.yml create mode 100644 artifacthub/library/pod-security-policy/host-filesystem/1.1.0/kustomization.yaml create mode 100644 artifacthub/library/pod-security-policy/host-filesystem/1.1.0/samples/psp-host-filesystem/constraint.yaml create mode 100644 artifacthub/library/pod-security-policy/host-filesystem/1.1.0/samples/psp-host-filesystem/disallowed_ephemeral.yaml create mode 100644 artifacthub/library/pod-security-policy/host-filesystem/1.1.0/samples/psp-host-filesystem/example_allowed.yaml create mode 100644 artifacthub/library/pod-security-policy/host-filesystem/1.1.0/samples/psp-host-filesystem/example_disallowed.yaml create mode 100644 artifacthub/library/pod-security-policy/host-filesystem/1.1.0/samples/psp-host-filesystem/update.yaml create mode 100644 artifacthub/library/pod-security-policy/host-filesystem/1.1.0/suite.yaml create mode 100644 artifacthub/library/pod-security-policy/host-filesystem/1.1.0/template.yaml create mode 100644 src/pod-security-policy/host-filesystem/src.cel diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.0.0/samples/psp-host-filesystem/disallowed_ephemeral.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.0.0/samples/psp-host-filesystem/disallowed_ephemeral.yaml index beece55c0..7d18cc85e 100644 --- a/artifacthub/library/pod-security-policy/host-filesystem/1.0.0/samples/psp-host-filesystem/disallowed_ephemeral.yaml +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.0.0/samples/psp-host-filesystem/disallowed_ephemeral.yaml @@ -2,8 +2,6 @@ apiVersion: v1 kind: Pod metadata: name: nginx-host-filesystem - labels: - app: nginx-host-filesystem-disallowed spec: ephemeralContainers: - name: nginx diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.0.0/samples/psp-host-filesystem/example_allowed.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.0.0/samples/psp-host-filesystem/example_allowed.yaml index abc60d882..806101e8c 100644 --- a/artifacthub/library/pod-security-policy/host-filesystem/1.0.0/samples/psp-host-filesystem/example_allowed.yaml +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.0.0/samples/psp-host-filesystem/example_allowed.yaml @@ -2,8 +2,6 @@ apiVersion: v1 kind: Pod metadata: name: nginx-host-filesystem - labels: - app: nginx-host-filesystem-disallowed spec: containers: - name: nginx diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.0.0/samples/psp-host-filesystem/example_disallowed.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.0.0/samples/psp-host-filesystem/example_disallowed.yaml index 53107694f..51ecf14b5 100644 --- a/artifacthub/library/pod-security-policy/host-filesystem/1.0.0/samples/psp-host-filesystem/example_disallowed.yaml +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.0.0/samples/psp-host-filesystem/example_disallowed.yaml @@ -2,8 +2,6 @@ apiVersion: v1 kind: Pod metadata: name: nginx-host-filesystem - labels: - app: nginx-host-filesystem-disallowed spec: containers: - name: nginx diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.0.1/samples/psp-host-filesystem/disallowed_ephemeral.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.0.1/samples/psp-host-filesystem/disallowed_ephemeral.yaml index beece55c0..7d18cc85e 100644 --- a/artifacthub/library/pod-security-policy/host-filesystem/1.0.1/samples/psp-host-filesystem/disallowed_ephemeral.yaml +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.0.1/samples/psp-host-filesystem/disallowed_ephemeral.yaml @@ -2,8 +2,6 @@ apiVersion: v1 kind: Pod metadata: name: nginx-host-filesystem - labels: - app: nginx-host-filesystem-disallowed spec: ephemeralContainers: - name: nginx diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.0.1/samples/psp-host-filesystem/example_allowed.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.0.1/samples/psp-host-filesystem/example_allowed.yaml index abc60d882..806101e8c 100644 --- a/artifacthub/library/pod-security-policy/host-filesystem/1.0.1/samples/psp-host-filesystem/example_allowed.yaml +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.0.1/samples/psp-host-filesystem/example_allowed.yaml @@ -2,8 +2,6 @@ apiVersion: v1 kind: Pod metadata: name: nginx-host-filesystem - labels: - app: nginx-host-filesystem-disallowed spec: containers: - name: nginx diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.0.1/samples/psp-host-filesystem/example_disallowed.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.0.1/samples/psp-host-filesystem/example_disallowed.yaml index 53107694f..51ecf14b5 100644 --- a/artifacthub/library/pod-security-policy/host-filesystem/1.0.1/samples/psp-host-filesystem/example_disallowed.yaml +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.0.1/samples/psp-host-filesystem/example_disallowed.yaml @@ -2,8 +2,6 @@ apiVersion: v1 kind: Pod metadata: name: nginx-host-filesystem - labels: - app: nginx-host-filesystem-disallowed spec: containers: - name: nginx diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.0.2/samples/psp-host-filesystem/disallowed_ephemeral.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.0.2/samples/psp-host-filesystem/disallowed_ephemeral.yaml index beece55c0..7d18cc85e 100644 --- a/artifacthub/library/pod-security-policy/host-filesystem/1.0.2/samples/psp-host-filesystem/disallowed_ephemeral.yaml +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.0.2/samples/psp-host-filesystem/disallowed_ephemeral.yaml @@ -2,8 +2,6 @@ apiVersion: v1 kind: Pod metadata: name: nginx-host-filesystem - labels: - app: nginx-host-filesystem-disallowed spec: ephemeralContainers: - name: nginx diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.0.2/samples/psp-host-filesystem/example_allowed.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.0.2/samples/psp-host-filesystem/example_allowed.yaml index abc60d882..806101e8c 100644 --- a/artifacthub/library/pod-security-policy/host-filesystem/1.0.2/samples/psp-host-filesystem/example_allowed.yaml +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.0.2/samples/psp-host-filesystem/example_allowed.yaml @@ -2,8 +2,6 @@ apiVersion: v1 kind: Pod metadata: name: nginx-host-filesystem - labels: - app: nginx-host-filesystem-disallowed spec: containers: - name: nginx diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.0.2/samples/psp-host-filesystem/example_disallowed.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.0.2/samples/psp-host-filesystem/example_disallowed.yaml index 53107694f..51ecf14b5 100644 --- a/artifacthub/library/pod-security-policy/host-filesystem/1.0.2/samples/psp-host-filesystem/example_disallowed.yaml +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.0.2/samples/psp-host-filesystem/example_disallowed.yaml @@ -2,8 +2,6 @@ apiVersion: v1 kind: Pod metadata: name: nginx-host-filesystem - labels: - app: nginx-host-filesystem-disallowed spec: containers: - name: nginx diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/artifacthub-pkg.yml b/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/artifacthub-pkg.yml new file mode 100644 index 000000000..7fe2cbdb6 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/artifacthub-pkg.yml @@ -0,0 +1,22 @@ +version: 1.1.0 +name: k8spsphostfilesystem +displayName: Host Filesystem +createdAt: "2024-06-05T20:31:16Z" +description: Controls usage of the host filesystem. Corresponds to the `allowedHostPaths` field in a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#volumes-and-file-systems +digest: 3883941aac9b7598a79c9bf6517c77a401b99d165bf584dce604a00eef79f8e7 +license: Apache-2.0 +homeURL: https://open-policy-agent.github.io/gatekeeper-library/website/host-filesystem +keywords: + - gatekeeper + - open-policy-agent + - policies +readme: |- + # Host Filesystem + Controls usage of the host filesystem. Corresponds to the `allowedHostPaths` field in a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#volumes-and-file-systems +install: |- + ### Usage + ```shell + kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/template.yaml + ``` +provider: + name: Gatekeeper Library diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/kustomization.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/kustomization.yaml new file mode 100644 index 000000000..7d70d11b7 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - template.yaml diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/samples/psp-host-filesystem/constraint.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/samples/psp-host-filesystem/constraint.yaml new file mode 100644 index 000000000..7cbd7b824 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/samples/psp-host-filesystem/constraint.yaml @@ -0,0 +1,13 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPHostFilesystem +metadata: + name: psp-host-filesystem +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + allowedHostPaths: + - readOnly: true + pathPrefix: "/foo" diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/samples/psp-host-filesystem/disallowed_ephemeral.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/samples/psp-host-filesystem/disallowed_ephemeral.yaml new file mode 100644 index 000000000..7d18cc85e --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/samples/psp-host-filesystem/disallowed_ephemeral.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-filesystem +spec: + ephemeralContainers: + - name: nginx + image: nginx + volumeMounts: + - mountPath: /cache + name: cache-volume + readOnly: true + volumes: + - name: cache-volume + hostPath: + path: /tmp # directory location on host diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/samples/psp-host-filesystem/example_allowed.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/samples/psp-host-filesystem/example_allowed.yaml new file mode 100644 index 000000000..806101e8c --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/samples/psp-host-filesystem/example_allowed.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-filesystem +spec: + containers: + - name: nginx + image: nginx + volumeMounts: + - mountPath: /cache + name: cache-volume + readOnly: true + volumes: + - name: cache-volume + hostPath: + path: /foo/bar diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/samples/psp-host-filesystem/example_disallowed.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/samples/psp-host-filesystem/example_disallowed.yaml new file mode 100644 index 000000000..51ecf14b5 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/samples/psp-host-filesystem/example_disallowed.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-filesystem +spec: + containers: + - name: nginx + image: nginx + volumeMounts: + - mountPath: /cache + name: cache-volume + readOnly: true + volumes: + - name: cache-volume + hostPath: + path: /tmp # directory location on host diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/samples/psp-host-filesystem/update.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/samples/psp-host-filesystem/update.yaml new file mode 100644 index 000000000..68b28a536 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/samples/psp-host-filesystem/update.yaml @@ -0,0 +1,23 @@ +kind: AdmissionReview +apiVersion: admission.k8s.io/v1beta1 +request: + operation: "UPDATE" + object: + apiVersion: v1 + kind: Pod + metadata: + name: nginx-host-filesystem + labels: + app: nginx-host-filesystem-disallowed + spec: + containers: + - name: nginx + image: nginx + volumeMounts: + - mountPath: /cache + name: cache-volume + readOnly: true + volumes: + - name: cache-volume + hostPath: + path: /tmp # directory location on host diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/suite.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/suite.yaml new file mode 100644 index 000000000..5441df8cc --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/suite.yaml @@ -0,0 +1,25 @@ +kind: Suite +apiVersion: test.gatekeeper.sh/v1alpha1 +metadata: + name: host-filesystem +tests: + - name: host-filesystem + template: template.yaml + constraint: samples/psp-host-filesystem/constraint.yaml + cases: + - name: example-disallowed + object: samples/psp-host-filesystem/example_disallowed.yaml + assertions: + - violations: yes + - name: example-allowed + object: samples/psp-host-filesystem/example_allowed.yaml + assertions: + - violations: no + - name: disallowed-ephemeral + object: samples/psp-host-filesystem/disallowed_ephemeral.yaml + assertions: + - violations: yes + - name: update + object: samples/psp-host-filesystem/update.yaml + assertions: + - violations: no diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/template.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/template.yaml new file mode 100644 index 000000000..6158ec1ca --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.1.0/template.yaml @@ -0,0 +1,181 @@ +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: k8spsphostfilesystem + annotations: + metadata.gatekeeper.sh/title: "Host Filesystem" + metadata.gatekeeper.sh/version: 1.1.0 + description: >- + Controls usage of the host filesystem. Corresponds to the + `allowedHostPaths` field in a PodSecurityPolicy. For more information, + see + https://kubernetes.io/docs/concepts/policy/pod-security-policy/#volumes-and-file-systems +spec: + crd: + spec: + names: + kind: K8sPSPHostFilesystem + validation: + # Schema for the `parameters` field + openAPIV3Schema: + type: object + description: >- + Controls usage of the host filesystem. Corresponds to the + `allowedHostPaths` field in a PodSecurityPolicy. For more information, + see + https://kubernetes.io/docs/concepts/policy/pod-security-policy/#volumes-and-file-systems + properties: + allowedHostPaths: + type: array + description: "An array of hostpath objects, representing paths and read/write configuration." + items: + type: object + properties: + pathPrefix: + type: string + description: "The path prefix that the host volume must match." + readOnly: + type: boolean + description: "when set to true, any container volumeMounts matching the pathPrefix must include `readOnly: true`." + targets: + - target: admission.k8s.gatekeeper.sh + code: + - engine: K8sNativeValidation + source: + variables: + - name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' + - name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' + - name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' + - name: allContainers + expression: 'variables.containers + variables.initContainers + variables.ephemeralContainers' + - name: allowedPaths + expression: | + !has(variables.params.allowedHostPaths) ? [] : variables.params.allowedHostPaths + - name: volumes + expression: | + variables.anyObject.spec.volumes.filter(volume, has(volume.hostPath)) + - name: badHostPaths + expression: | + variables.volumes.filter(volume, + (size(variables.allowedPaths) == 0) || + !(variables.allowedPaths.exists(allowedPath, + volume.hostPath.path.startsWith(allowedPath.pathPrefix) && ( + (!has(allowedPath.readOnly) || !(allowedPath.readOnly)) || + (has(allowedPath.readOnly) && allowedPath.readOnly && !variables.allContainers.exists(c, + c.volumeMounts.exists(m, m.name == volume.name && (!has(m.readOnly) || !m.readOnly))))))) + ).map(volume, "{ hostPath: { path : " + volume.hostPath.path + " }, name: " + volume.name + "}").map(volume, "HostPath volume " + volume + " is not allowed, pod: " + object.metadata.name + ". Allowed path: " + variables.allowedPaths.map(path, path.pathPrefix + ", readOnly: " + (path.readOnly ? "true" : "false") + "}").join(", ")) + validations: + - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.badHostPaths) == 0' + messageExpression: 'variables.badHostPaths.join("\n")' + - engine: Rego + source: + rego: | + package k8spsphostfilesystem + + import data.lib.exclude_update.is_update + + violation[{"msg": msg, "details": {}}] { + # spec.volumes field is immutable. + not is_update(input.review) + + volume := input_hostpath_volumes[_] + allowedPaths := get_allowed_paths(input) + input_hostpath_violation(allowedPaths, volume) + msg := sprintf("HostPath volume %v is not allowed, pod: %v. Allowed path: %v", [volume, input.review.object.metadata.name, allowedPaths]) + } + + input_hostpath_violation(allowedPaths, _) { + # An empty list means all host paths are blocked + allowedPaths == [] + } + input_hostpath_violation(allowedPaths, volume) { + not input_hostpath_allowed(allowedPaths, volume) + } + + get_allowed_paths(arg) = out { + not arg.parameters + out = [] + } + get_allowed_paths(arg) = out { + not arg.parameters.allowedHostPaths + out = [] + } + get_allowed_paths(arg) = out { + out = arg.parameters.allowedHostPaths + } + + input_hostpath_allowed(allowedPaths, volume) { + allowedHostPath := allowedPaths[_] + path_matches(allowedHostPath.pathPrefix, volume.hostPath.path) + not allowedHostPath.readOnly == true + } + + input_hostpath_allowed(allowedPaths, volume) { + allowedHostPath := allowedPaths[_] + path_matches(allowedHostPath.pathPrefix, volume.hostPath.path) + allowedHostPath.readOnly + not writeable_input_volume_mounts(volume.name) + } + + writeable_input_volume_mounts(volume_name) { + container := input_containers[_] + mount := container.volumeMounts[_] + mount.name == volume_name + not mount.readOnly + } + + # This allows "/foo", "/foo/", "/foo/bar" etc., but + # disallows "/fool", "/etc/foo" etc. + path_matches(prefix, path) { + a := path_array(prefix) + b := path_array(path) + prefix_matches(a, b) + } + path_array(p) = out { + p != "/" + out := split(trim(p, "/"), "/") + } + # This handles the special case for "/", since + # split(trim("/", "/"), "/") == [""] + path_array("/") = [] + + prefix_matches(a, b) { + count(a) <= count(b) + not any_not_equal_upto(a, b, count(a)) + } + + any_not_equal_upto(a, b, n) { + a[i] != b[i] + i < n + } + + input_hostpath_volumes[v] { + v := input.review.object.spec.volumes[_] + has_field(v, "hostPath") + } + + # has_field returns whether an object has a field + has_field(object, field) = true { + object[field] + } + input_containers[c] { + c := input.review.object.spec.containers[_] + } + + input_containers[c] { + c := input.review.object.spec.initContainers[_] + } + + input_containers[c] { + c := input.review.object.spec.ephemeralContainers[_] + } + libs: + - | + package lib.exclude_update + + is_update(review) { + review.operation == "UPDATE" + } diff --git a/library/pod-security-policy/host-filesystem/samples/psp-host-filesystem/disallowed_ephemeral.yaml b/library/pod-security-policy/host-filesystem/samples/psp-host-filesystem/disallowed_ephemeral.yaml index beece55c0..7d18cc85e 100644 --- a/library/pod-security-policy/host-filesystem/samples/psp-host-filesystem/disallowed_ephemeral.yaml +++ b/library/pod-security-policy/host-filesystem/samples/psp-host-filesystem/disallowed_ephemeral.yaml @@ -2,8 +2,6 @@ apiVersion: v1 kind: Pod metadata: name: nginx-host-filesystem - labels: - app: nginx-host-filesystem-disallowed spec: ephemeralContainers: - name: nginx diff --git a/library/pod-security-policy/host-filesystem/samples/psp-host-filesystem/example_allowed.yaml b/library/pod-security-policy/host-filesystem/samples/psp-host-filesystem/example_allowed.yaml index abc60d882..806101e8c 100644 --- a/library/pod-security-policy/host-filesystem/samples/psp-host-filesystem/example_allowed.yaml +++ b/library/pod-security-policy/host-filesystem/samples/psp-host-filesystem/example_allowed.yaml @@ -2,8 +2,6 @@ apiVersion: v1 kind: Pod metadata: name: nginx-host-filesystem - labels: - app: nginx-host-filesystem-disallowed spec: containers: - name: nginx diff --git a/library/pod-security-policy/host-filesystem/samples/psp-host-filesystem/example_disallowed.yaml b/library/pod-security-policy/host-filesystem/samples/psp-host-filesystem/example_disallowed.yaml index 53107694f..51ecf14b5 100644 --- a/library/pod-security-policy/host-filesystem/samples/psp-host-filesystem/example_disallowed.yaml +++ b/library/pod-security-policy/host-filesystem/samples/psp-host-filesystem/example_disallowed.yaml @@ -2,8 +2,6 @@ apiVersion: v1 kind: Pod metadata: name: nginx-host-filesystem - labels: - app: nginx-host-filesystem-disallowed spec: containers: - name: nginx diff --git a/library/pod-security-policy/host-filesystem/template.yaml b/library/pod-security-policy/host-filesystem/template.yaml index 5f506c351..6158ec1ca 100644 --- a/library/pod-security-policy/host-filesystem/template.yaml +++ b/library/pod-security-policy/host-filesystem/template.yaml @@ -4,7 +4,7 @@ metadata: name: k8spsphostfilesystem annotations: metadata.gatekeeper.sh/title: "Host Filesystem" - metadata.gatekeeper.sh/version: 1.0.2 + metadata.gatekeeper.sh/version: 1.1.0 description: >- Controls usage of the host filesystem. Corresponds to the `allowedHostPaths` field in a PodSecurityPolicy. For more information, @@ -39,110 +39,143 @@ spec: description: "when set to true, any container volumeMounts matching the pathPrefix must include `readOnly: true`." targets: - target: admission.k8s.gatekeeper.sh - rego: | - package k8spsphostfilesystem - - import data.lib.exclude_update.is_update - - violation[{"msg": msg, "details": {}}] { - # spec.volumes field is immutable. - not is_update(input.review) - - volume := input_hostpath_volumes[_] - allowedPaths := get_allowed_paths(input) - input_hostpath_violation(allowedPaths, volume) - msg := sprintf("HostPath volume %v is not allowed, pod: %v. Allowed path: %v", [volume, input.review.object.metadata.name, allowedPaths]) - } - - input_hostpath_violation(allowedPaths, _) { - # An empty list means all host paths are blocked - allowedPaths == [] - } - input_hostpath_violation(allowedPaths, volume) { - not input_hostpath_allowed(allowedPaths, volume) - } - - get_allowed_paths(arg) = out { - not arg.parameters - out = [] - } - get_allowed_paths(arg) = out { - not arg.parameters.allowedHostPaths - out = [] - } - get_allowed_paths(arg) = out { - out = arg.parameters.allowedHostPaths - } - - input_hostpath_allowed(allowedPaths, volume) { - allowedHostPath := allowedPaths[_] - path_matches(allowedHostPath.pathPrefix, volume.hostPath.path) - not allowedHostPath.readOnly == true - } - - input_hostpath_allowed(allowedPaths, volume) { - allowedHostPath := allowedPaths[_] - path_matches(allowedHostPath.pathPrefix, volume.hostPath.path) - allowedHostPath.readOnly - not writeable_input_volume_mounts(volume.name) - } - - writeable_input_volume_mounts(volume_name) { - container := input_containers[_] - mount := container.volumeMounts[_] - mount.name == volume_name - not mount.readOnly - } - - # This allows "/foo", "/foo/", "/foo/bar" etc., but - # disallows "/fool", "/etc/foo" etc. - path_matches(prefix, path) { - a := path_array(prefix) - b := path_array(path) - prefix_matches(a, b) - } - path_array(p) = out { - p != "/" - out := split(trim(p, "/"), "/") - } - # This handles the special case for "/", since - # split(trim("/", "/"), "/") == [""] - path_array("/") = [] - - prefix_matches(a, b) { - count(a) <= count(b) - not any_not_equal_upto(a, b, count(a)) - } - - any_not_equal_upto(a, b, n) { - a[i] != b[i] - i < n - } - - input_hostpath_volumes[v] { - v := input.review.object.spec.volumes[_] - has_field(v, "hostPath") - } - - # has_field returns whether an object has a field - has_field(object, field) = true { - object[field] - } - input_containers[c] { - c := input.review.object.spec.containers[_] - } - - input_containers[c] { - c := input.review.object.spec.initContainers[_] - } - - input_containers[c] { - c := input.review.object.spec.ephemeralContainers[_] - } - libs: - - | - package lib.exclude_update - - is_update(review) { - review.operation == "UPDATE" - } + code: + - engine: K8sNativeValidation + source: + variables: + - name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' + - name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' + - name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' + - name: allContainers + expression: 'variables.containers + variables.initContainers + variables.ephemeralContainers' + - name: allowedPaths + expression: | + !has(variables.params.allowedHostPaths) ? [] : variables.params.allowedHostPaths + - name: volumes + expression: | + variables.anyObject.spec.volumes.filter(volume, has(volume.hostPath)) + - name: badHostPaths + expression: | + variables.volumes.filter(volume, + (size(variables.allowedPaths) == 0) || + !(variables.allowedPaths.exists(allowedPath, + volume.hostPath.path.startsWith(allowedPath.pathPrefix) && ( + (!has(allowedPath.readOnly) || !(allowedPath.readOnly)) || + (has(allowedPath.readOnly) && allowedPath.readOnly && !variables.allContainers.exists(c, + c.volumeMounts.exists(m, m.name == volume.name && (!has(m.readOnly) || !m.readOnly))))))) + ).map(volume, "{ hostPath: { path : " + volume.hostPath.path + " }, name: " + volume.name + "}").map(volume, "HostPath volume " + volume + " is not allowed, pod: " + object.metadata.name + ". Allowed path: " + variables.allowedPaths.map(path, path.pathPrefix + ", readOnly: " + (path.readOnly ? "true" : "false") + "}").join(", ")) + validations: + - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.badHostPaths) == 0' + messageExpression: 'variables.badHostPaths.join("\n")' + - engine: Rego + source: + rego: | + package k8spsphostfilesystem + + import data.lib.exclude_update.is_update + + violation[{"msg": msg, "details": {}}] { + # spec.volumes field is immutable. + not is_update(input.review) + + volume := input_hostpath_volumes[_] + allowedPaths := get_allowed_paths(input) + input_hostpath_violation(allowedPaths, volume) + msg := sprintf("HostPath volume %v is not allowed, pod: %v. Allowed path: %v", [volume, input.review.object.metadata.name, allowedPaths]) + } + + input_hostpath_violation(allowedPaths, _) { + # An empty list means all host paths are blocked + allowedPaths == [] + } + input_hostpath_violation(allowedPaths, volume) { + not input_hostpath_allowed(allowedPaths, volume) + } + + get_allowed_paths(arg) = out { + not arg.parameters + out = [] + } + get_allowed_paths(arg) = out { + not arg.parameters.allowedHostPaths + out = [] + } + get_allowed_paths(arg) = out { + out = arg.parameters.allowedHostPaths + } + + input_hostpath_allowed(allowedPaths, volume) { + allowedHostPath := allowedPaths[_] + path_matches(allowedHostPath.pathPrefix, volume.hostPath.path) + not allowedHostPath.readOnly == true + } + + input_hostpath_allowed(allowedPaths, volume) { + allowedHostPath := allowedPaths[_] + path_matches(allowedHostPath.pathPrefix, volume.hostPath.path) + allowedHostPath.readOnly + not writeable_input_volume_mounts(volume.name) + } + + writeable_input_volume_mounts(volume_name) { + container := input_containers[_] + mount := container.volumeMounts[_] + mount.name == volume_name + not mount.readOnly + } + + # This allows "/foo", "/foo/", "/foo/bar" etc., but + # disallows "/fool", "/etc/foo" etc. + path_matches(prefix, path) { + a := path_array(prefix) + b := path_array(path) + prefix_matches(a, b) + } + path_array(p) = out { + p != "/" + out := split(trim(p, "/"), "/") + } + # This handles the special case for "/", since + # split(trim("/", "/"), "/") == [""] + path_array("/") = [] + + prefix_matches(a, b) { + count(a) <= count(b) + not any_not_equal_upto(a, b, count(a)) + } + + any_not_equal_upto(a, b, n) { + a[i] != b[i] + i < n + } + + input_hostpath_volumes[v] { + v := input.review.object.spec.volumes[_] + has_field(v, "hostPath") + } + + # has_field returns whether an object has a field + has_field(object, field) = true { + object[field] + } + input_containers[c] { + c := input.review.object.spec.containers[_] + } + + input_containers[c] { + c := input.review.object.spec.initContainers[_] + } + + input_containers[c] { + c := input.review.object.spec.ephemeralContainers[_] + } + libs: + - | + package lib.exclude_update + + is_update(review) { + review.operation == "UPDATE" + } diff --git a/src/pod-security-policy/host-filesystem/constraint.tmpl b/src/pod-security-policy/host-filesystem/constraint.tmpl index d442ec331..ad5b446c3 100644 --- a/src/pod-security-policy/host-filesystem/constraint.tmpl +++ b/src/pod-security-policy/host-filesystem/constraint.tmpl @@ -4,7 +4,7 @@ metadata: name: k8spsphostfilesystem annotations: metadata.gatekeeper.sh/title: "Host Filesystem" - metadata.gatekeeper.sh/version: 1.0.2 + metadata.gatekeeper.sh/version: 1.1.0 description: >- Controls usage of the host filesystem. Corresponds to the `allowedHostPaths` field in a PodSecurityPolicy. For more information, @@ -39,8 +39,14 @@ spec: description: "when set to true, any container volumeMounts matching the pathPrefix must include `readOnly: true`." targets: - target: admission.k8s.gatekeeper.sh - rego: | -{{ file.Read "src/pod-security-policy/host-filesystem/src.rego" | strings.Indent 8 | strings.TrimSuffix "\n" }} - libs: - - | -{{ file.Read "src/pod-security-policy/host-filesystem/lib_exclude_update.rego" | strings.Indent 10 | strings.TrimSuffix "\n" }} + code: + - engine: K8sNativeValidation + source: +{{ file.Read "src/pod-security-policy/host-filesystem/src.cel" | strings.Indent 10 | strings.TrimSuffix "\n" }} + - engine: Rego + source: + rego: | +{{ file.Read "src/pod-security-policy/host-filesystem/src.rego" | strings.Indent 12 | strings.TrimSuffix "\n" }} + libs: + - | +{{ file.Read "src/pod-security-policy/host-filesystem/lib_exclude_update.rego" | strings.Indent 14 | strings.TrimSuffix "\n" }} diff --git a/src/pod-security-policy/host-filesystem/src.cel b/src/pod-security-policy/host-filesystem/src.cel new file mode 100644 index 000000000..3497a3e28 --- /dev/null +++ b/src/pod-security-policy/host-filesystem/src.cel @@ -0,0 +1,28 @@ +variables: +- name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' +- name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' +- name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' +- name: allContainers + expression: 'variables.containers + variables.initContainers + variables.ephemeralContainers' +- name: allowedPaths + expression: | + !has(variables.params.allowedHostPaths) ? [] : variables.params.allowedHostPaths +- name: volumes + expression: | + variables.anyObject.spec.volumes.filter(volume, has(volume.hostPath)) +- name: badHostPaths + expression: | + variables.volumes.filter(volume, + (size(variables.allowedPaths) == 0) || + !(variables.allowedPaths.exists(allowedPath, + volume.hostPath.path.startsWith(allowedPath.pathPrefix) && ( + (!has(allowedPath.readOnly) || !(allowedPath.readOnly)) || + (has(allowedPath.readOnly) && allowedPath.readOnly && !variables.allContainers.exists(c, + c.volumeMounts.exists(m, m.name == volume.name && (!has(m.readOnly) || !m.readOnly))))))) + ).map(volume, "{ hostPath: { path : " + volume.hostPath.path + " }, name: " + volume.name + "}").map(volume, "HostPath volume " + volume + " is not allowed, pod: " + object.metadata.name + ". Allowed path: " + variables.allowedPaths.map(path, path.pathPrefix + ", readOnly: " + (path.readOnly ? "true" : "false") + "}").join(", ")) +validations: +- expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.badHostPaths) == 0' + messageExpression: 'variables.badHostPaths.join("\n")' \ No newline at end of file diff --git a/website/docs/validation/host-filesystem.md b/website/docs/validation/host-filesystem.md index 57e7d436f..07704e0bd 100644 --- a/website/docs/validation/host-filesystem.md +++ b/website/docs/validation/host-filesystem.md @@ -16,7 +16,7 @@ metadata: name: k8spsphostfilesystem annotations: metadata.gatekeeper.sh/title: "Host Filesystem" - metadata.gatekeeper.sh/version: 1.0.2 + metadata.gatekeeper.sh/version: 1.1.0 description: >- Controls usage of the host filesystem. Corresponds to the `allowedHostPaths` field in a PodSecurityPolicy. For more information, @@ -51,113 +51,146 @@ spec: description: "when set to true, any container volumeMounts matching the pathPrefix must include `readOnly: true`." targets: - target: admission.k8s.gatekeeper.sh - rego: | - package k8spsphostfilesystem - - import data.lib.exclude_update.is_update - - violation[{"msg": msg, "details": {}}] { - # spec.volumes field is immutable. - not is_update(input.review) - - volume := input_hostpath_volumes[_] - allowedPaths := get_allowed_paths(input) - input_hostpath_violation(allowedPaths, volume) - msg := sprintf("HostPath volume %v is not allowed, pod: %v. Allowed path: %v", [volume, input.review.object.metadata.name, allowedPaths]) - } - - input_hostpath_violation(allowedPaths, _) { - # An empty list means all host paths are blocked - allowedPaths == [] - } - input_hostpath_violation(allowedPaths, volume) { - not input_hostpath_allowed(allowedPaths, volume) - } - - get_allowed_paths(arg) = out { - not arg.parameters - out = [] - } - get_allowed_paths(arg) = out { - not arg.parameters.allowedHostPaths - out = [] - } - get_allowed_paths(arg) = out { - out = arg.parameters.allowedHostPaths - } - - input_hostpath_allowed(allowedPaths, volume) { - allowedHostPath := allowedPaths[_] - path_matches(allowedHostPath.pathPrefix, volume.hostPath.path) - not allowedHostPath.readOnly == true - } - - input_hostpath_allowed(allowedPaths, volume) { - allowedHostPath := allowedPaths[_] - path_matches(allowedHostPath.pathPrefix, volume.hostPath.path) - allowedHostPath.readOnly - not writeable_input_volume_mounts(volume.name) - } - - writeable_input_volume_mounts(volume_name) { - container := input_containers[_] - mount := container.volumeMounts[_] - mount.name == volume_name - not mount.readOnly - } - - # This allows "/foo", "/foo/", "/foo/bar" etc., but - # disallows "/fool", "/etc/foo" etc. - path_matches(prefix, path) { - a := path_array(prefix) - b := path_array(path) - prefix_matches(a, b) - } - path_array(p) = out { - p != "/" - out := split(trim(p, "/"), "/") - } - # This handles the special case for "/", since - # split(trim("/", "/"), "/") == [""] - path_array("/") = [] - - prefix_matches(a, b) { - count(a) <= count(b) - not any_not_equal_upto(a, b, count(a)) - } - - any_not_equal_upto(a, b, n) { - a[i] != b[i] - i < n - } - - input_hostpath_volumes[v] { - v := input.review.object.spec.volumes[_] - has_field(v, "hostPath") - } - - # has_field returns whether an object has a field - has_field(object, field) = true { - object[field] - } - input_containers[c] { - c := input.review.object.spec.containers[_] - } - - input_containers[c] { - c := input.review.object.spec.initContainers[_] - } - - input_containers[c] { - c := input.review.object.spec.ephemeralContainers[_] - } - libs: - - | - package lib.exclude_update - - is_update(review) { - review.operation == "UPDATE" - } + code: + - engine: K8sNativeValidation + source: + variables: + - name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' + - name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' + - name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' + - name: allContainers + expression: 'variables.containers + variables.initContainers + variables.ephemeralContainers' + - name: allowedPaths + expression: | + !has(variables.params.allowedHostPaths) ? [] : variables.params.allowedHostPaths + - name: volumes + expression: | + variables.anyObject.spec.volumes.filter(volume, has(volume.hostPath)) + - name: badHostPaths + expression: | + variables.volumes.filter(volume, + (size(variables.allowedPaths) == 0) || + !(variables.allowedPaths.exists(allowedPath, + volume.hostPath.path.startsWith(allowedPath.pathPrefix) && ( + (!has(allowedPath.readOnly) || !(allowedPath.readOnly)) || + (has(allowedPath.readOnly) && allowedPath.readOnly && !variables.allContainers.exists(c, + c.volumeMounts.exists(m, m.name == volume.name && (!has(m.readOnly) || !m.readOnly))))))) + ).map(volume, "{ hostPath: { path : " + volume.hostPath.path + " }, name: " + volume.name + "}").map(volume, "HostPath volume " + volume + " is not allowed, pod: " + object.metadata.name + ". Allowed path: " + variables.allowedPaths.map(path, path.pathPrefix + ", readOnly: " + (path.readOnly ? "true" : "false") + "}").join(", ")) + validations: + - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.badHostPaths) == 0' + messageExpression: 'variables.badHostPaths.join("\n")' + - engine: Rego + source: + rego: | + package k8spsphostfilesystem + + import data.lib.exclude_update.is_update + + violation[{"msg": msg, "details": {}}] { + # spec.volumes field is immutable. + not is_update(input.review) + + volume := input_hostpath_volumes[_] + allowedPaths := get_allowed_paths(input) + input_hostpath_violation(allowedPaths, volume) + msg := sprintf("HostPath volume %v is not allowed, pod: %v. Allowed path: %v", [volume, input.review.object.metadata.name, allowedPaths]) + } + + input_hostpath_violation(allowedPaths, _) { + # An empty list means all host paths are blocked + allowedPaths == [] + } + input_hostpath_violation(allowedPaths, volume) { + not input_hostpath_allowed(allowedPaths, volume) + } + + get_allowed_paths(arg) = out { + not arg.parameters + out = [] + } + get_allowed_paths(arg) = out { + not arg.parameters.allowedHostPaths + out = [] + } + get_allowed_paths(arg) = out { + out = arg.parameters.allowedHostPaths + } + + input_hostpath_allowed(allowedPaths, volume) { + allowedHostPath := allowedPaths[_] + path_matches(allowedHostPath.pathPrefix, volume.hostPath.path) + not allowedHostPath.readOnly == true + } + + input_hostpath_allowed(allowedPaths, volume) { + allowedHostPath := allowedPaths[_] + path_matches(allowedHostPath.pathPrefix, volume.hostPath.path) + allowedHostPath.readOnly + not writeable_input_volume_mounts(volume.name) + } + + writeable_input_volume_mounts(volume_name) { + container := input_containers[_] + mount := container.volumeMounts[_] + mount.name == volume_name + not mount.readOnly + } + + # This allows "/foo", "/foo/", "/foo/bar" etc., but + # disallows "/fool", "/etc/foo" etc. + path_matches(prefix, path) { + a := path_array(prefix) + b := path_array(path) + prefix_matches(a, b) + } + path_array(p) = out { + p != "/" + out := split(trim(p, "/"), "/") + } + # This handles the special case for "/", since + # split(trim("/", "/"), "/") == [""] + path_array("/") = [] + + prefix_matches(a, b) { + count(a) <= count(b) + not any_not_equal_upto(a, b, count(a)) + } + + any_not_equal_upto(a, b, n) { + a[i] != b[i] + i < n + } + + input_hostpath_volumes[v] { + v := input.review.object.spec.volumes[_] + has_field(v, "hostPath") + } + + # has_field returns whether an object has a field + has_field(object, field) = true { + object[field] + } + input_containers[c] { + c := input.review.object.spec.containers[_] + } + + input_containers[c] { + c := input.review.object.spec.initContainers[_] + } + + input_containers[c] { + c := input.review.object.spec.ephemeralContainers[_] + } + libs: + - | + package lib.exclude_update + + is_update(review) { + review.operation == "UPDATE" + } ``` @@ -205,8 +238,6 @@ apiVersion: v1 kind: Pod metadata: name: nginx-host-filesystem - labels: - app: nginx-host-filesystem-disallowed spec: containers: - name: nginx @@ -237,8 +268,6 @@ apiVersion: v1 kind: Pod metadata: name: nginx-host-filesystem - labels: - app: nginx-host-filesystem-disallowed spec: containers: - name: nginx @@ -269,8 +298,6 @@ apiVersion: v1 kind: Pod metadata: name: nginx-host-filesystem - labels: - app: nginx-host-filesystem-disallowed spec: ephemeralContainers: - name: nginx From d59972f3a9cc09feb046758155c84653df3fc46f Mon Sep 17 00:00:00 2001 From: Max Smythe Date: Tue, 3 Sep 2024 19:12:12 -0700 Subject: [PATCH 10/19] Add CEL to K8sPSPCapabilities template (#535) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add CEL to K8sPSPCapabilities template Signed-off-by: Max Smythe * bump minor version Signed-off-by: Max Smythe --------- Signed-off-by: Max Smythe Co-authored-by: Jaydipkumar Arvindbhai Gabani Co-authored-by: Sertaç Özercan <852750+sozercan@users.noreply.github.com> --- .../capabilities/1.1.0/artifacthub-pkg.yml | 22 ++ .../capabilities/1.1.0/kustomization.yaml | 2 + .../samples/capabilities-demo/constraint.yaml | 14 + .../disallowed_ephemeral.yaml | 21 ++ .../capabilities-demo/example_allowed.yaml | 22 ++ .../capabilities-demo/example_disallowed.yaml | 21 ++ .../samples/capabilities-demo/update.yaml | 26 ++ .../capabilities/1.1.0/suite.yaml | 25 ++ .../capabilities/1.1.0/template.yaml | 224 ++++++++++++++ .../capabilities/template.yaml | 288 +++++++++++------- .../capabilities/constraint.tmpl | 22 +- src/pod-security-policy/capabilities/src.cel | 59 ++++ website/docs/validation/capabilities.md | 288 +++++++++++------- 13 files changed, 802 insertions(+), 232 deletions(-) create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.0/artifacthub-pkg.yml create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.0/kustomization.yaml create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/constraint.yaml create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/disallowed_ephemeral.yaml create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/example_allowed.yaml create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/example_disallowed.yaml create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/update.yaml create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.0/suite.yaml create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.0/template.yaml create mode 100644 src/pod-security-policy/capabilities/src.cel diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.0/artifacthub-pkg.yml b/artifacthub/library/pod-security-policy/capabilities/1.1.0/artifacthub-pkg.yml new file mode 100644 index 000000000..5523381c3 --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.0/artifacthub-pkg.yml @@ -0,0 +1,22 @@ +version: 1.1.0 +name: k8spspcapabilities +displayName: Capabilities +createdAt: "2024-05-29T23:37:22Z" +description: Controls Linux capabilities on containers. Corresponds to the `allowedCapabilities` and `requiredDropCapabilities` fields in a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#capabilities +digest: 1b837e4add0952bb782bf0d0bc5e12ac0b0543ee4d23f88e9a282b531bfb8ff5 +license: Apache-2.0 +homeURL: https://open-policy-agent.github.io/gatekeeper-library/website/capabilities +keywords: + - gatekeeper + - open-policy-agent + - policies +readme: |- + # Capabilities + Controls Linux capabilities on containers. Corresponds to the `allowedCapabilities` and `requiredDropCapabilities` fields in a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#capabilities +install: |- + ### Usage + ```shell + kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/artifacthub/library/pod-security-policy/capabilities/1.1.0/template.yaml + ``` +provider: + name: Gatekeeper Library diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.0/kustomization.yaml b/artifacthub/library/pod-security-policy/capabilities/1.1.0/kustomization.yaml new file mode 100644 index 000000000..7d70d11b7 --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.0/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - template.yaml diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/constraint.yaml b/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/constraint.yaml new file mode 100644 index 000000000..3f856082f --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/constraint.yaml @@ -0,0 +1,14 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPCapabilities +metadata: + name: capabilities-demo +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + namespaces: + - "default" + parameters: + allowedCapabilities: ["something"] + requiredDropCapabilities: ["must_drop"] diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/disallowed_ephemeral.yaml b/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/disallowed_ephemeral.yaml new file mode 100644 index 000000000..5467c826e --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/disallowed_ephemeral.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Pod +metadata: + name: opa-disallowed + labels: + owner: me.agilebank.demo +spec: + ephemeralContainers: + - name: opa + image: openpolicyagent/opa:0.9.2 + args: + - "run" + - "--server" + - "--addr=localhost:8080" + securityContext: + capabilities: + add: ["disallowedcapability"] + resources: + limits: + cpu: "100m" + memory: "30Mi" diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/example_allowed.yaml b/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/example_allowed.yaml new file mode 100644 index 000000000..41bf6a0ed --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/example_allowed.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: Pod +metadata: + name: opa-allowed + labels: + owner: me.agilebank.demo +spec: + containers: + - name: opa + image: openpolicyagent/opa:0.9.2 + args: + - "run" + - "--server" + - "--addr=localhost:8080" + securityContext: + capabilities: + add: ["something"] + drop: ["must_drop", "another_one"] + resources: + limits: + cpu: "100m" + memory: "30Mi" diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/example_disallowed.yaml b/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/example_disallowed.yaml new file mode 100644 index 000000000..fdd886189 --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/example_disallowed.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Pod +metadata: + name: opa-disallowed + labels: + owner: me.agilebank.demo +spec: + containers: + - name: opa + image: openpolicyagent/opa:0.9.2 + args: + - "run" + - "--server" + - "--addr=localhost:8080" + securityContext: + capabilities: + add: ["disallowedcapability"] + resources: + limits: + cpu: "100m" + memory: "30Mi" \ No newline at end of file diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/update.yaml b/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/update.yaml new file mode 100644 index 000000000..df8ea0070 --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.0/samples/capabilities-demo/update.yaml @@ -0,0 +1,26 @@ +kind: AdmissionReview +apiVersion: admission.k8s.io/v1beta1 +request: + operation: "UPDATE" + object: + apiVersion: v1 + kind: Pod + metadata: + name: opa-disallowed + labels: + owner: me.agilebank.demo + spec: + containers: + - name: opa + image: openpolicyagent/opa:0.9.2 + args: + - "run" + - "--server" + - "--addr=localhost:8080" + securityContext: + capabilities: + add: ["disallowedcapability"] + resources: + limits: + cpu: "100m" + memory: "30Mi" diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.0/suite.yaml b/artifacthub/library/pod-security-policy/capabilities/1.1.0/suite.yaml new file mode 100644 index 000000000..48c2fcb46 --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.0/suite.yaml @@ -0,0 +1,25 @@ +kind: Suite +apiVersion: test.gatekeeper.sh/v1alpha1 +metadata: + name: capabilities +tests: + - name: capabilities + template: template.yaml + constraint: samples/capabilities-demo/constraint.yaml + cases: + - name: example-disallowed + object: samples/capabilities-demo/example_disallowed.yaml + assertions: + - violations: yes + - name: example-allowed + object: samples/capabilities-demo/example_allowed.yaml + assertions: + - violations: no + - name: disallowed-ephemeral + object: samples/capabilities-demo/disallowed_ephemeral.yaml + assertions: + - violations: yes + - name: update + object: samples/capabilities-demo/update.yaml + assertions: + - violations: no diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.0/template.yaml b/artifacthub/library/pod-security-policy/capabilities/1.1.0/template.yaml new file mode 100644 index 000000000..a9766fd60 --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.0/template.yaml @@ -0,0 +1,224 @@ +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: k8spspcapabilities + annotations: + metadata.gatekeeper.sh/title: "Capabilities" + metadata.gatekeeper.sh/version: 1.1.0 + description: >- + Controls Linux capabilities on containers. Corresponds to the + `allowedCapabilities` and `requiredDropCapabilities` fields in a + PodSecurityPolicy. For more information, see + https://kubernetes.io/docs/concepts/policy/pod-security-policy/#capabilities +spec: + crd: + spec: + names: + kind: K8sPSPCapabilities + validation: + # Schema for the `parameters` field + openAPIV3Schema: + type: object + description: >- + Controls Linux capabilities on containers. Corresponds to the + `allowedCapabilities` and `requiredDropCapabilities` fields in a + PodSecurityPolicy. For more information, see + https://kubernetes.io/docs/concepts/policy/pod-security-policy/#capabilities + 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 + allowedCapabilities: + type: array + description: "A list of Linux capabilities that can be added to a container." + items: + type: string + requiredDropCapabilities: + type: array + description: "A list of Linux capabilities that are required to be dropped from a container." + items: + type: string + targets: + - target: admission.k8s.gatekeeper.sh + code: + - engine: K8sNativeValidation + source: + variables: + - name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' + - name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' + - name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' + - name: allContainers + expression: 'variables.containers + variables.initContainers + variables.ephemeralContainers' + - 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)) + ).map(container, container.image) + - name: allowedCapabilities + expression: 'has(variables.params.allowedCapabilities) ? variables.params.allowedCapabilities : []' + - name: allCapabilitiesAllowed + expression: '"*" in variables.allowedCapabilities' + - name: disallowedCapabilitiesByContainer + expression: | + variables.allContainers.map(container, !(container.image in variables.exemptImages) && + !variables.allCapabilitiesAllowed && has(container.securityContext) && has(container.securityContext.capabilities) && has(container.securityContext.capabilities.add) && + container.securityContext.capabilities.add.exists(capability, !(capability in variables.allowedCapabilities)), + [container.name, dyn(container.securityContext.capabilities.add.filter(capability, !(capability in variables.allowedCapabilities)).join(", "))] + ) + - name: requiredDropCapabilities + expression: 'has(variables.params.requiredDropCapabilities) ? variables.params.requiredDropCapabilities : []' + - name: missingDropCapabilitiesByContainer + expression: | + variables.allContainers.map(container, !(container.image in variables.exemptImages) && + size(variables.requiredDropCapabilities) > 0 && ( + !has(container.securityContext) || !has(container.securityContext.capabilities) || !has(container.securityContext.capabilities.drop) || ( + !("all" in container.securityContext.capabilities.drop) && + variables.requiredDropCapabilities.exists(capability, !(capability in container.securityContext.capabilities.drop)) + ) + ), + [container.name, + !has(container.securityContext) ? variables.requiredDropCapabilities : + !has(container.securityContext.capabilities) ? variables.requiredDropCapabilities : + !has(container.securityContext.capabilities.drop) ? variables.requiredDropCapabilities : + variables.requiredDropCapabilities.filter(capability, !(capability in container.securityContext.capabilities.drop)) + ] + ) + validations: + - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.disallowedCapabilitiesByContainer) == 0' + messageExpression: | + "containers have disallowed capabilities: " + variables.disallowedCapabilitiesByContainer.map(pair, "{container: " + pair[0] + ", capabilities: [" + pair[1] + "]}").join(", ") + - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.missingDropCapabilitiesByContainer) == 0' + messageExpression: | + "containers are not dropping all required capabilities: " + variables.missingDropCapabilitiesByContainer.map(pair, "{container: " + pair[0] + ", capabilities: [" + pair[1].join(", ") + "]}").join(", ") + - engine: Rego + source: + rego: | + package capabilities + + import data.lib.exclude_update.is_update + import data.lib.exempt_container.is_exempt + + violation[{"msg": msg}] { + # spec.containers.securityContext.capabilities field is immutable. + not is_update(input.review) + + container := input.review.object.spec.containers[_] + not is_exempt(container) + has_disallowed_capabilities(container) + msg := sprintf("container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) + } + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.containers[_] + not is_exempt(container) + missing_drop_capabilities(container) + msg := sprintf("container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) + } + + + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.initContainers[_] + not is_exempt(container) + has_disallowed_capabilities(container) + msg := sprintf("init container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) + } + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.initContainers[_] + not is_exempt(container) + missing_drop_capabilities(container) + msg := sprintf("init container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) + } + + + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.ephemeralContainers[_] + not is_exempt(container) + has_disallowed_capabilities(container) + msg := sprintf("ephemeral container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) + } + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.ephemeralContainers[_] + not is_exempt(container) + missing_drop_capabilities(container) + msg := sprintf("ephemeral container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) + } + + + has_disallowed_capabilities(container) { + allowed := {c | c := lower(input.parameters.allowedCapabilities[_])} + not allowed["*"] + capabilities := {c | c := lower(container.securityContext.capabilities.add[_])} + + count(capabilities - allowed) > 0 + } + + missing_drop_capabilities(container) { + must_drop := {c | c := lower(input.parameters.requiredDropCapabilities[_])} + all := {"all"} + dropped := {c | c := lower(container.securityContext.capabilities.drop[_])} + + count(must_drop - dropped) > 0 + count(all - dropped) > 0 + } + + get_default(obj, param, _) := obj[param] + + get_default(obj, param, _default) := _default { + not obj[param] + not obj[param] == false + } + 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) + } diff --git a/library/pod-security-policy/capabilities/template.yaml b/library/pod-security-policy/capabilities/template.yaml index 2501a1538..a9766fd60 100644 --- a/library/pod-security-policy/capabilities/template.yaml +++ b/library/pod-security-policy/capabilities/template.yaml @@ -4,7 +4,7 @@ metadata: name: k8spspcapabilities annotations: metadata.gatekeeper.sh/title: "Capabilities" - metadata.gatekeeper.sh/version: 1.0.2 + metadata.gatekeeper.sh/version: 1.1.0 description: >- Controls Linux capabilities on containers. Corresponds to the `allowedCapabilities` and `requiredDropCapabilities` fields in a @@ -47,114 +47,178 @@ spec: type: string targets: - target: admission.k8s.gatekeeper.sh - rego: | - package capabilities - - import data.lib.exclude_update.is_update - import data.lib.exempt_container.is_exempt - - violation[{"msg": msg}] { - # spec.containers.securityContext.capabilities field is immutable. - not is_update(input.review) - - container := input.review.object.spec.containers[_] - not is_exempt(container) - has_disallowed_capabilities(container) - msg := sprintf("container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) - } - - violation[{"msg": msg}] { - not is_update(input.review) - container := input.review.object.spec.containers[_] - not is_exempt(container) - missing_drop_capabilities(container) - msg := sprintf("container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) - } - - - - violation[{"msg": msg}] { - not is_update(input.review) - container := input.review.object.spec.initContainers[_] - not is_exempt(container) - has_disallowed_capabilities(container) - msg := sprintf("init container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) - } - - violation[{"msg": msg}] { - not is_update(input.review) - container := input.review.object.spec.initContainers[_] - not is_exempt(container) - missing_drop_capabilities(container) - msg := sprintf("init container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) - } - - - - violation[{"msg": msg}] { - not is_update(input.review) - container := input.review.object.spec.ephemeralContainers[_] - not is_exempt(container) - has_disallowed_capabilities(container) - msg := sprintf("ephemeral container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) - } - - violation[{"msg": msg}] { - not is_update(input.review) - container := input.review.object.spec.ephemeralContainers[_] - not is_exempt(container) - missing_drop_capabilities(container) - msg := sprintf("ephemeral container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) - } - - - has_disallowed_capabilities(container) { - allowed := {c | c := lower(input.parameters.allowedCapabilities[_])} - not allowed["*"] - capabilities := {c | c := lower(container.securityContext.capabilities.add[_])} - - count(capabilities - allowed) > 0 - } - - missing_drop_capabilities(container) { - must_drop := {c | c := lower(input.parameters.requiredDropCapabilities[_])} - all := {"all"} - dropped := {c | c := lower(container.securityContext.capabilities.drop[_])} - - count(must_drop - dropped) > 0 - count(all - dropped) > 0 - } - - get_default(obj, param, _) := obj[param] - - get_default(obj, param, _default) := _default { - not obj[param] - not obj[param] == false - } - 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) - } + code: + - engine: K8sNativeValidation + source: + variables: + - name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' + - name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' + - name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' + - name: allContainers + expression: 'variables.containers + variables.initContainers + variables.ephemeralContainers' + - 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)) + ).map(container, container.image) + - name: allowedCapabilities + expression: 'has(variables.params.allowedCapabilities) ? variables.params.allowedCapabilities : []' + - name: allCapabilitiesAllowed + expression: '"*" in variables.allowedCapabilities' + - name: disallowedCapabilitiesByContainer + expression: | + variables.allContainers.map(container, !(container.image in variables.exemptImages) && + !variables.allCapabilitiesAllowed && has(container.securityContext) && has(container.securityContext.capabilities) && has(container.securityContext.capabilities.add) && + container.securityContext.capabilities.add.exists(capability, !(capability in variables.allowedCapabilities)), + [container.name, dyn(container.securityContext.capabilities.add.filter(capability, !(capability in variables.allowedCapabilities)).join(", "))] + ) + - name: requiredDropCapabilities + expression: 'has(variables.params.requiredDropCapabilities) ? variables.params.requiredDropCapabilities : []' + - name: missingDropCapabilitiesByContainer + expression: | + variables.allContainers.map(container, !(container.image in variables.exemptImages) && + size(variables.requiredDropCapabilities) > 0 && ( + !has(container.securityContext) || !has(container.securityContext.capabilities) || !has(container.securityContext.capabilities.drop) || ( + !("all" in container.securityContext.capabilities.drop) && + variables.requiredDropCapabilities.exists(capability, !(capability in container.securityContext.capabilities.drop)) + ) + ), + [container.name, + !has(container.securityContext) ? variables.requiredDropCapabilities : + !has(container.securityContext.capabilities) ? variables.requiredDropCapabilities : + !has(container.securityContext.capabilities.drop) ? variables.requiredDropCapabilities : + variables.requiredDropCapabilities.filter(capability, !(capability in container.securityContext.capabilities.drop)) + ] + ) + validations: + - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.disallowedCapabilitiesByContainer) == 0' + messageExpression: | + "containers have disallowed capabilities: " + variables.disallowedCapabilitiesByContainer.map(pair, "{container: " + pair[0] + ", capabilities: [" + pair[1] + "]}").join(", ") + - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.missingDropCapabilitiesByContainer) == 0' + messageExpression: | + "containers are not dropping all required capabilities: " + variables.missingDropCapabilitiesByContainer.map(pair, "{container: " + pair[0] + ", capabilities: [" + pair[1].join(", ") + "]}").join(", ") + - engine: Rego + source: + rego: | + package capabilities + + import data.lib.exclude_update.is_update + import data.lib.exempt_container.is_exempt + + violation[{"msg": msg}] { + # spec.containers.securityContext.capabilities field is immutable. + not is_update(input.review) + + container := input.review.object.spec.containers[_] + not is_exempt(container) + has_disallowed_capabilities(container) + msg := sprintf("container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) + } + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.containers[_] + not is_exempt(container) + missing_drop_capabilities(container) + msg := sprintf("container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) + } + + + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.initContainers[_] + not is_exempt(container) + has_disallowed_capabilities(container) + msg := sprintf("init container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) + } + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.initContainers[_] + not is_exempt(container) + missing_drop_capabilities(container) + msg := sprintf("init container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) + } + + + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.ephemeralContainers[_] + not is_exempt(container) + has_disallowed_capabilities(container) + msg := sprintf("ephemeral container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) + } + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.ephemeralContainers[_] + not is_exempt(container) + missing_drop_capabilities(container) + msg := sprintf("ephemeral container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) + } + + + has_disallowed_capabilities(container) { + allowed := {c | c := lower(input.parameters.allowedCapabilities[_])} + not allowed["*"] + capabilities := {c | c := lower(container.securityContext.capabilities.add[_])} + + count(capabilities - allowed) > 0 + } + + missing_drop_capabilities(container) { + must_drop := {c | c := lower(input.parameters.requiredDropCapabilities[_])} + all := {"all"} + dropped := {c | c := lower(container.securityContext.capabilities.drop[_])} + + count(must_drop - dropped) > 0 + count(all - dropped) > 0 + } + + get_default(obj, param, _) := obj[param] + + get_default(obj, param, _default) := _default { + not obj[param] + not obj[param] == false + } + 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) + } diff --git a/src/pod-security-policy/capabilities/constraint.tmpl b/src/pod-security-policy/capabilities/constraint.tmpl index 9ca6bed26..02c8abe32 100644 --- a/src/pod-security-policy/capabilities/constraint.tmpl +++ b/src/pod-security-policy/capabilities/constraint.tmpl @@ -4,7 +4,7 @@ metadata: name: k8spspcapabilities annotations: metadata.gatekeeper.sh/title: "Capabilities" - metadata.gatekeeper.sh/version: 1.0.2 + metadata.gatekeeper.sh/version: 1.1.0 description: >- Controls Linux capabilities on containers. Corresponds to the `allowedCapabilities` and `requiredDropCapabilities` fields in a @@ -47,10 +47,16 @@ spec: type: string targets: - target: admission.k8s.gatekeeper.sh - rego: | -{{ file.Read "src/pod-security-policy/capabilities/src.rego" | strings.Indent 8 | strings.TrimSuffix "\n" }} - libs: - - | -{{ file.Read "src/pod-security-policy/capabilities/lib_exclude_update.rego" | strings.Indent 10 | strings.TrimSuffix "\n" }} - - | -{{ file.Read "src/pod-security-policy/capabilities/lib_exempt_container.rego" | strings.Indent 10 | strings.TrimSuffix "\n" }} + code: + - engine: K8sNativeValidation + source: +{{ file.Read "src/pod-security-policy/capabilities/src.cel" | strings.Indent 10 | strings.TrimSuffix "\n" }} + - engine: Rego + source: + rego: | +{{ file.Read "src/pod-security-policy/capabilities/src.rego" | strings.Indent 12 | strings.TrimSuffix "\n" }} + libs: + - | +{{ file.Read "src/pod-security-policy/capabilities/lib_exclude_update.rego" | strings.Indent 12 | strings.TrimSuffix "\n" }} + - | +{{ file.Read "src/pod-security-policy/capabilities/lib_exempt_container.rego" | strings.Indent 12 | strings.TrimSuffix "\n" }} diff --git a/src/pod-security-policy/capabilities/src.cel b/src/pod-security-policy/capabilities/src.cel new file mode 100644 index 000000000..42c70ff6a --- /dev/null +++ b/src/pod-security-policy/capabilities/src.cel @@ -0,0 +1,59 @@ +variables: +- name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' +- name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' +- name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' +- name: allContainers + expression: 'variables.containers + variables.initContainers + variables.ephemeralContainers' +- 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)) + ).map(container, container.image) +- name: allowedCapabilities + expression: 'has(variables.params.allowedCapabilities) ? variables.params.allowedCapabilities : []' +- name: allCapabilitiesAllowed + expression: '"*" in variables.allowedCapabilities' +- name: disallowedCapabilitiesByContainer + expression: | + variables.allContainers.map(container, !(container.image in variables.exemptImages) && + !variables.allCapabilitiesAllowed && has(container.securityContext) && has(container.securityContext.capabilities) && has(container.securityContext.capabilities.add) && + container.securityContext.capabilities.add.exists(capability, !(capability in variables.allowedCapabilities)), + [container.name, dyn(container.securityContext.capabilities.add.filter(capability, !(capability in variables.allowedCapabilities)).join(", "))] + ) +- name: requiredDropCapabilities + expression: 'has(variables.params.requiredDropCapabilities) ? variables.params.requiredDropCapabilities : []' +- name: missingDropCapabilitiesByContainer + expression: | + variables.allContainers.map(container, !(container.image in variables.exemptImages) && + size(variables.requiredDropCapabilities) > 0 && ( + !has(container.securityContext) || !has(container.securityContext.capabilities) || !has(container.securityContext.capabilities.drop) || ( + !("all" in container.securityContext.capabilities.drop) && + variables.requiredDropCapabilities.exists(capability, !(capability in container.securityContext.capabilities.drop)) + ) + ), + [container.name, + !has(container.securityContext) ? variables.requiredDropCapabilities : + !has(container.securityContext.capabilities) ? variables.requiredDropCapabilities : + !has(container.securityContext.capabilities.drop) ? variables.requiredDropCapabilities : + variables.requiredDropCapabilities.filter(capability, !(capability in container.securityContext.capabilities.drop)) + ] + ) +validations: +- expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.disallowedCapabilitiesByContainer) == 0' + messageExpression: | + "containers have disallowed capabilities: " + variables.disallowedCapabilitiesByContainer.map(pair, "{container: " + pair[0] + ", capabilities: [" + pair[1] + "]}").join(", ") +- expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.missingDropCapabilitiesByContainer) == 0' + messageExpression: | + "containers are not dropping all required capabilities: " + variables.missingDropCapabilitiesByContainer.map(pair, "{container: " + pair[0] + ", capabilities: [" + pair[1].join(", ") + "]}").join(", ") \ No newline at end of file diff --git a/website/docs/validation/capabilities.md b/website/docs/validation/capabilities.md index 5048c6627..b119ae89b 100644 --- a/website/docs/validation/capabilities.md +++ b/website/docs/validation/capabilities.md @@ -16,7 +16,7 @@ metadata: name: k8spspcapabilities annotations: metadata.gatekeeper.sh/title: "Capabilities" - metadata.gatekeeper.sh/version: 1.0.2 + metadata.gatekeeper.sh/version: 1.1.0 description: >- Controls Linux capabilities on containers. Corresponds to the `allowedCapabilities` and `requiredDropCapabilities` fields in a @@ -59,117 +59,181 @@ spec: type: string targets: - target: admission.k8s.gatekeeper.sh - rego: | - package capabilities - - import data.lib.exclude_update.is_update - import data.lib.exempt_container.is_exempt - - violation[{"msg": msg}] { - # spec.containers.securityContext.capabilities field is immutable. - not is_update(input.review) - - container := input.review.object.spec.containers[_] - not is_exempt(container) - has_disallowed_capabilities(container) - msg := sprintf("container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) - } - - violation[{"msg": msg}] { - not is_update(input.review) - container := input.review.object.spec.containers[_] - not is_exempt(container) - missing_drop_capabilities(container) - msg := sprintf("container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) - } - - - - violation[{"msg": msg}] { - not is_update(input.review) - container := input.review.object.spec.initContainers[_] - not is_exempt(container) - has_disallowed_capabilities(container) - msg := sprintf("init container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) - } - - violation[{"msg": msg}] { - not is_update(input.review) - container := input.review.object.spec.initContainers[_] - not is_exempt(container) - missing_drop_capabilities(container) - msg := sprintf("init container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) - } - - - - violation[{"msg": msg}] { - not is_update(input.review) - container := input.review.object.spec.ephemeralContainers[_] - not is_exempt(container) - has_disallowed_capabilities(container) - msg := sprintf("ephemeral container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) - } - - violation[{"msg": msg}] { - not is_update(input.review) - container := input.review.object.spec.ephemeralContainers[_] - not is_exempt(container) - missing_drop_capabilities(container) - msg := sprintf("ephemeral container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) - } - - - has_disallowed_capabilities(container) { - allowed := {c | c := lower(input.parameters.allowedCapabilities[_])} - not allowed["*"] - capabilities := {c | c := lower(container.securityContext.capabilities.add[_])} - - count(capabilities - allowed) > 0 - } - - missing_drop_capabilities(container) { - must_drop := {c | c := lower(input.parameters.requiredDropCapabilities[_])} - all := {"all"} - dropped := {c | c := lower(container.securityContext.capabilities.drop[_])} - - count(must_drop - dropped) > 0 - count(all - dropped) > 0 - } - - get_default(obj, param, _) := obj[param] - - get_default(obj, param, _default) := _default { - not obj[param] - not obj[param] == false - } - 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) - } + code: + - engine: K8sNativeValidation + source: + variables: + - name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' + - name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' + - name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' + - name: allContainers + expression: 'variables.containers + variables.initContainers + variables.ephemeralContainers' + - 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)) + ).map(container, container.image) + - name: allowedCapabilities + expression: 'has(variables.params.allowedCapabilities) ? variables.params.allowedCapabilities : []' + - name: allCapabilitiesAllowed + expression: '"*" in variables.allowedCapabilities' + - name: disallowedCapabilitiesByContainer + expression: | + variables.allContainers.map(container, !(container.image in variables.exemptImages) && + !variables.allCapabilitiesAllowed && has(container.securityContext) && has(container.securityContext.capabilities) && has(container.securityContext.capabilities.add) && + container.securityContext.capabilities.add.exists(capability, !(capability in variables.allowedCapabilities)), + [container.name, dyn(container.securityContext.capabilities.add.filter(capability, !(capability in variables.allowedCapabilities)).join(", "))] + ) + - name: requiredDropCapabilities + expression: 'has(variables.params.requiredDropCapabilities) ? variables.params.requiredDropCapabilities : []' + - name: missingDropCapabilitiesByContainer + expression: | + variables.allContainers.map(container, !(container.image in variables.exemptImages) && + size(variables.requiredDropCapabilities) > 0 && ( + !has(container.securityContext) || !has(container.securityContext.capabilities) || !has(container.securityContext.capabilities.drop) || ( + !("all" in container.securityContext.capabilities.drop) && + variables.requiredDropCapabilities.exists(capability, !(capability in container.securityContext.capabilities.drop)) + ) + ), + [container.name, + !has(container.securityContext) ? variables.requiredDropCapabilities : + !has(container.securityContext.capabilities) ? variables.requiredDropCapabilities : + !has(container.securityContext.capabilities.drop) ? variables.requiredDropCapabilities : + variables.requiredDropCapabilities.filter(capability, !(capability in container.securityContext.capabilities.drop)) + ] + ) + validations: + - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.disallowedCapabilitiesByContainer) == 0' + messageExpression: | + "containers have disallowed capabilities: " + variables.disallowedCapabilitiesByContainer.map(pair, "{container: " + pair[0] + ", capabilities: [" + pair[1] + "]}").join(", ") + - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.missingDropCapabilitiesByContainer) == 0' + messageExpression: | + "containers are not dropping all required capabilities: " + variables.missingDropCapabilitiesByContainer.map(pair, "{container: " + pair[0] + ", capabilities: [" + pair[1].join(", ") + "]}").join(", ") + - engine: Rego + source: + rego: | + package capabilities + + import data.lib.exclude_update.is_update + import data.lib.exempt_container.is_exempt + + violation[{"msg": msg}] { + # spec.containers.securityContext.capabilities field is immutable. + not is_update(input.review) + + container := input.review.object.spec.containers[_] + not is_exempt(container) + has_disallowed_capabilities(container) + msg := sprintf("container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) + } + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.containers[_] + not is_exempt(container) + missing_drop_capabilities(container) + msg := sprintf("container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) + } + + + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.initContainers[_] + not is_exempt(container) + has_disallowed_capabilities(container) + msg := sprintf("init container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) + } + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.initContainers[_] + not is_exempt(container) + missing_drop_capabilities(container) + msg := sprintf("init container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) + } + + + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.ephemeralContainers[_] + not is_exempt(container) + has_disallowed_capabilities(container) + msg := sprintf("ephemeral container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) + } + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.ephemeralContainers[_] + not is_exempt(container) + missing_drop_capabilities(container) + msg := sprintf("ephemeral container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) + } + + + has_disallowed_capabilities(container) { + allowed := {c | c := lower(input.parameters.allowedCapabilities[_])} + not allowed["*"] + capabilities := {c | c := lower(container.securityContext.capabilities.add[_])} + + count(capabilities - allowed) > 0 + } + + missing_drop_capabilities(container) { + must_drop := {c | c := lower(input.parameters.requiredDropCapabilities[_])} + all := {"all"} + dropped := {c | c := lower(container.securityContext.capabilities.drop[_])} + + count(must_drop - dropped) > 0 + count(all - dropped) > 0 + } + + get_default(obj, param, _) := obj[param] + + get_default(obj, param, _default) := _default { + not obj[param] + not obj[param] == false + } + 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) + } ``` From 598df7473390c862e4cf36785b4fc9e6115130da Mon Sep 17 00:00:00 2001 From: Max Smythe Date: Tue, 3 Sep 2024 19:58:47 -0700 Subject: [PATCH 11/19] feat: Update apparmor: add CEL, support securityContext (#533) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Update apparmor: add CEL, support securityContext Signed-off-by: Max Smythe * Test no profile Signed-off-by: Max Smythe * fix securitycontext path root for pod in rego Signed-off-by: Max Smythe * update minor version Signed-off-by: Max Smythe * remove metadata var Signed-off-by: Max Smythe * Fix Rego lint Signed-off-by: Max Smythe * Treat each container type separately Signed-off-by: Max Smythe --------- Signed-off-by: Max Smythe Co-authored-by: Sertaç Özercan <852750+sozercan@users.noreply.github.com> --- .../apparmor/1.1.0/artifacthub-pkg.yml | 22 + .../apparmor/1.1.0/kustomization.yaml | 2 + .../samples/psp-apparmor/constraint.yaml | 12 + .../psp-apparmor/disallowed_ephemeral.yaml | 13 + .../samples/psp-apparmor/example_allowed.yaml | 13 + .../example_allowed_container.yaml | 14 + .../example_allowed_override.yaml | 17 + .../psp-apparmor/example_allowed_pod.yaml | 14 + .../psp-apparmor/example_disallowed.yaml | 13 + .../example_disallowed_no_profile.yaml | 10 + .../example_disallowed_override.yaml | 17 + .../apparmor/1.1.0/suite.yaml | 41 ++ .../apparmor/1.1.0/template.yaml | 218 ++++++++++ .../samples/psp-apparmor/constraint.yaml | 2 +- .../samples/psp-apparmor/example_allowed.yaml | 2 +- .../example_allowed_container.yaml | 14 + .../example_allowed_override.yaml | 17 + .../psp-apparmor/example_allowed_pod.yaml | 14 + .../example_disallowed_no_profile.yaml | 10 + .../example_disallowed_override.yaml | 17 + .../pod-security-policy/apparmor/suite.yaml | 20 + .../apparmor/template.yaml | 231 ++++++++--- .../apparmor/constraint.tmpl | 19 +- src/pod-security-policy/apparmor/src.cel | 83 ++++ src/pod-security-policy/apparmor/src.rego | 48 ++- website/docs/validation/apparmor.md | 377 +++++++++++++++--- 26 files changed, 1129 insertions(+), 131 deletions(-) create mode 100644 artifacthub/library/pod-security-policy/apparmor/1.1.0/artifacthub-pkg.yml create mode 100644 artifacthub/library/pod-security-policy/apparmor/1.1.0/kustomization.yaml create mode 100644 artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/constraint.yaml create mode 100644 artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/disallowed_ephemeral.yaml create mode 100644 artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed.yaml create mode 100644 artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed_container.yaml create mode 100644 artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed_override.yaml create mode 100644 artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed_pod.yaml create mode 100644 artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_disallowed.yaml create mode 100644 artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_disallowed_no_profile.yaml create mode 100644 artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_disallowed_override.yaml create mode 100644 artifacthub/library/pod-security-policy/apparmor/1.1.0/suite.yaml create mode 100644 artifacthub/library/pod-security-policy/apparmor/1.1.0/template.yaml create mode 100644 library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_container.yaml create mode 100644 library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_override.yaml create mode 100644 library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_pod.yaml create mode 100644 library/pod-security-policy/apparmor/samples/psp-apparmor/example_disallowed_no_profile.yaml create mode 100644 library/pod-security-policy/apparmor/samples/psp-apparmor/example_disallowed_override.yaml create mode 100644 src/pod-security-policy/apparmor/src.cel diff --git a/artifacthub/library/pod-security-policy/apparmor/1.1.0/artifacthub-pkg.yml b/artifacthub/library/pod-security-policy/apparmor/1.1.0/artifacthub-pkg.yml new file mode 100644 index 000000000..2212230ed --- /dev/null +++ b/artifacthub/library/pod-security-policy/apparmor/1.1.0/artifacthub-pkg.yml @@ -0,0 +1,22 @@ +version: 1.1.0 +name: k8spspapparmor +displayName: App Armor +createdAt: "2024-05-29T23:39:01Z" +description: Configures an allow-list of AppArmor profiles for use by containers. This corresponds to specific annotations applied to a PodSecurityPolicy. For information on AppArmor, see https://kubernetes.io/docs/tutorials/clusters/apparmor/ +digest: d77b1285f65085153e6a6e6ac86dc32195591df467c3162abe8cc6c806cab69a +license: Apache-2.0 +homeURL: https://open-policy-agent.github.io/gatekeeper-library/website/apparmor +keywords: + - gatekeeper + - open-policy-agent + - policies +readme: |- + # App Armor + Configures an allow-list of AppArmor profiles for use by containers. This corresponds to specific annotations applied to a PodSecurityPolicy. For information on AppArmor, see https://kubernetes.io/docs/tutorials/clusters/apparmor/ +install: |- + ### Usage + ```shell + kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/artifacthub/library/pod-security-policy/apparmor/1.1.0/template.yaml + ``` +provider: + name: Gatekeeper Library diff --git a/artifacthub/library/pod-security-policy/apparmor/1.1.0/kustomization.yaml b/artifacthub/library/pod-security-policy/apparmor/1.1.0/kustomization.yaml new file mode 100644 index 000000000..7d70d11b7 --- /dev/null +++ b/artifacthub/library/pod-security-policy/apparmor/1.1.0/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - template.yaml diff --git a/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/constraint.yaml b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/constraint.yaml new file mode 100644 index 000000000..cf9b9d3d1 --- /dev/null +++ b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/constraint.yaml @@ -0,0 +1,12 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPAppArmor +metadata: + name: psp-apparmor +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + allowedProfiles: + - localhost/custom diff --git a/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/disallowed_ephemeral.yaml b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/disallowed_ephemeral.yaml new file mode 100644 index 000000000..cd6fc5f81 --- /dev/null +++ b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/disallowed_ephemeral.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-apparmor-disallowed + annotations: + # apparmor.security.beta.kubernetes.io/pod: unconfined # runtime/default + container.apparmor.security.beta.kubernetes.io/nginx: unconfined + labels: + app: nginx-apparmor +spec: + ephemeralContainers: + - name: nginx + image: nginx diff --git a/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed.yaml b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed.yaml new file mode 100644 index 000000000..204fa00b5 --- /dev/null +++ b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-apparmor-allowed + annotations: + # apparmor.security.beta.kubernetes.io/pod: unconfined # runtime/default + container.apparmor.security.beta.kubernetes.io/nginx: localhost/custom + labels: + app: nginx-apparmor +spec: + containers: + - name: nginx + image: nginx diff --git a/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed_container.yaml b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed_container.yaml new file mode 100644 index 000000000..3d28665d5 --- /dev/null +++ b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed_container.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-apparmor-allowed + labels: + app: nginx-apparmor +spec: + containers: + - name: nginx + image: nginx + securityContext: + appArmorProfile: + type: "Localhost" + localhostProfile: "custom" diff --git a/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed_override.yaml b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed_override.yaml new file mode 100644 index 000000000..171694985 --- /dev/null +++ b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed_override.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-apparmor-allowed + labels: + app: nginx-apparmor +spec: + securityContext: + appArmorProfile: + type: "Unconfined" + containers: + - name: nginx + image: nginx + securityContext: + appArmorProfile: + type: "Localhost" + localhostProfile: "custom" diff --git a/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed_pod.yaml b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed_pod.yaml new file mode 100644 index 000000000..6f71d6b3c --- /dev/null +++ b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_allowed_pod.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-apparmor-allowed + labels: + app: nginx-apparmor +spec: + securityContext: + appArmorProfile: + type: "Localhost" + localhostProfile: "custom" + containers: + - name: nginx + image: nginx diff --git a/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_disallowed.yaml b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_disallowed.yaml new file mode 100644 index 000000000..8ffdaf768 --- /dev/null +++ b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_disallowed.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-apparmor-disallowed + annotations: + # apparmor.security.beta.kubernetes.io/pod: unconfined # runtime/default + container.apparmor.security.beta.kubernetes.io/nginx: unconfined + labels: + app: nginx-apparmor +spec: + containers: + - name: nginx + image: nginx diff --git a/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_disallowed_no_profile.yaml b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_disallowed_no_profile.yaml new file mode 100644 index 000000000..4e99cd25d --- /dev/null +++ b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_disallowed_no_profile.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-apparmor-disallowed + labels: + app: nginx-apparmor +spec: + containers: + - name: nginx + image: nginx diff --git a/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_disallowed_override.yaml b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_disallowed_override.yaml new file mode 100644 index 000000000..aa4c98301 --- /dev/null +++ b/artifacthub/library/pod-security-policy/apparmor/1.1.0/samples/psp-apparmor/example_disallowed_override.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-apparmor-allowed + labels: + app: nginx-apparmor +spec: + securityContext: + appArmorProfile: + type: "Localhost" + localhostProfile: "custom" + containers: + - name: nginx + image: nginx + securityContext: + appArmorProfile: + type: "Unconfined" diff --git a/artifacthub/library/pod-security-policy/apparmor/1.1.0/suite.yaml b/artifacthub/library/pod-security-policy/apparmor/1.1.0/suite.yaml new file mode 100644 index 000000000..861048ba9 --- /dev/null +++ b/artifacthub/library/pod-security-policy/apparmor/1.1.0/suite.yaml @@ -0,0 +1,41 @@ +kind: Suite +apiVersion: test.gatekeeper.sh/v1alpha1 +metadata: + name: apparmor +tests: + - name: apparmor + template: template.yaml + constraint: samples/psp-apparmor/constraint.yaml + cases: + - name: example-allowed + object: samples/psp-apparmor/example_allowed.yaml + assertions: + - violations: no + - name: example-allowed-container + object: samples/psp-apparmor/example_allowed_container.yaml + assertions: + - violations: no + - name: example-allowed-pod + object: samples/psp-apparmor/example_allowed_pod.yaml + assertions: + - violations: no + - name: example-allowed-override + object: samples/psp-apparmor/example_allowed_override.yaml + assertions: + - violations: no + - name: example-disallowed + object: samples/psp-apparmor/example_disallowed.yaml + assertions: + - violations: yes + - name: example-disallowed-override + object: samples/psp-apparmor/example_disallowed_override.yaml + assertions: + - violations: yes + - name: example-disallowed-no-profile + object: samples/psp-apparmor/example_disallowed_no_profile.yaml + assertions: + - violations: yes + - name: disallowed-ephemeral + object: samples/psp-apparmor/disallowed_ephemeral.yaml + assertions: + - violations: yes diff --git a/artifacthub/library/pod-security-policy/apparmor/1.1.0/template.yaml b/artifacthub/library/pod-security-policy/apparmor/1.1.0/template.yaml new file mode 100644 index 000000000..63aa13f68 --- /dev/null +++ b/artifacthub/library/pod-security-policy/apparmor/1.1.0/template.yaml @@ -0,0 +1,218 @@ +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: k8spspapparmor + annotations: + metadata.gatekeeper.sh/title: "App Armor" + metadata.gatekeeper.sh/version: 1.1.0 + description: >- + Configures an allow-list of AppArmor profiles for use by containers. + This corresponds to specific annotations applied to a PodSecurityPolicy. + For information on AppArmor, see + https://kubernetes.io/docs/tutorials/clusters/apparmor/ +spec: + crd: + spec: + names: + kind: K8sPSPAppArmor + validation: + # Schema for the `parameters` field + openAPIV3Schema: + type: object + description: >- + Configures an allow-list of AppArmor profiles for use by containers. + This corresponds to specific annotations applied to a PodSecurityPolicy. + For information on AppArmor, see + https://kubernetes.io/docs/tutorials/clusters/apparmor/ + 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 + allowedProfiles: + description: "An array of AppArmor profiles. Examples: `runtime/default`, `unconfined`." + type: array + items: + type: string + targets: + - target: admission.k8s.gatekeeper.sh + code: + - engine: K8sNativeValidation + source: + variables: + - name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' + - name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' + - name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' + - name: podAppArmor + expression: 'has(variables.anyObject.spec.securityContext) && has(variables.anyObject.spec.securityContext.appArmorProfile) ? variables.anyObject.spec.securityContext.appArmorProfile : null' + - name: canonicalPodAppArmor + expression: | + variables.podAppArmor == null ? "runtime/default" : + variables.podAppArmor.type == "RuntimeDefault" ? "runtime/default" : + variables.podAppArmor.type == "Unconfined" ? "unconfined" : + variables.podAppArmor.type == "Localhost" ? "localhost/" + variables.podAppArmor.localhostProfile : "" + # break this mapping up by container type (regular/init/ephemeral) to avoid problems with name collisions, + # which may be a problem when running shift-left (no K8s API server to enforce uniqueness of container names) + - name: appArmorByContainer + expression: | + variables.containers.map(container, [container.name, + has(container.securityContext) && has(container.securityContext.appArmorProfile) ? + (container.securityContext.appArmorProfile.type == "RuntimeDefault" ? "runtime/default" : + container.securityContext.appArmorProfile.type == "Unconfined" ? "unconfined" : + container.securityContext.appArmorProfile.type == "Localhost" ? "localhost/" + container.securityContext.appArmorProfile.localhostProfile : "") : + has(variables.anyObject.metadata.annotations) && ("container.apparmor.security.beta.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations ? + variables.anyObject.metadata.annotations["container.apparmor.security.beta.kubernetes.io/" + container.name] : + variables.canonicalPodAppArmor + ]) + - name: appArmorByInitContainer + expression: | + variables.initContainers.map(container, [container.name, + has(container.securityContext) && has(container.securityContext.appArmorProfile) ? + (container.securityContext.appArmorProfile.type == "RuntimeDefault" ? "runtime/default" : + container.securityContext.appArmorProfile.type == "Unconfined" ? "unconfined" : + container.securityContext.appArmorProfile.type == "Localhost" ? "localhost/" + container.securityContext.appArmorProfile.localhostProfile : "") : + has(variables.anyObject.metadata.annotations) && ("container.apparmor.security.beta.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations ? + variables.anyObject.metadata.annotations["container.apparmor.security.beta.kubernetes.io/" + container.name] : + variables.canonicalPodAppArmor + ]) + - name: appArmorByEphemeralContainer + expression: | + variables.ephemeralContainers.map(container, [container.name, + has(container.securityContext) && has(container.securityContext.appArmorProfile) ? + (container.securityContext.appArmorProfile.type == "RuntimeDefault" ? "runtime/default" : + container.securityContext.appArmorProfile.type == "Unconfined" ? "unconfined" : + container.securityContext.appArmorProfile.type == "Localhost" ? "localhost/" + container.securityContext.appArmorProfile.localhostProfile : "") : + has(variables.anyObject.metadata.annotations) && ("container.apparmor.security.beta.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations ? + variables.anyObject.metadata.annotations["container.apparmor.security.beta.kubernetes.io/" + container.name] : + variables.canonicalPodAppArmor + ]) + - 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)) + ).map(container, container.image) + validations: + - expression: | + variables.containers.all(container, + (container.image in variables.exemptImages) || + variables.appArmorByContainer.exists(pair, pair[0] == container.name && pair[1] in variables.params.allowedProfiles) + ) + messageExpression: '"AppArmor profile is not allowed. Allowed Profiles: " + variables.params.allowedProfiles.join(", ")' + - expression: | + variables.initContainers.all(container, + (container.image in variables.exemptImages) || + variables.appArmorByInitContainer.exists(pair, pair[0] == container.name && pair[1] in variables.params.allowedProfiles) + ) + messageExpression: '"AppArmor profile is not allowed. Allowed Profiles: " + variables.params.allowedProfiles.join(", ")' + - expression: | + variables.ephemeralContainers.all(container, + (container.image in variables.exemptImages) || + variables.appArmorByEphemeralContainer.exists(pair, pair[0] == container.name && pair[1] in variables.params.allowedProfiles) + ) + messageExpression: '"AppArmor profile is not allowed. Allowed Profiles: " + variables.params.allowedProfiles.join(", ")' + - engine: Rego + source: + rego: | + package k8spspapparmor + + import data.lib.exempt_container.is_exempt + + violation[{"msg": msg, "details": {}}] { + container := input_containers[_] + not is_exempt(container) + not input_apparmor_allowed(input.review.object, container) + msg := sprintf("AppArmor profile is not allowed, pod: %v, container: %v. Allowed profiles: %v", [input.review.object.metadata.name, container.name, input.parameters.allowedProfiles]) + } + + input_apparmor_allowed(pod, container) { + get_apparmor_profile(pod, container) == input.parameters.allowedProfiles[_] + } + + input_containers[c] { + c := input.review.object.spec.containers[_] + } + input_containers[c] { + c := input.review.object.spec.initContainers[_] + } + input_containers[c] { + c := input.review.object.spec.ephemeralContainers[_] + } + + get_apparmor_profile(_, container) = out { + profile := object.get(container, ["securityContext", "appArmorProfile"], null) + profile != null + out := canonicalize_apparmor_profile(profile) + } + + get_apparmor_profile(pod, container) = out { + profile := object.get(container, ["securityContext", "appArmorProfile"], null) + profile == null + out := pod.metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])] + } + + get_apparmor_profile(pod, container) = out { + profile := object.get(container, ["securityContext", "appArmorProfile"], null) + profile == null + not pod.metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])] + out := canonicalize_apparmor_profile(object.get(pod, ["spec", "securityContext", "appArmorProfile"], null)) + } + + canonicalize_apparmor_profile(profile) = out { + profile.type == "RuntimeDefault" + out := "runtime/default" + } + + canonicalize_apparmor_profile(profile) = out { + profile.type == "Unconfined" + out := "unconfined" + } + + canonicalize_apparmor_profile(profile) = out { + profile.type = "Localhost" + out := sprintf("localhost/%s", [profile.localhostProfile]) + } + + canonicalize_apparmor_profile(profile) = out { + profile == null + out := "runtime/default" + } + libs: + - | + 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) + } + diff --git a/library/pod-security-policy/apparmor/samples/psp-apparmor/constraint.yaml b/library/pod-security-policy/apparmor/samples/psp-apparmor/constraint.yaml index 7024f1b2e..cf9b9d3d1 100644 --- a/library/pod-security-policy/apparmor/samples/psp-apparmor/constraint.yaml +++ b/library/pod-security-policy/apparmor/samples/psp-apparmor/constraint.yaml @@ -9,4 +9,4 @@ spec: kinds: ["Pod"] parameters: allowedProfiles: - - runtime/default + - localhost/custom diff --git a/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed.yaml b/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed.yaml index 661f2f00d..204fa00b5 100644 --- a/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed.yaml +++ b/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed.yaml @@ -4,7 +4,7 @@ metadata: name: nginx-apparmor-allowed annotations: # apparmor.security.beta.kubernetes.io/pod: unconfined # runtime/default - container.apparmor.security.beta.kubernetes.io/nginx: runtime/default + container.apparmor.security.beta.kubernetes.io/nginx: localhost/custom labels: app: nginx-apparmor spec: diff --git a/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_container.yaml b/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_container.yaml new file mode 100644 index 000000000..3d28665d5 --- /dev/null +++ b/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_container.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-apparmor-allowed + labels: + app: nginx-apparmor +spec: + containers: + - name: nginx + image: nginx + securityContext: + appArmorProfile: + type: "Localhost" + localhostProfile: "custom" diff --git a/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_override.yaml b/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_override.yaml new file mode 100644 index 000000000..171694985 --- /dev/null +++ b/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_override.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-apparmor-allowed + labels: + app: nginx-apparmor +spec: + securityContext: + appArmorProfile: + type: "Unconfined" + containers: + - name: nginx + image: nginx + securityContext: + appArmorProfile: + type: "Localhost" + localhostProfile: "custom" diff --git a/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_pod.yaml b/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_pod.yaml new file mode 100644 index 000000000..6f71d6b3c --- /dev/null +++ b/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_pod.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-apparmor-allowed + labels: + app: nginx-apparmor +spec: + securityContext: + appArmorProfile: + type: "Localhost" + localhostProfile: "custom" + containers: + - name: nginx + image: nginx diff --git a/library/pod-security-policy/apparmor/samples/psp-apparmor/example_disallowed_no_profile.yaml b/library/pod-security-policy/apparmor/samples/psp-apparmor/example_disallowed_no_profile.yaml new file mode 100644 index 000000000..4e99cd25d --- /dev/null +++ b/library/pod-security-policy/apparmor/samples/psp-apparmor/example_disallowed_no_profile.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-apparmor-disallowed + labels: + app: nginx-apparmor +spec: + containers: + - name: nginx + image: nginx diff --git a/library/pod-security-policy/apparmor/samples/psp-apparmor/example_disallowed_override.yaml b/library/pod-security-policy/apparmor/samples/psp-apparmor/example_disallowed_override.yaml new file mode 100644 index 000000000..aa4c98301 --- /dev/null +++ b/library/pod-security-policy/apparmor/samples/psp-apparmor/example_disallowed_override.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-apparmor-allowed + labels: + app: nginx-apparmor +spec: + securityContext: + appArmorProfile: + type: "Localhost" + localhostProfile: "custom" + containers: + - name: nginx + image: nginx + securityContext: + appArmorProfile: + type: "Unconfined" diff --git a/library/pod-security-policy/apparmor/suite.yaml b/library/pod-security-policy/apparmor/suite.yaml index a75ebe1eb..861048ba9 100644 --- a/library/pod-security-policy/apparmor/suite.yaml +++ b/library/pod-security-policy/apparmor/suite.yaml @@ -11,10 +11,30 @@ tests: object: samples/psp-apparmor/example_allowed.yaml assertions: - violations: no + - name: example-allowed-container + object: samples/psp-apparmor/example_allowed_container.yaml + assertions: + - violations: no + - name: example-allowed-pod + object: samples/psp-apparmor/example_allowed_pod.yaml + assertions: + - violations: no + - name: example-allowed-override + object: samples/psp-apparmor/example_allowed_override.yaml + assertions: + - violations: no - name: example-disallowed object: samples/psp-apparmor/example_disallowed.yaml assertions: - violations: yes + - name: example-disallowed-override + object: samples/psp-apparmor/example_disallowed_override.yaml + assertions: + - violations: yes + - name: example-disallowed-no-profile + object: samples/psp-apparmor/example_disallowed_no_profile.yaml + assertions: + - violations: yes - name: disallowed-ephemeral object: samples/psp-apparmor/disallowed_ephemeral.yaml assertions: diff --git a/library/pod-security-policy/apparmor/template.yaml b/library/pod-security-policy/apparmor/template.yaml index babad1318..63aa13f68 100644 --- a/library/pod-security-policy/apparmor/template.yaml +++ b/library/pod-security-policy/apparmor/template.yaml @@ -4,7 +4,7 @@ metadata: name: k8spspapparmor annotations: metadata.gatekeeper.sh/title: "App Armor" - metadata.gatekeeper.sh/version: 1.0.0 + metadata.gatekeeper.sh/version: 1.1.0 description: >- Configures an allow-list of AppArmor profiles for use by containers. This corresponds to specific annotations applied to a PodSecurityPolicy. @@ -42,58 +42,177 @@ spec: type: string targets: - target: admission.k8s.gatekeeper.sh - rego: | - package k8spspapparmor - - import data.lib.exempt_container.is_exempt - - violation[{"msg": msg, "details": {}}] { - metadata := input.review.object.metadata - container := input_containers[_] - not is_exempt(container) - not input_apparmor_allowed(container, metadata) - msg := sprintf("AppArmor profile is not allowed, pod: %v, container: %v. Allowed profiles: %v", [input.review.object.metadata.name, container.name, input.parameters.allowedProfiles]) - } - - input_apparmor_allowed(container, metadata) { - get_annotation_for(container, metadata) == input.parameters.allowedProfiles[_] - } - - input_containers[c] { - c := input.review.object.spec.containers[_] - } - input_containers[c] { - c := input.review.object.spec.initContainers[_] - } - input_containers[c] { - c := input.review.object.spec.ephemeralContainers[_] - } - - get_annotation_for(container, metadata) = out { - out = metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])] - } - get_annotation_for(container, metadata) = out { - not metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])] - out = "runtime/default" - } - libs: - - | - 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) - } + code: + - engine: K8sNativeValidation + source: + variables: + - name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' + - name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' + - name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' + - name: podAppArmor + expression: 'has(variables.anyObject.spec.securityContext) && has(variables.anyObject.spec.securityContext.appArmorProfile) ? variables.anyObject.spec.securityContext.appArmorProfile : null' + - name: canonicalPodAppArmor + expression: | + variables.podAppArmor == null ? "runtime/default" : + variables.podAppArmor.type == "RuntimeDefault" ? "runtime/default" : + variables.podAppArmor.type == "Unconfined" ? "unconfined" : + variables.podAppArmor.type == "Localhost" ? "localhost/" + variables.podAppArmor.localhostProfile : "" + # break this mapping up by container type (regular/init/ephemeral) to avoid problems with name collisions, + # which may be a problem when running shift-left (no K8s API server to enforce uniqueness of container names) + - name: appArmorByContainer + expression: | + variables.containers.map(container, [container.name, + has(container.securityContext) && has(container.securityContext.appArmorProfile) ? + (container.securityContext.appArmorProfile.type == "RuntimeDefault" ? "runtime/default" : + container.securityContext.appArmorProfile.type == "Unconfined" ? "unconfined" : + container.securityContext.appArmorProfile.type == "Localhost" ? "localhost/" + container.securityContext.appArmorProfile.localhostProfile : "") : + has(variables.anyObject.metadata.annotations) && ("container.apparmor.security.beta.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations ? + variables.anyObject.metadata.annotations["container.apparmor.security.beta.kubernetes.io/" + container.name] : + variables.canonicalPodAppArmor + ]) + - name: appArmorByInitContainer + expression: | + variables.initContainers.map(container, [container.name, + has(container.securityContext) && has(container.securityContext.appArmorProfile) ? + (container.securityContext.appArmorProfile.type == "RuntimeDefault" ? "runtime/default" : + container.securityContext.appArmorProfile.type == "Unconfined" ? "unconfined" : + container.securityContext.appArmorProfile.type == "Localhost" ? "localhost/" + container.securityContext.appArmorProfile.localhostProfile : "") : + has(variables.anyObject.metadata.annotations) && ("container.apparmor.security.beta.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations ? + variables.anyObject.metadata.annotations["container.apparmor.security.beta.kubernetes.io/" + container.name] : + variables.canonicalPodAppArmor + ]) + - name: appArmorByEphemeralContainer + expression: | + variables.ephemeralContainers.map(container, [container.name, + has(container.securityContext) && has(container.securityContext.appArmorProfile) ? + (container.securityContext.appArmorProfile.type == "RuntimeDefault" ? "runtime/default" : + container.securityContext.appArmorProfile.type == "Unconfined" ? "unconfined" : + container.securityContext.appArmorProfile.type == "Localhost" ? "localhost/" + container.securityContext.appArmorProfile.localhostProfile : "") : + has(variables.anyObject.metadata.annotations) && ("container.apparmor.security.beta.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations ? + variables.anyObject.metadata.annotations["container.apparmor.security.beta.kubernetes.io/" + container.name] : + variables.canonicalPodAppArmor + ]) + - 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)) + ).map(container, container.image) + validations: + - expression: | + variables.containers.all(container, + (container.image in variables.exemptImages) || + variables.appArmorByContainer.exists(pair, pair[0] == container.name && pair[1] in variables.params.allowedProfiles) + ) + messageExpression: '"AppArmor profile is not allowed. Allowed Profiles: " + variables.params.allowedProfiles.join(", ")' + - expression: | + variables.initContainers.all(container, + (container.image in variables.exemptImages) || + variables.appArmorByInitContainer.exists(pair, pair[0] == container.name && pair[1] in variables.params.allowedProfiles) + ) + messageExpression: '"AppArmor profile is not allowed. Allowed Profiles: " + variables.params.allowedProfiles.join(", ")' + - expression: | + variables.ephemeralContainers.all(container, + (container.image in variables.exemptImages) || + variables.appArmorByEphemeralContainer.exists(pair, pair[0] == container.name && pair[1] in variables.params.allowedProfiles) + ) + messageExpression: '"AppArmor profile is not allowed. Allowed Profiles: " + variables.params.allowedProfiles.join(", ")' + - engine: Rego + source: + rego: | + package k8spspapparmor + + import data.lib.exempt_container.is_exempt + + violation[{"msg": msg, "details": {}}] { + container := input_containers[_] + not is_exempt(container) + not input_apparmor_allowed(input.review.object, container) + msg := sprintf("AppArmor profile is not allowed, pod: %v, container: %v. Allowed profiles: %v", [input.review.object.metadata.name, container.name, input.parameters.allowedProfiles]) + } + + input_apparmor_allowed(pod, container) { + get_apparmor_profile(pod, container) == input.parameters.allowedProfiles[_] + } + + input_containers[c] { + c := input.review.object.spec.containers[_] + } + input_containers[c] { + c := input.review.object.spec.initContainers[_] + } + input_containers[c] { + c := input.review.object.spec.ephemeralContainers[_] + } + + get_apparmor_profile(_, container) = out { + profile := object.get(container, ["securityContext", "appArmorProfile"], null) + profile != null + out := canonicalize_apparmor_profile(profile) + } + + get_apparmor_profile(pod, container) = out { + profile := object.get(container, ["securityContext", "appArmorProfile"], null) + profile == null + out := pod.metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])] + } + + get_apparmor_profile(pod, container) = out { + profile := object.get(container, ["securityContext", "appArmorProfile"], null) + profile == null + not pod.metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])] + out := canonicalize_apparmor_profile(object.get(pod, ["spec", "securityContext", "appArmorProfile"], null)) + } + + canonicalize_apparmor_profile(profile) = out { + profile.type == "RuntimeDefault" + out := "runtime/default" + } + + canonicalize_apparmor_profile(profile) = out { + profile.type == "Unconfined" + out := "unconfined" + } + + canonicalize_apparmor_profile(profile) = out { + profile.type = "Localhost" + out := sprintf("localhost/%s", [profile.localhostProfile]) + } + + canonicalize_apparmor_profile(profile) = out { + profile == null + out := "runtime/default" + } + libs: + - | + 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) + } + diff --git a/src/pod-security-policy/apparmor/constraint.tmpl b/src/pod-security-policy/apparmor/constraint.tmpl index 7951cb5ef..3bf86f42b 100644 --- a/src/pod-security-policy/apparmor/constraint.tmpl +++ b/src/pod-security-policy/apparmor/constraint.tmpl @@ -4,7 +4,7 @@ metadata: name: k8spspapparmor annotations: metadata.gatekeeper.sh/title: "App Armor" - metadata.gatekeeper.sh/version: 1.0.0 + metadata.gatekeeper.sh/version: 1.1.0 description: >- Configures an allow-list of AppArmor profiles for use by containers. This corresponds to specific annotations applied to a PodSecurityPolicy. @@ -42,8 +42,15 @@ spec: type: string targets: - target: admission.k8s.gatekeeper.sh - rego: | -{{ file.Read "src/pod-security-policy/apparmor/src.rego" | strings.Indent 8 | strings.TrimSuffix "\n" }} - libs: - - | -{{ file.Read "src/pod-security-policy/apparmor/lib_exempt_container.rego" | strings.Indent 10 | strings.TrimSuffix "\n" }} + code: + - engine: K8sNativeValidation + source: +{{ file.Read "src/pod-security-policy/apparmor/src.cel" | strings.Indent 10 | strings.TrimSuffix "\n" }} + - engine: Rego + source: + rego: | +{{ file.Read "src/pod-security-policy/apparmor/src.rego" | strings.Indent 12 | strings.TrimSuffix "\n" }} + libs: + - | +{{ file.Read "src/pod-security-policy/apparmor/lib_exempt_container.rego" | strings.Indent 12 | strings.TrimSuffix "\n" }} + diff --git a/src/pod-security-policy/apparmor/src.cel b/src/pod-security-policy/apparmor/src.cel new file mode 100644 index 000000000..25539abca --- /dev/null +++ b/src/pod-security-policy/apparmor/src.cel @@ -0,0 +1,83 @@ +variables: +- name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' +- name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' +- name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' +- name: podAppArmor + expression: 'has(variables.anyObject.spec.securityContext) && has(variables.anyObject.spec.securityContext.appArmorProfile) ? variables.anyObject.spec.securityContext.appArmorProfile : null' +- name: canonicalPodAppArmor + expression: | + variables.podAppArmor == null ? "runtime/default" : + variables.podAppArmor.type == "RuntimeDefault" ? "runtime/default" : + variables.podAppArmor.type == "Unconfined" ? "unconfined" : + variables.podAppArmor.type == "Localhost" ? "localhost/" + variables.podAppArmor.localhostProfile : "" +# break this mapping up by container type (regular/init/ephemeral) to avoid problems with name collisions, +# which may be a problem when running shift-left (no K8s API server to enforce uniqueness of container names) +- name: appArmorByContainer + expression: | + variables.containers.map(container, [container.name, + has(container.securityContext) && has(container.securityContext.appArmorProfile) ? + (container.securityContext.appArmorProfile.type == "RuntimeDefault" ? "runtime/default" : + container.securityContext.appArmorProfile.type == "Unconfined" ? "unconfined" : + container.securityContext.appArmorProfile.type == "Localhost" ? "localhost/" + container.securityContext.appArmorProfile.localhostProfile : "") : + has(variables.anyObject.metadata.annotations) && ("container.apparmor.security.beta.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations ? + variables.anyObject.metadata.annotations["container.apparmor.security.beta.kubernetes.io/" + container.name] : + variables.canonicalPodAppArmor + ]) +- name: appArmorByInitContainer + expression: | + variables.initContainers.map(container, [container.name, + has(container.securityContext) && has(container.securityContext.appArmorProfile) ? + (container.securityContext.appArmorProfile.type == "RuntimeDefault" ? "runtime/default" : + container.securityContext.appArmorProfile.type == "Unconfined" ? "unconfined" : + container.securityContext.appArmorProfile.type == "Localhost" ? "localhost/" + container.securityContext.appArmorProfile.localhostProfile : "") : + has(variables.anyObject.metadata.annotations) && ("container.apparmor.security.beta.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations ? + variables.anyObject.metadata.annotations["container.apparmor.security.beta.kubernetes.io/" + container.name] : + variables.canonicalPodAppArmor + ]) +- name: appArmorByEphemeralContainer + expression: | + variables.ephemeralContainers.map(container, [container.name, + has(container.securityContext) && has(container.securityContext.appArmorProfile) ? + (container.securityContext.appArmorProfile.type == "RuntimeDefault" ? "runtime/default" : + container.securityContext.appArmorProfile.type == "Unconfined" ? "unconfined" : + container.securityContext.appArmorProfile.type == "Localhost" ? "localhost/" + container.securityContext.appArmorProfile.localhostProfile : "") : + has(variables.anyObject.metadata.annotations) && ("container.apparmor.security.beta.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations ? + variables.anyObject.metadata.annotations["container.apparmor.security.beta.kubernetes.io/" + container.name] : + variables.canonicalPodAppArmor + ]) +- 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)) + ).map(container, container.image) +validations: +- expression: | + variables.containers.all(container, + (container.image in variables.exemptImages) || + variables.appArmorByContainer.exists(pair, pair[0] == container.name && pair[1] in variables.params.allowedProfiles) + ) + messageExpression: '"AppArmor profile is not allowed. Allowed Profiles: " + variables.params.allowedProfiles.join(", ")' +- expression: | + variables.initContainers.all(container, + (container.image in variables.exemptImages) || + variables.appArmorByInitContainer.exists(pair, pair[0] == container.name && pair[1] in variables.params.allowedProfiles) + ) + messageExpression: '"AppArmor profile is not allowed. Allowed Profiles: " + variables.params.allowedProfiles.join(", ")' +- expression: | + variables.ephemeralContainers.all(container, + (container.image in variables.exemptImages) || + variables.appArmorByEphemeralContainer.exists(pair, pair[0] == container.name && pair[1] in variables.params.allowedProfiles) + ) + messageExpression: '"AppArmor profile is not allowed. Allowed Profiles: " + variables.params.allowedProfiles.join(", ")' \ No newline at end of file diff --git a/src/pod-security-policy/apparmor/src.rego b/src/pod-security-policy/apparmor/src.rego index 613df8030..ef4e3fb76 100644 --- a/src/pod-security-policy/apparmor/src.rego +++ b/src/pod-security-policy/apparmor/src.rego @@ -3,15 +3,14 @@ package k8spspapparmor import data.lib.exempt_container.is_exempt violation[{"msg": msg, "details": {}}] { - metadata := input.review.object.metadata container := input_containers[_] not is_exempt(container) - not input_apparmor_allowed(container, metadata) + not input_apparmor_allowed(input.review.object, container) msg := sprintf("AppArmor profile is not allowed, pod: %v, container: %v. Allowed profiles: %v", [input.review.object.metadata.name, container.name, input.parameters.allowedProfiles]) } -input_apparmor_allowed(container, metadata) { - get_annotation_for(container, metadata) == input.parameters.allowedProfiles[_] +input_apparmor_allowed(pod, container) { + get_apparmor_profile(pod, container) == input.parameters.allowedProfiles[_] } input_containers[c] { @@ -24,10 +23,41 @@ input_containers[c] { c := input.review.object.spec.ephemeralContainers[_] } -get_annotation_for(container, metadata) = out { - out = metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])] +get_apparmor_profile(_, container) = out { + profile := object.get(container, ["securityContext", "appArmorProfile"], null) + profile != null + out := canonicalize_apparmor_profile(profile) } -get_annotation_for(container, metadata) = out { - not metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])] - out = "runtime/default" + +get_apparmor_profile(pod, container) = out { + profile := object.get(container, ["securityContext", "appArmorProfile"], null) + profile == null + out := pod.metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])] +} + +get_apparmor_profile(pod, container) = out { + profile := object.get(container, ["securityContext", "appArmorProfile"], null) + profile == null + not pod.metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])] + out := canonicalize_apparmor_profile(object.get(pod, ["spec", "securityContext", "appArmorProfile"], null)) +} + +canonicalize_apparmor_profile(profile) = out { + profile.type == "RuntimeDefault" + out := "runtime/default" +} + +canonicalize_apparmor_profile(profile) = out { + profile.type == "Unconfined" + out := "unconfined" +} + +canonicalize_apparmor_profile(profile) = out { + profile.type = "Localhost" + out := sprintf("localhost/%s", [profile.localhostProfile]) +} + +canonicalize_apparmor_profile(profile) = out { + profile == null + out := "runtime/default" } diff --git a/website/docs/validation/apparmor.md b/website/docs/validation/apparmor.md index c1e2c16d8..6ed740ddf 100644 --- a/website/docs/validation/apparmor.md +++ b/website/docs/validation/apparmor.md @@ -16,7 +16,7 @@ metadata: name: k8spspapparmor annotations: metadata.gatekeeper.sh/title: "App Armor" - metadata.gatekeeper.sh/version: 1.0.0 + metadata.gatekeeper.sh/version: 1.1.0 description: >- Configures an allow-list of AppArmor profiles for use by containers. This corresponds to specific annotations applied to a PodSecurityPolicy. @@ -54,61 +54,180 @@ spec: type: string targets: - target: admission.k8s.gatekeeper.sh - rego: | - package k8spspapparmor - - import data.lib.exempt_container.is_exempt - - violation[{"msg": msg, "details": {}}] { - metadata := input.review.object.metadata - container := input_containers[_] - not is_exempt(container) - not input_apparmor_allowed(container, metadata) - msg := sprintf("AppArmor profile is not allowed, pod: %v, container: %v. Allowed profiles: %v", [input.review.object.metadata.name, container.name, input.parameters.allowedProfiles]) - } - - input_apparmor_allowed(container, metadata) { - get_annotation_for(container, metadata) == input.parameters.allowedProfiles[_] - } - - input_containers[c] { - c := input.review.object.spec.containers[_] - } - input_containers[c] { - c := input.review.object.spec.initContainers[_] - } - input_containers[c] { - c := input.review.object.spec.ephemeralContainers[_] - } - - get_annotation_for(container, metadata) = out { - out = metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])] - } - get_annotation_for(container, metadata) = out { - not metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])] - out = "runtime/default" - } - libs: - - | - 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) - } + code: + - engine: K8sNativeValidation + source: + variables: + - name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' + - name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' + - name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' + - name: podAppArmor + expression: 'has(variables.anyObject.spec.securityContext) && has(variables.anyObject.spec.securityContext.appArmorProfile) ? variables.anyObject.spec.securityContext.appArmorProfile : null' + - name: canonicalPodAppArmor + expression: | + variables.podAppArmor == null ? "runtime/default" : + variables.podAppArmor.type == "RuntimeDefault" ? "runtime/default" : + variables.podAppArmor.type == "Unconfined" ? "unconfined" : + variables.podAppArmor.type == "Localhost" ? "localhost/" + variables.podAppArmor.localhostProfile : "" + # break this mapping up by container type (regular/init/ephemeral) to avoid problems with name collisions, + # which may be a problem when running shift-left (no K8s API server to enforce uniqueness of container names) + - name: appArmorByContainer + expression: | + variables.containers.map(container, [container.name, + has(container.securityContext) && has(container.securityContext.appArmorProfile) ? + (container.securityContext.appArmorProfile.type == "RuntimeDefault" ? "runtime/default" : + container.securityContext.appArmorProfile.type == "Unconfined" ? "unconfined" : + container.securityContext.appArmorProfile.type == "Localhost" ? "localhost/" + container.securityContext.appArmorProfile.localhostProfile : "") : + has(variables.anyObject.metadata.annotations) && ("container.apparmor.security.beta.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations ? + variables.anyObject.metadata.annotations["container.apparmor.security.beta.kubernetes.io/" + container.name] : + variables.canonicalPodAppArmor + ]) + - name: appArmorByInitContainer + expression: | + variables.initContainers.map(container, [container.name, + has(container.securityContext) && has(container.securityContext.appArmorProfile) ? + (container.securityContext.appArmorProfile.type == "RuntimeDefault" ? "runtime/default" : + container.securityContext.appArmorProfile.type == "Unconfined" ? "unconfined" : + container.securityContext.appArmorProfile.type == "Localhost" ? "localhost/" + container.securityContext.appArmorProfile.localhostProfile : "") : + has(variables.anyObject.metadata.annotations) && ("container.apparmor.security.beta.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations ? + variables.anyObject.metadata.annotations["container.apparmor.security.beta.kubernetes.io/" + container.name] : + variables.canonicalPodAppArmor + ]) + - name: appArmorByEphemeralContainer + expression: | + variables.ephemeralContainers.map(container, [container.name, + has(container.securityContext) && has(container.securityContext.appArmorProfile) ? + (container.securityContext.appArmorProfile.type == "RuntimeDefault" ? "runtime/default" : + container.securityContext.appArmorProfile.type == "Unconfined" ? "unconfined" : + container.securityContext.appArmorProfile.type == "Localhost" ? "localhost/" + container.securityContext.appArmorProfile.localhostProfile : "") : + has(variables.anyObject.metadata.annotations) && ("container.apparmor.security.beta.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations ? + variables.anyObject.metadata.annotations["container.apparmor.security.beta.kubernetes.io/" + container.name] : + variables.canonicalPodAppArmor + ]) + - 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)) + ).map(container, container.image) + validations: + - expression: | + variables.containers.all(container, + (container.image in variables.exemptImages) || + variables.appArmorByContainer.exists(pair, pair[0] == container.name && pair[1] in variables.params.allowedProfiles) + ) + messageExpression: '"AppArmor profile is not allowed. Allowed Profiles: " + variables.params.allowedProfiles.join(", ")' + - expression: | + variables.initContainers.all(container, + (container.image in variables.exemptImages) || + variables.appArmorByInitContainer.exists(pair, pair[0] == container.name && pair[1] in variables.params.allowedProfiles) + ) + messageExpression: '"AppArmor profile is not allowed. Allowed Profiles: " + variables.params.allowedProfiles.join(", ")' + - expression: | + variables.ephemeralContainers.all(container, + (container.image in variables.exemptImages) || + variables.appArmorByEphemeralContainer.exists(pair, pair[0] == container.name && pair[1] in variables.params.allowedProfiles) + ) + messageExpression: '"AppArmor profile is not allowed. Allowed Profiles: " + variables.params.allowedProfiles.join(", ")' + - engine: Rego + source: + rego: | + package k8spspapparmor + + import data.lib.exempt_container.is_exempt + + violation[{"msg": msg, "details": {}}] { + container := input_containers[_] + not is_exempt(container) + not input_apparmor_allowed(input.review.object, container) + msg := sprintf("AppArmor profile is not allowed, pod: %v, container: %v. Allowed profiles: %v", [input.review.object.metadata.name, container.name, input.parameters.allowedProfiles]) + } + + input_apparmor_allowed(pod, container) { + get_apparmor_profile(pod, container) == input.parameters.allowedProfiles[_] + } + + input_containers[c] { + c := input.review.object.spec.containers[_] + } + input_containers[c] { + c := input.review.object.spec.initContainers[_] + } + input_containers[c] { + c := input.review.object.spec.ephemeralContainers[_] + } + + get_apparmor_profile(_, container) = out { + profile := object.get(container, ["securityContext", "appArmorProfile"], null) + profile != null + out := canonicalize_apparmor_profile(profile) + } + + get_apparmor_profile(pod, container) = out { + profile := object.get(container, ["securityContext", "appArmorProfile"], null) + profile == null + out := pod.metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])] + } + + get_apparmor_profile(pod, container) = out { + profile := object.get(container, ["securityContext", "appArmorProfile"], null) + profile == null + not pod.metadata.annotations[sprintf("container.apparmor.security.beta.kubernetes.io/%v", [container.name])] + out := canonicalize_apparmor_profile(object.get(pod, ["spec", "securityContext", "appArmorProfile"], null)) + } + + canonicalize_apparmor_profile(profile) = out { + profile.type == "RuntimeDefault" + out := "runtime/default" + } + + canonicalize_apparmor_profile(profile) = out { + profile.type == "Unconfined" + out := "unconfined" + } + + canonicalize_apparmor_profile(profile) = out { + profile.type = "Localhost" + out := sprintf("localhost/%s", [profile.localhostProfile]) + } + + canonicalize_apparmor_profile(profile) = out { + profile == null + out := "runtime/default" + } + libs: + - | + 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) + } + ``` @@ -135,7 +254,7 @@ spec: kinds: ["Pod"] parameters: allowedProfiles: - - runtime/default + - localhost/custom ``` @@ -157,7 +276,7 @@ metadata: name: nginx-apparmor-allowed annotations: # apparmor.security.beta.kubernetes.io/pod: unconfined # runtime/default - container.apparmor.security.beta.kubernetes.io/nginx: runtime/default + container.apparmor.security.beta.kubernetes.io/nginx: localhost/custom labels: app: nginx-apparmor spec: @@ -173,6 +292,93 @@ Usage kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed.yaml ``` + +
+example-allowed-container + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-apparmor-allowed + labels: + app: nginx-apparmor +spec: + containers: + - name: nginx + image: nginx + securityContext: + appArmorProfile: + type: "Localhost" + localhostProfile: "custom" + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_container.yaml +``` + +
+
+example-allowed-pod + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-apparmor-allowed + labels: + app: nginx-apparmor +spec: + securityContext: + appArmorProfile: + type: "Localhost" + localhostProfile: "custom" + containers: + - name: nginx + image: nginx + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_pod.yaml +``` + +
+
+example-allowed-override + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-apparmor-allowed + labels: + app: nginx-apparmor +spec: + securityContext: + appArmorProfile: + type: "Unconfined" + containers: + - name: nginx + image: nginx + securityContext: + appArmorProfile: + type: "Localhost" + localhostProfile: "custom" + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/apparmor/samples/psp-apparmor/example_allowed_override.yaml +``` +
example-disallowed @@ -200,6 +406,61 @@ Usage kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/apparmor/samples/psp-apparmor/example_disallowed.yaml ``` +
+
+example-disallowed-override + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-apparmor-allowed + labels: + app: nginx-apparmor +spec: + securityContext: + appArmorProfile: + type: "Localhost" + localhostProfile: "custom" + containers: + - name: nginx + image: nginx + securityContext: + appArmorProfile: + type: "Unconfined" + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/apparmor/samples/psp-apparmor/example_disallowed_override.yaml +``` + +
+
+example-disallowed-no-profile + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-apparmor-disallowed + labels: + app: nginx-apparmor +spec: + containers: + - name: nginx + image: nginx + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/apparmor/samples/psp-apparmor/example_disallowed_no_profile.yaml +``` +
disallowed-ephemeral From 799d77bd46a6a016bb00c5a83011cf7004b76d7c Mon Sep 17 00:00:00 2001 From: Julian Katz Date: Wed, 4 Sep 2024 14:02:46 -0700 Subject: [PATCH 12/19] fix(k8sPSPHostFilesystem): null-check on volumes (#595) A K8sNativeValidation implementation of this template was added in #547. When testing it, I found that a Pod lacking the `volumes` field would yield a null-pointer style error on the CEL expression: ``` unexpected number of violations: got 1 violations but want none: got messages [expression '(has(request.operation) && request.operation == "UPDATE") || size(variables.badHostPaths) == 0' resulted in error: composited variable "badHostPaths" fails to evaluate: composited variable "volumes" fails to evaluate: no such key: volumes] ``` This PR adds a `has(` check to prevent that null pointer, and adds a suite test case that fails without the code change. Signed-off-by: juliankatz --- .../host-filesystem/1.1.1/artifacthub-pkg.yml | 22 +++ .../host-filesystem/1.1.1/kustomization.yaml | 2 + .../samples/no-host-paths/constraint.yaml | 9 + .../example_allowed_no_volumes.yaml | 8 + .../psp-host-filesystem/constraint.yaml | 13 ++ .../disallowed_ephemeral.yaml | 16 ++ .../psp-host-filesystem/example_allowed.yaml | 16 ++ .../example_disallowed.yaml | 16 ++ .../samples/psp-host-filesystem/update.yaml | 23 +++ .../host-filesystem/1.1.1/suite.yaml | 37 ++++ .../host-filesystem/1.1.1/template.yaml | 181 ++++++++++++++++++ .../samples/no-host-paths/constraint.yaml | 9 + .../example_allowed_no_volumes.yaml | 8 + .../host-filesystem/suite.yaml | 12 ++ .../host-filesystem/template.yaml | 4 +- .../host-filesystem/constraint.tmpl | 2 +- .../host-filesystem/src.cel | 4 +- website/docs/validation/host-filesystem.md | 85 +++++++- 18 files changed, 460 insertions(+), 7 deletions(-) create mode 100644 artifacthub/library/pod-security-policy/host-filesystem/1.1.1/artifacthub-pkg.yml create mode 100644 artifacthub/library/pod-security-policy/host-filesystem/1.1.1/kustomization.yaml create mode 100644 artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/no-host-paths/constraint.yaml create mode 100644 artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/no-host-paths/example_allowed_no_volumes.yaml create mode 100644 artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/psp-host-filesystem/constraint.yaml create mode 100644 artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/psp-host-filesystem/disallowed_ephemeral.yaml create mode 100644 artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/psp-host-filesystem/example_allowed.yaml create mode 100644 artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/psp-host-filesystem/example_disallowed.yaml create mode 100644 artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/psp-host-filesystem/update.yaml create mode 100644 artifacthub/library/pod-security-policy/host-filesystem/1.1.1/suite.yaml create mode 100644 artifacthub/library/pod-security-policy/host-filesystem/1.1.1/template.yaml create mode 100644 library/pod-security-policy/host-filesystem/samples/no-host-paths/constraint.yaml create mode 100644 library/pod-security-policy/host-filesystem/samples/no-host-paths/example_allowed_no_volumes.yaml diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/artifacthub-pkg.yml b/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/artifacthub-pkg.yml new file mode 100644 index 000000000..aabcf908c --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/artifacthub-pkg.yml @@ -0,0 +1,22 @@ +version: 1.1.1 +name: k8spsphostfilesystem +displayName: Host Filesystem +createdAt: "2024-09-04T20:18:45Z" +description: Controls usage of the host filesystem. Corresponds to the `allowedHostPaths` field in a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#volumes-and-file-systems +digest: 8181746b3471a38dd2536d511f05b8b49a6170fa99731637515e62ee58909e12 +license: Apache-2.0 +homeURL: https://open-policy-agent.github.io/gatekeeper-library/website/host-filesystem +keywords: + - gatekeeper + - open-policy-agent + - policies +readme: |- + # Host Filesystem + Controls usage of the host filesystem. Corresponds to the `allowedHostPaths` field in a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#volumes-and-file-systems +install: |- + ### Usage + ```shell + kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/template.yaml + ``` +provider: + name: Gatekeeper Library diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/kustomization.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/kustomization.yaml new file mode 100644 index 000000000..7d70d11b7 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - template.yaml diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/no-host-paths/constraint.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/no-host-paths/constraint.yaml new file mode 100644 index 000000000..2e828a7e3 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/no-host-paths/constraint.yaml @@ -0,0 +1,9 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPHostFilesystem +metadata: + name: no-host-paths +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/no-host-paths/example_allowed_no_volumes.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/no-host-paths/example_allowed_no_volumes.yaml new file mode 100644 index 000000000..f7c59253f --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/no-host-paths/example_allowed_no_volumes.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-no-volumes +spec: + containers: + - name: nginx + image: nginx diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/psp-host-filesystem/constraint.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/psp-host-filesystem/constraint.yaml new file mode 100644 index 000000000..7cbd7b824 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/psp-host-filesystem/constraint.yaml @@ -0,0 +1,13 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPHostFilesystem +metadata: + name: psp-host-filesystem +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + allowedHostPaths: + - readOnly: true + pathPrefix: "/foo" diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/psp-host-filesystem/disallowed_ephemeral.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/psp-host-filesystem/disallowed_ephemeral.yaml new file mode 100644 index 000000000..7d18cc85e --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/psp-host-filesystem/disallowed_ephemeral.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-filesystem +spec: + ephemeralContainers: + - name: nginx + image: nginx + volumeMounts: + - mountPath: /cache + name: cache-volume + readOnly: true + volumes: + - name: cache-volume + hostPath: + path: /tmp # directory location on host diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/psp-host-filesystem/example_allowed.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/psp-host-filesystem/example_allowed.yaml new file mode 100644 index 000000000..806101e8c --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/psp-host-filesystem/example_allowed.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-filesystem +spec: + containers: + - name: nginx + image: nginx + volumeMounts: + - mountPath: /cache + name: cache-volume + readOnly: true + volumes: + - name: cache-volume + hostPath: + path: /foo/bar diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/psp-host-filesystem/example_disallowed.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/psp-host-filesystem/example_disallowed.yaml new file mode 100644 index 000000000..51ecf14b5 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/psp-host-filesystem/example_disallowed.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-filesystem +spec: + containers: + - name: nginx + image: nginx + volumeMounts: + - mountPath: /cache + name: cache-volume + readOnly: true + volumes: + - name: cache-volume + hostPath: + path: /tmp # directory location on host diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/psp-host-filesystem/update.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/psp-host-filesystem/update.yaml new file mode 100644 index 000000000..68b28a536 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/samples/psp-host-filesystem/update.yaml @@ -0,0 +1,23 @@ +kind: AdmissionReview +apiVersion: admission.k8s.io/v1beta1 +request: + operation: "UPDATE" + object: + apiVersion: v1 + kind: Pod + metadata: + name: nginx-host-filesystem + labels: + app: nginx-host-filesystem-disallowed + spec: + containers: + - name: nginx + image: nginx + volumeMounts: + - mountPath: /cache + name: cache-volume + readOnly: true + volumes: + - name: cache-volume + hostPath: + path: /tmp # directory location on host diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/suite.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/suite.yaml new file mode 100644 index 000000000..7087a24e4 --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/suite.yaml @@ -0,0 +1,37 @@ +kind: Suite +apiVersion: test.gatekeeper.sh/v1alpha1 +metadata: + name: host-filesystem +tests: + - name: host-filesystem + template: template.yaml + constraint: samples/psp-host-filesystem/constraint.yaml + cases: + - name: example-disallowed + object: samples/psp-host-filesystem/example_disallowed.yaml + assertions: + - violations: yes + - name: example-allowed + object: samples/psp-host-filesystem/example_allowed.yaml + assertions: + - violations: no + - name: disallowed-ephemeral + object: samples/psp-host-filesystem/disallowed_ephemeral.yaml + assertions: + - violations: yes + - name: update + object: samples/psp-host-filesystem/update.yaml + assertions: + - violations: no + - name: no-host-paths + template: template.yaml + constraint: samples/no-host-paths/constraint.yaml + cases: + - name: previously-allowed-path-disallowed + object: samples/psp-host-filesystem/example_allowed.yaml + assertions: + - violations: yes + - name: no-volumes-is-allowed + object: samples/no-host-paths/example_allowed_no_volumes.yaml + assertions: + - violations: no diff --git a/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/template.yaml b/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/template.yaml new file mode 100644 index 000000000..7aaea930d --- /dev/null +++ b/artifacthub/library/pod-security-policy/host-filesystem/1.1.1/template.yaml @@ -0,0 +1,181 @@ +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: k8spsphostfilesystem + annotations: + metadata.gatekeeper.sh/title: "Host Filesystem" + metadata.gatekeeper.sh/version: 1.1.1 + description: >- + Controls usage of the host filesystem. Corresponds to the + `allowedHostPaths` field in a PodSecurityPolicy. For more information, + see + https://kubernetes.io/docs/concepts/policy/pod-security-policy/#volumes-and-file-systems +spec: + crd: + spec: + names: + kind: K8sPSPHostFilesystem + validation: + # Schema for the `parameters` field + openAPIV3Schema: + type: object + description: >- + Controls usage of the host filesystem. Corresponds to the + `allowedHostPaths` field in a PodSecurityPolicy. For more information, + see + https://kubernetes.io/docs/concepts/policy/pod-security-policy/#volumes-and-file-systems + properties: + allowedHostPaths: + type: array + description: "An array of hostpath objects, representing paths and read/write configuration." + items: + type: object + properties: + pathPrefix: + type: string + description: "The path prefix that the host volume must match." + readOnly: + type: boolean + description: "when set to true, any container volumeMounts matching the pathPrefix must include `readOnly: true`." + targets: + - target: admission.k8s.gatekeeper.sh + code: + - engine: K8sNativeValidation + source: + variables: + - name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' + - name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' + - name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' + - name: allContainers + expression: 'variables.containers + variables.initContainers + variables.ephemeralContainers' + - name: allowedPaths + expression: | + !has(variables.params.allowedHostPaths) ? [] : variables.params.allowedHostPaths + - name: volumes + expression: | + !has(variables.anyObject.spec.volumes) ? [] : variables.anyObject.spec.volumes.filter(volume, has(volume.hostPath)) + - name: badHostPaths + expression: | + variables.volumes.filter(volume, + (size(variables.allowedPaths) == 0) || + !(variables.allowedPaths.exists(allowedPath, + volume.hostPath.path.startsWith(allowedPath.pathPrefix) && ( + (!has(allowedPath.readOnly) || !(allowedPath.readOnly)) || + (has(allowedPath.readOnly) && allowedPath.readOnly && !variables.allContainers.exists(c, + c.volumeMounts.exists(m, m.name == volume.name && (!has(m.readOnly) || !m.readOnly))))))) + ).map(volume, "{ hostPath: { path : " + volume.hostPath.path + " }, name: " + volume.name + "}").map(volume, "HostPath volume " + volume + " is not allowed, pod: " + object.metadata.name + ". Allowed path: " + variables.allowedPaths.map(path, path.pathPrefix + ", readOnly: " + (path.readOnly ? "true" : "false") + "}").join(", ")) + validations: + - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.badHostPaths) == 0' + messageExpression: 'variables.badHostPaths.join("\n")' + - engine: Rego + source: + rego: | + package k8spsphostfilesystem + + import data.lib.exclude_update.is_update + + violation[{"msg": msg, "details": {}}] { + # spec.volumes field is immutable. + not is_update(input.review) + + volume := input_hostpath_volumes[_] + allowedPaths := get_allowed_paths(input) + input_hostpath_violation(allowedPaths, volume) + msg := sprintf("HostPath volume %v is not allowed, pod: %v. Allowed path: %v", [volume, input.review.object.metadata.name, allowedPaths]) + } + + input_hostpath_violation(allowedPaths, _) { + # An empty list means all host paths are blocked + allowedPaths == [] + } + input_hostpath_violation(allowedPaths, volume) { + not input_hostpath_allowed(allowedPaths, volume) + } + + get_allowed_paths(arg) = out { + not arg.parameters + out = [] + } + get_allowed_paths(arg) = out { + not arg.parameters.allowedHostPaths + out = [] + } + get_allowed_paths(arg) = out { + out = arg.parameters.allowedHostPaths + } + + input_hostpath_allowed(allowedPaths, volume) { + allowedHostPath := allowedPaths[_] + path_matches(allowedHostPath.pathPrefix, volume.hostPath.path) + not allowedHostPath.readOnly == true + } + + input_hostpath_allowed(allowedPaths, volume) { + allowedHostPath := allowedPaths[_] + path_matches(allowedHostPath.pathPrefix, volume.hostPath.path) + allowedHostPath.readOnly + not writeable_input_volume_mounts(volume.name) + } + + writeable_input_volume_mounts(volume_name) { + container := input_containers[_] + mount := container.volumeMounts[_] + mount.name == volume_name + not mount.readOnly + } + + # This allows "/foo", "/foo/", "/foo/bar" etc., but + # disallows "/fool", "/etc/foo" etc. + path_matches(prefix, path) { + a := path_array(prefix) + b := path_array(path) + prefix_matches(a, b) + } + path_array(p) = out { + p != "/" + out := split(trim(p, "/"), "/") + } + # This handles the special case for "/", since + # split(trim("/", "/"), "/") == [""] + path_array("/") = [] + + prefix_matches(a, b) { + count(a) <= count(b) + not any_not_equal_upto(a, b, count(a)) + } + + any_not_equal_upto(a, b, n) { + a[i] != b[i] + i < n + } + + input_hostpath_volumes[v] { + v := input.review.object.spec.volumes[_] + has_field(v, "hostPath") + } + + # has_field returns whether an object has a field + has_field(object, field) = true { + object[field] + } + input_containers[c] { + c := input.review.object.spec.containers[_] + } + + input_containers[c] { + c := input.review.object.spec.initContainers[_] + } + + input_containers[c] { + c := input.review.object.spec.ephemeralContainers[_] + } + libs: + - | + package lib.exclude_update + + is_update(review) { + review.operation == "UPDATE" + } diff --git a/library/pod-security-policy/host-filesystem/samples/no-host-paths/constraint.yaml b/library/pod-security-policy/host-filesystem/samples/no-host-paths/constraint.yaml new file mode 100644 index 000000000..2e828a7e3 --- /dev/null +++ b/library/pod-security-policy/host-filesystem/samples/no-host-paths/constraint.yaml @@ -0,0 +1,9 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPHostFilesystem +metadata: + name: no-host-paths +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] diff --git a/library/pod-security-policy/host-filesystem/samples/no-host-paths/example_allowed_no_volumes.yaml b/library/pod-security-policy/host-filesystem/samples/no-host-paths/example_allowed_no_volumes.yaml new file mode 100644 index 000000000..f7c59253f --- /dev/null +++ b/library/pod-security-policy/host-filesystem/samples/no-host-paths/example_allowed_no_volumes.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-no-volumes +spec: + containers: + - name: nginx + image: nginx diff --git a/library/pod-security-policy/host-filesystem/suite.yaml b/library/pod-security-policy/host-filesystem/suite.yaml index 5441df8cc..7087a24e4 100644 --- a/library/pod-security-policy/host-filesystem/suite.yaml +++ b/library/pod-security-policy/host-filesystem/suite.yaml @@ -23,3 +23,15 @@ tests: object: samples/psp-host-filesystem/update.yaml assertions: - violations: no + - name: no-host-paths + template: template.yaml + constraint: samples/no-host-paths/constraint.yaml + cases: + - name: previously-allowed-path-disallowed + object: samples/psp-host-filesystem/example_allowed.yaml + assertions: + - violations: yes + - name: no-volumes-is-allowed + object: samples/no-host-paths/example_allowed_no_volumes.yaml + assertions: + - violations: no diff --git a/library/pod-security-policy/host-filesystem/template.yaml b/library/pod-security-policy/host-filesystem/template.yaml index 6158ec1ca..7aaea930d 100644 --- a/library/pod-security-policy/host-filesystem/template.yaml +++ b/library/pod-security-policy/host-filesystem/template.yaml @@ -4,7 +4,7 @@ metadata: name: k8spsphostfilesystem annotations: metadata.gatekeeper.sh/title: "Host Filesystem" - metadata.gatekeeper.sh/version: 1.1.0 + metadata.gatekeeper.sh/version: 1.1.1 description: >- Controls usage of the host filesystem. Corresponds to the `allowedHostPaths` field in a PodSecurityPolicy. For more information, @@ -56,7 +56,7 @@ spec: !has(variables.params.allowedHostPaths) ? [] : variables.params.allowedHostPaths - name: volumes expression: | - variables.anyObject.spec.volumes.filter(volume, has(volume.hostPath)) + !has(variables.anyObject.spec.volumes) ? [] : variables.anyObject.spec.volumes.filter(volume, has(volume.hostPath)) - name: badHostPaths expression: | variables.volumes.filter(volume, diff --git a/src/pod-security-policy/host-filesystem/constraint.tmpl b/src/pod-security-policy/host-filesystem/constraint.tmpl index ad5b446c3..9b8b3c2b9 100644 --- a/src/pod-security-policy/host-filesystem/constraint.tmpl +++ b/src/pod-security-policy/host-filesystem/constraint.tmpl @@ -4,7 +4,7 @@ metadata: name: k8spsphostfilesystem annotations: metadata.gatekeeper.sh/title: "Host Filesystem" - metadata.gatekeeper.sh/version: 1.1.0 + metadata.gatekeeper.sh/version: 1.1.1 description: >- Controls usage of the host filesystem. Corresponds to the `allowedHostPaths` field in a PodSecurityPolicy. For more information, diff --git a/src/pod-security-policy/host-filesystem/src.cel b/src/pod-security-policy/host-filesystem/src.cel index 3497a3e28..a9db6941b 100644 --- a/src/pod-security-policy/host-filesystem/src.cel +++ b/src/pod-security-policy/host-filesystem/src.cel @@ -12,7 +12,7 @@ variables: !has(variables.params.allowedHostPaths) ? [] : variables.params.allowedHostPaths - name: volumes expression: | - variables.anyObject.spec.volumes.filter(volume, has(volume.hostPath)) + !has(variables.anyObject.spec.volumes) ? [] : variables.anyObject.spec.volumes.filter(volume, has(volume.hostPath)) - name: badHostPaths expression: | variables.volumes.filter(volume, @@ -25,4 +25,4 @@ variables: ).map(volume, "{ hostPath: { path : " + volume.hostPath.path + " }, name: " + volume.name + "}").map(volume, "HostPath volume " + volume + " is not allowed, pod: " + object.metadata.name + ". Allowed path: " + variables.allowedPaths.map(path, path.pathPrefix + ", readOnly: " + (path.readOnly ? "true" : "false") + "}").join(", ")) validations: - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.badHostPaths) == 0' - messageExpression: 'variables.badHostPaths.join("\n")' \ No newline at end of file + messageExpression: 'variables.badHostPaths.join("\n")' diff --git a/website/docs/validation/host-filesystem.md b/website/docs/validation/host-filesystem.md index 07704e0bd..57e79f1ab 100644 --- a/website/docs/validation/host-filesystem.md +++ b/website/docs/validation/host-filesystem.md @@ -16,7 +16,7 @@ metadata: name: k8spsphostfilesystem annotations: metadata.gatekeeper.sh/title: "Host Filesystem" - metadata.gatekeeper.sh/version: 1.1.0 + metadata.gatekeeper.sh/version: 1.1.1 description: >- Controls usage of the host filesystem. Corresponds to the `allowedHostPaths` field in a PodSecurityPolicy. For more information, @@ -68,7 +68,7 @@ spec: !has(variables.params.allowedHostPaths) ? [] : variables.params.allowedHostPaths - name: volumes expression: | - variables.anyObject.spec.volumes.filter(volume, has(volume.hostPath)) + !has(variables.anyObject.spec.volumes) ? [] : variables.anyObject.spec.volumes.filter(volume, has(volume.hostPath)) - name: badHostPaths expression: | variables.volumes.filter(volume, @@ -322,4 +322,85 @@ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-
+
+no-host-paths + +
+constraint + +```yaml +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPHostFilesystem +metadata: + name: no-host-paths +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/host-filesystem/samples/no-host-paths/constraint.yaml +``` + +
+ +
+previously-allowed-path-disallowed + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-host-filesystem +spec: + containers: + - name: nginx + image: nginx + volumeMounts: + - mountPath: /cache + name: cache-volume + readOnly: true + volumes: + - name: cache-volume + hostPath: + path: /foo/bar + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/host-filesystem/samples/psp-host-filesystem/example_allowed.yaml +``` + +
+
+no-volumes-is-allowed + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-no-volumes +spec: + containers: + - name: nginx + image: nginx + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/host-filesystem/samples/no-host-paths/example_allowed_no_volumes.yaml +``` + +
+ +
\ No newline at end of file From 7a9e47d11ebf99a153afaf1b35ad72de14958ae9 Mon Sep 17 00:00:00 2001 From: Julian Katz Date: Fri, 6 Sep 2024 17:21:11 -0700 Subject: [PATCH 13/19] chore(k8spspcapabilities): double newlines can cause yamllint breakage (#596) The default value for newlines in the yamllint linter (a commonly used tool for linting yaml) is 2. See https://yamllint.readthedocs.io/en/stable/rules.html#module-yamllint.rules.empty_lines for more info. This PR removes some unnecessary newlines in a rego file that trigger this linter rule. Signed-off-by: juliankatz --- .../capabilities/1.1.1/artifacthub-pkg.yml | 22 ++ .../capabilities/1.1.1/kustomization.yaml | 2 + .../samples/capabilities-demo/constraint.yaml | 14 ++ .../disallowed_ephemeral.yaml | 21 ++ .../capabilities-demo/example_allowed.yaml | 22 ++ .../capabilities-demo/example_disallowed.yaml | 21 ++ .../samples/capabilities-demo/update.yaml | 26 +++ .../capabilities/1.1.1/suite.yaml | 25 ++ .../capabilities/1.1.1/template.yaml | 219 ++++++++++++++++++ .../capabilities/template.yaml | 7 +- .../capabilities/constraint.tmpl | 2 +- src/pod-security-policy/capabilities/src.rego | 5 - website/docs/validation/capabilities.md | 7 +- 13 files changed, 375 insertions(+), 18 deletions(-) create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.1/artifacthub-pkg.yml create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.1/kustomization.yaml create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.1/samples/capabilities-demo/constraint.yaml create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.1/samples/capabilities-demo/disallowed_ephemeral.yaml create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.1/samples/capabilities-demo/example_allowed.yaml create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.1/samples/capabilities-demo/example_disallowed.yaml create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.1/samples/capabilities-demo/update.yaml create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.1/suite.yaml create mode 100644 artifacthub/library/pod-security-policy/capabilities/1.1.1/template.yaml diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.1/artifacthub-pkg.yml b/artifacthub/library/pod-security-policy/capabilities/1.1.1/artifacthub-pkg.yml new file mode 100644 index 000000000..827e4e179 --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.1/artifacthub-pkg.yml @@ -0,0 +1,22 @@ +version: 1.1.1 +name: k8spspcapabilities +displayName: Capabilities +createdAt: "2024-09-07T00:11:09Z" +description: Controls Linux capabilities on containers. Corresponds to the `allowedCapabilities` and `requiredDropCapabilities` fields in a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#capabilities +digest: 5abceb80cc25bb7daf6523ed1ebc485b8be6c763cf478e86a0269c4ce578772c +license: Apache-2.0 +homeURL: https://open-policy-agent.github.io/gatekeeper-library/website/capabilities +keywords: + - gatekeeper + - open-policy-agent + - policies +readme: |- + # Capabilities + Controls Linux capabilities on containers. Corresponds to the `allowedCapabilities` and `requiredDropCapabilities` fields in a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#capabilities +install: |- + ### Usage + ```shell + kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/artifacthub/library/pod-security-policy/capabilities/1.1.1/template.yaml + ``` +provider: + name: Gatekeeper Library diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.1/kustomization.yaml b/artifacthub/library/pod-security-policy/capabilities/1.1.1/kustomization.yaml new file mode 100644 index 000000000..7d70d11b7 --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.1/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - template.yaml diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.1/samples/capabilities-demo/constraint.yaml b/artifacthub/library/pod-security-policy/capabilities/1.1.1/samples/capabilities-demo/constraint.yaml new file mode 100644 index 000000000..3f856082f --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.1/samples/capabilities-demo/constraint.yaml @@ -0,0 +1,14 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPCapabilities +metadata: + name: capabilities-demo +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + namespaces: + - "default" + parameters: + allowedCapabilities: ["something"] + requiredDropCapabilities: ["must_drop"] diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.1/samples/capabilities-demo/disallowed_ephemeral.yaml b/artifacthub/library/pod-security-policy/capabilities/1.1.1/samples/capabilities-demo/disallowed_ephemeral.yaml new file mode 100644 index 000000000..5467c826e --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.1/samples/capabilities-demo/disallowed_ephemeral.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Pod +metadata: + name: opa-disallowed + labels: + owner: me.agilebank.demo +spec: + ephemeralContainers: + - name: opa + image: openpolicyagent/opa:0.9.2 + args: + - "run" + - "--server" + - "--addr=localhost:8080" + securityContext: + capabilities: + add: ["disallowedcapability"] + resources: + limits: + cpu: "100m" + memory: "30Mi" diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.1/samples/capabilities-demo/example_allowed.yaml b/artifacthub/library/pod-security-policy/capabilities/1.1.1/samples/capabilities-demo/example_allowed.yaml new file mode 100644 index 000000000..41bf6a0ed --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.1/samples/capabilities-demo/example_allowed.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: Pod +metadata: + name: opa-allowed + labels: + owner: me.agilebank.demo +spec: + containers: + - name: opa + image: openpolicyagent/opa:0.9.2 + args: + - "run" + - "--server" + - "--addr=localhost:8080" + securityContext: + capabilities: + add: ["something"] + drop: ["must_drop", "another_one"] + resources: + limits: + cpu: "100m" + memory: "30Mi" diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.1/samples/capabilities-demo/example_disallowed.yaml b/artifacthub/library/pod-security-policy/capabilities/1.1.1/samples/capabilities-demo/example_disallowed.yaml new file mode 100644 index 000000000..fdd886189 --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.1/samples/capabilities-demo/example_disallowed.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Pod +metadata: + name: opa-disallowed + labels: + owner: me.agilebank.demo +spec: + containers: + - name: opa + image: openpolicyagent/opa:0.9.2 + args: + - "run" + - "--server" + - "--addr=localhost:8080" + securityContext: + capabilities: + add: ["disallowedcapability"] + resources: + limits: + cpu: "100m" + memory: "30Mi" \ No newline at end of file diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.1/samples/capabilities-demo/update.yaml b/artifacthub/library/pod-security-policy/capabilities/1.1.1/samples/capabilities-demo/update.yaml new file mode 100644 index 000000000..df8ea0070 --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.1/samples/capabilities-demo/update.yaml @@ -0,0 +1,26 @@ +kind: AdmissionReview +apiVersion: admission.k8s.io/v1beta1 +request: + operation: "UPDATE" + object: + apiVersion: v1 + kind: Pod + metadata: + name: opa-disallowed + labels: + owner: me.agilebank.demo + spec: + containers: + - name: opa + image: openpolicyagent/opa:0.9.2 + args: + - "run" + - "--server" + - "--addr=localhost:8080" + securityContext: + capabilities: + add: ["disallowedcapability"] + resources: + limits: + cpu: "100m" + memory: "30Mi" diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.1/suite.yaml b/artifacthub/library/pod-security-policy/capabilities/1.1.1/suite.yaml new file mode 100644 index 000000000..48c2fcb46 --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.1/suite.yaml @@ -0,0 +1,25 @@ +kind: Suite +apiVersion: test.gatekeeper.sh/v1alpha1 +metadata: + name: capabilities +tests: + - name: capabilities + template: template.yaml + constraint: samples/capabilities-demo/constraint.yaml + cases: + - name: example-disallowed + object: samples/capabilities-demo/example_disallowed.yaml + assertions: + - violations: yes + - name: example-allowed + object: samples/capabilities-demo/example_allowed.yaml + assertions: + - violations: no + - name: disallowed-ephemeral + object: samples/capabilities-demo/disallowed_ephemeral.yaml + assertions: + - violations: yes + - name: update + object: samples/capabilities-demo/update.yaml + assertions: + - violations: no diff --git a/artifacthub/library/pod-security-policy/capabilities/1.1.1/template.yaml b/artifacthub/library/pod-security-policy/capabilities/1.1.1/template.yaml new file mode 100644 index 000000000..52ab23e88 --- /dev/null +++ b/artifacthub/library/pod-security-policy/capabilities/1.1.1/template.yaml @@ -0,0 +1,219 @@ +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: k8spspcapabilities + annotations: + metadata.gatekeeper.sh/title: "Capabilities" + metadata.gatekeeper.sh/version: 1.1.1 + description: >- + Controls Linux capabilities on containers. Corresponds to the + `allowedCapabilities` and `requiredDropCapabilities` fields in a + PodSecurityPolicy. For more information, see + https://kubernetes.io/docs/concepts/policy/pod-security-policy/#capabilities +spec: + crd: + spec: + names: + kind: K8sPSPCapabilities + validation: + # Schema for the `parameters` field + openAPIV3Schema: + type: object + description: >- + Controls Linux capabilities on containers. Corresponds to the + `allowedCapabilities` and `requiredDropCapabilities` fields in a + PodSecurityPolicy. For more information, see + https://kubernetes.io/docs/concepts/policy/pod-security-policy/#capabilities + 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 + allowedCapabilities: + type: array + description: "A list of Linux capabilities that can be added to a container." + items: + type: string + requiredDropCapabilities: + type: array + description: "A list of Linux capabilities that are required to be dropped from a container." + items: + type: string + targets: + - target: admission.k8s.gatekeeper.sh + code: + - engine: K8sNativeValidation + source: + variables: + - name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' + - name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' + - name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' + - name: allContainers + expression: 'variables.containers + variables.initContainers + variables.ephemeralContainers' + - 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)) + ).map(container, container.image) + - name: allowedCapabilities + expression: 'has(variables.params.allowedCapabilities) ? variables.params.allowedCapabilities : []' + - name: allCapabilitiesAllowed + expression: '"*" in variables.allowedCapabilities' + - name: disallowedCapabilitiesByContainer + expression: | + variables.allContainers.map(container, !(container.image in variables.exemptImages) && + !variables.allCapabilitiesAllowed && has(container.securityContext) && has(container.securityContext.capabilities) && has(container.securityContext.capabilities.add) && + container.securityContext.capabilities.add.exists(capability, !(capability in variables.allowedCapabilities)), + [container.name, dyn(container.securityContext.capabilities.add.filter(capability, !(capability in variables.allowedCapabilities)).join(", "))] + ) + - name: requiredDropCapabilities + expression: 'has(variables.params.requiredDropCapabilities) ? variables.params.requiredDropCapabilities : []' + - name: missingDropCapabilitiesByContainer + expression: | + variables.allContainers.map(container, !(container.image in variables.exemptImages) && + size(variables.requiredDropCapabilities) > 0 && ( + !has(container.securityContext) || !has(container.securityContext.capabilities) || !has(container.securityContext.capabilities.drop) || ( + !("all" in container.securityContext.capabilities.drop) && + variables.requiredDropCapabilities.exists(capability, !(capability in container.securityContext.capabilities.drop)) + ) + ), + [container.name, + !has(container.securityContext) ? variables.requiredDropCapabilities : + !has(container.securityContext.capabilities) ? variables.requiredDropCapabilities : + !has(container.securityContext.capabilities.drop) ? variables.requiredDropCapabilities : + variables.requiredDropCapabilities.filter(capability, !(capability in container.securityContext.capabilities.drop)) + ] + ) + validations: + - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.disallowedCapabilitiesByContainer) == 0' + messageExpression: | + "containers have disallowed capabilities: " + variables.disallowedCapabilitiesByContainer.map(pair, "{container: " + pair[0] + ", capabilities: [" + pair[1] + "]}").join(", ") + - expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.missingDropCapabilitiesByContainer) == 0' + messageExpression: | + "containers are not dropping all required capabilities: " + variables.missingDropCapabilitiesByContainer.map(pair, "{container: " + pair[0] + ", capabilities: [" + pair[1].join(", ") + "]}").join(", ") + - engine: Rego + source: + rego: | + package capabilities + + import data.lib.exclude_update.is_update + import data.lib.exempt_container.is_exempt + + violation[{"msg": msg}] { + # spec.containers.securityContext.capabilities field is immutable. + not is_update(input.review) + + container := input.review.object.spec.containers[_] + not is_exempt(container) + has_disallowed_capabilities(container) + msg := sprintf("container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) + } + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.containers[_] + not is_exempt(container) + missing_drop_capabilities(container) + msg := sprintf("container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) + } + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.initContainers[_] + not is_exempt(container) + has_disallowed_capabilities(container) + msg := sprintf("init container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) + } + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.initContainers[_] + not is_exempt(container) + missing_drop_capabilities(container) + msg := sprintf("init container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) + } + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.ephemeralContainers[_] + not is_exempt(container) + has_disallowed_capabilities(container) + msg := sprintf("ephemeral container <%v> has a disallowed capability. Allowed capabilities are %v", [container.name, get_default(input.parameters, "allowedCapabilities", "NONE")]) + } + + violation[{"msg": msg}] { + not is_update(input.review) + container := input.review.object.spec.ephemeralContainers[_] + not is_exempt(container) + missing_drop_capabilities(container) + msg := sprintf("ephemeral container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) + } + + has_disallowed_capabilities(container) { + allowed := {c | c := lower(input.parameters.allowedCapabilities[_])} + not allowed["*"] + capabilities := {c | c := lower(container.securityContext.capabilities.add[_])} + + count(capabilities - allowed) > 0 + } + + missing_drop_capabilities(container) { + must_drop := {c | c := lower(input.parameters.requiredDropCapabilities[_])} + all := {"all"} + dropped := {c | c := lower(container.securityContext.capabilities.drop[_])} + + count(must_drop - dropped) > 0 + count(all - dropped) > 0 + } + + get_default(obj, param, _) := obj[param] + + get_default(obj, param, _default) := _default { + not obj[param] + not obj[param] == false + } + 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) + } diff --git a/library/pod-security-policy/capabilities/template.yaml b/library/pod-security-policy/capabilities/template.yaml index a9766fd60..52ab23e88 100644 --- a/library/pod-security-policy/capabilities/template.yaml +++ b/library/pod-security-policy/capabilities/template.yaml @@ -4,7 +4,7 @@ metadata: name: k8spspcapabilities annotations: metadata.gatekeeper.sh/title: "Capabilities" - metadata.gatekeeper.sh/version: 1.1.0 + metadata.gatekeeper.sh/version: 1.1.1 description: >- Controls Linux capabilities on containers. Corresponds to the `allowedCapabilities` and `requiredDropCapabilities` fields in a @@ -135,8 +135,6 @@ spec: msg := sprintf("container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) } - - violation[{"msg": msg}] { not is_update(input.review) container := input.review.object.spec.initContainers[_] @@ -153,8 +151,6 @@ spec: msg := sprintf("init container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) } - - violation[{"msg": msg}] { not is_update(input.review) container := input.review.object.spec.ephemeralContainers[_] @@ -171,7 +167,6 @@ spec: msg := sprintf("ephemeral container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) } - has_disallowed_capabilities(container) { allowed := {c | c := lower(input.parameters.allowedCapabilities[_])} not allowed["*"] diff --git a/src/pod-security-policy/capabilities/constraint.tmpl b/src/pod-security-policy/capabilities/constraint.tmpl index 02c8abe32..1b5c68ae9 100644 --- a/src/pod-security-policy/capabilities/constraint.tmpl +++ b/src/pod-security-policy/capabilities/constraint.tmpl @@ -4,7 +4,7 @@ metadata: name: k8spspcapabilities annotations: metadata.gatekeeper.sh/title: "Capabilities" - metadata.gatekeeper.sh/version: 1.1.0 + metadata.gatekeeper.sh/version: 1.1.1 description: >- Controls Linux capabilities on containers. Corresponds to the `allowedCapabilities` and `requiredDropCapabilities` fields in a diff --git a/src/pod-security-policy/capabilities/src.rego b/src/pod-security-policy/capabilities/src.rego index 35038a95c..b007c8400 100644 --- a/src/pod-security-policy/capabilities/src.rego +++ b/src/pod-security-policy/capabilities/src.rego @@ -21,8 +21,6 @@ violation[{"msg": msg}] { msg := sprintf("container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) } - - violation[{"msg": msg}] { not is_update(input.review) container := input.review.object.spec.initContainers[_] @@ -39,8 +37,6 @@ violation[{"msg": msg}] { msg := sprintf("init container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) } - - violation[{"msg": msg}] { not is_update(input.review) container := input.review.object.spec.ephemeralContainers[_] @@ -57,7 +53,6 @@ violation[{"msg": msg}] { msg := sprintf("ephemeral container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) } - has_disallowed_capabilities(container) { allowed := {c | c := lower(input.parameters.allowedCapabilities[_])} not allowed["*"] diff --git a/website/docs/validation/capabilities.md b/website/docs/validation/capabilities.md index b119ae89b..1732f5e64 100644 --- a/website/docs/validation/capabilities.md +++ b/website/docs/validation/capabilities.md @@ -16,7 +16,7 @@ metadata: name: k8spspcapabilities annotations: metadata.gatekeeper.sh/title: "Capabilities" - metadata.gatekeeper.sh/version: 1.1.0 + metadata.gatekeeper.sh/version: 1.1.1 description: >- Controls Linux capabilities on containers. Corresponds to the `allowedCapabilities` and `requiredDropCapabilities` fields in a @@ -147,8 +147,6 @@ spec: msg := sprintf("container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) } - - violation[{"msg": msg}] { not is_update(input.review) container := input.review.object.spec.initContainers[_] @@ -165,8 +163,6 @@ spec: msg := sprintf("init container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) } - - violation[{"msg": msg}] { not is_update(input.review) container := input.review.object.spec.ephemeralContainers[_] @@ -183,7 +179,6 @@ spec: msg := sprintf("ephemeral container <%v> is not dropping all required capabilities. Container must drop all of %v or \"ALL\"", [container.name, input.parameters.requiredDropCapabilities]) } - has_disallowed_capabilities(container) { allowed := {c | c := lower(input.parameters.allowedCapabilities[_])} not allowed["*"] From 42e49558067ddb461b65fb5e1db9708ead63e16c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 10:11:06 -0700 Subject: [PATCH 14/19] chore: bump the all group with 2 updates (#598) Bumps the all group with 2 updates: [step-security/harden-runner](https://github.com/step-security/harden-runner) and [github/codeql-action](https://github.com/github/codeql-action). Updates `step-security/harden-runner` from 2.9.1 to 2.10.1 - [Release notes](https://github.com/step-security/harden-runner/releases) - [Commits](https://github.com/step-security/harden-runner/compare/5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde...91182cccc01eb5e619899d80e4e971d6181294a7) Updates `github/codeql-action` from 3.26.6 to 3.26.7 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/4dd16135b69a43b6c8efb853346f8437d92d3c93...8214744c546c1e5c8f03dde8fab3a7353211988d) --- updated-dependencies: - dependency-name: step-security/harden-runner dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 8 ++++---- .github/workflows/dependency-review.yml | 2 +- .github/workflows/scorecards.yml | 4 ++-- .github/workflows/website.yaml | 2 +- .github/workflows/workflow.yaml | 12 ++++++------ 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 83584c2d2..ec8a27216 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit @@ -50,7 +50,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 + uses: github/codeql-action/init@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -60,7 +60,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 + uses: github/codeql-action/autobuild@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -73,6 +73,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 + uses: github/codeql-action/analyze@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index bf2dcfbae..c1209291a 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index f06e45705..274792b2e 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit @@ -71,6 +71,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 + uses: github/codeql-action/upload-sarif@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 with: sarif_file: results.sarif diff --git a/.github/workflows/website.yaml b/.github/workflows/website.yaml index 45e51bc65..ba50fef9e 100644 --- a/.github/workflows/website.yaml +++ b/.github/workflows/website.yaml @@ -25,7 +25,7 @@ jobs: working-directory: website steps: - name: Harden Runner - uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit diff --git a/.github/workflows/workflow.yaml b/.github/workflows/workflow.yaml index dd8e93ebc..30956901b 100644 --- a/.github/workflows/workflow.yaml +++ b/.github/workflows/workflow.yaml @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit @@ -49,7 +49,7 @@ jobs: name: Unit test on ${{ matrix.os }} opa ${{ matrix.opa }} steps: - name: Harden Runner - uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit @@ -71,7 +71,7 @@ jobs: steps: - name: Harden Runner if: ${{ !(matrix.gatekeeper == '3.15.1' && matrix.engine == 'cel') }} # remove this condition once 3.17 is out - uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit @@ -110,7 +110,7 @@ jobs: name: "Require a suite.yaml file alongside every template.yaml" steps: - name: Harden Runner - uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit @@ -123,7 +123,7 @@ jobs: name: "Require a sync.yaml file and metadata.gatekeeper.sh/requires-sync-data annotation for every template.yaml using data.inventory" steps: - name: Harden Runner - uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit @@ -141,7 +141,7 @@ jobs: steps: - name: Harden Runner if: ${{ !(matrix.gatekeeper == '3.15.1' && matrix.engine == 'cel') }} # remove this condition once 3.17 is out - uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit From fd2b020b7b65adb2cdd16ccd2c8f3d542ef66ddb Mon Sep 17 00:00:00 2001 From: Jaydip Gabani Date: Tue, 1 Oct 2024 10:01:27 -0700 Subject: [PATCH 15/19] fixing CEL error (#600) Signed-off-by: Jaydip Gabani --- .../1.1.2/artifacthub-pkg.yml | 22 +++ .../1.1.2/kustomization.yaml | 2 + .../psp-privileged-container/constraint.yaml | 13 ++ .../disallowed_ephemeral.yaml | 12 ++ .../example_allowed.yaml | 12 ++ .../example_allowed_exempt.yaml | 12 ++ .../example_disallowed.yaml | 17 +++ .../psp-privileged-container/update.yaml | 17 +++ .../privileged-containers/1.1.2/suite.yaml | 29 ++++ .../privileged-containers/1.1.2/template.yaml | 129 ++++++++++++++++++ .../example_disallowed.yaml | 5 + .../privileged-containers/template.yaml | 8 +- .../privileged-containers/constraint.tmpl | 2 +- .../privileged-containers/src.cel | 6 +- .../docs/validation/privileged-containers.md | 13 +- 15 files changed, 287 insertions(+), 12 deletions(-) create mode 100644 artifacthub/library/pod-security-policy/privileged-containers/1.1.2/artifacthub-pkg.yml create mode 100644 artifacthub/library/pod-security-policy/privileged-containers/1.1.2/kustomization.yaml create mode 100644 artifacthub/library/pod-security-policy/privileged-containers/1.1.2/samples/psp-privileged-container/constraint.yaml create mode 100644 artifacthub/library/pod-security-policy/privileged-containers/1.1.2/samples/psp-privileged-container/disallowed_ephemeral.yaml create mode 100644 artifacthub/library/pod-security-policy/privileged-containers/1.1.2/samples/psp-privileged-container/example_allowed.yaml create mode 100644 artifacthub/library/pod-security-policy/privileged-containers/1.1.2/samples/psp-privileged-container/example_allowed_exempt.yaml create mode 100644 artifacthub/library/pod-security-policy/privileged-containers/1.1.2/samples/psp-privileged-container/example_disallowed.yaml create mode 100644 artifacthub/library/pod-security-policy/privileged-containers/1.1.2/samples/psp-privileged-container/update.yaml create mode 100644 artifacthub/library/pod-security-policy/privileged-containers/1.1.2/suite.yaml create mode 100644 artifacthub/library/pod-security-policy/privileged-containers/1.1.2/template.yaml diff --git a/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/artifacthub-pkg.yml b/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/artifacthub-pkg.yml new file mode 100644 index 000000000..1780929e6 --- /dev/null +++ b/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/artifacthub-pkg.yml @@ -0,0 +1,22 @@ +version: 1.1.2 +name: k8spspprivilegedcontainer +displayName: Privileged Container +createdAt: "2024-09-19T20:41:16Z" +description: Controls the ability of any container to enable privileged mode. Corresponds to the `privileged` field in a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#privileged +digest: c59b8fdc5827901a5d35fb33825275427b6cf27e38d5f2d975ac52fcc022deff +license: Apache-2.0 +homeURL: https://open-policy-agent.github.io/gatekeeper-library/website/privileged-containers +keywords: + - gatekeeper + - open-policy-agent + - policies +readme: |- + # Privileged Container + Controls the ability of any container to enable privileged mode. Corresponds to the `privileged` field in a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#privileged +install: |- + ### Usage + ```shell + kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/template.yaml + ``` +provider: + name: Gatekeeper Library diff --git a/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/kustomization.yaml b/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/kustomization.yaml new file mode 100644 index 000000000..7d70d11b7 --- /dev/null +++ b/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - template.yaml diff --git a/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/samples/psp-privileged-container/constraint.yaml b/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/samples/psp-privileged-container/constraint.yaml new file mode 100644 index 000000000..bf2fe519d --- /dev/null +++ b/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/samples/psp-privileged-container/constraint.yaml @@ -0,0 +1,13 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPPrivilegedContainer +metadata: + name: psp-privileged-container +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + excludedNamespaces: ["kube-system"] + parameters: + exemptImages: + - "safeimages.com/*" diff --git a/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/samples/psp-privileged-container/disallowed_ephemeral.yaml b/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/samples/psp-privileged-container/disallowed_ephemeral.yaml new file mode 100644 index 000000000..e8c8b9945 --- /dev/null +++ b/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/samples/psp-privileged-container/disallowed_ephemeral.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-privileged-disallowed + labels: + app: nginx-privileged +spec: + ephemeralContainers: + - name: nginx + image: nginx + securityContext: + privileged: true diff --git a/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/samples/psp-privileged-container/example_allowed.yaml b/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/samples/psp-privileged-container/example_allowed.yaml new file mode 100644 index 000000000..bb65a2c0e --- /dev/null +++ b/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/samples/psp-privileged-container/example_allowed.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-privileged-allowed + labels: + app: nginx-privileged +spec: + containers: + - name: nginx + image: nginx + securityContext: + privileged: false diff --git a/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/samples/psp-privileged-container/example_allowed_exempt.yaml b/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/samples/psp-privileged-container/example_allowed_exempt.yaml new file mode 100644 index 000000000..782a6036a --- /dev/null +++ b/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/samples/psp-privileged-container/example_allowed_exempt.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-privileged-allowed-exempt + labels: + app: nginx-privileged +spec: + containers: + - name: nginx + image: safeimages.com/nginx + securityContext: + privileged: true diff --git a/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/samples/psp-privileged-container/example_disallowed.yaml b/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/samples/psp-privileged-container/example_disallowed.yaml new file mode 100644 index 000000000..92353f5b3 --- /dev/null +++ b/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/samples/psp-privileged-container/example_disallowed.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-privileged-disallowed + labels: + app: nginx-privileged +spec: + containers: + - name: nginx + image: nginx + securityContext: + privileged: true + initContainers: + - name: nginx-init + image: nginx + securityContext: + privileged: true diff --git a/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/samples/psp-privileged-container/update.yaml b/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/samples/psp-privileged-container/update.yaml new file mode 100644 index 000000000..08f36044c --- /dev/null +++ b/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/samples/psp-privileged-container/update.yaml @@ -0,0 +1,17 @@ +kind: AdmissionReview +apiVersion: admission.k8s.io/v1beta1 +request: + operation: "UPDATE" + object: + apiVersion: v1 + kind: Pod + metadata: + name: nginx-privileged-disallowed + labels: + app: nginx-privileged + spec: + containers: + - name: nginx + image: nginx + securityContext: + privileged: true diff --git a/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/suite.yaml b/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/suite.yaml new file mode 100644 index 000000000..3dd4ff863 --- /dev/null +++ b/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/suite.yaml @@ -0,0 +1,29 @@ +kind: Suite +apiVersion: test.gatekeeper.sh/v1alpha1 +metadata: + name: privileged-containers +tests: +- name: privileged-containers-disallowed + template: template.yaml + constraint: samples/psp-privileged-container/constraint.yaml + cases: + - name: example-disallowed + object: samples/psp-privileged-container/example_disallowed.yaml + assertions: + - violations: yes + - name: example-allowed + object: samples/psp-privileged-container/example_allowed.yaml + assertions: + - violations: no + - name: disallowed-ephemeral + object: samples/psp-privileged-container/disallowed_ephemeral.yaml + assertions: + - violations: yes + - name: update + object: samples/psp-privileged-container/update.yaml + assertions: + - violations: no + - name: exempted-image + object: samples/psp-privileged-container/example_allowed_exempt.yaml + assertions: + - violations: no diff --git a/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/template.yaml b/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/template.yaml new file mode 100644 index 000000000..68adef487 --- /dev/null +++ b/artifacthub/library/pod-security-policy/privileged-containers/1.1.2/template.yaml @@ -0,0 +1,129 @@ +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: k8spspprivilegedcontainer + annotations: + metadata.gatekeeper.sh/title: "Privileged Container" + metadata.gatekeeper.sh/version: 1.1.2 + description: >- + Controls the ability of any container to enable privileged mode. + Corresponds to the `privileged` field in a PodSecurityPolicy. For more + information, see + https://kubernetes.io/docs/concepts/policy/pod-security-policy/#privileged +spec: + crd: + spec: + names: + kind: K8sPSPPrivilegedContainer + validation: + openAPIV3Schema: + type: object + description: >- + Controls the ability of any container to enable privileged mode. + Corresponds to the `privileged` field in a PodSecurityPolicy. For more + information, see + https://kubernetes.io/docs/concepts/policy/pod-security-policy/#privileged + 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 + targets: + - target: admission.k8s.gatekeeper.sh + code: + - engine: K8sNativeValidation + source: + variables: + - name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' + - name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' + - name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' + - 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)) + ).map(container, container.image) + - name: badContainers + expression: | + (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, + !(container.image in variables.exemptImages) && + (has(container.securityContext) && has(container.securityContext.privileged) && container.securityContext.privileged) + ).map(container, "Privileged container is not allowed: " + container.name +", securityContext.privileged: true") + - name: isUpdate + expression: has(request.operation) && request.operation == "UPDATE" + validations: + - expression: variables.isUpdate || size(variables.badContainers) == 0 + messageExpression: 'variables.badContainers.join(", ")' + - engine: Rego + source: + rego: | + package k8spspprivileged + + import data.lib.exclude_update.is_update + import data.lib.exempt_container.is_exempt + + violation[{"msg": msg, "details": {}}] { + # spec.containers.privileged field is immutable. + not is_update(input.review) + + c := input_containers[_] + not is_exempt(c) + c.securityContext.privileged + msg := sprintf("Privileged container is not allowed: %v, securityContext: %v", [c.name, c.securityContext]) + } + + input_containers[c] { + c := input.review.object.spec.containers[_] + } + + input_containers[c] { + c := input.review.object.spec.initContainers[_] + } + + input_containers[c] { + c := input.review.object.spec.ephemeralContainers[_] + } + 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) + } diff --git a/library/pod-security-policy/privileged-containers/samples/psp-privileged-container/example_disallowed.yaml b/library/pod-security-policy/privileged-containers/samples/psp-privileged-container/example_disallowed.yaml index 936a24f8e..92353f5b3 100644 --- a/library/pod-security-policy/privileged-containers/samples/psp-privileged-container/example_disallowed.yaml +++ b/library/pod-security-policy/privileged-containers/samples/psp-privileged-container/example_disallowed.yaml @@ -10,3 +10,8 @@ spec: image: nginx securityContext: privileged: true + initContainers: + - name: nginx-init + image: nginx + securityContext: + privileged: true diff --git a/library/pod-security-policy/privileged-containers/template.yaml b/library/pod-security-policy/privileged-containers/template.yaml index c5a09674c..68adef487 100644 --- a/library/pod-security-policy/privileged-containers/template.yaml +++ b/library/pod-security-policy/privileged-containers/template.yaml @@ -4,7 +4,7 @@ metadata: name: k8spspprivilegedcontainer annotations: metadata.gatekeeper.sh/title: "Privileged Container" - metadata.gatekeeper.sh/version: 1.1.1 + metadata.gatekeeper.sh/version: 1.1.2 description: >- Controls the ability of any container to enable privileged mode. Corresponds to the `privileged` field in a PodSecurityPolicy. For more @@ -64,13 +64,13 @@ spec: expression: | (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, !(container.image in variables.exemptImages) && - (has(container.securityContext) && has(container.securityContext.privileged) && container.securityContext.privileged == true) - ).map(container, "Privileged container is not allowed: " + container.name +", securityContext: " + container.securityContext) + (has(container.securityContext) && has(container.securityContext.privileged) && container.securityContext.privileged) + ).map(container, "Privileged container is not allowed: " + container.name +", securityContext.privileged: true") - name: isUpdate expression: has(request.operation) && request.operation == "UPDATE" validations: - expression: variables.isUpdate || size(variables.badContainers) == 0 - messageExpression: 'variables.badContainers.join("\n")' + messageExpression: 'variables.badContainers.join(", ")' - engine: Rego source: rego: | diff --git a/src/pod-security-policy/privileged-containers/constraint.tmpl b/src/pod-security-policy/privileged-containers/constraint.tmpl index 4711137dd..913a36e98 100644 --- a/src/pod-security-policy/privileged-containers/constraint.tmpl +++ b/src/pod-security-policy/privileged-containers/constraint.tmpl @@ -4,7 +4,7 @@ metadata: name: k8spspprivilegedcontainer annotations: metadata.gatekeeper.sh/title: "Privileged Container" - metadata.gatekeeper.sh/version: 1.1.1 + metadata.gatekeeper.sh/version: 1.1.2 description: >- Controls the ability of any container to enable privileged mode. Corresponds to the `privileged` field in a PodSecurityPolicy. For more diff --git a/src/pod-security-policy/privileged-containers/src.cel b/src/pod-security-policy/privileged-containers/src.cel index b0c77fbc4..5793600ff 100644 --- a/src/pod-security-policy/privileged-containers/src.cel +++ b/src/pod-security-policy/privileged-containers/src.cel @@ -23,10 +23,10 @@ variables: expression: | (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, !(container.image in variables.exemptImages) && - (has(container.securityContext) && has(container.securityContext.privileged) && container.securityContext.privileged == true) - ).map(container, "Privileged container is not allowed: " + container.name +", securityContext: " + container.securityContext) + (has(container.securityContext) && has(container.securityContext.privileged) && container.securityContext.privileged) + ).map(container, "Privileged container is not allowed: " + container.name +", securityContext.privileged: true") - name: isUpdate expression: has(request.operation) && request.operation == "UPDATE" validations: - expression: variables.isUpdate || size(variables.badContainers) == 0 - messageExpression: 'variables.badContainers.join("\n")' + messageExpression: 'variables.badContainers.join(", ")' diff --git a/website/docs/validation/privileged-containers.md b/website/docs/validation/privileged-containers.md index 8354c1976..4bbd1fbd3 100644 --- a/website/docs/validation/privileged-containers.md +++ b/website/docs/validation/privileged-containers.md @@ -16,7 +16,7 @@ metadata: name: k8spspprivilegedcontainer annotations: metadata.gatekeeper.sh/title: "Privileged Container" - metadata.gatekeeper.sh/version: 1.1.1 + metadata.gatekeeper.sh/version: 1.1.2 description: >- Controls the ability of any container to enable privileged mode. Corresponds to the `privileged` field in a PodSecurityPolicy. For more @@ -76,13 +76,13 @@ spec: expression: | (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, !(container.image in variables.exemptImages) && - (has(container.securityContext) && has(container.securityContext.privileged) && container.securityContext.privileged == true) - ).map(container, "Privileged container is not allowed: " + container.name +", securityContext: " + container.securityContext) + (has(container.securityContext) && has(container.securityContext.privileged) && container.securityContext.privileged) + ).map(container, "Privileged container is not allowed: " + container.name +", securityContext.privileged: true") - name: isUpdate expression: has(request.operation) && request.operation == "UPDATE" validations: - expression: variables.isUpdate || size(variables.badContainers) == 0 - messageExpression: 'variables.badContainers.join("\n")' + messageExpression: 'variables.badContainers.join(", ")' - engine: Rego source: rego: | @@ -194,6 +194,11 @@ spec: image: nginx securityContext: privileged: true + initContainers: + - name: nginx-init + image: nginx + securityContext: + privileged: true ``` From af249553d7abb64bf5ffd64fbe395e12d5026bdb Mon Sep 17 00:00:00 2001 From: Jaydip Gabani Date: Mon, 28 Oct 2024 12:24:51 -0700 Subject: [PATCH 16/19] chore: adding cel for psp-seccomp policy (#540) * chore: adding cel for psp-seccomp policy Signed-off-by: Jaydip Gabani * using anyObject variable Signed-off-by: Jaydip Gabani * moving rego under engine Signed-off-by: Jaydip Gabani * tracking SC to annotations, adding v2 for seccomp with option to not read from annotations Signed-off-by: Jaydip Gabani * fixing bugs Signed-off-by: Jaydip Gabani * fixing bugs Signed-off-by: Jaydip Gabani * removing naming_translations and streamlining name mapping Signed-off-by: Jaydip Gabani * adding seccompv2 in kustomize Signed-off-by: Jaydip Gabani * fixing rego bugs Signed-off-by: Jaydip Gabani * adding seccompv2 Signed-off-by: Jaydip Gabani * fixing unused variables Signed-off-by: Jaydip Gabani * adding new line Signed-off-by: Jaydip Gabani * updating seccompv2 description Signed-off-by: Jaydip Gabani * removing unused example Signed-off-by: Jaydip Gabani * updating seccompv2 to remove string building for localhost profiles Signed-off-by: Jaydip Gabani * updating rego variable name Signed-off-by: Jaydip Gabani * adding examples for localhost profile Signed-off-by: Jaydip Gabani * fixing CEL code Signed-off-by: Jaydip Gabani * addressing nit, updating description for seccompv2 Signed-off-by: Jaydip Gabani --------- Signed-off-by: Jaydip Gabani --- .../seccomp/1.1.0/artifacthub-pkg.yml | 22 + .../seccomp/1.1.0/kustomization.yaml | 2 + .../1.1.0/samples/psp-seccomp/constraint.yaml | 15 + .../psp-seccomp/disallowed_ephemeral.yaml | 12 + .../samples/psp-seccomp/example_allowed.yaml | 12 + .../samples/psp-seccomp/example_allowed2.yaml | 12 + .../example_allowed_exempt_image.yaml | 13 + .../example_allowed_localhost.yaml | 14 + .../psp-seccomp/example_disallowed.yaml | 12 + .../psp-seccomp/example_disallowed2.yaml | 12 + .../example_disallowed_localhost.yaml | 14 + .../seccomp/1.1.0/suite.yaml | 45 ++ .../seccomp/1.1.0/template.yaml | 398 +++++++++++ .../seccompv2/1.0.0/artifacthub-pkg.yml | 22 + .../seccompv2/1.0.0/kustomization.yaml | 2 + .../1.0.0/samples/psp-seccomp/constraint.yaml | 17 + .../psp-seccomp/disallowed_ephemeral.yaml | 10 + .../samples/psp-seccomp/example_allowed.yaml | 13 + .../example_allowed_exempt_image.yaml | 13 + .../example_allowed_localhost.yaml | 14 + .../psp-seccomp/example_disallowed.yaml | 13 + .../psp-seccomp/example_disallowed2.yaml | 13 + .../seccompv2/1.0.0/suite.yaml | 36 + .../seccompv2/1.0.0/template.yaml | 301 +++++++++ .../pod-security-policy/kustomization.yaml | 1 + .../samples/psp-seccomp/constraint.yaml | 4 +- .../example_allowed_exempt_image.yaml | 13 + .../example_allowed_localhost.yaml | 14 + .../example_disallowed_localhost.yaml | 14 + .../pod-security-policy/seccomp/suite.yaml | 13 + .../pod-security-policy/seccomp/template.yaml | 543 +++++++++------ .../seccompv2/kustomization.yaml | 2 + .../samples/psp-seccomp/constraint.yaml | 17 + .../psp-seccomp/disallowed_ephemeral.yaml | 10 + .../samples/psp-seccomp/example_allowed.yaml | 13 + .../example_allowed_exempt_image.yaml | 13 + .../example_allowed_localhost.yaml | 14 + .../psp-seccomp/example_disallowed.yaml | 13 + .../psp-seccomp/example_disallowed2.yaml | 13 + .../pod-security-policy/seccompv2/suite.yaml | 36 + .../seccompv2/template.yaml | 301 +++++++++ .../seccomp/constraint.tmpl | 18 +- src/pod-security-policy/seccomp/src.cel | 141 ++++ src/pod-security-policy/seccomp/src.rego | 95 +-- src/pod-security-policy/seccomp/src_test.rego | 10 +- .../seccompv2/constraint.tmpl | 73 ++ .../seccompv2/lib_exempt_container.rego | 19 + src/pod-security-policy/seccompv2/src.cel | 101 +++ src/pod-security-policy/seccompv2/src.rego | 111 +++ .../seccompv2/src_test.rego | 361 ++++++++++ website/docs/validation/seccomp.md | 631 ++++++++++++------ website/docs/validation/seccompv2.md | 519 ++++++++++++++ website/sidebars.js | 1 + 53 files changed, 3653 insertions(+), 498 deletions(-) create mode 100644 artifacthub/library/pod-security-policy/seccomp/1.1.0/artifacthub-pkg.yml create mode 100644 artifacthub/library/pod-security-policy/seccomp/1.1.0/kustomization.yaml create mode 100644 artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/constraint.yaml create mode 100644 artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/disallowed_ephemeral.yaml create mode 100644 artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed.yaml create mode 100644 artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed2.yaml create mode 100644 artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed_exempt_image.yaml create mode 100644 artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed_localhost.yaml create mode 100644 artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_disallowed.yaml create mode 100644 artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_disallowed2.yaml create mode 100644 artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_disallowed_localhost.yaml create mode 100644 artifacthub/library/pod-security-policy/seccomp/1.1.0/suite.yaml create mode 100644 artifacthub/library/pod-security-policy/seccomp/1.1.0/template.yaml create mode 100644 artifacthub/library/pod-security-policy/seccompv2/1.0.0/artifacthub-pkg.yml create mode 100644 artifacthub/library/pod-security-policy/seccompv2/1.0.0/kustomization.yaml create mode 100644 artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/constraint.yaml create mode 100644 artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/disallowed_ephemeral.yaml create mode 100644 artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_allowed.yaml create mode 100644 artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_allowed_exempt_image.yaml create mode 100644 artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_allowed_localhost.yaml create mode 100644 artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_disallowed.yaml create mode 100644 artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_disallowed2.yaml create mode 100644 artifacthub/library/pod-security-policy/seccompv2/1.0.0/suite.yaml create mode 100644 artifacthub/library/pod-security-policy/seccompv2/1.0.0/template.yaml create mode 100644 library/pod-security-policy/seccomp/samples/psp-seccomp/example_allowed_exempt_image.yaml create mode 100644 library/pod-security-policy/seccomp/samples/psp-seccomp/example_allowed_localhost.yaml create mode 100644 library/pod-security-policy/seccomp/samples/psp-seccomp/example_disallowed_localhost.yaml create mode 100644 library/pod-security-policy/seccompv2/kustomization.yaml create mode 100644 library/pod-security-policy/seccompv2/samples/psp-seccomp/constraint.yaml create mode 100644 library/pod-security-policy/seccompv2/samples/psp-seccomp/disallowed_ephemeral.yaml create mode 100644 library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed.yaml create mode 100644 library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed_exempt_image.yaml create mode 100644 library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed_localhost.yaml create mode 100644 library/pod-security-policy/seccompv2/samples/psp-seccomp/example_disallowed.yaml create mode 100644 library/pod-security-policy/seccompv2/samples/psp-seccomp/example_disallowed2.yaml create mode 100644 library/pod-security-policy/seccompv2/suite.yaml create mode 100644 library/pod-security-policy/seccompv2/template.yaml create mode 100644 src/pod-security-policy/seccomp/src.cel create mode 100644 src/pod-security-policy/seccompv2/constraint.tmpl create mode 100644 src/pod-security-policy/seccompv2/lib_exempt_container.rego create mode 100644 src/pod-security-policy/seccompv2/src.cel create mode 100644 src/pod-security-policy/seccompv2/src.rego create mode 100644 src/pod-security-policy/seccompv2/src_test.rego create mode 100644 website/docs/validation/seccompv2.md diff --git a/artifacthub/library/pod-security-policy/seccomp/1.1.0/artifacthub-pkg.yml b/artifacthub/library/pod-security-policy/seccomp/1.1.0/artifacthub-pkg.yml new file mode 100644 index 000000000..318f91b02 --- /dev/null +++ b/artifacthub/library/pod-security-policy/seccomp/1.1.0/artifacthub-pkg.yml @@ -0,0 +1,22 @@ +version: 1.1.0 +name: k8spspseccomp +displayName: Seccomp +createdAt: "2024-06-03T13:44:11Z" +description: Controls the seccomp profile used by containers. Corresponds to the `seccomp.security.alpha.kubernetes.io/allowedProfileNames` annotation on a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#seccomp +digest: 483941dab0df9cb51189b131e309bf927928b69b46ed51986d2f51e30fe758af +license: Apache-2.0 +homeURL: https://open-policy-agent.github.io/gatekeeper-library/website/seccomp +keywords: + - gatekeeper + - open-policy-agent + - policies +readme: |- + # Seccomp + Controls the seccomp profile used by containers. Corresponds to the `seccomp.security.alpha.kubernetes.io/allowedProfileNames` annotation on a PodSecurityPolicy. For more information, see https://kubernetes.io/docs/concepts/policy/pod-security-policy/#seccomp +install: |- + ### Usage + ```shell + kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/artifacthub/library/pod-security-policy/seccomp/1.1.0/template.yaml + ``` +provider: + name: Gatekeeper Library diff --git a/artifacthub/library/pod-security-policy/seccomp/1.1.0/kustomization.yaml b/artifacthub/library/pod-security-policy/seccomp/1.1.0/kustomization.yaml new file mode 100644 index 000000000..7d70d11b7 --- /dev/null +++ b/artifacthub/library/pod-security-policy/seccomp/1.1.0/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - template.yaml diff --git a/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/constraint.yaml b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/constraint.yaml new file mode 100644 index 000000000..0d28be8d9 --- /dev/null +++ b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/constraint.yaml @@ -0,0 +1,15 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPSeccomp +metadata: + name: psp-seccomp +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + exemptImages: + - nginx-exempt + allowedProfiles: + - runtime/default + - localhost/profile.json \ No newline at end of file diff --git a/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/disallowed_ephemeral.yaml b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/disallowed_ephemeral.yaml new file mode 100644 index 000000000..1555d700e --- /dev/null +++ b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/disallowed_ephemeral.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-disallowed + annotations: + container.seccomp.security.alpha.kubernetes.io/nginx: unconfined + labels: + app: nginx-seccomp +spec: + ephemeralContainers: + - name: nginx + image: nginx diff --git a/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed.yaml b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed.yaml new file mode 100644 index 000000000..2ff43d307 --- /dev/null +++ b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-allowed + annotations: + container.seccomp.security.alpha.kubernetes.io/nginx: runtime/default + labels: + app: nginx-seccomp +spec: + containers: + - name: nginx + image: nginx diff --git a/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed2.yaml b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed2.yaml new file mode 100644 index 000000000..f8766e774 --- /dev/null +++ b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed2.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-allowed2 + annotations: + seccomp.security.alpha.kubernetes.io/pod: runtime/default + labels: + app: nginx-seccomp +spec: + containers: + - name: nginx + image: nginx diff --git a/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed_exempt_image.yaml b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed_exempt_image.yaml new file mode 100644 index 000000000..d5f42987d --- /dev/null +++ b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed_exempt_image.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-disallowed + labels: + app: nginx-seccomp +spec: + containers: + - name: nginx + image: nginx-exempt + securityContext: + seccompProfile: + type: Unconfined diff --git a/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed_localhost.yaml b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed_localhost.yaml new file mode 100644 index 000000000..856f3217b --- /dev/null +++ b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_allowed_localhost.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-allowed-localhost + labels: + app: nginx-seccomp +spec: + containers: + - name: nginx + image: nginx + securityContext: + seccompProfile: + type: Localhost + localhostProfile: profile.json diff --git a/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_disallowed.yaml b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_disallowed.yaml new file mode 100644 index 000000000..8e94ca7e6 --- /dev/null +++ b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_disallowed.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-disallowed + annotations: + container.seccomp.security.alpha.kubernetes.io/nginx: unconfined + labels: + app: nginx-seccomp +spec: + containers: + - name: nginx + image: nginx diff --git a/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_disallowed2.yaml b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_disallowed2.yaml new file mode 100644 index 000000000..6008d8f72 --- /dev/null +++ b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_disallowed2.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-disallowed2 + annotations: + seccomp.security.alpha.kubernetes.io/pod: unconfined + labels: + app: nginx-seccomp +spec: + containers: + - name: nginx + image: nginx diff --git a/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_disallowed_localhost.yaml b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_disallowed_localhost.yaml new file mode 100644 index 000000000..c566f7ca4 --- /dev/null +++ b/artifacthub/library/pod-security-policy/seccomp/1.1.0/samples/psp-seccomp/example_disallowed_localhost.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-disallowed-localhost + labels: + app: nginx-seccomp +spec: + containers: + - name: nginx + image: nginx + securityContext: + seccompProfile: + type: Localhost + localhostProfile: profile.log diff --git a/artifacthub/library/pod-security-policy/seccomp/1.1.0/suite.yaml b/artifacthub/library/pod-security-policy/seccomp/1.1.0/suite.yaml new file mode 100644 index 000000000..0347d33fb --- /dev/null +++ b/artifacthub/library/pod-security-policy/seccomp/1.1.0/suite.yaml @@ -0,0 +1,45 @@ +kind: Suite +apiVersion: test.gatekeeper.sh/v1alpha1 +metadata: + name: seccomp +tests: +- name: default-seccomp-required + template: template.yaml + constraint: samples/psp-seccomp/constraint.yaml + cases: + - name: example-disallowed-global + object: samples/psp-seccomp/example_disallowed2.yaml + assertions: + - violations: 1 + message: "Seccomp profile 'unconfined' is not allowed for container 'nginx'. Found at: annotation seccomp.security.alpha.kubernetes.io/pod" + - name: example-disallowed-container + object: samples/psp-seccomp/example_disallowed.yaml + assertions: + - violations: 1 + message: "Seccomp profile 'unconfined' is not allowed for container 'nginx'. Found at: annotation container.seccomp.security.alpha.kubernetes.io/nginx" + - name: example-allowed-container + object: samples/psp-seccomp/example_allowed.yaml + assertions: + - violations: no + - name: example-allowed-global + object: samples/psp-seccomp/example_allowed2.yaml + assertions: + - violations: no + - name: disallowed-ephemeral + object: samples/psp-seccomp/disallowed_ephemeral.yaml + assertions: + - violations: 1 + message: "Seccomp profile 'unconfined' is not allowed for container 'nginx'. Found at: annotation container.seccomp.security.alpha.kubernetes.io/nginx" + - name: example-allowed-container-exempt-image + object: samples/psp-seccomp/example_allowed_exempt_image.yaml + assertions: + - violations: no + - name: example-allowed-container-localhost-profile + object: samples/psp-seccomp/example_allowed_localhost.yaml + assertions: + - violations: no + - name: example-disallowed-container-localhost-profile + object: samples/psp-seccomp/example_disallowed_localhost.yaml + assertions: + - violations: 1 + message: "Seccomp profile 'localhost/profile.log' is not allowed for container 'nginx'. Found at: container securityContext." \ No newline at end of file diff --git a/artifacthub/library/pod-security-policy/seccomp/1.1.0/template.yaml b/artifacthub/library/pod-security-policy/seccomp/1.1.0/template.yaml new file mode 100644 index 000000000..d1b676d97 --- /dev/null +++ b/artifacthub/library/pod-security-policy/seccomp/1.1.0/template.yaml @@ -0,0 +1,398 @@ +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: k8spspseccomp + annotations: + metadata.gatekeeper.sh/title: "Seccomp" + metadata.gatekeeper.sh/version: 1.1.0 + description: >- + Controls the seccomp profile used by containers. Corresponds to the + `seccomp.security.alpha.kubernetes.io/allowedProfileNames` annotation on + a PodSecurityPolicy. For more information, see + https://kubernetes.io/docs/concepts/policy/pod-security-policy/#seccomp +spec: + crd: + spec: + names: + kind: K8sPSPSeccomp + validation: + # Schema for the `parameters` field + openAPIV3Schema: + type: object + description: >- + Controls the seccomp profile used by containers. Corresponds to the + `seccomp.security.alpha.kubernetes.io/allowedProfileNames` annotation on + a PodSecurityPolicy. For more information, see + https://kubernetes.io/docs/concepts/policy/pod-security-policy/#seccomp + 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 + allowedProfiles: + type: array + description: >- + An array of allowed profile values for seccomp on Pods/Containers. + + Can use the annotation naming scheme: `runtime/default`, `docker/default`, `unconfined` and/or + `localhost/some-profile.json`. The item `localhost/*` will allow any localhost based profile. + + Can also use the securityContext naming scheme: `RuntimeDefault`, `Unconfined` + and/or `Localhost`. For securityContext `Localhost`, use the parameter `allowedLocalhostProfiles` + to list the allowed profile JSON files. + + The policy code will translate between the two schemes so it is not necessary to use both. + + Putting a `*` in this array allows all Profiles to be used. + + This field is required since with an empty list this policy will block all workloads. + items: + type: string + allowedLocalhostFiles: + type: array + description: >- + When using securityContext naming scheme for seccomp and including `Localhost` this array holds + the allowed profile JSON files. + + Putting a `*` in this array will allows all JSON files to be used. + + This field is required to allow `Localhost` in securityContext as with an empty list it will block. + items: + type: string + targets: + - target: admission.k8s.gatekeeper.sh + code: + - engine: K8sNativeValidation + source: + variables: + - name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' + - name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' + - name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' + - name: allowAllProfiles + expression: | + has(variables.params.allowedProfiles) && variables.params.allowedProfiles.exists(profile, profile == "*") + - 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))).map(container, container.image) + - name: unverifiedContainers + expression: | + (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, + !variables.allowAllProfiles && + !(container.image in variables.exemptImages)) + - name: inputAllowedProfiles + expression: | + !has(variables.params.allowedProfiles) ? [] : variables.params.allowedProfiles + - name: allowedLocalhostFiles + expression: | + has(variables.params.allowedLocalhostFiles) ? variables.params.allowedLocalhostFiles : [] + - name: allowedProfilesTranslation + expression: | + (variables.inputAllowedProfiles.filter(profile, + profile != "Localhost").map(profile, profile == "Unconfined" ? "unconfined" : profile)) + + (variables.inputAllowedProfiles.exists(profile, profile == "RuntimeDefault") ? ["runtime/default", "docker/default"] : []) + - name: allowSecurityContextLocalhost + expression: | + variables.inputAllowedProfiles.exists(profile, profile == "Localhost") + - name: derivedAllowedLocalhostFiles + expression: | + variables.allowSecurityContextLocalhost ? variables.params.allowedLocalhostFiles.map(file, "localhost/" + file) : [] + - name: localhostWildcardAllowed + expression: | + variables.inputAllowedProfiles.exists(profile, profile == "localhost/*") || variables.derivedAllowedLocalhostFiles.exists(profile, profile == "localhost/*") + - name: allowedProfiles + expression: | + (variables.allowedProfilesTranslation + variables.derivedAllowedLocalhostFiles) + - name: hasPodSeccomp + expression: | + has(variables.anyObject.spec.securityContext) && has(variables.anyObject.spec.securityContext.seccompProfile) + - name: hasPodAnnotations + expression: | + has(variables.anyObject.metadata.annotations) && ("seccomp.security.alpha.kubernetes.io/pod" in variables.anyObject.metadata.annotations) + - name: podAnnotationsProfiles + expression: | + variables.unverifiedContainers.filter(container, + !(has(container.securityContext) && has(container.securityContext.seccompProfile)) && + !(has(variables.anyObject.metadata.annotations) && (("container.seccomp.security.alpha.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations)) && + !variables.hasPodSeccomp && + variables.hasPodAnnotations + ).map(container, { + "container" : container.name, + "profile" : variables.anyObject.metadata.annotations["seccomp.security.alpha.kubernetes.io/pod"], + "file" : dyn(""), + "location" : dyn("annotation seccomp.security.alpha.kubernetes.io/pod"), + }) + - name: containerAnnotationsProfiles + expression: | + variables.unverifiedContainers.filter(container, + !(has(container.securityContext) && has(container.securityContext.seccompProfile)) && + !variables.hasPodSeccomp && + has(variables.anyObject.metadata.annotations) && (("container.seccomp.security.alpha.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations) + ).map(container, { + "container" : container.name, + "profile" : variables.anyObject.metadata.annotations["container.seccomp.security.alpha.kubernetes.io/" + container.name], + "file" : dyn(""), + "location" : dyn("annotation container.seccomp.security.alpha.kubernetes.io/" + container.name), + }) + - name: podLocalHostProfile + expression: | + variables.hasPodSeccomp && has(variables.anyObject.spec.securityContext.seccompProfile.localhostProfile) ? variables.anyObject.spec.securityContext.seccompProfile.localhostProfile : "" + - name: canonicalPodSecurityContextProfile + expression: | + has(variables.hasPodSeccomp) && has(variables.anyObject.spec.securityContext.seccompProfile.type) ? + (variables.anyObject.spec.securityContext.seccompProfile.type == "RuntimeDefault" ? ( + variables.allowedProfiles.exists(profile, profile == "runtime/default") ? "runtime/default" : variables.allowedProfiles.exists(profile, profile == "docker/default") ? "docker/default" : "runtime/default") : + variables.anyObject.spec.securityContext.seccompProfile.type == "Unconfined" ? "unconfined" : variables.anyObject.spec.securityContext.seccompProfile.type == "Localhost" ? "localhost/" + variables.podLocalHostProfile : "") + : "" + - name: podSecurityContextProfiles + expression: | + variables.unverifiedContainers.filter(container, + !(has(container.securityContext) && has(container.securityContext.seccompProfile)) && + variables.hasPodSeccomp + ).map(container, { + "container" : container.name, + "profile" : dyn(variables.canonicalPodSecurityContextProfile), + "file" : variables.podLocalHostProfile, + "location" : dyn("pod securityContext"), + }) + - name: containerSecurityContextProfiles + expression: | + variables.unverifiedContainers.filter(container, + has(container.securityContext) && has(container.securityContext.seccompProfile) + ).map(container, { + "container" : container.name, + "profile" : dyn(has(container.securityContext.seccompProfile.type) ? (container.securityContext.seccompProfile.type == "RuntimeDefault" ? ( + variables.allowedProfiles.exists(profile, profile == "runtime/default") ? "runtime/default" : variables.allowedProfiles.exists(profile, profile == "docker/default") ? "docker/default" : "runtime/default") : + container.securityContext.seccompProfile.type == "Unconfined" ? "unconfined" : container.securityContext.seccompProfile.type == "Localhost" ? "localhost/" + container.securityContext.seccompProfile.localhostProfile : "") + : ""), + "file" : has(container.securityContext.seccompProfile.localhostProfile) ? container.securityContext.seccompProfile.localhostProfile : dyn(""), + "location" : dyn("container securityContext"), + }) + - name: containerProfilesMissing + expression: | + variables.unverifiedContainers.filter(container, + !(has(container.securityContext) && has(container.securityContext.seccompProfile)) && + !(has(variables.anyObject.metadata.annotations) && (("container.seccomp.security.alpha.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations)) && + !variables.hasPodSeccomp && + !variables.hasPodAnnotations + ).map(container, { + "container" : container.name, + "profile" : dyn("not configured"), + "file" : dyn(""), + "location" : dyn("no explicit profile found"), + }) + - name: allContainerProfiles + expression: | + variables.podAnnotationsProfiles + variables.containerAnnotationsProfiles + variables.podSecurityContextProfiles + variables.containerSecurityContextProfiles + variables.containerProfilesMissing + - name: badContainerProfiles + expression: | + variables.allContainerProfiles.filter(badContainerProfile, + !((badContainerProfile.profile in variables.allowedProfiles) || (badContainerProfile.profile.startsWith("localhost/") && variables.localhostWildcardAllowed)) + ).map(badProfile, "Seccomp profile '" + badProfile.profile + "' is not allowed for container '" + badProfile.container + "'. Found at: " + badProfile.location + ". Allowed profiles: " + variables.allowedProfiles.join(", ")) + validations: + - expression: 'size(variables.badContainerProfiles) == 0' + messageExpression: | + variables.badContainerProfiles.join(", ") + - engine: Rego + source: + rego: | + package k8spspseccomp + + import data.lib.exempt_container.is_exempt + + container_annotation_key_prefix = "container.seccomp.security.alpha.kubernetes.io/" + + pod_annotation_key = "seccomp.security.alpha.kubernetes.io/pod" + + violation[{"msg": msg}] { + not input_wildcard_allowed_profiles + allowed_profiles := get_allowed_profiles + container := input_containers[name] + not is_exempt(container) + result := get_profile(container) + not allowed_profile(result.profile, result.file, allowed_profiles) + msg := get_message(result.profile, result.file, name, result.location, allowed_profiles) + } + + get_message(profile, _, name, location, allowed_profiles) = message { + message := sprintf("Seccomp profile '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, name, location, allowed_profiles]) + } + + input_wildcard_allowed_profiles { + input.parameters.allowedProfiles[_] == "*" + } + + input_wildcard_allowed_files { + input.parameters.allowedLocalhostFiles[_] == "*" + } + + input_wildcard_allowed_files { + "localhost/*" == input.parameters.allowedProfiles[_] + } + + # Simple allowed Profiles + allowed_profile(profile, _, allowed) { + not startswith(profile, "localhost/") + profile == allowed[_] + } + + # annotation localhost with wildcard + allowed_profile(profile, _, allowed) { + "localhost/*" == allowed[_] + startswith(profile, "localhost/") + } + + # annotation localhost without wildcard + allowed_profile(profile, _, allowed) { + startswith(profile, "localhost/") + profile == allowed[_] + } + + # The profiles explicitly in the list + get_allowed_profiles[allowed] { + allowed := input.parameters.allowedProfiles[_] + } + + # Seccomp Localhost to annotation translation + get_allowed_profiles[allowed] { + profile := input.parameters.allowedProfiles[_] + not contains(profile, "/") + file := object.get(input.parameters, "allowedLocalhostFiles", [])[_] + allowed := canonicalize_seccomp_profile({"type": profile, "localhostProfile": file}, "")[_] + } + + # Container profile as defined in pod annotation + get_profile(container) = {"profile": profile, "file": "", "location": location} { + not has_securitycontext_container(container) + not has_annotation(get_container_annotation_key(container.name)) + not has_securitycontext_pod + profile := input.review.object.metadata.annotations[pod_annotation_key] + location := sprintf("annotation %v", [pod_annotation_key]) + } + + # Container profile as defined in container annotation + get_profile(container) = {"profile": profile, "file": "", "location": location} { + not has_securitycontext_container(container) + not has_securitycontext_pod + container_annotation := get_container_annotation_key(container.name) + has_annotation(container_annotation) + profile := input.review.object.metadata.annotations[container_annotation] + location := sprintf("annotation %v", [container_annotation]) + } + + # Container profile as defined in pods securityContext + get_profile(container) = {"profile": profile, "file": file, "location": location} { + not has_securitycontext_container(container) + profile := canonicalize_seccomp_profile(input.review.object.spec.securityContext.seccompProfile, canonicalize_runtime_default_profile)[_] + file := object.get(input.review.object.spec.securityContext.seccompProfile, "localhostProfile", "") + location := "pod securityContext" + } + + # Container profile as defined in containers securityContext + get_profile(container) = {"profile": profile, "file": file, "location": location} { + has_securitycontext_container(container) + profile := canonicalize_seccomp_profile(container.securityContext.seccompProfile, canonicalize_runtime_default_profile)[_] + file := object.get(container.securityContext.seccompProfile, "localhostProfile", "") + location := "container securityContext" + } + + # Container profile missing + get_profile(container) = {"profile": "not configured", "file": "", "location": "no explicit profile found"} { + not has_securitycontext_container(container) + not has_securitycontext_pod + not has_annotation(get_container_annotation_key(container.name)) + not has_annotation(pod_annotation_key) + } + + has_annotation(annotation) { + input.review.object.metadata.annotations[annotation] + } + + has_securitycontext_pod { + input.review.object.spec.securityContext.seccompProfile + } + + has_securitycontext_container(container) { + container.securityContext.seccompProfile + } + + get_container_annotation_key(name) = annotation { + annotation := concat("", [container_annotation_key_prefix, name]) + } + + input_containers[container.name] = container { + container := input.review.object.spec.containers[_] + } + + input_containers[container.name] = container { + container := input.review.object.spec.initContainers[_] + } + + input_containers[container.name] = container { + container := input.review.object.spec.ephemeralContainers[_] + } + + canonicalize_runtime_default_profile() = out { + "runtime/default" == input.parameters.allowedProfiles[_] + out := "runtime/default" + } else = out { + "docker/default" == input.parameters.allowedProfiles[_] + out := "docker/default" + } else = out { + out := "runtime/default" + } + + canonicalize_seccomp_profile(profile, def) = out { + profile.type == "RuntimeDefault" + def == "" + out := ["runtime/default", "docker/default"] + } else = out { + profile.type == "RuntimeDefault" + def != "" + out := [def] + } else = out { + profile.type == "Localhost" + out := [sprintf("localhost/%s", [profile.localhostProfile])] + } else = out { + profile.type == "Unconfined" + out := ["unconfined"] + } + libs: + - | + 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) + } diff --git a/artifacthub/library/pod-security-policy/seccompv2/1.0.0/artifacthub-pkg.yml b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/artifacthub-pkg.yml new file mode 100644 index 000000000..f573badaf --- /dev/null +++ b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/artifacthub-pkg.yml @@ -0,0 +1,22 @@ +version: 1.0.0 +name: k8spspseccompv2 +displayName: Seccomp V2 +createdAt: "2024-09-05T01:36:31Z" +description: Controls the seccomp profile used by containers. Corresponds to the `securityContext.seccompProfile` field. +digest: c6dbfe96ca7a4be156bee4bf42aef07a1424127fd9dc7d222b4e934c1919811f +license: Apache-2.0 +homeURL: https://open-policy-agent.github.io/gatekeeper-library/website/seccompv2 +keywords: + - gatekeeper + - open-policy-agent + - policies +readme: |- + # Seccomp V2 + Controls the seccomp profile used by containers. Corresponds to the `securityContext.seccompProfile` field. +install: |- + ### Usage + ```shell + kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/artifacthub/library/pod-security-policy/seccompv2/1.0.0/template.yaml + ``` +provider: + name: Gatekeeper Library diff --git a/artifacthub/library/pod-security-policy/seccompv2/1.0.0/kustomization.yaml b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/kustomization.yaml new file mode 100644 index 000000000..7d70d11b7 --- /dev/null +++ b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - template.yaml diff --git a/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/constraint.yaml b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/constraint.yaml new file mode 100644 index 000000000..a85141492 --- /dev/null +++ b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/constraint.yaml @@ -0,0 +1,17 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPSeccompV2 +metadata: + name: psp-seccomp +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + exemptImages: + - nginx-exempt + allowedProfiles: + - RuntimeDefault + - Localhost + allowedLocalhostFiles: + - "*" diff --git a/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/disallowed_ephemeral.yaml b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/disallowed_ephemeral.yaml new file mode 100644 index 000000000..4b32243a9 --- /dev/null +++ b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/disallowed_ephemeral.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-disallowed + labels: + app: nginx-seccomp +spec: + ephemeralContainers: + - name: nginx + image: nginx diff --git a/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_allowed.yaml b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_allowed.yaml new file mode 100644 index 000000000..65dfc5be8 --- /dev/null +++ b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_allowed.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-allowed + labels: + app: nginx-seccomp +spec: + containers: + - name: nginx + image: nginx + securityContext: + seccompProfile: + type: RuntimeDefault diff --git a/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_allowed_exempt_image.yaml b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_allowed_exempt_image.yaml new file mode 100644 index 000000000..d5f42987d --- /dev/null +++ b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_allowed_exempt_image.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-disallowed + labels: + app: nginx-seccomp +spec: + containers: + - name: nginx + image: nginx-exempt + securityContext: + seccompProfile: + type: Unconfined diff --git a/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_allowed_localhost.yaml b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_allowed_localhost.yaml new file mode 100644 index 000000000..856f3217b --- /dev/null +++ b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_allowed_localhost.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-allowed-localhost + labels: + app: nginx-seccomp +spec: + containers: + - name: nginx + image: nginx + securityContext: + seccompProfile: + type: Localhost + localhostProfile: profile.json diff --git a/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_disallowed.yaml b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_disallowed.yaml new file mode 100644 index 000000000..40c115b86 --- /dev/null +++ b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_disallowed.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-disallowed + labels: + app: nginx-seccomp +spec: + containers: + - name: nginx + image: nginx + securityContext: + seccompProfile: + type: Unconfined diff --git a/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_disallowed2.yaml b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_disallowed2.yaml new file mode 100644 index 000000000..e08463f18 --- /dev/null +++ b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/samples/psp-seccomp/example_disallowed2.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-disallowed2 + labels: + app: nginx-seccomp +spec: + securityContext: + seccompProfile: + type: Unconfined + containers: + - name: nginx + image: nginx diff --git a/artifacthub/library/pod-security-policy/seccompv2/1.0.0/suite.yaml b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/suite.yaml new file mode 100644 index 000000000..f9daa264d --- /dev/null +++ b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/suite.yaml @@ -0,0 +1,36 @@ +kind: Suite +apiVersion: test.gatekeeper.sh/v1alpha1 +metadata: + name: seccomp +tests: +- name: default-seccomp-required + template: template.yaml + constraint: samples/psp-seccomp/constraint.yaml + cases: + - name: example-disallowed-global + object: samples/psp-seccomp/example_disallowed2.yaml + assertions: + - violations: 1 + message: "Seccomp profile 'Unconfined' is not allowed for container 'nginx'. Found at: pod securityContext." + - name: example-disallowed-container + object: samples/psp-seccomp/example_disallowed.yaml + assertions: + - violations: 1 + message: "Seccomp profile 'Unconfined' is not allowed for container 'nginx'. Found at: container securityContext." + - name: example-allowed-container + object: samples/psp-seccomp/example_allowed.yaml + assertions: + - violations: no + - name: example-allowed-container + object: samples/psp-seccomp/example_allowed_localhost.yaml + assertions: + - violations: no + - name: example-allowed-container-exempt-image + object: samples/psp-seccomp/example_allowed_exempt_image.yaml + assertions: + - violations: no + - name: disallowed-ephemeral + object: samples/psp-seccomp/disallowed_ephemeral.yaml + assertions: + - violations: 1 + message: "Seccomp profile 'not configured' is not allowed for container 'nginx'. Found at: no explicit profile found" diff --git a/artifacthub/library/pod-security-policy/seccompv2/1.0.0/template.yaml b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/template.yaml new file mode 100644 index 000000000..ce0a53202 --- /dev/null +++ b/artifacthub/library/pod-security-policy/seccompv2/1.0.0/template.yaml @@ -0,0 +1,301 @@ +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: k8spspseccompv2 + annotations: + metadata.gatekeeper.sh/title: "Seccomp V2" + metadata.gatekeeper.sh/version: 1.0.0 + description: >- + Controls the seccomp profile used by containers. Corresponds to the + `securityContext.seccompProfile` field. Security contexts from the annotation is not considered as Kubernetes no longer reads security contexts from the annotation. +spec: + crd: + spec: + names: + kind: K8sPSPSeccompV2 + validation: + # Schema for the `parameters` field + openAPIV3Schema: + type: object + description: >- + Controls the seccomp profile used by containers. Corresponds to the + `securityContext.seccompProfile` field. Security contexts from the annotation is not considered as Kubernetes no longer reads security contexts from the annotation. + 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 + allowedProfiles: + type: array + description: >- + An array of allowed profile values for seccomp on Pods/Containers. + + Can use the securityContext naming scheme: `RuntimeDefault`, `Unconfined` + and/or `Localhost`. For securityContext `Localhost`, use the parameter `allowedLocalhostFiles` + to list the allowed profile JSON files. + + The policy code will translate between the two schemes so it is not necessary to use both. + + Putting a `*` in this array allows all Profiles to be used. + + This field is required since with an empty list this policy will block all workloads. + items: + type: string + allowedLocalhostFiles: + type: array + description: >- + When using securityContext naming scheme for seccomp and including `Localhost` this array holds + the allowed profile JSON files. + + Putting a `*` in this array will allows all JSON files to be used. + + This field is required to allow `Localhost` in securityContext as with an empty list it will block. + items: + type: string + targets: + - target: admission.k8s.gatekeeper.sh + code: + - engine: K8sNativeValidation + source: + variables: + - name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' + - name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' + - name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' + - name: allowAllProfiles + expression: | + has(variables.params.allowedProfiles) && variables.params.allowedProfiles.exists(profile, profile == "*") + - 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))).map(container, container.image) + - name: unverifiedContainers + expression: | + (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, + !variables.allowAllProfiles && + !(container.image in variables.exemptImages)) + - name: inputNonLocalHostProfiles + expression: | + variables.params.allowedProfiles.filter(profile, profile != "Localhost").map(profile, {"type": profile}) + - name: inputLocalHostProfiles + expression: | + variables.params.allowedProfiles.exists(profile, profile == "Localhost") ? variables.params.allowedLocalhostFiles.map(file, {"type": "Localhost", "localHostProfile": string(file)}) : [] + - name: inputAllowedProfiles + expression: | + variables.inputNonLocalHostProfiles + variables.inputLocalHostProfiles + - name: hasPodSeccomp + expression: | + has(variables.anyObject.spec.securityContext) && has(variables.anyObject.spec.securityContext.seccompProfile) + - name: podLocalHostProfile + expression: | + variables.hasPodSeccomp && has(variables.anyObject.spec.securityContext.seccompProfile.localhostProfile) ? variables.anyObject.spec.securityContext.seccompProfile.localhostProfile : "" + - name: podSecurityContextProfileType + expression: | + has(variables.hasPodSeccomp) && has(variables.anyObject.spec.securityContext.seccompProfile.type) ? variables.anyObject.spec.securityContext.seccompProfile.type + : "" + - name: podSecurityContextProfiles + expression: | + variables.unverifiedContainers.filter(container, + !(has(container.securityContext) && has(container.securityContext.seccompProfile)) && + variables.hasPodSeccomp + ).map(container, { + "container" : container.name, + "profile" : dyn(variables.podSecurityContextProfileType), + "file" : variables.podLocalHostProfile, + "location" : dyn("pod securityContext"), + }) + - name: containerSecurityContextProfiles + expression: | + variables.unverifiedContainers.filter(container, + has(container.securityContext) && has(container.securityContext.seccompProfile) + ).map(container, { + "container" : container.name, + "profile" : dyn(container.securityContext.seccompProfile.type), + "file" : has(container.securityContext.seccompProfile.localhostProfile) ? container.securityContext.seccompProfile.localhostProfile : dyn(""), + "location" : dyn("container securityContext"), + }) + - name: containerProfilesMissing + expression: | + variables.unverifiedContainers.filter(container, + !(has(container.securityContext) && has(container.securityContext.seccompProfile)) && + !variables.hasPodSeccomp + ).map(container, { + "container" : container.name, + "profile" : dyn("not configured"), + "file" : dyn(""), + "location" : dyn("no explicit profile found"), + }) + - name: allContainerProfiles + expression: | + variables.podSecurityContextProfiles + variables.containerSecurityContextProfiles + variables.containerProfilesMissing + - name: badContainerProfilesWithoutFiles + expression: | + variables.allContainerProfiles.filter(container, + container.profile != "Localhost" && + !variables.inputAllowedProfiles.exists(profile, profile.type == container.profile) + ).map(badProfile, "Seccomp profile '" + badProfile.profile + "' is not allowed for container '" + badProfile.container + "'. Found at: " + badProfile.location + ". Allowed profiles: " + variables.inputAllowedProfiles.map(profile, "{\"type\": \"" + profile.type + "\"" + (has(profile.localHostProfile) ? ", \"localHostProfile\": \"" + profile.localHostProfile + "\"}" : "}")).join(", ")) + - name: badContainerProfilesWithFiles + expression: | + variables.allContainerProfiles.filter(container, + container.profile == "Localhost" && + !variables.inputAllowedProfiles.exists(profile, profile.type == "Localhost" && (has(profile.localHostProfile) && (profile.localHostProfile == container.file || profile.localHostProfile == "*"))) + ).map(badProfile, "Seccomp profile '" + badProfile.profile + "' With file '" + badProfile.file + "' is not allowed for container '" + badProfile.container + "'. Found at: " + badProfile.location + ". Allowed profiles: " + variables.inputAllowedProfiles.map(profile, "{\"type\": \"" + profile.type + "\"" + (has(profile.localHostProfile) ? ", \"localHostProfile\": \"" + profile.localHostProfile + "\"}" : "}")).join(", ")) + validations: + - expression: 'size(variables.badContainerProfilesWithoutFiles) == 0' + messageExpression: | + variables.badContainerProfilesWithoutFiles.join(", ") + - expression: 'size(variables.badContainerProfilesWithFiles) == 0' + messageExpression: | + variables.badContainerProfilesWithFiles.join(", ") + - engine: Rego + source: + rego: | + package k8spspseccomp + + import data.lib.exempt_container.is_exempt + + violation[{"msg": msg}] { + not input_wildcard_allowed_profiles + allowed_profiles := get_allowed_profiles + container := input_containers[name] + not is_exempt(container) + result := get_profile(container) + not allowed_profile(result.profile, result.file, allowed_profiles) + msg := get_message(result.profile, result.file, name, result.location, allowed_profiles) + } + + get_message(profile, _, name, location, allowed_profiles) = message { + profile != "Localhost" + message := sprintf("Seccomp profile '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, name, location, allowed_profiles]) + } + + get_message(profile, file, name, location, allowed_profiles) = message { + profile == "Localhost" + message := sprintf("Seccomp profile '%v' with file '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, file, name, location, allowed_profiles]) + } + + input_wildcard_allowed_profiles { + input.parameters.allowedProfiles[_] == "*" + } + + input_wildcard_allowed_files { + input.parameters.allowedLocalhostFiles[_] == "*" + } + + allowed_profile(_, _, _) { + input_wildcard_allowed_profiles + } + + allowed_profile(profile, _, _) { + profile == "Localhost" + input_wildcard_allowed_files + } + + # Simple allowed Profiles + allowed_profile(profile, _, allowed) { + profile != "Localhost" + allow_profile = allowed[_] + profile == allow_profile.type + } + + # annotation localhost without wildcard + allowed_profile(profile, file, allowed) { + profile == "Localhost" + allow_profile = allowed[_] + allow_profile.type == "Localhost" + file == allow_profile.localHostProfile + } + + # The profiles explicitly in the list + get_allowed_profiles[allowed] { + profile := input.parameters.allowedProfiles[_] + profile != "Localhost" + allowed := {"type": profile} + } + + get_allowed_profiles[allowed] { + profile := input.parameters.allowedProfiles[_] + profile == "Localhost" + file := object.get(input.parameters, "allowedLocalhostFiles", [""])[_] + allowed := {"type": "Localhost", "localHostProfile": file} + } + + # Container profile as defined in containers securityContext + get_profile(container) = {"profile": profile, "file": file, "location": location} { + has_securitycontext_container(container) + profile := container.securityContext.seccompProfile.type + file := object.get(container.securityContext.seccompProfile, "localhostProfile", "") + location := "container securityContext" + } + + # Container profile as defined in pods securityContext + get_profile(container) = {"profile": profile, "file": file, "location": location} { + not has_securitycontext_container(container) + profile := input.review.object.spec.securityContext.seccompProfile.type + file := object.get(input.review.object.spec.securityContext.seccompProfile, "localhostProfile", "") + location := "pod securityContext" + } + + # Container profile missing + get_profile(container) = {"profile": "not configured", "file": "", "location": "no explicit profile found"} { + not has_securitycontext_container(container) + not has_securitycontext_pod + } + + has_securitycontext_pod { + input.review.object.spec.securityContext.seccompProfile + } + + has_securitycontext_container(container) { + container.securityContext.seccompProfile + } + + input_containers[container.name] = container { + container := input.review.object.spec.containers[_] + } + + input_containers[container.name] = container { + container := input.review.object.spec.initContainers[_] + } + + input_containers[container.name] = container { + container := input.review.object.spec.ephemeralContainers[_] + } + libs: + - | + 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) + } diff --git a/library/pod-security-policy/kustomization.yaml b/library/pod-security-policy/kustomization.yaml index 63d1d6d44..75e0bd1cf 100644 --- a/library/pod-security-policy/kustomization.yaml +++ b/library/pod-security-policy/kustomization.yaml @@ -17,3 +17,4 @@ resources: - selinux - users - volumes +- seccompv2 diff --git a/library/pod-security-policy/seccomp/samples/psp-seccomp/constraint.yaml b/library/pod-security-policy/seccomp/samples/psp-seccomp/constraint.yaml index d26af154e..0d28be8d9 100644 --- a/library/pod-security-policy/seccomp/samples/psp-seccomp/constraint.yaml +++ b/library/pod-security-policy/seccomp/samples/psp-seccomp/constraint.yaml @@ -8,6 +8,8 @@ spec: - apiGroups: [""] kinds: ["Pod"] parameters: + exemptImages: + - nginx-exempt allowedProfiles: - runtime/default - - docker/default + - localhost/profile.json \ No newline at end of file diff --git a/library/pod-security-policy/seccomp/samples/psp-seccomp/example_allowed_exempt_image.yaml b/library/pod-security-policy/seccomp/samples/psp-seccomp/example_allowed_exempt_image.yaml new file mode 100644 index 000000000..d5f42987d --- /dev/null +++ b/library/pod-security-policy/seccomp/samples/psp-seccomp/example_allowed_exempt_image.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-disallowed + labels: + app: nginx-seccomp +spec: + containers: + - name: nginx + image: nginx-exempt + securityContext: + seccompProfile: + type: Unconfined diff --git a/library/pod-security-policy/seccomp/samples/psp-seccomp/example_allowed_localhost.yaml b/library/pod-security-policy/seccomp/samples/psp-seccomp/example_allowed_localhost.yaml new file mode 100644 index 000000000..856f3217b --- /dev/null +++ b/library/pod-security-policy/seccomp/samples/psp-seccomp/example_allowed_localhost.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-allowed-localhost + labels: + app: nginx-seccomp +spec: + containers: + - name: nginx + image: nginx + securityContext: + seccompProfile: + type: Localhost + localhostProfile: profile.json diff --git a/library/pod-security-policy/seccomp/samples/psp-seccomp/example_disallowed_localhost.yaml b/library/pod-security-policy/seccomp/samples/psp-seccomp/example_disallowed_localhost.yaml new file mode 100644 index 000000000..c566f7ca4 --- /dev/null +++ b/library/pod-security-policy/seccomp/samples/psp-seccomp/example_disallowed_localhost.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-disallowed-localhost + labels: + app: nginx-seccomp +spec: + containers: + - name: nginx + image: nginx + securityContext: + seccompProfile: + type: Localhost + localhostProfile: profile.log diff --git a/library/pod-security-policy/seccomp/suite.yaml b/library/pod-security-policy/seccomp/suite.yaml index 62336b26e..0347d33fb 100644 --- a/library/pod-security-policy/seccomp/suite.yaml +++ b/library/pod-security-policy/seccomp/suite.yaml @@ -30,3 +30,16 @@ tests: assertions: - violations: 1 message: "Seccomp profile 'unconfined' is not allowed for container 'nginx'. Found at: annotation container.seccomp.security.alpha.kubernetes.io/nginx" + - name: example-allowed-container-exempt-image + object: samples/psp-seccomp/example_allowed_exempt_image.yaml + assertions: + - violations: no + - name: example-allowed-container-localhost-profile + object: samples/psp-seccomp/example_allowed_localhost.yaml + assertions: + - violations: no + - name: example-disallowed-container-localhost-profile + object: samples/psp-seccomp/example_disallowed_localhost.yaml + assertions: + - violations: 1 + message: "Seccomp profile 'localhost/profile.log' is not allowed for container 'nginx'. Found at: container securityContext." \ No newline at end of file diff --git a/library/pod-security-policy/seccomp/template.yaml b/library/pod-security-policy/seccomp/template.yaml index d252a1ace..d1b676d97 100644 --- a/library/pod-security-policy/seccomp/template.yaml +++ b/library/pod-security-policy/seccomp/template.yaml @@ -4,7 +4,7 @@ metadata: name: k8spspseccomp annotations: metadata.gatekeeper.sh/title: "Seccomp" - metadata.gatekeeper.sh/version: 1.0.1 + metadata.gatekeeper.sh/version: 1.1.0 description: >- Controls the seccomp profile used by containers. Corresponds to the `seccomp.security.alpha.kubernetes.io/allowedProfileNames` annotation on @@ -67,215 +67,332 @@ spec: type: string targets: - target: admission.k8s.gatekeeper.sh - rego: | - package k8spspseccomp - - import data.lib.exempt_container.is_exempt - - container_annotation_key_prefix = "container.seccomp.security.alpha.kubernetes.io/" - - pod_annotation_key = "seccomp.security.alpha.kubernetes.io/pod" - - naming_translation = { - # securityContext -> annotation - "RuntimeDefault": ["runtime/default", "docker/default"], - "Unconfined": ["unconfined"], - "Localhost": ["localhost"], - # annotation -> securityContext - "runtime/default": ["RuntimeDefault"], - "docker/default": ["RuntimeDefault"], - "unconfined": ["Unconfined"], - "localhost": ["Localhost"], - } - - violation[{"msg": msg}] { - not input_wildcard_allowed_profiles - allowed_profiles := get_allowed_profiles - container := input_containers[name] - not is_exempt(container) - result := get_profile(container) - not allowed_profile(result.profile, result.file, allowed_profiles) - msg := get_message(result.profile, result.file, name, result.location, allowed_profiles) - } - - get_message(profile, _, name, location, allowed_profiles) = message { - not profile == "Localhost" - message := sprintf("Seccomp profile '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, name, location, allowed_profiles]) - } - - get_message(profile, file, name, location, allowed_profiles) = message { - profile == "Localhost" - message := sprintf("Seccomp profile '%v' with file '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, file, name, location, allowed_profiles]) - } - - input_wildcard_allowed_profiles { - input.parameters.allowedProfiles[_] == "*" - } - - input_wildcard_allowed_files { - input.parameters.allowedLocalhostFiles[_] == "*" - } - - input_wildcard_allowed_files { - "localhost/*" == input.parameters.allowedProfiles[_] - } - - # Simple allowed Profiles - allowed_profile(profile, _, allowed) { - not startswith(lower(profile), "localhost") - profile == allowed[_] - } - - # seccomp Localhost without wildcard - allowed_profile(profile, file, allowed) { - profile == "Localhost" - not input_wildcard_allowed_files - profile == allowed[_] - allowed_files := {x | x := object.get(input.parameters, "allowedLocalhostFiles", [])[_]} | get_annotation_localhost_files - file == allowed_files[_] - } - - # seccomp Localhost with wildcard - allowed_profile(profile, _, allowed) { - profile == "Localhost" - input_wildcard_allowed_files - profile == allowed[_] - } - - # annotation localhost with wildcard - allowed_profile(profile, _, allowed) { - "localhost/*" == allowed[_] - startswith(profile, "localhost/") - } - - # annotation localhost without wildcard - allowed_profile(profile, _, allowed) { - startswith(profile, "localhost/") - profile == allowed[_] - } - - # Localhost files from annotation scheme - get_annotation_localhost_files[file] { - profile := input.parameters.allowedProfiles[_] - startswith(profile, "localhost/") - file := replace(profile, "localhost/", "") - } - - # The profiles explicitly in the list - get_allowed_profiles[allowed] { - allowed := input.parameters.allowedProfiles[_] - } - - # The simply translated profiles - get_allowed_profiles[allowed] { - profile := input.parameters.allowedProfiles[_] - not startswith(lower(profile), "localhost") - allowed := naming_translation[profile][_] - } - - # Seccomp Localhost to annotation translation - get_allowed_profiles[allowed] { - profile := input.parameters.allowedProfiles[_] - profile == "Localhost" - file := object.get(input.parameters, "allowedLocalhostFiles", [])[_] - allowed := sprintf("%v/%v", [naming_translation[profile][_], file]) - } - - # Annotation localhost to Seccomp translation - get_allowed_profiles[allowed] { - profile := input.parameters.allowedProfiles[_] - startswith(profile, "localhost") - allowed := naming_translation.localhost[_] - } - - # Container profile as defined in pod annotation - get_profile(container) = {"profile": profile, "file": "", "location": location} { - not has_securitycontext_container(container) - not has_annotation(get_container_annotation_key(container.name)) - not has_securitycontext_pod - profile := input.review.object.metadata.annotations[pod_annotation_key] - location := sprintf("annotation %v", [pod_annotation_key]) - } - - # Container profile as defined in container annotation - get_profile(container) = {"profile": profile, "file": "", "location": location} { - not has_securitycontext_container(container) - not has_securitycontext_pod - container_annotation := get_container_annotation_key(container.name) - has_annotation(container_annotation) - profile := input.review.object.metadata.annotations[container_annotation] - location := sprintf("annotation %v", [container_annotation]) - } - - # Container profile as defined in pods securityContext - get_profile(container) = {"profile": profile, "file": file, "location": location} { - not has_securitycontext_container(container) - profile := input.review.object.spec.securityContext.seccompProfile.type - file := object.get(input.review.object.spec.securityContext.seccompProfile, "localhostProfile", "") - location := "pod securityContext" - } - - # Container profile as defined in containers securityContext - get_profile(container) = {"profile": profile, "file": file, "location": location} { - has_securitycontext_container(container) - profile := container.securityContext.seccompProfile.type - file := object.get(container.securityContext.seccompProfile, "localhostProfile", "") - location := "container securityContext" - } - - # Container profile missing - get_profile(container) = {"profile": "not configured", "file": "", "location": "no explicit profile found"} { - not has_annotation(get_container_annotation_key(container.name)) - not has_annotation(pod_annotation_key) - not has_securitycontext_pod - not has_securitycontext_container(container) - } - - has_annotation(annotation) { - input.review.object.metadata.annotations[annotation] - } - - has_securitycontext_pod { - input.review.object.spec.securityContext.seccompProfile - } - - has_securitycontext_container(container) { - container.securityContext.seccompProfile - } - - get_container_annotation_key(name) = annotation { - annotation := concat("", [container_annotation_key_prefix, name]) - } - - input_containers[container.name] = container { - container := input.review.object.spec.containers[_] - } - - input_containers[container.name] = container { - container := input.review.object.spec.initContainers[_] - } - - input_containers[container.name] = container { - container := input.review.object.spec.ephemeralContainers[_] - } - libs: - - | - 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) - } + code: + - engine: K8sNativeValidation + source: + variables: + - name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' + - name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' + - name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' + - name: allowAllProfiles + expression: | + has(variables.params.allowedProfiles) && variables.params.allowedProfiles.exists(profile, profile == "*") + - 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))).map(container, container.image) + - name: unverifiedContainers + expression: | + (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, + !variables.allowAllProfiles && + !(container.image in variables.exemptImages)) + - name: inputAllowedProfiles + expression: | + !has(variables.params.allowedProfiles) ? [] : variables.params.allowedProfiles + - name: allowedLocalhostFiles + expression: | + has(variables.params.allowedLocalhostFiles) ? variables.params.allowedLocalhostFiles : [] + - name: allowedProfilesTranslation + expression: | + (variables.inputAllowedProfiles.filter(profile, + profile != "Localhost").map(profile, profile == "Unconfined" ? "unconfined" : profile)) + + (variables.inputAllowedProfiles.exists(profile, profile == "RuntimeDefault") ? ["runtime/default", "docker/default"] : []) + - name: allowSecurityContextLocalhost + expression: | + variables.inputAllowedProfiles.exists(profile, profile == "Localhost") + - name: derivedAllowedLocalhostFiles + expression: | + variables.allowSecurityContextLocalhost ? variables.params.allowedLocalhostFiles.map(file, "localhost/" + file) : [] + - name: localhostWildcardAllowed + expression: | + variables.inputAllowedProfiles.exists(profile, profile == "localhost/*") || variables.derivedAllowedLocalhostFiles.exists(profile, profile == "localhost/*") + - name: allowedProfiles + expression: | + (variables.allowedProfilesTranslation + variables.derivedAllowedLocalhostFiles) + - name: hasPodSeccomp + expression: | + has(variables.anyObject.spec.securityContext) && has(variables.anyObject.spec.securityContext.seccompProfile) + - name: hasPodAnnotations + expression: | + has(variables.anyObject.metadata.annotations) && ("seccomp.security.alpha.kubernetes.io/pod" in variables.anyObject.metadata.annotations) + - name: podAnnotationsProfiles + expression: | + variables.unverifiedContainers.filter(container, + !(has(container.securityContext) && has(container.securityContext.seccompProfile)) && + !(has(variables.anyObject.metadata.annotations) && (("container.seccomp.security.alpha.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations)) && + !variables.hasPodSeccomp && + variables.hasPodAnnotations + ).map(container, { + "container" : container.name, + "profile" : variables.anyObject.metadata.annotations["seccomp.security.alpha.kubernetes.io/pod"], + "file" : dyn(""), + "location" : dyn("annotation seccomp.security.alpha.kubernetes.io/pod"), + }) + - name: containerAnnotationsProfiles + expression: | + variables.unverifiedContainers.filter(container, + !(has(container.securityContext) && has(container.securityContext.seccompProfile)) && + !variables.hasPodSeccomp && + has(variables.anyObject.metadata.annotations) && (("container.seccomp.security.alpha.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations) + ).map(container, { + "container" : container.name, + "profile" : variables.anyObject.metadata.annotations["container.seccomp.security.alpha.kubernetes.io/" + container.name], + "file" : dyn(""), + "location" : dyn("annotation container.seccomp.security.alpha.kubernetes.io/" + container.name), + }) + - name: podLocalHostProfile + expression: | + variables.hasPodSeccomp && has(variables.anyObject.spec.securityContext.seccompProfile.localhostProfile) ? variables.anyObject.spec.securityContext.seccompProfile.localhostProfile : "" + - name: canonicalPodSecurityContextProfile + expression: | + has(variables.hasPodSeccomp) && has(variables.anyObject.spec.securityContext.seccompProfile.type) ? + (variables.anyObject.spec.securityContext.seccompProfile.type == "RuntimeDefault" ? ( + variables.allowedProfiles.exists(profile, profile == "runtime/default") ? "runtime/default" : variables.allowedProfiles.exists(profile, profile == "docker/default") ? "docker/default" : "runtime/default") : + variables.anyObject.spec.securityContext.seccompProfile.type == "Unconfined" ? "unconfined" : variables.anyObject.spec.securityContext.seccompProfile.type == "Localhost" ? "localhost/" + variables.podLocalHostProfile : "") + : "" + - name: podSecurityContextProfiles + expression: | + variables.unverifiedContainers.filter(container, + !(has(container.securityContext) && has(container.securityContext.seccompProfile)) && + variables.hasPodSeccomp + ).map(container, { + "container" : container.name, + "profile" : dyn(variables.canonicalPodSecurityContextProfile), + "file" : variables.podLocalHostProfile, + "location" : dyn("pod securityContext"), + }) + - name: containerSecurityContextProfiles + expression: | + variables.unverifiedContainers.filter(container, + has(container.securityContext) && has(container.securityContext.seccompProfile) + ).map(container, { + "container" : container.name, + "profile" : dyn(has(container.securityContext.seccompProfile.type) ? (container.securityContext.seccompProfile.type == "RuntimeDefault" ? ( + variables.allowedProfiles.exists(profile, profile == "runtime/default") ? "runtime/default" : variables.allowedProfiles.exists(profile, profile == "docker/default") ? "docker/default" : "runtime/default") : + container.securityContext.seccompProfile.type == "Unconfined" ? "unconfined" : container.securityContext.seccompProfile.type == "Localhost" ? "localhost/" + container.securityContext.seccompProfile.localhostProfile : "") + : ""), + "file" : has(container.securityContext.seccompProfile.localhostProfile) ? container.securityContext.seccompProfile.localhostProfile : dyn(""), + "location" : dyn("container securityContext"), + }) + - name: containerProfilesMissing + expression: | + variables.unverifiedContainers.filter(container, + !(has(container.securityContext) && has(container.securityContext.seccompProfile)) && + !(has(variables.anyObject.metadata.annotations) && (("container.seccomp.security.alpha.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations)) && + !variables.hasPodSeccomp && + !variables.hasPodAnnotations + ).map(container, { + "container" : container.name, + "profile" : dyn("not configured"), + "file" : dyn(""), + "location" : dyn("no explicit profile found"), + }) + - name: allContainerProfiles + expression: | + variables.podAnnotationsProfiles + variables.containerAnnotationsProfiles + variables.podSecurityContextProfiles + variables.containerSecurityContextProfiles + variables.containerProfilesMissing + - name: badContainerProfiles + expression: | + variables.allContainerProfiles.filter(badContainerProfile, + !((badContainerProfile.profile in variables.allowedProfiles) || (badContainerProfile.profile.startsWith("localhost/") && variables.localhostWildcardAllowed)) + ).map(badProfile, "Seccomp profile '" + badProfile.profile + "' is not allowed for container '" + badProfile.container + "'. Found at: " + badProfile.location + ". Allowed profiles: " + variables.allowedProfiles.join(", ")) + validations: + - expression: 'size(variables.badContainerProfiles) == 0' + messageExpression: | + variables.badContainerProfiles.join(", ") + - engine: Rego + source: + rego: | + package k8spspseccomp + + import data.lib.exempt_container.is_exempt + + container_annotation_key_prefix = "container.seccomp.security.alpha.kubernetes.io/" + + pod_annotation_key = "seccomp.security.alpha.kubernetes.io/pod" + + violation[{"msg": msg}] { + not input_wildcard_allowed_profiles + allowed_profiles := get_allowed_profiles + container := input_containers[name] + not is_exempt(container) + result := get_profile(container) + not allowed_profile(result.profile, result.file, allowed_profiles) + msg := get_message(result.profile, result.file, name, result.location, allowed_profiles) + } + + get_message(profile, _, name, location, allowed_profiles) = message { + message := sprintf("Seccomp profile '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, name, location, allowed_profiles]) + } + + input_wildcard_allowed_profiles { + input.parameters.allowedProfiles[_] == "*" + } + + input_wildcard_allowed_files { + input.parameters.allowedLocalhostFiles[_] == "*" + } + + input_wildcard_allowed_files { + "localhost/*" == input.parameters.allowedProfiles[_] + } + + # Simple allowed Profiles + allowed_profile(profile, _, allowed) { + not startswith(profile, "localhost/") + profile == allowed[_] + } + + # annotation localhost with wildcard + allowed_profile(profile, _, allowed) { + "localhost/*" == allowed[_] + startswith(profile, "localhost/") + } + + # annotation localhost without wildcard + allowed_profile(profile, _, allowed) { + startswith(profile, "localhost/") + profile == allowed[_] + } + + # The profiles explicitly in the list + get_allowed_profiles[allowed] { + allowed := input.parameters.allowedProfiles[_] + } + + # Seccomp Localhost to annotation translation + get_allowed_profiles[allowed] { + profile := input.parameters.allowedProfiles[_] + not contains(profile, "/") + file := object.get(input.parameters, "allowedLocalhostFiles", [])[_] + allowed := canonicalize_seccomp_profile({"type": profile, "localhostProfile": file}, "")[_] + } + + # Container profile as defined in pod annotation + get_profile(container) = {"profile": profile, "file": "", "location": location} { + not has_securitycontext_container(container) + not has_annotation(get_container_annotation_key(container.name)) + not has_securitycontext_pod + profile := input.review.object.metadata.annotations[pod_annotation_key] + location := sprintf("annotation %v", [pod_annotation_key]) + } + + # Container profile as defined in container annotation + get_profile(container) = {"profile": profile, "file": "", "location": location} { + not has_securitycontext_container(container) + not has_securitycontext_pod + container_annotation := get_container_annotation_key(container.name) + has_annotation(container_annotation) + profile := input.review.object.metadata.annotations[container_annotation] + location := sprintf("annotation %v", [container_annotation]) + } + + # Container profile as defined in pods securityContext + get_profile(container) = {"profile": profile, "file": file, "location": location} { + not has_securitycontext_container(container) + profile := canonicalize_seccomp_profile(input.review.object.spec.securityContext.seccompProfile, canonicalize_runtime_default_profile)[_] + file := object.get(input.review.object.spec.securityContext.seccompProfile, "localhostProfile", "") + location := "pod securityContext" + } + + # Container profile as defined in containers securityContext + get_profile(container) = {"profile": profile, "file": file, "location": location} { + has_securitycontext_container(container) + profile := canonicalize_seccomp_profile(container.securityContext.seccompProfile, canonicalize_runtime_default_profile)[_] + file := object.get(container.securityContext.seccompProfile, "localhostProfile", "") + location := "container securityContext" + } + + # Container profile missing + get_profile(container) = {"profile": "not configured", "file": "", "location": "no explicit profile found"} { + not has_securitycontext_container(container) + not has_securitycontext_pod + not has_annotation(get_container_annotation_key(container.name)) + not has_annotation(pod_annotation_key) + } + + has_annotation(annotation) { + input.review.object.metadata.annotations[annotation] + } + + has_securitycontext_pod { + input.review.object.spec.securityContext.seccompProfile + } + + has_securitycontext_container(container) { + container.securityContext.seccompProfile + } + + get_container_annotation_key(name) = annotation { + annotation := concat("", [container_annotation_key_prefix, name]) + } + + input_containers[container.name] = container { + container := input.review.object.spec.containers[_] + } + + input_containers[container.name] = container { + container := input.review.object.spec.initContainers[_] + } + + input_containers[container.name] = container { + container := input.review.object.spec.ephemeralContainers[_] + } + + canonicalize_runtime_default_profile() = out { + "runtime/default" == input.parameters.allowedProfiles[_] + out := "runtime/default" + } else = out { + "docker/default" == input.parameters.allowedProfiles[_] + out := "docker/default" + } else = out { + out := "runtime/default" + } + + canonicalize_seccomp_profile(profile, def) = out { + profile.type == "RuntimeDefault" + def == "" + out := ["runtime/default", "docker/default"] + } else = out { + profile.type == "RuntimeDefault" + def != "" + out := [def] + } else = out { + profile.type == "Localhost" + out := [sprintf("localhost/%s", [profile.localhostProfile])] + } else = out { + profile.type == "Unconfined" + out := ["unconfined"] + } + libs: + - | + 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) + } diff --git a/library/pod-security-policy/seccompv2/kustomization.yaml b/library/pod-security-policy/seccompv2/kustomization.yaml new file mode 100644 index 000000000..7d70d11b7 --- /dev/null +++ b/library/pod-security-policy/seccompv2/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - template.yaml diff --git a/library/pod-security-policy/seccompv2/samples/psp-seccomp/constraint.yaml b/library/pod-security-policy/seccompv2/samples/psp-seccomp/constraint.yaml new file mode 100644 index 000000000..a85141492 --- /dev/null +++ b/library/pod-security-policy/seccompv2/samples/psp-seccomp/constraint.yaml @@ -0,0 +1,17 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPSeccompV2 +metadata: + name: psp-seccomp +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + exemptImages: + - nginx-exempt + allowedProfiles: + - RuntimeDefault + - Localhost + allowedLocalhostFiles: + - "*" diff --git a/library/pod-security-policy/seccompv2/samples/psp-seccomp/disallowed_ephemeral.yaml b/library/pod-security-policy/seccompv2/samples/psp-seccomp/disallowed_ephemeral.yaml new file mode 100644 index 000000000..4b32243a9 --- /dev/null +++ b/library/pod-security-policy/seccompv2/samples/psp-seccomp/disallowed_ephemeral.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-disallowed + labels: + app: nginx-seccomp +spec: + ephemeralContainers: + - name: nginx + image: nginx diff --git a/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed.yaml b/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed.yaml new file mode 100644 index 000000000..65dfc5be8 --- /dev/null +++ b/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-allowed + labels: + app: nginx-seccomp +spec: + containers: + - name: nginx + image: nginx + securityContext: + seccompProfile: + type: RuntimeDefault diff --git a/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed_exempt_image.yaml b/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed_exempt_image.yaml new file mode 100644 index 000000000..d5f42987d --- /dev/null +++ b/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed_exempt_image.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-disallowed + labels: + app: nginx-seccomp +spec: + containers: + - name: nginx + image: nginx-exempt + securityContext: + seccompProfile: + type: Unconfined diff --git a/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed_localhost.yaml b/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed_localhost.yaml new file mode 100644 index 000000000..856f3217b --- /dev/null +++ b/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed_localhost.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-allowed-localhost + labels: + app: nginx-seccomp +spec: + containers: + - name: nginx + image: nginx + securityContext: + seccompProfile: + type: Localhost + localhostProfile: profile.json diff --git a/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_disallowed.yaml b/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_disallowed.yaml new file mode 100644 index 000000000..40c115b86 --- /dev/null +++ b/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_disallowed.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-disallowed + labels: + app: nginx-seccomp +spec: + containers: + - name: nginx + image: nginx + securityContext: + seccompProfile: + type: Unconfined diff --git a/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_disallowed2.yaml b/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_disallowed2.yaml new file mode 100644 index 000000000..e08463f18 --- /dev/null +++ b/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_disallowed2.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-disallowed2 + labels: + app: nginx-seccomp +spec: + securityContext: + seccompProfile: + type: Unconfined + containers: + - name: nginx + image: nginx diff --git a/library/pod-security-policy/seccompv2/suite.yaml b/library/pod-security-policy/seccompv2/suite.yaml new file mode 100644 index 000000000..f9daa264d --- /dev/null +++ b/library/pod-security-policy/seccompv2/suite.yaml @@ -0,0 +1,36 @@ +kind: Suite +apiVersion: test.gatekeeper.sh/v1alpha1 +metadata: + name: seccomp +tests: +- name: default-seccomp-required + template: template.yaml + constraint: samples/psp-seccomp/constraint.yaml + cases: + - name: example-disallowed-global + object: samples/psp-seccomp/example_disallowed2.yaml + assertions: + - violations: 1 + message: "Seccomp profile 'Unconfined' is not allowed for container 'nginx'. Found at: pod securityContext." + - name: example-disallowed-container + object: samples/psp-seccomp/example_disallowed.yaml + assertions: + - violations: 1 + message: "Seccomp profile 'Unconfined' is not allowed for container 'nginx'. Found at: container securityContext." + - name: example-allowed-container + object: samples/psp-seccomp/example_allowed.yaml + assertions: + - violations: no + - name: example-allowed-container + object: samples/psp-seccomp/example_allowed_localhost.yaml + assertions: + - violations: no + - name: example-allowed-container-exempt-image + object: samples/psp-seccomp/example_allowed_exempt_image.yaml + assertions: + - violations: no + - name: disallowed-ephemeral + object: samples/psp-seccomp/disallowed_ephemeral.yaml + assertions: + - violations: 1 + message: "Seccomp profile 'not configured' is not allowed for container 'nginx'. Found at: no explicit profile found" diff --git a/library/pod-security-policy/seccompv2/template.yaml b/library/pod-security-policy/seccompv2/template.yaml new file mode 100644 index 000000000..ce0a53202 --- /dev/null +++ b/library/pod-security-policy/seccompv2/template.yaml @@ -0,0 +1,301 @@ +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: k8spspseccompv2 + annotations: + metadata.gatekeeper.sh/title: "Seccomp V2" + metadata.gatekeeper.sh/version: 1.0.0 + description: >- + Controls the seccomp profile used by containers. Corresponds to the + `securityContext.seccompProfile` field. Security contexts from the annotation is not considered as Kubernetes no longer reads security contexts from the annotation. +spec: + crd: + spec: + names: + kind: K8sPSPSeccompV2 + validation: + # Schema for the `parameters` field + openAPIV3Schema: + type: object + description: >- + Controls the seccomp profile used by containers. Corresponds to the + `securityContext.seccompProfile` field. Security contexts from the annotation is not considered as Kubernetes no longer reads security contexts from the annotation. + 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 + allowedProfiles: + type: array + description: >- + An array of allowed profile values for seccomp on Pods/Containers. + + Can use the securityContext naming scheme: `RuntimeDefault`, `Unconfined` + and/or `Localhost`. For securityContext `Localhost`, use the parameter `allowedLocalhostFiles` + to list the allowed profile JSON files. + + The policy code will translate between the two schemes so it is not necessary to use both. + + Putting a `*` in this array allows all Profiles to be used. + + This field is required since with an empty list this policy will block all workloads. + items: + type: string + allowedLocalhostFiles: + type: array + description: >- + When using securityContext naming scheme for seccomp and including `Localhost` this array holds + the allowed profile JSON files. + + Putting a `*` in this array will allows all JSON files to be used. + + This field is required to allow `Localhost` in securityContext as with an empty list it will block. + items: + type: string + targets: + - target: admission.k8s.gatekeeper.sh + code: + - engine: K8sNativeValidation + source: + variables: + - name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' + - name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' + - name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' + - name: allowAllProfiles + expression: | + has(variables.params.allowedProfiles) && variables.params.allowedProfiles.exists(profile, profile == "*") + - 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))).map(container, container.image) + - name: unverifiedContainers + expression: | + (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, + !variables.allowAllProfiles && + !(container.image in variables.exemptImages)) + - name: inputNonLocalHostProfiles + expression: | + variables.params.allowedProfiles.filter(profile, profile != "Localhost").map(profile, {"type": profile}) + - name: inputLocalHostProfiles + expression: | + variables.params.allowedProfiles.exists(profile, profile == "Localhost") ? variables.params.allowedLocalhostFiles.map(file, {"type": "Localhost", "localHostProfile": string(file)}) : [] + - name: inputAllowedProfiles + expression: | + variables.inputNonLocalHostProfiles + variables.inputLocalHostProfiles + - name: hasPodSeccomp + expression: | + has(variables.anyObject.spec.securityContext) && has(variables.anyObject.spec.securityContext.seccompProfile) + - name: podLocalHostProfile + expression: | + variables.hasPodSeccomp && has(variables.anyObject.spec.securityContext.seccompProfile.localhostProfile) ? variables.anyObject.spec.securityContext.seccompProfile.localhostProfile : "" + - name: podSecurityContextProfileType + expression: | + has(variables.hasPodSeccomp) && has(variables.anyObject.spec.securityContext.seccompProfile.type) ? variables.anyObject.spec.securityContext.seccompProfile.type + : "" + - name: podSecurityContextProfiles + expression: | + variables.unverifiedContainers.filter(container, + !(has(container.securityContext) && has(container.securityContext.seccompProfile)) && + variables.hasPodSeccomp + ).map(container, { + "container" : container.name, + "profile" : dyn(variables.podSecurityContextProfileType), + "file" : variables.podLocalHostProfile, + "location" : dyn("pod securityContext"), + }) + - name: containerSecurityContextProfiles + expression: | + variables.unverifiedContainers.filter(container, + has(container.securityContext) && has(container.securityContext.seccompProfile) + ).map(container, { + "container" : container.name, + "profile" : dyn(container.securityContext.seccompProfile.type), + "file" : has(container.securityContext.seccompProfile.localhostProfile) ? container.securityContext.seccompProfile.localhostProfile : dyn(""), + "location" : dyn("container securityContext"), + }) + - name: containerProfilesMissing + expression: | + variables.unverifiedContainers.filter(container, + !(has(container.securityContext) && has(container.securityContext.seccompProfile)) && + !variables.hasPodSeccomp + ).map(container, { + "container" : container.name, + "profile" : dyn("not configured"), + "file" : dyn(""), + "location" : dyn("no explicit profile found"), + }) + - name: allContainerProfiles + expression: | + variables.podSecurityContextProfiles + variables.containerSecurityContextProfiles + variables.containerProfilesMissing + - name: badContainerProfilesWithoutFiles + expression: | + variables.allContainerProfiles.filter(container, + container.profile != "Localhost" && + !variables.inputAllowedProfiles.exists(profile, profile.type == container.profile) + ).map(badProfile, "Seccomp profile '" + badProfile.profile + "' is not allowed for container '" + badProfile.container + "'. Found at: " + badProfile.location + ". Allowed profiles: " + variables.inputAllowedProfiles.map(profile, "{\"type\": \"" + profile.type + "\"" + (has(profile.localHostProfile) ? ", \"localHostProfile\": \"" + profile.localHostProfile + "\"}" : "}")).join(", ")) + - name: badContainerProfilesWithFiles + expression: | + variables.allContainerProfiles.filter(container, + container.profile == "Localhost" && + !variables.inputAllowedProfiles.exists(profile, profile.type == "Localhost" && (has(profile.localHostProfile) && (profile.localHostProfile == container.file || profile.localHostProfile == "*"))) + ).map(badProfile, "Seccomp profile '" + badProfile.profile + "' With file '" + badProfile.file + "' is not allowed for container '" + badProfile.container + "'. Found at: " + badProfile.location + ". Allowed profiles: " + variables.inputAllowedProfiles.map(profile, "{\"type\": \"" + profile.type + "\"" + (has(profile.localHostProfile) ? ", \"localHostProfile\": \"" + profile.localHostProfile + "\"}" : "}")).join(", ")) + validations: + - expression: 'size(variables.badContainerProfilesWithoutFiles) == 0' + messageExpression: | + variables.badContainerProfilesWithoutFiles.join(", ") + - expression: 'size(variables.badContainerProfilesWithFiles) == 0' + messageExpression: | + variables.badContainerProfilesWithFiles.join(", ") + - engine: Rego + source: + rego: | + package k8spspseccomp + + import data.lib.exempt_container.is_exempt + + violation[{"msg": msg}] { + not input_wildcard_allowed_profiles + allowed_profiles := get_allowed_profiles + container := input_containers[name] + not is_exempt(container) + result := get_profile(container) + not allowed_profile(result.profile, result.file, allowed_profiles) + msg := get_message(result.profile, result.file, name, result.location, allowed_profiles) + } + + get_message(profile, _, name, location, allowed_profiles) = message { + profile != "Localhost" + message := sprintf("Seccomp profile '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, name, location, allowed_profiles]) + } + + get_message(profile, file, name, location, allowed_profiles) = message { + profile == "Localhost" + message := sprintf("Seccomp profile '%v' with file '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, file, name, location, allowed_profiles]) + } + + input_wildcard_allowed_profiles { + input.parameters.allowedProfiles[_] == "*" + } + + input_wildcard_allowed_files { + input.parameters.allowedLocalhostFiles[_] == "*" + } + + allowed_profile(_, _, _) { + input_wildcard_allowed_profiles + } + + allowed_profile(profile, _, _) { + profile == "Localhost" + input_wildcard_allowed_files + } + + # Simple allowed Profiles + allowed_profile(profile, _, allowed) { + profile != "Localhost" + allow_profile = allowed[_] + profile == allow_profile.type + } + + # annotation localhost without wildcard + allowed_profile(profile, file, allowed) { + profile == "Localhost" + allow_profile = allowed[_] + allow_profile.type == "Localhost" + file == allow_profile.localHostProfile + } + + # The profiles explicitly in the list + get_allowed_profiles[allowed] { + profile := input.parameters.allowedProfiles[_] + profile != "Localhost" + allowed := {"type": profile} + } + + get_allowed_profiles[allowed] { + profile := input.parameters.allowedProfiles[_] + profile == "Localhost" + file := object.get(input.parameters, "allowedLocalhostFiles", [""])[_] + allowed := {"type": "Localhost", "localHostProfile": file} + } + + # Container profile as defined in containers securityContext + get_profile(container) = {"profile": profile, "file": file, "location": location} { + has_securitycontext_container(container) + profile := container.securityContext.seccompProfile.type + file := object.get(container.securityContext.seccompProfile, "localhostProfile", "") + location := "container securityContext" + } + + # Container profile as defined in pods securityContext + get_profile(container) = {"profile": profile, "file": file, "location": location} { + not has_securitycontext_container(container) + profile := input.review.object.spec.securityContext.seccompProfile.type + file := object.get(input.review.object.spec.securityContext.seccompProfile, "localhostProfile", "") + location := "pod securityContext" + } + + # Container profile missing + get_profile(container) = {"profile": "not configured", "file": "", "location": "no explicit profile found"} { + not has_securitycontext_container(container) + not has_securitycontext_pod + } + + has_securitycontext_pod { + input.review.object.spec.securityContext.seccompProfile + } + + has_securitycontext_container(container) { + container.securityContext.seccompProfile + } + + input_containers[container.name] = container { + container := input.review.object.spec.containers[_] + } + + input_containers[container.name] = container { + container := input.review.object.spec.initContainers[_] + } + + input_containers[container.name] = container { + container := input.review.object.spec.ephemeralContainers[_] + } + libs: + - | + 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) + } diff --git a/src/pod-security-policy/seccomp/constraint.tmpl b/src/pod-security-policy/seccomp/constraint.tmpl index 7a5c95538..5fdd6d074 100644 --- a/src/pod-security-policy/seccomp/constraint.tmpl +++ b/src/pod-security-policy/seccomp/constraint.tmpl @@ -4,7 +4,7 @@ metadata: name: k8spspseccomp annotations: metadata.gatekeeper.sh/title: "Seccomp" - metadata.gatekeeper.sh/version: 1.0.1 + metadata.gatekeeper.sh/version: 1.1.0 description: >- Controls the seccomp profile used by containers. Corresponds to the `seccomp.security.alpha.kubernetes.io/allowedProfileNames` annotation on @@ -67,8 +67,14 @@ spec: type: string targets: - target: admission.k8s.gatekeeper.sh - rego: | -{{ file.Read "src/pod-security-policy/seccomp/src.rego" | strings.Indent 8 | strings.TrimSuffix "\n" }} - libs: - - | -{{ file.Read "src/pod-security-policy/seccomp/lib_exempt_container.rego" | strings.Indent 10 | strings.TrimSuffix "\n" }} + code: + - engine: K8sNativeValidation + source: +{{ file.Read "src/pod-security-policy/seccomp/src.cel" | strings.Indent 10 | strings.TrimSuffix "\n" }} + - engine: Rego + source: + rego: | +{{ file.Read "src/pod-security-policy/seccomp/src.rego" | strings.Indent 12 | strings.TrimSuffix "\n" }} + libs: + - | +{{ file.Read "src/pod-security-policy/seccomp/lib_exempt_container.rego" | strings.Indent 14 | strings.TrimSuffix "\n" }} diff --git a/src/pod-security-policy/seccomp/src.cel b/src/pod-security-policy/seccomp/src.cel new file mode 100644 index 000000000..bdb8b218b --- /dev/null +++ b/src/pod-security-policy/seccomp/src.cel @@ -0,0 +1,141 @@ +variables: +- name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' +- name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' +- name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' +- name: allowAllProfiles + expression: | + has(variables.params.allowedProfiles) && variables.params.allowedProfiles.exists(profile, profile == "*") +- 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))).map(container, container.image) +- name: unverifiedContainers + expression: | + (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, + !variables.allowAllProfiles && + !(container.image in variables.exemptImages)) +- name: inputAllowedProfiles + expression: | + !has(variables.params.allowedProfiles) ? [] : variables.params.allowedProfiles +- name: allowedLocalhostFiles + expression: | + has(variables.params.allowedLocalhostFiles) ? variables.params.allowedLocalhostFiles : [] +- name: allowedProfilesTranslation + expression: | + (variables.inputAllowedProfiles.filter(profile, + profile != "Localhost").map(profile, profile == "Unconfined" ? "unconfined" : profile)) + + (variables.inputAllowedProfiles.exists(profile, profile == "RuntimeDefault") ? ["runtime/default", "docker/default"] : []) +- name: allowSecurityContextLocalhost + expression: | + variables.inputAllowedProfiles.exists(profile, profile == "Localhost") +- name: derivedAllowedLocalhostFiles + expression: | + variables.allowSecurityContextLocalhost ? variables.params.allowedLocalhostFiles.map(file, "localhost/" + file) : [] +- name: localhostWildcardAllowed + expression: | + variables.inputAllowedProfiles.exists(profile, profile == "localhost/*") || variables.derivedAllowedLocalhostFiles.exists(profile, profile == "localhost/*") +- name: allowedProfiles + expression: | + (variables.allowedProfilesTranslation + variables.derivedAllowedLocalhostFiles) +- name: hasPodSeccomp + expression: | + has(variables.anyObject.spec.securityContext) && has(variables.anyObject.spec.securityContext.seccompProfile) +- name: hasPodAnnotations + expression: | + has(variables.anyObject.metadata.annotations) && ("seccomp.security.alpha.kubernetes.io/pod" in variables.anyObject.metadata.annotations) +- name: podAnnotationsProfiles + expression: | + variables.unverifiedContainers.filter(container, + !(has(container.securityContext) && has(container.securityContext.seccompProfile)) && + !(has(variables.anyObject.metadata.annotations) && (("container.seccomp.security.alpha.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations)) && + !variables.hasPodSeccomp && + variables.hasPodAnnotations + ).map(container, { + "container" : container.name, + "profile" : variables.anyObject.metadata.annotations["seccomp.security.alpha.kubernetes.io/pod"], + "file" : dyn(""), + "location" : dyn("annotation seccomp.security.alpha.kubernetes.io/pod"), + }) +- name: containerAnnotationsProfiles + expression: | + variables.unverifiedContainers.filter(container, + !(has(container.securityContext) && has(container.securityContext.seccompProfile)) && + !variables.hasPodSeccomp && + has(variables.anyObject.metadata.annotations) && (("container.seccomp.security.alpha.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations) + ).map(container, { + "container" : container.name, + "profile" : variables.anyObject.metadata.annotations["container.seccomp.security.alpha.kubernetes.io/" + container.name], + "file" : dyn(""), + "location" : dyn("annotation container.seccomp.security.alpha.kubernetes.io/" + container.name), + }) +- name: podLocalHostProfile + expression: | + variables.hasPodSeccomp && has(variables.anyObject.spec.securityContext.seccompProfile.localhostProfile) ? variables.anyObject.spec.securityContext.seccompProfile.localhostProfile : "" +- name: canonicalPodSecurityContextProfile + expression: | + has(variables.hasPodSeccomp) && has(variables.anyObject.spec.securityContext.seccompProfile.type) ? + (variables.anyObject.spec.securityContext.seccompProfile.type == "RuntimeDefault" ? ( + variables.allowedProfiles.exists(profile, profile == "runtime/default") ? "runtime/default" : variables.allowedProfiles.exists(profile, profile == "docker/default") ? "docker/default" : "runtime/default") : + variables.anyObject.spec.securityContext.seccompProfile.type == "Unconfined" ? "unconfined" : variables.anyObject.spec.securityContext.seccompProfile.type == "Localhost" ? "localhost/" + variables.podLocalHostProfile : "") + : "" +- name: podSecurityContextProfiles + expression: | + variables.unverifiedContainers.filter(container, + !(has(container.securityContext) && has(container.securityContext.seccompProfile)) && + variables.hasPodSeccomp + ).map(container, { + "container" : container.name, + "profile" : dyn(variables.canonicalPodSecurityContextProfile), + "file" : variables.podLocalHostProfile, + "location" : dyn("pod securityContext"), + }) +- name: containerSecurityContextProfiles + expression: | + variables.unverifiedContainers.filter(container, + has(container.securityContext) && has(container.securityContext.seccompProfile) + ).map(container, { + "container" : container.name, + "profile" : dyn(has(container.securityContext.seccompProfile.type) ? (container.securityContext.seccompProfile.type == "RuntimeDefault" ? ( + variables.allowedProfiles.exists(profile, profile == "runtime/default") ? "runtime/default" : variables.allowedProfiles.exists(profile, profile == "docker/default") ? "docker/default" : "runtime/default") : + container.securityContext.seccompProfile.type == "Unconfined" ? "unconfined" : container.securityContext.seccompProfile.type == "Localhost" ? "localhost/" + container.securityContext.seccompProfile.localhostProfile : "") + : ""), + "file" : has(container.securityContext.seccompProfile.localhostProfile) ? container.securityContext.seccompProfile.localhostProfile : dyn(""), + "location" : dyn("container securityContext"), + }) +- name: containerProfilesMissing + expression: | + variables.unverifiedContainers.filter(container, + !(has(container.securityContext) && has(container.securityContext.seccompProfile)) && + !(has(variables.anyObject.metadata.annotations) && (("container.seccomp.security.alpha.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations)) && + !variables.hasPodSeccomp && + !variables.hasPodAnnotations + ).map(container, { + "container" : container.name, + "profile" : dyn("not configured"), + "file" : dyn(""), + "location" : dyn("no explicit profile found"), + }) +- name: allContainerProfiles + expression: | + variables.podAnnotationsProfiles + variables.containerAnnotationsProfiles + variables.podSecurityContextProfiles + variables.containerSecurityContextProfiles + variables.containerProfilesMissing +- name: badContainerProfiles + expression: | + variables.allContainerProfiles.filter(badContainerProfile, + !((badContainerProfile.profile in variables.allowedProfiles) || (badContainerProfile.profile.startsWith("localhost/") && variables.localhostWildcardAllowed)) + ).map(badProfile, "Seccomp profile '" + badProfile.profile + "' is not allowed for container '" + badProfile.container + "'. Found at: " + badProfile.location + ". Allowed profiles: " + variables.allowedProfiles.join(", ")) +validations: +- expression: 'size(variables.badContainerProfiles) == 0' + messageExpression: | + variables.badContainerProfiles.join(", ") diff --git a/src/pod-security-policy/seccomp/src.rego b/src/pod-security-policy/seccomp/src.rego index 61185ddb9..d06a271d1 100644 --- a/src/pod-security-policy/seccomp/src.rego +++ b/src/pod-security-policy/seccomp/src.rego @@ -6,18 +6,6 @@ container_annotation_key_prefix = "container.seccomp.security.alpha.kubernetes.i pod_annotation_key = "seccomp.security.alpha.kubernetes.io/pod" -naming_translation = { - # securityContext -> annotation - "RuntimeDefault": ["runtime/default", "docker/default"], - "Unconfined": ["unconfined"], - "Localhost": ["localhost"], - # annotation -> securityContext - "runtime/default": ["RuntimeDefault"], - "docker/default": ["RuntimeDefault"], - "unconfined": ["Unconfined"], - "localhost": ["Localhost"], -} - violation[{"msg": msg}] { not input_wildcard_allowed_profiles allowed_profiles := get_allowed_profiles @@ -29,15 +17,9 @@ violation[{"msg": msg}] { } get_message(profile, _, name, location, allowed_profiles) = message { - not profile == "Localhost" message := sprintf("Seccomp profile '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, name, location, allowed_profiles]) } -get_message(profile, file, name, location, allowed_profiles) = message { - profile == "Localhost" - message := sprintf("Seccomp profile '%v' with file '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, file, name, location, allowed_profiles]) -} - input_wildcard_allowed_profiles { input.parameters.allowedProfiles[_] == "*" } @@ -52,23 +34,7 @@ input_wildcard_allowed_files { # Simple allowed Profiles allowed_profile(profile, _, allowed) { - not startswith(lower(profile), "localhost") - profile == allowed[_] -} - -# seccomp Localhost without wildcard -allowed_profile(profile, file, allowed) { - profile == "Localhost" - not input_wildcard_allowed_files - profile == allowed[_] - allowed_files := {x | x := object.get(input.parameters, "allowedLocalhostFiles", [])[_]} | get_annotation_localhost_files - file == allowed_files[_] -} - -# seccomp Localhost with wildcard -allowed_profile(profile, _, allowed) { - profile == "Localhost" - input_wildcard_allowed_files + not startswith(profile, "localhost/") profile == allowed[_] } @@ -84,38 +50,17 @@ allowed_profile(profile, _, allowed) { profile == allowed[_] } -# Localhost files from annotation scheme -get_annotation_localhost_files[file] { - profile := input.parameters.allowedProfiles[_] - startswith(profile, "localhost/") - file := replace(profile, "localhost/", "") -} - # The profiles explicitly in the list get_allowed_profiles[allowed] { allowed := input.parameters.allowedProfiles[_] } -# The simply translated profiles -get_allowed_profiles[allowed] { - profile := input.parameters.allowedProfiles[_] - not startswith(lower(profile), "localhost") - allowed := naming_translation[profile][_] -} - # Seccomp Localhost to annotation translation get_allowed_profiles[allowed] { profile := input.parameters.allowedProfiles[_] - profile == "Localhost" + not contains(profile, "/") file := object.get(input.parameters, "allowedLocalhostFiles", [])[_] - allowed := sprintf("%v/%v", [naming_translation[profile][_], file]) -} - -# Annotation localhost to Seccomp translation -get_allowed_profiles[allowed] { - profile := input.parameters.allowedProfiles[_] - startswith(profile, "localhost") - allowed := naming_translation.localhost[_] + allowed := canonicalize_seccomp_profile({"type": profile, "localhostProfile": file}, "")[_] } # Container profile as defined in pod annotation @@ -140,7 +85,7 @@ get_profile(container) = {"profile": profile, "file": "", "location": location} # Container profile as defined in pods securityContext get_profile(container) = {"profile": profile, "file": file, "location": location} { not has_securitycontext_container(container) - profile := input.review.object.spec.securityContext.seccompProfile.type + profile := canonicalize_seccomp_profile(input.review.object.spec.securityContext.seccompProfile, canonicalize_runtime_default_profile)[_] file := object.get(input.review.object.spec.securityContext.seccompProfile, "localhostProfile", "") location := "pod securityContext" } @@ -148,17 +93,17 @@ get_profile(container) = {"profile": profile, "file": file, "location": location # Container profile as defined in containers securityContext get_profile(container) = {"profile": profile, "file": file, "location": location} { has_securitycontext_container(container) - profile := container.securityContext.seccompProfile.type + profile := canonicalize_seccomp_profile(container.securityContext.seccompProfile, canonicalize_runtime_default_profile)[_] file := object.get(container.securityContext.seccompProfile, "localhostProfile", "") location := "container securityContext" } # Container profile missing get_profile(container) = {"profile": "not configured", "file": "", "location": "no explicit profile found"} { + not has_securitycontext_container(container) + not has_securitycontext_pod not has_annotation(get_container_annotation_key(container.name)) not has_annotation(pod_annotation_key) - not has_securitycontext_pod - not has_securitycontext_container(container) } has_annotation(annotation) { @@ -188,3 +133,29 @@ input_containers[container.name] = container { input_containers[container.name] = container { container := input.review.object.spec.ephemeralContainers[_] } + +canonicalize_runtime_default_profile() = out { + "runtime/default" == input.parameters.allowedProfiles[_] + out := "runtime/default" +} else = out { + "docker/default" == input.parameters.allowedProfiles[_] + out := "docker/default" +} else = out { + out := "runtime/default" +} + +canonicalize_seccomp_profile(profile, def) = out { + profile.type == "RuntimeDefault" + def == "" + out := ["runtime/default", "docker/default"] +} else = out { + profile.type == "RuntimeDefault" + def != "" + out := [def] +} else = out { + profile.type == "Localhost" + out := [sprintf("localhost/%s", [profile.localhostProfile])] +} else = out { + profile.type == "Unconfined" + out := ["unconfined"] +} diff --git a/src/pod-security-policy/seccomp/src_test.rego b/src/pod-security-policy/seccomp/src_test.rego index 161afa359..74bba46c9 100644 --- a/src/pod-security-policy/seccomp/src_test.rego +++ b/src/pod-security-policy/seccomp/src_test.rego @@ -417,7 +417,7 @@ test_input_both_seccomp_pod_context_container_annotation_multiple_mixed { test_translation_seccomp_allowed_annotation_all { inp := {"parameters": input_parameters_annotation} output := get_allowed_profiles with input as inp - output == allowed_full_translated + output == allowed_full_translated_annotation_style } test_translation_seccomp_allowed_context_all { @@ -645,3 +645,11 @@ allowed_full_translated = { "RuntimeDefault", "docker/default", "runtime/default", "Unconfined", "unconfined", } + +allowed_full_translated_annotation_style = { + "runtime/default", + "docker/default", + "localhost/profile1.json", + "localhost/profile2.json", + "unconfined", +} diff --git a/src/pod-security-policy/seccompv2/constraint.tmpl b/src/pod-security-policy/seccompv2/constraint.tmpl new file mode 100644 index 000000000..d33f99ea4 --- /dev/null +++ b/src/pod-security-policy/seccompv2/constraint.tmpl @@ -0,0 +1,73 @@ +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: k8spspseccompv2 + annotations: + metadata.gatekeeper.sh/title: "Seccomp V2" + metadata.gatekeeper.sh/version: 1.0.0 + description: >- + Controls the seccomp profile used by containers. Corresponds to the + `securityContext.seccompProfile` field. Security contexts from the annotation is not considered as Kubernetes no longer reads security contexts from the annotation. +spec: + crd: + spec: + names: + kind: K8sPSPSeccompV2 + validation: + # Schema for the `parameters` field + openAPIV3Schema: + type: object + description: >- + Controls the seccomp profile used by containers. Corresponds to the + `securityContext.seccompProfile` field. Security contexts from the annotation is not considered as Kubernetes no longer reads security contexts from the annotation. + 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 + allowedProfiles: + type: array + description: >- + An array of allowed profile values for seccomp on Pods/Containers. + + Can use the securityContext naming scheme: `RuntimeDefault`, `Unconfined` + and/or `Localhost`. For securityContext `Localhost`, use the parameter `allowedLocalhostFiles` + to list the allowed profile JSON files. + + The policy code will translate between the two schemes so it is not necessary to use both. + + Putting a `*` in this array allows all Profiles to be used. + + This field is required since with an empty list this policy will block all workloads. + items: + type: string + allowedLocalhostFiles: + type: array + description: >- + When using securityContext naming scheme for seccomp and including `Localhost` this array holds + the allowed profile JSON files. + + Putting a `*` in this array will allows all JSON files to be used. + + This field is required to allow `Localhost` in securityContext as with an empty list it will block. + items: + type: string + targets: + - target: admission.k8s.gatekeeper.sh + code: + - engine: K8sNativeValidation + source: +{{ file.Read "src/pod-security-policy/seccompv2/src.cel" | strings.Indent 10 | strings.TrimSuffix "\n" }} + - engine: Rego + source: + rego: | +{{ file.Read "src/pod-security-policy/seccompv2/src.rego" | strings.Indent 12 | strings.TrimSuffix "\n" }} + libs: + - | +{{ file.Read "src/pod-security-policy/seccompv2/lib_exempt_container.rego" | strings.Indent 14 | strings.TrimSuffix "\n" }} diff --git a/src/pod-security-policy/seccompv2/lib_exempt_container.rego b/src/pod-security-policy/seccompv2/lib_exempt_container.rego new file mode 100644 index 000000000..c483416be --- /dev/null +++ b/src/pod-security-policy/seccompv2/lib_exempt_container.rego @@ -0,0 +1,19 @@ +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) +} diff --git a/src/pod-security-policy/seccompv2/src.cel b/src/pod-security-policy/seccompv2/src.cel new file mode 100644 index 000000000..c2847062a --- /dev/null +++ b/src/pod-security-policy/seccompv2/src.cel @@ -0,0 +1,101 @@ +variables: +- name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' +- name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' +- name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' +- name: allowAllProfiles + expression: | + has(variables.params.allowedProfiles) && variables.params.allowedProfiles.exists(profile, profile == "*") +- 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))).map(container, container.image) +- name: unverifiedContainers + expression: | + (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, + !variables.allowAllProfiles && + !(container.image in variables.exemptImages)) +- name: inputNonLocalHostProfiles + expression: | + variables.params.allowedProfiles.filter(profile, profile != "Localhost").map(profile, {"type": profile}) +- name: inputLocalHostProfiles + expression: | + variables.params.allowedProfiles.exists(profile, profile == "Localhost") ? variables.params.allowedLocalhostFiles.map(file, {"type": "Localhost", "localHostProfile": string(file)}) : [] +- name: inputAllowedProfiles + expression: | + variables.inputNonLocalHostProfiles + variables.inputLocalHostProfiles +- name: hasPodSeccomp + expression: | + has(variables.anyObject.spec.securityContext) && has(variables.anyObject.spec.securityContext.seccompProfile) +- name: podLocalHostProfile + expression: | + variables.hasPodSeccomp && has(variables.anyObject.spec.securityContext.seccompProfile.localhostProfile) ? variables.anyObject.spec.securityContext.seccompProfile.localhostProfile : "" +- name: podSecurityContextProfileType + expression: | + has(variables.hasPodSeccomp) && has(variables.anyObject.spec.securityContext.seccompProfile.type) ? variables.anyObject.spec.securityContext.seccompProfile.type + : "" +- name: podSecurityContextProfiles + expression: | + variables.unverifiedContainers.filter(container, + !(has(container.securityContext) && has(container.securityContext.seccompProfile)) && + variables.hasPodSeccomp + ).map(container, { + "container" : container.name, + "profile" : dyn(variables.podSecurityContextProfileType), + "file" : variables.podLocalHostProfile, + "location" : dyn("pod securityContext"), + }) +- name: containerSecurityContextProfiles + expression: | + variables.unverifiedContainers.filter(container, + has(container.securityContext) && has(container.securityContext.seccompProfile) + ).map(container, { + "container" : container.name, + "profile" : dyn(container.securityContext.seccompProfile.type), + "file" : has(container.securityContext.seccompProfile.localhostProfile) ? container.securityContext.seccompProfile.localhostProfile : dyn(""), + "location" : dyn("container securityContext"), + }) +- name: containerProfilesMissing + expression: | + variables.unverifiedContainers.filter(container, + !(has(container.securityContext) && has(container.securityContext.seccompProfile)) && + !variables.hasPodSeccomp + ).map(container, { + "container" : container.name, + "profile" : dyn("not configured"), + "file" : dyn(""), + "location" : dyn("no explicit profile found"), + }) +- name: allContainerProfiles + expression: | + variables.podSecurityContextProfiles + variables.containerSecurityContextProfiles + variables.containerProfilesMissing +- name: badContainerProfilesWithoutFiles + expression: | + variables.allContainerProfiles.filter(container, + container.profile != "Localhost" && + !variables.inputAllowedProfiles.exists(profile, profile.type == container.profile) + ).map(badProfile, "Seccomp profile '" + badProfile.profile + "' is not allowed for container '" + badProfile.container + "'. Found at: " + badProfile.location + ". Allowed profiles: " + variables.inputAllowedProfiles.map(profile, "{\"type\": \"" + profile.type + "\"" + (has(profile.localHostProfile) ? ", \"localHostProfile\": \"" + profile.localHostProfile + "\"}" : "}")).join(", ")) +- name: badContainerProfilesWithFiles + expression: | + variables.allContainerProfiles.filter(container, + container.profile == "Localhost" && + !variables.inputAllowedProfiles.exists(profile, profile.type == "Localhost" && (has(profile.localHostProfile) && (profile.localHostProfile == container.file || profile.localHostProfile == "*"))) + ).map(badProfile, "Seccomp profile '" + badProfile.profile + "' With file '" + badProfile.file + "' is not allowed for container '" + badProfile.container + "'. Found at: " + badProfile.location + ". Allowed profiles: " + variables.inputAllowedProfiles.map(profile, "{\"type\": \"" + profile.type + "\"" + (has(profile.localHostProfile) ? ", \"localHostProfile\": \"" + profile.localHostProfile + "\"}" : "}")).join(", ")) +validations: +- expression: 'size(variables.badContainerProfilesWithoutFiles) == 0' + messageExpression: | + variables.badContainerProfilesWithoutFiles.join(", ") +- expression: 'size(variables.badContainerProfilesWithFiles) == 0' + messageExpression: | + variables.badContainerProfilesWithFiles.join(", ") diff --git a/src/pod-security-policy/seccompv2/src.rego b/src/pod-security-policy/seccompv2/src.rego new file mode 100644 index 000000000..eee2ee037 --- /dev/null +++ b/src/pod-security-policy/seccompv2/src.rego @@ -0,0 +1,111 @@ +package k8spspseccomp + +import data.lib.exempt_container.is_exempt + +violation[{"msg": msg}] { + not input_wildcard_allowed_profiles + allowed_profiles := get_allowed_profiles + container := input_containers[name] + not is_exempt(container) + result := get_profile(container) + not allowed_profile(result.profile, result.file, allowed_profiles) + msg := get_message(result.profile, result.file, name, result.location, allowed_profiles) +} + +get_message(profile, _, name, location, allowed_profiles) = message { + profile != "Localhost" + message := sprintf("Seccomp profile '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, name, location, allowed_profiles]) +} + +get_message(profile, file, name, location, allowed_profiles) = message { + profile == "Localhost" + message := sprintf("Seccomp profile '%v' with file '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, file, name, location, allowed_profiles]) +} + +input_wildcard_allowed_profiles { + input.parameters.allowedProfiles[_] == "*" +} + +input_wildcard_allowed_files { + input.parameters.allowedLocalhostFiles[_] == "*" +} + +allowed_profile(_, _, _) { + input_wildcard_allowed_profiles +} + +allowed_profile(profile, _, _) { + profile == "Localhost" + input_wildcard_allowed_files +} + +# Simple allowed Profiles +allowed_profile(profile, _, allowed) { + profile != "Localhost" + allow_profile = allowed[_] + profile == allow_profile.type +} + +# annotation localhost without wildcard +allowed_profile(profile, file, allowed) { + profile == "Localhost" + allow_profile = allowed[_] + allow_profile.type == "Localhost" + file == allow_profile.localHostProfile +} + +# The profiles explicitly in the list +get_allowed_profiles[allowed] { + profile := input.parameters.allowedProfiles[_] + profile != "Localhost" + allowed := {"type": profile} +} + +get_allowed_profiles[allowed] { + profile := input.parameters.allowedProfiles[_] + profile == "Localhost" + file := object.get(input.parameters, "allowedLocalhostFiles", [""])[_] + allowed := {"type": "Localhost", "localHostProfile": file} +} + +# Container profile as defined in containers securityContext +get_profile(container) = {"profile": profile, "file": file, "location": location} { + has_securitycontext_container(container) + profile := container.securityContext.seccompProfile.type + file := object.get(container.securityContext.seccompProfile, "localhostProfile", "") + location := "container securityContext" +} + +# Container profile as defined in pods securityContext +get_profile(container) = {"profile": profile, "file": file, "location": location} { + not has_securitycontext_container(container) + profile := input.review.object.spec.securityContext.seccompProfile.type + file := object.get(input.review.object.spec.securityContext.seccompProfile, "localhostProfile", "") + location := "pod securityContext" +} + +# Container profile missing +get_profile(container) = {"profile": "not configured", "file": "", "location": "no explicit profile found"} { + not has_securitycontext_container(container) + not has_securitycontext_pod +} + +has_securitycontext_pod { + input.review.object.spec.securityContext.seccompProfile +} + +has_securitycontext_container(container) { + container.securityContext.seccompProfile +} + +input_containers[container.name] = container { + container := input.review.object.spec.containers[_] +} + +input_containers[container.name] = container { + container := input.review.object.spec.initContainers[_] +} + +input_containers[container.name] = container { + container := input.review.object.spec.ephemeralContainers[_] +} diff --git a/src/pod-security-policy/seccompv2/src_test.rego b/src/pod-security-policy/seccompv2/src_test.rego new file mode 100644 index 000000000..372815b36 --- /dev/null +++ b/src/pod-security-policy/seccompv2/src_test.rego @@ -0,0 +1,361 @@ +package k8spspseccomp + +# securityContext based seccomp with containers + +test_input_seccomp_allowed_in_list { + inp := {"review": get_object({}, context_runtimedefault, single_container, {}), "parameters": input_parameter_in_list} + results := violation with input as inp + count(results) == 0 +} + +test_input_seccomp_allowed_all { + inp := {"review": get_object({}, context_runtimedefault, single_container, {}), "parameters": input_parameters_wildcard} + results := violation with input as inp + count(results) == 0 +} + +test_input_seccomp_container_allowed_all { + inp := {"review": get_object({}, {}, single_container_sc, {}), "parameters": input_parameters_wildcard} + results := violation with input as inp + count(results) == 0 +} + +test_input_seccomp_container_allowed_in_list { + inp := {"review": get_object({}, {}, single_container_sc, {}), "parameters": input_parameter_in_list} + results := violation with input as inp + count(results) == 0 +} + +test_input_seccomp_containers_allowed_in_list { + inp := {"review": get_object({}, {}, multiple_containers_sc, {}), "parameters": input_parameter_in_list} + results := violation with input as inp + count(results) == 0 +} + +test_input_seccomp_containers_allowed_in_list_localhost { + inp := {"review": get_object({}, {}, single_container_sc_localhost, {}), "parameters": input_parameters_in_list_locahost_file} + results := violation with input as inp + count(results) == 0 +} + +test_input_seccomp_containers_allowed_in_list_multiple { + inp := {"review": get_object({}, {}, multiple_containers_sc_mixed, {}), "parameters": input_parameters_in_list} + results := violation with input as inp + count(results) == 0 +} + +test_input_seccomp_not_allowed_not_in_list { + inp := {"review": get_object({}, context_runtimedefault, single_container, {}), "parameters": input_parameters_not_in_list} + results := violation with input as inp + count(results) == 1 +} + +test_input_seccomp_empty_parameters { + inp := {"review": get_object({}, context_runtimedefault, single_container, {}), "parameters": input_parameters_empty} + results := violation with input as inp + count(results) == 1 +} + +test_input_seccomp_pod_localhost_allowed_wrong_file { + inp := {"review": get_object({}, context_localhost, single_container, {}), "parameters": input_parameters_sc} + results := violation with input as inp + count(results) == 1 +} + +test_input_seccomp_pod_localhost_allowed_no_specified_file { + inp := {"review": get_object({}, context_localhost, single_container, {}), "parameters": input_parameters_sc_localhost_no_file} + results := violation with input as inp + count(results) == 1 +} + +test_input_seccomp_containers_mixed { + inp := {"review": get_object({}, {}, multiple_containers_sc_mixed, {}), "parameters": input_parameter_in_list} + results := violation with input as inp + count(results) == 1 +} + +test_input_seccomp_containers_mixed_missing { + inp := {"review": get_object({}, {}, multiple_containers_sc_missing, {}), "parameters": input_parameter_in_list} + results := violation with input as inp + count(results) == 1 +} + +test_input_seccomp_container_not_allowed_not_in_list { + inp := {"review": get_object({}, {}, single_container_sc, {}), "parameters": input_parameters_not_in_list} + results := violation with input as inp + count(results) == 1 +} + +test_input_seccomp_containers_not_allowed_not_in_list { + inp := {"review": get_object({}, {}, multiple_containers_sc, {}), "parameters": input_parameters_not_in_list} + results := violation with input as inp + count(results) == 2 +} + +test_input_seccomp_not_allowed_multiple_not_configured { + inp := {"review": get_object({}, {}, multiple_containers, {}), "parameters": input_parameter_in_list} + results := violation with input as inp + count(results) == 2 +} + +# securityContext based seccomp with pod + +test_input_seccomp_pod_multiple_allowed_all { + inp := {"review": get_object({}, context_runtimedefault, multiple_containers, {}), "parameters": input_parameters_wildcard} + results := violation with input as inp + count(results) == 0 +} + +test_input_seccomp_pod_localhost_allowed_both_wildcard_file { + inp := {"review": get_object({}, context_localhost, single_container, {}), "parameters": input_parameters_localhost_wildcard_both} + results := violation with input as inp + count(results) == 0 +} + +test_input_seccomp_pod_container { + inp := {"review": get_object({}, context_runtimedefault, multiple_containers_sc_missing, {}), "parameters": input_parameter_in_list} + results := violation with input as inp + count(results) == 0 +} + +test_input_seccomp_pod_container_both_allowed { + inp := {"review": get_object({}, context_runtimedefault, multiple_containers_sc_missing, {}), "parameters": input_parameters_in_list} + results := violation with input as inp + count(results) == 0 +} + +test_input_seccomp_pod_container_mixed_not_allowed_but_exempt { + inp := {"review": get_object({}, context_runtimedefault, single_container, {}), "parameters": input_parameters_exempt} + results := violation with input as inp + count(results) == 0 +} + +test_input_seccomp_pod_multiple_allowed_in_list { + inp := {"review": get_object({}, context_runtimedefault, multiple_containers, {}), "parameters": input_parameter_in_list} + results := violation with input as inp + count(results) == 0 +} + +test_input_seccomp_pod_localhost_allowed_wildcard_file { + inp := {"review": get_object({}, context_localhost, single_container, {}), "parameters": input_parameters_sc_localhost_wildcard_file} + results := violation with input as inp + count(results) == 0 +} + +test_input_seccomp_pod_multiple_empty_parameters { + inp := {"review": get_object({}, context_runtimedefault, multiple_containers, {}), "parameters": input_parameters_empty} + results := violation with input as inp + count(results) == 2 +} + +test_input_seccomp_pod_multiple_not_allowed_not_in_list { + inp := {"review": get_object({}, context_runtimedefault, multiple_containers, {}), "parameters": input_parameters_not_in_list} + results := violation with input as inp + count(results) == 2 +} + +test_input_seccomp_pod_container_not_allowed { + inp := {"review": get_object({}, context_runtimedefault, multiple_containers_sc_missing, {}), "parameters": input_parameters_not_in_list} + results := violation with input as inp + count(results) == 2 +} + +test_input_seccomp_pod_container_mixed_allowed { + inp := {"review": get_object({}, context_localhost, multiple_containers_sc_missing, {}), "parameters": input_parameter_in_list} + results := violation with input as inp + count(results) == 1 +} + +test_input_seccomp_pod_container_mixed_not_allowed { + inp := {"review": get_object({}, context_localhost, multiple_containers_sc_missing, {}), "parameters": input_parameters_not_in_list} + results := violation with input as inp + count(results) == 2 +} + +# securityContext based seccomp with init containers +test_input_seccomp_pod_initcontainer_both_allowed { + inp := {"review": get_object({}, context_runtimedefault, {}, multiple_containers_sc_missing), "parameters": input_parameters_in_list} + results := violation with input as inp + count(results) == 0 +} + +test_input_seccomp_pod_initcontainer_mixed_allowed { + inp := {"review": get_object({}, context_localhost, {}, multiple_containers_sc_missing), "parameters": input_parameter_in_list} + results := violation with input as inp + count(results) == 1 +} + +test_input_seccomp_pod_initcontainer_mixed_not_allowed { + inp := {"review": get_object({}, context_localhost, {}, multiple_containers_sc_missing), "parameters": input_parameters_not_in_list} + results := violation with input as inp + count(results) == 2 +} + +# Localhost seccomp profile build + +test_translation_seccomp_allowed_context_localhost_wildcard_file { + inp := {"parameters": input_parameters_localhost_wildcard_both} + output := get_allowed_profiles with input as inp + output == {{"type": "Localhost", "localHostProfile": "*"}} +} + +test_translation_seccomp_allowed_context_localhost_no_file { + inp := {"parameters": input_parameters_sc_localhost_no_file} + output := get_allowed_profiles with input as inp + output == {{"localHostProfile": "", "type": "Localhost"}} +} + +test_translation_seccomp_allowed_context_localhost_with_file { + inp := {"parameters": input_parameters_sc_localhost_with_file} + output := get_allowed_profiles with input as inp + output == {{"type": "Localhost", "localHostProfile": "profile.json"}} +} + +test_translation_seccomp_allowed_context_mixed { + inp := {"parameters": input_parameters_in_list} + output := get_allowed_profiles with input as inp + output == {{"type": "Localhost", "localHostProfile": "profile.json"}, {"type": "RuntimeDefault"}} +} + +# Create Review Object +get_object(annotations, podcontext, containers, initcontainers) = {"object": { + "metadata": { + "name": "nginx", + "annotations": annotations, + }, + "spec": { + "containers": containers, + "initContainers": initcontainers, + "securityContext": podcontext, + }, +}} + +# Test Containers +single_container = [{ + "name": "nginx", + "image": "nginx", +}] + +multiple_containers = [ + { + "name": "nginx", + "image": "nginx", + }, + { + "name": "nginx2", + "image": "nginx", + }, +] + +single_container_sc = [{ + "name": "nginx", + "image": "nginx", + "securityContext": context_runtimedefault, +}] + +single_container_sc_localhost = [{ + "name": "nginx", + "image": "nginx", + "securityContext": context_localhost, +}] + +multiple_containers_sc = [ + { + "name": "nginx", + "image": "nginx", + "securityContext": context_runtimedefault, + }, + { + "name": "nginx2", + "image": "nginx", + "securityContext": context_runtimedefault, + }, +] + +multiple_containers_sc_mixed = [ + { + "name": "nginx", + "image": "nginx", + "securityContext": context_runtimedefault, + }, + { + "name": "nginx2", + "image": "nginx", + "securityContext": context_localhost, + }, +] + +multiple_containers_sc_missing = [ + { + "name": "nginx", + "image": "nginx", + "securityContext": context_runtimedefault, + }, + { + "name": "nginx2", + "image": "nginx", + }, +] + +# Test securityContexts +context_localhost = {"seccompProfile": {"type": "Localhost", "localhostProfile": "profile.json"}} + +context_runtimedefault = {"seccompProfile": {"type": "RuntimeDefault"}} + +# Test Parameters +input_parameters_empty = {"allowedProfiles": []} + +input_parameters_wildcard = {"allowedProfiles": ["*"]} + +input_parameter_in_list = {"allowedProfiles": [ + "RuntimeDefault", +]} + +input_parameters_in_list = { + "allowedProfiles": [ + "RuntimeDefault", + "Localhost", + ], + "allowedLocalhostFiles": ["profile.json"], +} + +input_parameters_in_list_locahost_file = { + "allowedProfiles": [ + "Localhost", + ], + "allowedLocalhostFiles": ["profile.json"], +} + +input_parameters_not_in_list = {"allowedProfiles": [ + "Unconfined", +]} + +input_parameters_exempt = { + "exemptImages": ["nginx"], + "allowedProfiles": ["Unconfined"], +} + +input_parameters_sc = { + "allowedProfiles": [ + "RuntimeDefault", + "Localhost", + "Unconfined", + ], + "allowedLocalhostFiles": [ + "profile1.json", + "profile2.json", + ], +} + +input_parameters_sc_localhost_no_file = { + "allowedProfiles": ["Localhost"], +} + +input_parameters_localhost_wildcard_both = {"allowedProfiles": ["Localhost"], "allowedLocalhostFiles": ["*"]} + +input_parameters_sc_localhost_wildcard_file = { + "allowedProfiles": ["Localhost"], + "allowedLocalhostFiles": ["*"], +} + +input_parameters_sc_localhost_with_file = {"allowedProfiles": ["Localhost"], "allowedLocalhostFiles": ["profile.json"]} \ No newline at end of file diff --git a/website/docs/validation/seccomp.md b/website/docs/validation/seccomp.md index d73218666..c56cc549e 100644 --- a/website/docs/validation/seccomp.md +++ b/website/docs/validation/seccomp.md @@ -16,7 +16,7 @@ metadata: name: k8spspseccomp annotations: metadata.gatekeeper.sh/title: "Seccomp" - metadata.gatekeeper.sh/version: 1.0.1 + metadata.gatekeeper.sh/version: 1.1.0 description: >- Controls the seccomp profile used by containers. Corresponds to the `seccomp.security.alpha.kubernetes.io/allowedProfileNames` annotation on @@ -79,218 +79,335 @@ spec: type: string targets: - target: admission.k8s.gatekeeper.sh - rego: | - package k8spspseccomp - - import data.lib.exempt_container.is_exempt - - container_annotation_key_prefix = "container.seccomp.security.alpha.kubernetes.io/" - - pod_annotation_key = "seccomp.security.alpha.kubernetes.io/pod" - - naming_translation = { - # securityContext -> annotation - "RuntimeDefault": ["runtime/default", "docker/default"], - "Unconfined": ["unconfined"], - "Localhost": ["localhost"], - # annotation -> securityContext - "runtime/default": ["RuntimeDefault"], - "docker/default": ["RuntimeDefault"], - "unconfined": ["Unconfined"], - "localhost": ["Localhost"], - } - - violation[{"msg": msg}] { - not input_wildcard_allowed_profiles - allowed_profiles := get_allowed_profiles - container := input_containers[name] - not is_exempt(container) - result := get_profile(container) - not allowed_profile(result.profile, result.file, allowed_profiles) - msg := get_message(result.profile, result.file, name, result.location, allowed_profiles) - } - - get_message(profile, _, name, location, allowed_profiles) = message { - not profile == "Localhost" - message := sprintf("Seccomp profile '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, name, location, allowed_profiles]) - } - - get_message(profile, file, name, location, allowed_profiles) = message { - profile == "Localhost" - message := sprintf("Seccomp profile '%v' with file '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, file, name, location, allowed_profiles]) - } - - input_wildcard_allowed_profiles { - input.parameters.allowedProfiles[_] == "*" - } - - input_wildcard_allowed_files { - input.parameters.allowedLocalhostFiles[_] == "*" - } - - input_wildcard_allowed_files { - "localhost/*" == input.parameters.allowedProfiles[_] - } - - # Simple allowed Profiles - allowed_profile(profile, _, allowed) { - not startswith(lower(profile), "localhost") - profile == allowed[_] - } - - # seccomp Localhost without wildcard - allowed_profile(profile, file, allowed) { - profile == "Localhost" - not input_wildcard_allowed_files - profile == allowed[_] - allowed_files := {x | x := object.get(input.parameters, "allowedLocalhostFiles", [])[_]} | get_annotation_localhost_files - file == allowed_files[_] - } - - # seccomp Localhost with wildcard - allowed_profile(profile, _, allowed) { - profile == "Localhost" - input_wildcard_allowed_files - profile == allowed[_] - } - - # annotation localhost with wildcard - allowed_profile(profile, _, allowed) { - "localhost/*" == allowed[_] - startswith(profile, "localhost/") - } - - # annotation localhost without wildcard - allowed_profile(profile, _, allowed) { - startswith(profile, "localhost/") - profile == allowed[_] - } - - # Localhost files from annotation scheme - get_annotation_localhost_files[file] { - profile := input.parameters.allowedProfiles[_] - startswith(profile, "localhost/") - file := replace(profile, "localhost/", "") - } - - # The profiles explicitly in the list - get_allowed_profiles[allowed] { - allowed := input.parameters.allowedProfiles[_] - } - - # The simply translated profiles - get_allowed_profiles[allowed] { - profile := input.parameters.allowedProfiles[_] - not startswith(lower(profile), "localhost") - allowed := naming_translation[profile][_] - } - - # Seccomp Localhost to annotation translation - get_allowed_profiles[allowed] { - profile := input.parameters.allowedProfiles[_] - profile == "Localhost" - file := object.get(input.parameters, "allowedLocalhostFiles", [])[_] - allowed := sprintf("%v/%v", [naming_translation[profile][_], file]) - } - - # Annotation localhost to Seccomp translation - get_allowed_profiles[allowed] { - profile := input.parameters.allowedProfiles[_] - startswith(profile, "localhost") - allowed := naming_translation.localhost[_] - } - - # Container profile as defined in pod annotation - get_profile(container) = {"profile": profile, "file": "", "location": location} { - not has_securitycontext_container(container) - not has_annotation(get_container_annotation_key(container.name)) - not has_securitycontext_pod - profile := input.review.object.metadata.annotations[pod_annotation_key] - location := sprintf("annotation %v", [pod_annotation_key]) - } - - # Container profile as defined in container annotation - get_profile(container) = {"profile": profile, "file": "", "location": location} { - not has_securitycontext_container(container) - not has_securitycontext_pod - container_annotation := get_container_annotation_key(container.name) - has_annotation(container_annotation) - profile := input.review.object.metadata.annotations[container_annotation] - location := sprintf("annotation %v", [container_annotation]) - } - - # Container profile as defined in pods securityContext - get_profile(container) = {"profile": profile, "file": file, "location": location} { - not has_securitycontext_container(container) - profile := input.review.object.spec.securityContext.seccompProfile.type - file := object.get(input.review.object.spec.securityContext.seccompProfile, "localhostProfile", "") - location := "pod securityContext" - } - - # Container profile as defined in containers securityContext - get_profile(container) = {"profile": profile, "file": file, "location": location} { - has_securitycontext_container(container) - profile := container.securityContext.seccompProfile.type - file := object.get(container.securityContext.seccompProfile, "localhostProfile", "") - location := "container securityContext" - } - - # Container profile missing - get_profile(container) = {"profile": "not configured", "file": "", "location": "no explicit profile found"} { - not has_annotation(get_container_annotation_key(container.name)) - not has_annotation(pod_annotation_key) - not has_securitycontext_pod - not has_securitycontext_container(container) - } - - has_annotation(annotation) { - input.review.object.metadata.annotations[annotation] - } - - has_securitycontext_pod { - input.review.object.spec.securityContext.seccompProfile - } - - has_securitycontext_container(container) { - container.securityContext.seccompProfile - } - - get_container_annotation_key(name) = annotation { - annotation := concat("", [container_annotation_key_prefix, name]) - } - - input_containers[container.name] = container { - container := input.review.object.spec.containers[_] - } - - input_containers[container.name] = container { - container := input.review.object.spec.initContainers[_] - } - - input_containers[container.name] = container { - container := input.review.object.spec.ephemeralContainers[_] - } - libs: - - | - 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) - } + code: + - engine: K8sNativeValidation + source: + variables: + - name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' + - name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' + - name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' + - name: allowAllProfiles + expression: | + has(variables.params.allowedProfiles) && variables.params.allowedProfiles.exists(profile, profile == "*") + - 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))).map(container, container.image) + - name: unverifiedContainers + expression: | + (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, + !variables.allowAllProfiles && + !(container.image in variables.exemptImages)) + - name: inputAllowedProfiles + expression: | + !has(variables.params.allowedProfiles) ? [] : variables.params.allowedProfiles + - name: allowedLocalhostFiles + expression: | + has(variables.params.allowedLocalhostFiles) ? variables.params.allowedLocalhostFiles : [] + - name: allowedProfilesTranslation + expression: | + (variables.inputAllowedProfiles.filter(profile, + profile != "Localhost").map(profile, profile == "Unconfined" ? "unconfined" : profile)) + + (variables.inputAllowedProfiles.exists(profile, profile == "RuntimeDefault") ? ["runtime/default", "docker/default"] : []) + - name: allowSecurityContextLocalhost + expression: | + variables.inputAllowedProfiles.exists(profile, profile == "Localhost") + - name: derivedAllowedLocalhostFiles + expression: | + variables.allowSecurityContextLocalhost ? variables.params.allowedLocalhostFiles.map(file, "localhost/" + file) : [] + - name: localhostWildcardAllowed + expression: | + variables.inputAllowedProfiles.exists(profile, profile == "localhost/*") || variables.derivedAllowedLocalhostFiles.exists(profile, profile == "localhost/*") + - name: allowedProfiles + expression: | + (variables.allowedProfilesTranslation + variables.derivedAllowedLocalhostFiles) + - name: hasPodSeccomp + expression: | + has(variables.anyObject.spec.securityContext) && has(variables.anyObject.spec.securityContext.seccompProfile) + - name: hasPodAnnotations + expression: | + has(variables.anyObject.metadata.annotations) && ("seccomp.security.alpha.kubernetes.io/pod" in variables.anyObject.metadata.annotations) + - name: podAnnotationsProfiles + expression: | + variables.unverifiedContainers.filter(container, + !(has(container.securityContext) && has(container.securityContext.seccompProfile)) && + !(has(variables.anyObject.metadata.annotations) && (("container.seccomp.security.alpha.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations)) && + !variables.hasPodSeccomp && + variables.hasPodAnnotations + ).map(container, { + "container" : container.name, + "profile" : variables.anyObject.metadata.annotations["seccomp.security.alpha.kubernetes.io/pod"], + "file" : dyn(""), + "location" : dyn("annotation seccomp.security.alpha.kubernetes.io/pod"), + }) + - name: containerAnnotationsProfiles + expression: | + variables.unverifiedContainers.filter(container, + !(has(container.securityContext) && has(container.securityContext.seccompProfile)) && + !variables.hasPodSeccomp && + has(variables.anyObject.metadata.annotations) && (("container.seccomp.security.alpha.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations) + ).map(container, { + "container" : container.name, + "profile" : variables.anyObject.metadata.annotations["container.seccomp.security.alpha.kubernetes.io/" + container.name], + "file" : dyn(""), + "location" : dyn("annotation container.seccomp.security.alpha.kubernetes.io/" + container.name), + }) + - name: podLocalHostProfile + expression: | + variables.hasPodSeccomp && has(variables.anyObject.spec.securityContext.seccompProfile.localhostProfile) ? variables.anyObject.spec.securityContext.seccompProfile.localhostProfile : "" + - name: canonicalPodSecurityContextProfile + expression: | + has(variables.hasPodSeccomp) && has(variables.anyObject.spec.securityContext.seccompProfile.type) ? + (variables.anyObject.spec.securityContext.seccompProfile.type == "RuntimeDefault" ? ( + variables.allowedProfiles.exists(profile, profile == "runtime/default") ? "runtime/default" : variables.allowedProfiles.exists(profile, profile == "docker/default") ? "docker/default" : "runtime/default") : + variables.anyObject.spec.securityContext.seccompProfile.type == "Unconfined" ? "unconfined" : variables.anyObject.spec.securityContext.seccompProfile.type == "Localhost" ? "localhost/" + variables.podLocalHostProfile : "") + : "" + - name: podSecurityContextProfiles + expression: | + variables.unverifiedContainers.filter(container, + !(has(container.securityContext) && has(container.securityContext.seccompProfile)) && + variables.hasPodSeccomp + ).map(container, { + "container" : container.name, + "profile" : dyn(variables.canonicalPodSecurityContextProfile), + "file" : variables.podLocalHostProfile, + "location" : dyn("pod securityContext"), + }) + - name: containerSecurityContextProfiles + expression: | + variables.unverifiedContainers.filter(container, + has(container.securityContext) && has(container.securityContext.seccompProfile) + ).map(container, { + "container" : container.name, + "profile" : dyn(has(container.securityContext.seccompProfile.type) ? (container.securityContext.seccompProfile.type == "RuntimeDefault" ? ( + variables.allowedProfiles.exists(profile, profile == "runtime/default") ? "runtime/default" : variables.allowedProfiles.exists(profile, profile == "docker/default") ? "docker/default" : "runtime/default") : + container.securityContext.seccompProfile.type == "Unconfined" ? "unconfined" : container.securityContext.seccompProfile.type == "Localhost" ? "localhost/" + container.securityContext.seccompProfile.localhostProfile : "") + : ""), + "file" : has(container.securityContext.seccompProfile.localhostProfile) ? container.securityContext.seccompProfile.localhostProfile : dyn(""), + "location" : dyn("container securityContext"), + }) + - name: containerProfilesMissing + expression: | + variables.unverifiedContainers.filter(container, + !(has(container.securityContext) && has(container.securityContext.seccompProfile)) && + !(has(variables.anyObject.metadata.annotations) && (("container.seccomp.security.alpha.kubernetes.io/" + container.name) in variables.anyObject.metadata.annotations)) && + !variables.hasPodSeccomp && + !variables.hasPodAnnotations + ).map(container, { + "container" : container.name, + "profile" : dyn("not configured"), + "file" : dyn(""), + "location" : dyn("no explicit profile found"), + }) + - name: allContainerProfiles + expression: | + variables.podAnnotationsProfiles + variables.containerAnnotationsProfiles + variables.podSecurityContextProfiles + variables.containerSecurityContextProfiles + variables.containerProfilesMissing + - name: badContainerProfiles + expression: | + variables.allContainerProfiles.filter(badContainerProfile, + !((badContainerProfile.profile in variables.allowedProfiles) || (badContainerProfile.profile.startsWith("localhost/") && variables.localhostWildcardAllowed)) + ).map(badProfile, "Seccomp profile '" + badProfile.profile + "' is not allowed for container '" + badProfile.container + "'. Found at: " + badProfile.location + ". Allowed profiles: " + variables.allowedProfiles.join(", ")) + validations: + - expression: 'size(variables.badContainerProfiles) == 0' + messageExpression: | + variables.badContainerProfiles.join(", ") + - engine: Rego + source: + rego: | + package k8spspseccomp + + import data.lib.exempt_container.is_exempt + + container_annotation_key_prefix = "container.seccomp.security.alpha.kubernetes.io/" + + pod_annotation_key = "seccomp.security.alpha.kubernetes.io/pod" + + violation[{"msg": msg}] { + not input_wildcard_allowed_profiles + allowed_profiles := get_allowed_profiles + container := input_containers[name] + not is_exempt(container) + result := get_profile(container) + not allowed_profile(result.profile, result.file, allowed_profiles) + msg := get_message(result.profile, result.file, name, result.location, allowed_profiles) + } + + get_message(profile, _, name, location, allowed_profiles) = message { + message := sprintf("Seccomp profile '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, name, location, allowed_profiles]) + } + + input_wildcard_allowed_profiles { + input.parameters.allowedProfiles[_] == "*" + } + + input_wildcard_allowed_files { + input.parameters.allowedLocalhostFiles[_] == "*" + } + + input_wildcard_allowed_files { + "localhost/*" == input.parameters.allowedProfiles[_] + } + + # Simple allowed Profiles + allowed_profile(profile, _, allowed) { + not startswith(profile, "localhost/") + profile == allowed[_] + } + + # annotation localhost with wildcard + allowed_profile(profile, _, allowed) { + "localhost/*" == allowed[_] + startswith(profile, "localhost/") + } + + # annotation localhost without wildcard + allowed_profile(profile, _, allowed) { + startswith(profile, "localhost/") + profile == allowed[_] + } + + # The profiles explicitly in the list + get_allowed_profiles[allowed] { + allowed := input.parameters.allowedProfiles[_] + } + + # Seccomp Localhost to annotation translation + get_allowed_profiles[allowed] { + profile := input.parameters.allowedProfiles[_] + not contains(profile, "/") + file := object.get(input.parameters, "allowedLocalhostFiles", [])[_] + allowed := canonicalize_seccomp_profile({"type": profile, "localhostProfile": file}, "")[_] + } + + # Container profile as defined in pod annotation + get_profile(container) = {"profile": profile, "file": "", "location": location} { + not has_securitycontext_container(container) + not has_annotation(get_container_annotation_key(container.name)) + not has_securitycontext_pod + profile := input.review.object.metadata.annotations[pod_annotation_key] + location := sprintf("annotation %v", [pod_annotation_key]) + } + + # Container profile as defined in container annotation + get_profile(container) = {"profile": profile, "file": "", "location": location} { + not has_securitycontext_container(container) + not has_securitycontext_pod + container_annotation := get_container_annotation_key(container.name) + has_annotation(container_annotation) + profile := input.review.object.metadata.annotations[container_annotation] + location := sprintf("annotation %v", [container_annotation]) + } + + # Container profile as defined in pods securityContext + get_profile(container) = {"profile": profile, "file": file, "location": location} { + not has_securitycontext_container(container) + profile := canonicalize_seccomp_profile(input.review.object.spec.securityContext.seccompProfile, canonicalize_runtime_default_profile)[_] + file := object.get(input.review.object.spec.securityContext.seccompProfile, "localhostProfile", "") + location := "pod securityContext" + } + + # Container profile as defined in containers securityContext + get_profile(container) = {"profile": profile, "file": file, "location": location} { + has_securitycontext_container(container) + profile := canonicalize_seccomp_profile(container.securityContext.seccompProfile, canonicalize_runtime_default_profile)[_] + file := object.get(container.securityContext.seccompProfile, "localhostProfile", "") + location := "container securityContext" + } + + # Container profile missing + get_profile(container) = {"profile": "not configured", "file": "", "location": "no explicit profile found"} { + not has_securitycontext_container(container) + not has_securitycontext_pod + not has_annotation(get_container_annotation_key(container.name)) + not has_annotation(pod_annotation_key) + } + + has_annotation(annotation) { + input.review.object.metadata.annotations[annotation] + } + + has_securitycontext_pod { + input.review.object.spec.securityContext.seccompProfile + } + + has_securitycontext_container(container) { + container.securityContext.seccompProfile + } + + get_container_annotation_key(name) = annotation { + annotation := concat("", [container_annotation_key_prefix, name]) + } + + input_containers[container.name] = container { + container := input.review.object.spec.containers[_] + } + + input_containers[container.name] = container { + container := input.review.object.spec.initContainers[_] + } + + input_containers[container.name] = container { + container := input.review.object.spec.ephemeralContainers[_] + } + + canonicalize_runtime_default_profile() = out { + "runtime/default" == input.parameters.allowedProfiles[_] + out := "runtime/default" + } else = out { + "docker/default" == input.parameters.allowedProfiles[_] + out := "docker/default" + } else = out { + out := "runtime/default" + } + + canonicalize_seccomp_profile(profile, def) = out { + profile.type == "RuntimeDefault" + def == "" + out := ["runtime/default", "docker/default"] + } else = out { + profile.type == "RuntimeDefault" + def != "" + out := [def] + } else = out { + profile.type == "Localhost" + out := [sprintf("localhost/%s", [profile.localhostProfile])] + } else = out { + profile.type == "Unconfined" + out := ["unconfined"] + } + libs: + - | + 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) + } ``` @@ -316,10 +433,11 @@ spec: - apiGroups: [""] kinds: ["Pod"] parameters: + exemptImages: + - nginx-exempt allowedProfiles: - runtime/default - - docker/default - + - localhost/profile.json ``` Usage @@ -459,6 +577,89 @@ Usage kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/seccomp/samples/psp-seccomp/disallowed_ephemeral.yaml ``` + +
+example-allowed-container-exempt-image + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-disallowed + labels: + app: nginx-seccomp +spec: + containers: + - name: nginx + image: nginx-exempt + securityContext: + seccompProfile: + type: Unconfined + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/seccomp/samples/psp-seccomp/example_allowed_exempt_image.yaml +``` + +
+
+example-allowed-container-localhost-profile + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-allowed-localhost + labels: + app: nginx-seccomp +spec: + containers: + - name: nginx + image: nginx + securityContext: + seccompProfile: + type: Localhost + localhostProfile: profile.json + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/seccomp/samples/psp-seccomp/example_allowed_localhost.yaml +``` + +
+
+example-disallowed-container-localhost-profile + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-disallowed-localhost + labels: + app: nginx-seccomp +spec: + containers: + - name: nginx + image: nginx + securityContext: + seccompProfile: + type: Localhost + localhostProfile: profile.log + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/seccomp/samples/psp-seccomp/example_disallowed_localhost.yaml +``` +
diff --git a/website/docs/validation/seccompv2.md b/website/docs/validation/seccompv2.md new file mode 100644 index 000000000..f93abdb3b --- /dev/null +++ b/website/docs/validation/seccompv2.md @@ -0,0 +1,519 @@ +--- +id: seccompv2 +title: Seccomp V2 +--- + +# Seccomp V2 + +## Description +Controls the seccomp profile used by containers. Corresponds to the `securityContext.seccompProfile` field. Security contexts from the annotation is not considered as Kubernetes no longer reads security contexts from the annotation. + +## Template +```yaml +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: k8spspseccompv2 + annotations: + metadata.gatekeeper.sh/title: "Seccomp V2" + metadata.gatekeeper.sh/version: 1.0.0 + description: >- + Controls the seccomp profile used by containers. Corresponds to the + `securityContext.seccompProfile` field. Security contexts from the annotation is not considered as Kubernetes no longer reads security contexts from the annotation. +spec: + crd: + spec: + names: + kind: K8sPSPSeccompV2 + validation: + # Schema for the `parameters` field + openAPIV3Schema: + type: object + description: >- + Controls the seccomp profile used by containers. Corresponds to the + `securityContext.seccompProfile` field. Security contexts from the annotation is not considered as Kubernetes no longer reads security contexts from the annotation. + 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 + allowedProfiles: + type: array + description: >- + An array of allowed profile values for seccomp on Pods/Containers. + + Can use the securityContext naming scheme: `RuntimeDefault`, `Unconfined` + and/or `Localhost`. For securityContext `Localhost`, use the parameter `allowedLocalhostFiles` + to list the allowed profile JSON files. + + The policy code will translate between the two schemes so it is not necessary to use both. + + Putting a `*` in this array allows all Profiles to be used. + + This field is required since with an empty list this policy will block all workloads. + items: + type: string + allowedLocalhostFiles: + type: array + description: >- + When using securityContext naming scheme for seccomp and including `Localhost` this array holds + the allowed profile JSON files. + + Putting a `*` in this array will allows all JSON files to be used. + + This field is required to allow `Localhost` in securityContext as with an empty list it will block. + items: + type: string + targets: + - target: admission.k8s.gatekeeper.sh + code: + - engine: K8sNativeValidation + source: + variables: + - name: containers + expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []' + - name: initContainers + expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []' + - name: ephemeralContainers + expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []' + - name: allowAllProfiles + expression: | + has(variables.params.allowedProfiles) && variables.params.allowedProfiles.exists(profile, profile == "*") + - 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))).map(container, container.image) + - name: unverifiedContainers + expression: | + (variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container, + !variables.allowAllProfiles && + !(container.image in variables.exemptImages)) + - name: inputNonLocalHostProfiles + expression: | + variables.params.allowedProfiles.filter(profile, profile != "Localhost").map(profile, {"type": profile}) + - name: inputLocalHostProfiles + expression: | + variables.params.allowedProfiles.exists(profile, profile == "Localhost") ? variables.params.allowedLocalhostFiles.map(file, {"type": "Localhost", "localHostProfile": string(file)}) : [] + - name: inputAllowedProfiles + expression: | + variables.inputNonLocalHostProfiles + variables.inputLocalHostProfiles + - name: hasPodSeccomp + expression: | + has(variables.anyObject.spec.securityContext) && has(variables.anyObject.spec.securityContext.seccompProfile) + - name: podLocalHostProfile + expression: | + variables.hasPodSeccomp && has(variables.anyObject.spec.securityContext.seccompProfile.localhostProfile) ? variables.anyObject.spec.securityContext.seccompProfile.localhostProfile : "" + - name: podSecurityContextProfileType + expression: | + has(variables.hasPodSeccomp) && has(variables.anyObject.spec.securityContext.seccompProfile.type) ? variables.anyObject.spec.securityContext.seccompProfile.type + : "" + - name: podSecurityContextProfiles + expression: | + variables.unverifiedContainers.filter(container, + !(has(container.securityContext) && has(container.securityContext.seccompProfile)) && + variables.hasPodSeccomp + ).map(container, { + "container" : container.name, + "profile" : dyn(variables.podSecurityContextProfileType), + "file" : variables.podLocalHostProfile, + "location" : dyn("pod securityContext"), + }) + - name: containerSecurityContextProfiles + expression: | + variables.unverifiedContainers.filter(container, + has(container.securityContext) && has(container.securityContext.seccompProfile) + ).map(container, { + "container" : container.name, + "profile" : dyn(container.securityContext.seccompProfile.type), + "file" : has(container.securityContext.seccompProfile.localhostProfile) ? container.securityContext.seccompProfile.localhostProfile : dyn(""), + "location" : dyn("container securityContext"), + }) + - name: containerProfilesMissing + expression: | + variables.unverifiedContainers.filter(container, + !(has(container.securityContext) && has(container.securityContext.seccompProfile)) && + !variables.hasPodSeccomp + ).map(container, { + "container" : container.name, + "profile" : dyn("not configured"), + "file" : dyn(""), + "location" : dyn("no explicit profile found"), + }) + - name: allContainerProfiles + expression: | + variables.podSecurityContextProfiles + variables.containerSecurityContextProfiles + variables.containerProfilesMissing + - name: badContainerProfilesWithoutFiles + expression: | + variables.allContainerProfiles.filter(container, + container.profile != "Localhost" && + !variables.inputAllowedProfiles.exists(profile, profile.type == container.profile) + ).map(badProfile, "Seccomp profile '" + badProfile.profile + "' is not allowed for container '" + badProfile.container + "'. Found at: " + badProfile.location + ". Allowed profiles: " + variables.inputAllowedProfiles.map(profile, "{\"type\": \"" + profile.type + "\"" + (has(profile.localHostProfile) ? ", \"localHostProfile\": \"" + profile.localHostProfile + "\"}" : "}")).join(", ")) + - name: badContainerProfilesWithFiles + expression: | + variables.allContainerProfiles.filter(container, + container.profile == "Localhost" && + !variables.inputAllowedProfiles.exists(profile, profile.type == "Localhost" && (has(profile.localHostProfile) && (profile.localHostProfile == container.file || profile.localHostProfile == "*"))) + ).map(badProfile, "Seccomp profile '" + badProfile.profile + "' With file '" + badProfile.file + "' is not allowed for container '" + badProfile.container + "'. Found at: " + badProfile.location + ". Allowed profiles: " + variables.inputAllowedProfiles.map(profile, "{\"type\": \"" + profile.type + "\"" + (has(profile.localHostProfile) ? ", \"localHostProfile\": \"" + profile.localHostProfile + "\"}" : "}")).join(", ")) + validations: + - expression: 'size(variables.badContainerProfilesWithoutFiles) == 0' + messageExpression: | + variables.badContainerProfilesWithoutFiles.join(", ") + - expression: 'size(variables.badContainerProfilesWithFiles) == 0' + messageExpression: | + variables.badContainerProfilesWithFiles.join(", ") + - engine: Rego + source: + rego: | + package k8spspseccomp + + import data.lib.exempt_container.is_exempt + + violation[{"msg": msg}] { + not input_wildcard_allowed_profiles + allowed_profiles := get_allowed_profiles + container := input_containers[name] + not is_exempt(container) + result := get_profile(container) + not allowed_profile(result.profile, result.file, allowed_profiles) + msg := get_message(result.profile, result.file, name, result.location, allowed_profiles) + } + + get_message(profile, _, name, location, allowed_profiles) = message { + profile != "Localhost" + message := sprintf("Seccomp profile '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, name, location, allowed_profiles]) + } + + get_message(profile, file, name, location, allowed_profiles) = message { + profile == "Localhost" + message := sprintf("Seccomp profile '%v' with file '%v' is not allowed for container '%v'. Found at: %v. Allowed profiles: %v", [profile, file, name, location, allowed_profiles]) + } + + input_wildcard_allowed_profiles { + input.parameters.allowedProfiles[_] == "*" + } + + input_wildcard_allowed_files { + input.parameters.allowedLocalhostFiles[_] == "*" + } + + allowed_profile(_, _, _) { + input_wildcard_allowed_profiles + } + + allowed_profile(profile, _, _) { + profile == "Localhost" + input_wildcard_allowed_files + } + + # Simple allowed Profiles + allowed_profile(profile, _, allowed) { + profile != "Localhost" + allow_profile = allowed[_] + profile == allow_profile.type + } + + # annotation localhost without wildcard + allowed_profile(profile, file, allowed) { + profile == "Localhost" + allow_profile = allowed[_] + allow_profile.type == "Localhost" + file == allow_profile.localHostProfile + } + + # The profiles explicitly in the list + get_allowed_profiles[allowed] { + profile := input.parameters.allowedProfiles[_] + profile != "Localhost" + allowed := {"type": profile} + } + + get_allowed_profiles[allowed] { + profile := input.parameters.allowedProfiles[_] + profile == "Localhost" + file := object.get(input.parameters, "allowedLocalhostFiles", [""])[_] + allowed := {"type": "Localhost", "localHostProfile": file} + } + + # Container profile as defined in containers securityContext + get_profile(container) = {"profile": profile, "file": file, "location": location} { + has_securitycontext_container(container) + profile := container.securityContext.seccompProfile.type + file := object.get(container.securityContext.seccompProfile, "localhostProfile", "") + location := "container securityContext" + } + + # Container profile as defined in pods securityContext + get_profile(container) = {"profile": profile, "file": file, "location": location} { + not has_securitycontext_container(container) + profile := input.review.object.spec.securityContext.seccompProfile.type + file := object.get(input.review.object.spec.securityContext.seccompProfile, "localhostProfile", "") + location := "pod securityContext" + } + + # Container profile missing + get_profile(container) = {"profile": "not configured", "file": "", "location": "no explicit profile found"} { + not has_securitycontext_container(container) + not has_securitycontext_pod + } + + has_securitycontext_pod { + input.review.object.spec.securityContext.seccompProfile + } + + has_securitycontext_container(container) { + container.securityContext.seccompProfile + } + + input_containers[container.name] = container { + container := input.review.object.spec.containers[_] + } + + input_containers[container.name] = container { + container := input.review.object.spec.initContainers[_] + } + + input_containers[container.name] = container { + container := input.review.object.spec.ephemeralContainers[_] + } + libs: + - | + 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) + } + +``` + +### Usage +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/seccompv2/template.yaml +``` +## Examples +
+default-seccomp-required + +
+constraint + +```yaml +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sPSPSeccompV2 +metadata: + name: psp-seccomp +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["Pod"] + parameters: + exemptImages: + - nginx-exempt + allowedProfiles: + - RuntimeDefault + - Localhost + allowedLocalhostFiles: + - "*" + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/seccompv2/samples/psp-seccomp/constraint.yaml +``` + +
+ +
+example-disallowed-global + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-disallowed2 + labels: + app: nginx-seccomp +spec: + securityContext: + seccompProfile: + type: Unconfined + containers: + - name: nginx + image: nginx + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_disallowed2.yaml +``` + +
+
+example-disallowed-container + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-disallowed + labels: + app: nginx-seccomp +spec: + containers: + - name: nginx + image: nginx + securityContext: + seccompProfile: + type: Unconfined + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_disallowed.yaml +``` + +
+
+example-allowed-container + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-allowed + labels: + app: nginx-seccomp +spec: + containers: + - name: nginx + image: nginx + securityContext: + seccompProfile: + type: RuntimeDefault + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed.yaml +``` + +
+
+example-allowed-container + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-allowed-localhost + labels: + app: nginx-seccomp +spec: + containers: + - name: nginx + image: nginx + securityContext: + seccompProfile: + type: Localhost + localhostProfile: profile.json + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed_localhost.yaml +``` + +
+
+example-allowed-container-exempt-image + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-disallowed + labels: + app: nginx-seccomp +spec: + containers: + - name: nginx + image: nginx-exempt + securityContext: + seccompProfile: + type: Unconfined + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/seccompv2/samples/psp-seccomp/example_allowed_exempt_image.yaml +``` + +
+
+disallowed-ephemeral + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-seccomp-disallowed + labels: + app: nginx-seccomp +spec: + ephemeralContainers: + - name: nginx + image: nginx + +``` + +Usage + +```shell +kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/seccompv2/samples/psp-seccomp/disallowed_ephemeral.yaml +``` + +
+ + +
\ No newline at end of file diff --git a/website/sidebars.js b/website/sidebars.js index a14f9496c..5710b4a6e 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -68,6 +68,7 @@ module.exports = { 'validation/proc-mount', 'validation/read-only-root-filesystem', 'validation/seccomp', + 'validation/seccompv2', 'validation/selinux', 'validation/users', 'validation/volumes', From da229ba0e2807341b41b7e6c5eec3c2141b25e59 Mon Sep 17 00:00:00 2001 From: Mathieu Parent Date: Tue, 29 Oct 2024 14:51:53 +0100 Subject: [PATCH 17/19] fix: use newer seccompProfile spec in mutation (#599) Older is ignored Signed-off-by: Mathieu Parent Co-authored-by: Rita Zhang --- .../seccomp/samples/mutation.yaml | 19 +++++++++++-------- website/docs/mutation-examples/seccomp.md | 19 +++++++++++-------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/mutation/pod-security-policy/seccomp/samples/mutation.yaml b/mutation/pod-security-policy/seccomp/samples/mutation.yaml index 2f28058ab..fb45aba83 100644 --- a/mutation/pod-security-policy/seccomp/samples/mutation.yaml +++ b/mutation/pod-security-policy/seccomp/samples/mutation.yaml @@ -1,14 +1,17 @@ apiVersion: mutations.gatekeeper.sh/v1 -kind: AssignMetadata +kind: Assign metadata: name: k8spspseccomp spec: - match: - scope: Namespaced - kinds: - - apiGroups: [""] - kinds: ["Pod"] - location: metadata.annotations."seccomp.security.alpha.kubernetes.io/pod" + applyTo: + - groups: [""] + kinds: ["Pod"] + versions: ["v1"] + location: spec.securityContext.seccompProfile parameters: + pathTests: + - subPath: spec.securityContext.seccompProfile + condition: MustNotExist assign: - value: runtime/default + value: + type: RuntimeDefault diff --git a/website/docs/mutation-examples/seccomp.md b/website/docs/mutation-examples/seccomp.md index b6cfd7591..3719850b2 100644 --- a/website/docs/mutation-examples/seccomp.md +++ b/website/docs/mutation-examples/seccomp.md @@ -12,18 +12,21 @@ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper- ## Mutation Examples ```yaml apiVersion: mutations.gatekeeper.sh/v1 -kind: AssignMetadata +kind: Assign metadata: name: k8spspseccomp spec: - match: - scope: Namespaced - kinds: - - apiGroups: [""] - kinds: ["Pod"] - location: metadata.annotations."seccomp.security.alpha.kubernetes.io/pod" + applyTo: + - groups: [""] + kinds: ["Pod"] + versions: ["v1"] + location: spec.securityContext.seccompProfile parameters: + pathTests: + - subPath: spec.securityContext.seccompProfile + condition: MustNotExist assign: - value: runtime/default + value: + type: RuntimeDefault ``` \ No newline at end of file From 1de50fa379c75354083c186a9474eb328322c426 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Dec 2024 11:26:39 -0800 Subject: [PATCH 18/19] chore: bump the all group across 1 directory with 9 updates (#612) Bumps the all group with 9 updates in the / directory: | Package | From | To | | --- | --- | --- | | [step-security/harden-runner](https://github.com/step-security/harden-runner) | `2.10.1` | `2.10.2` | | [actions/checkout](https://github.com/actions/checkout) | `4.1.7` | `4.2.2` | | [github/codeql-action](https://github.com/github/codeql-action) | `3.26.7` | `3.27.5` | | [actions/dependency-review-action](https://github.com/actions/dependency-review-action) | `4.3.4` | `4.5.0` | | [actions/upload-artifact](https://github.com/actions/upload-artifact) | `4.4.0` | `4.4.3` | | [actions/setup-go](https://github.com/actions/setup-go) | `5.0.2` | `5.1.0` | | [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) | `6.1.0` | `6.1.1` | | [actions/setup-node](https://github.com/actions/setup-node) | `4.0.3` | `4.1.0` | | [actions/cache](https://github.com/actions/cache) | `4.0.2` | `4.1.2` | Updates `step-security/harden-runner` from 2.10.1 to 2.10.2 - [Release notes](https://github.com/step-security/harden-runner/releases) - [Commits](https://github.com/step-security/harden-runner/compare/91182cccc01eb5e619899d80e4e971d6181294a7...0080882f6c36860b6ba35c610c98ce87d4e2f26f) Updates `actions/checkout` from 4.1.7 to 4.2.2 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/692973e3d937129bcbf40652eb9f2f61becf3332...11bd71901bbe5b1630ceea73d27597364c9af683) Updates `github/codeql-action` from 3.26.7 to 3.27.5 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/8214744c546c1e5c8f03dde8fab3a7353211988d...f09c1c0a94de965c15400f5634aa42fac8fb8f88) Updates `actions/dependency-review-action` from 4.3.4 to 4.5.0 - [Release notes](https://github.com/actions/dependency-review-action/releases) - [Commits](https://github.com/actions/dependency-review-action/compare/5a2ce3f5b92ee19cbb1541a4984c76d921601d7c...3b139cfc5fae8b618d3eae3675e383bb1769c019) Updates `actions/upload-artifact` from 4.4.0 to 4.4.3 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/50769540e7f4bd5e21e526ee35c689e35e0d6874...b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882) Updates `actions/setup-go` from 5.0.2 to 5.1.0 - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32...41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed) Updates `golangci/golangci-lint-action` from 6.1.0 to 6.1.1 - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/aaa42aa0628b4ae2578232a66b541047968fac86...971e284b6050e8a5849b72094c50ab08da042db8) Updates `actions/setup-node` from 4.0.3 to 4.1.0 - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/1e60f620b9541d16bece96c5465dc8ee9832be0b...39370e3970a6d050c480ffad4ff0ed4d3fdee5af) Updates `actions/cache` from 4.0.2 to 4.1.2 - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/0c45773b623bea8c8e75f6c82b208c3cf94ea4f9...6849a6489940f00c2f30c0fb92c6274307ccb58a) --- updated-dependencies: - dependency-name: step-security/harden-runner dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all - dependency-name: actions/dependency-review-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all - dependency-name: actions/setup-go dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all - dependency-name: golangci/golangci-lint-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 10 ++++----- .github/workflows/dependency-review.yml | 6 +++--- .github/workflows/scorecards.yml | 8 +++---- .github/workflows/scripts.yaml | 6 +++--- .github/workflows/website.yaml | 8 +++---- .github/workflows/workflow.yaml | 28 ++++++++++++------------- 6 files changed, 33 insertions(+), 33 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ec8a27216..2e4276ccd 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,16 +41,16 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 with: egress-policy: audit - name: Checkout repository - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 + uses: github/codeql-action/init@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -60,7 +60,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 + uses: github/codeql-action/autobuild@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -73,6 +73,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 + uses: github/codeql-action/analyze@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index c1209291a..21a469b13 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,11 +17,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 with: egress-policy: audit - name: 'Checkout Repository' - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: 'Dependency Review' - uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4 + uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 274792b2e..ccfa232e9 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -31,12 +31,12 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 with: egress-policy: audit - name: "Checkout code" - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false @@ -63,7 +63,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: SARIF file path: results.sarif @@ -71,6 +71,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 + uses: github/codeql-action/upload-sarif@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 with: sarif_file: results.sarif diff --git a/.github/workflows/scripts.yaml b/.github/workflows/scripts.yaml index de75e63c5..7a56c683f 100644 --- a/.github/workflows/scripts.yaml +++ b/.github/workflows/scripts.yaml @@ -22,13 +22,13 @@ jobs: matrix: folder: [artifacthub, require-sync, validate, website] steps: - - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 with: go-version: '1.20' cache: false - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: golangci-lint - uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0 + uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1 with: version: v1.55.2 working-directory: scripts/${{ matrix.folder }} diff --git a/.github/workflows/website.yaml b/.github/workflows/website.yaml index ba50fef9e..6aa0a4c0d 100644 --- a/.github/workflows/website.yaml +++ b/.github/workflows/website.yaml @@ -25,14 +25,14 @@ jobs: working-directory: website steps: - name: Harden Runner - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 with: egress-policy: audit - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Node - uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 with: node-version: "18" @@ -41,7 +41,7 @@ jobs: run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: ${{ steps.yarn-cache.outputs.dir }} key: ${{ runner.os }}-website-${{ hashFiles('**/yarn.lock') }} diff --git a/.github/workflows/workflow.yaml b/.github/workflows/workflow.yaml index 30956901b..fcc48a9d3 100644 --- a/.github/workflows/workflow.yaml +++ b/.github/workflows/workflow.yaml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest name: "Test scripts" steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Unit test run: | make unit-test @@ -26,11 +26,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 with: egress-policy: audit - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Generate templates and docs run: | make generate generate-website-docs generate-artifacthub-artifacts @@ -49,11 +49,11 @@ jobs: name: Unit test on ${{ matrix.os }} opa ${{ matrix.opa }} steps: - name: Harden Runner - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 with: egress-policy: audit - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - run: | binary=$([[ "$OSTYPE" == "darwin"* ]] && echo "opa_darwin_amd64" || echo "opa_linux_amd64") sudo curl -L -o /usr/local/bin/opa https://github.com/open-policy-agent/opa/releases/download/${{ matrix.opa }}/$binary @@ -71,13 +71,13 @@ jobs: steps: - name: Harden Runner if: ${{ !(matrix.gatekeeper == '3.15.1' && matrix.engine == 'cel') }} # remove this condition once 3.17 is out - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 with: egress-policy: audit - name: Check out code into the Go module directory if: ${{ !(matrix.gatekeeper == '3.15.1' && matrix.engine == 'cel') }} - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Bootstrap integration test if: ${{ !(matrix.gatekeeper == '3.15.1' && matrix.engine == 'cel') }} @@ -99,7 +99,7 @@ jobs: kubectl logs -n gatekeeper-system -l control-plane=audit-controller --tail=-1 > logs-audit.json - name: Upload artifacts - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ always() }} with: name: logs-int-test-${{ matrix.gatekeeper }}-${{ matrix.engine }} @@ -110,11 +110,11 @@ jobs: name: "Require a suite.yaml file alongside every template.yaml" steps: - name: Harden Runner - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 with: egress-policy: audit - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Run script run: | make require-suites @@ -123,11 +123,11 @@ jobs: name: "Require a sync.yaml file and metadata.gatekeeper.sh/requires-sync-data annotation for every template.yaml using data.inventory" steps: - name: Harden Runner - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 with: egress-policy: audit - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Run script run: | make require-sync @@ -141,11 +141,11 @@ jobs: steps: - name: Harden Runner if: ${{ !(matrix.gatekeeper == '3.15.1' && matrix.engine == 'cel') }} # remove this condition once 3.17 is out - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 with: egress-policy: audit - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 if: ${{ !(matrix.gatekeeper == '3.15.1' && matrix.engine == 'cel') }} - run: | From 52cb14a2ef7a9d06908e1543524f283290b8b4f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 10:52:20 -0800 Subject: [PATCH 19/19] chore: bump the all group with 2 updates (#614) Bumps the all group with 2 updates: [github/codeql-action](https://github.com/github/codeql-action) and [actions/cache](https://github.com/actions/cache). Updates `github/codeql-action` from 3.27.5 to 3.27.6 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/f09c1c0a94de965c15400f5634aa42fac8fb8f88...aa578102511db1f4524ed59b8cc2bae4f6e88195) Updates `actions/cache` from 4.1.2 to 4.2.0 - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/6849a6489940f00c2f30c0fb92c6274307ccb58a...1bd1e32a3bdc45362d1e726936510720a7c30a57) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- .github/workflows/website.yaml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2e4276ccd..b71c31b9c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -50,7 +50,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 + uses: github/codeql-action/init@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -60,7 +60,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 + uses: github/codeql-action/autobuild@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -73,6 +73,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 + uses: github/codeql-action/analyze@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index ccfa232e9..316a0a5de 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -71,6 +71,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 + uses: github/codeql-action/upload-sarif@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6 with: sarif_file: results.sarif diff --git a/.github/workflows/website.yaml b/.github/workflows/website.yaml index 6aa0a4c0d..bc9f1ea42 100644 --- a/.github/workflows/website.yaml +++ b/.github/workflows/website.yaml @@ -41,7 +41,7 @@ jobs: run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: ${{ steps.yarn-cache.outputs.dir }} key: ${{ runner.os }}-website-${{ hashFiles('**/yarn.lock') }}