From cedf582594bf1f098c1e74b0e412dfb2fcff87e9 Mon Sep 17 00:00:00 2001 From: Sukumar Yethadka Date: Fri, 26 Jun 2026 08:22:52 +0000 Subject: [PATCH 1/3] Add workload security hardening checks Add 3 new checks (init-container-security-gap, projected-token-long-expiration, automount-service-account-token) and enhance host-network check description to document service mesh mTLS bypass risk. --- .../automount-service-account-token.yaml | 17 +++++++ pkg/builtinchecks/yamls/hostnetwork.yaml | 11 ++++- .../yamls/init-container-security-gap.yaml | 23 +++++++++ .../projected-token-long-expiration.yaml | 23 +++++++++ .../automount-service-account-token.yml | 35 ++++++++++++++ tests/checks/init-container-security-gap.yml | 47 +++++++++++++++++++ .../projected-token-long-expiration.yml | 36 ++++++++++++++ 7 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 pkg/builtinchecks/yamls/automount-service-account-token.yaml create mode 100644 pkg/builtinchecks/yamls/init-container-security-gap.yaml create mode 100644 pkg/builtinchecks/yamls/projected-token-long-expiration.yaml create mode 100644 tests/checks/automount-service-account-token.yml create mode 100644 tests/checks/init-container-security-gap.yml create mode 100644 tests/checks/projected-token-long-expiration.yml diff --git a/pkg/builtinchecks/yamls/automount-service-account-token.yaml b/pkg/builtinchecks/yamls/automount-service-account-token.yaml new file mode 100644 index 000000000..a84ac457b --- /dev/null +++ b/pkg/builtinchecks/yamls/automount-service-account-token.yaml @@ -0,0 +1,17 @@ +name: automount-service-account-token +description: >- + Pod does not explicitly disable automatic mounting of the service account + token. If the workload does not call the Kubernetes API, the mounted token + is unnecessary attack surface. +remediation: >- + Set automountServiceAccountToken: false in the pod spec if the workload + does not need to interact with the Kubernetes API. +scope: + objectKinds: + - DeploymentLike +template: cel-expression +params: + check: > + (!has(object.spec.template.spec.automountServiceAccountToken) || + object.spec.template.spec.automountServiceAccountToken == true) + ? 'service account token is auto-mounted — set automountServiceAccountToken: false if not needed' : '' diff --git a/pkg/builtinchecks/yamls/hostnetwork.yaml b/pkg/builtinchecks/yamls/hostnetwork.yaml index 15adc3aa6..068e533c8 100644 --- a/pkg/builtinchecks/yamls/hostnetwork.yaml +++ b/pkg/builtinchecks/yamls/hostnetwork.yaml @@ -1,6 +1,13 @@ name: "host-network" -description: "Alert on pods/deployment-likes with sharing host's network namespace" -remediation: "Ensure the host's network namespace is not shared." +description: >- + Alert on pods/deployment-likes sharing the host's network namespace. + Pods with hostNetwork: true bypass service mesh mTLS, silently disabling + encryption for inter-service traffic. They also gain access to + network-level attacks against other pods on the same node. +remediation: >- + Ensure the host's network namespace is not shared. If host networking + is required, be aware that service mesh mTLS is bypassed and consider + alternative encryption for inter-service communication. scope: objectKinds: - DeploymentLike diff --git a/pkg/builtinchecks/yamls/init-container-security-gap.yaml b/pkg/builtinchecks/yamls/init-container-security-gap.yaml new file mode 100644 index 000000000..008e5cf3c --- /dev/null +++ b/pkg/builtinchecks/yamls/init-container-security-gap.yaml @@ -0,0 +1,23 @@ +name: init-container-security-gap +description: >- + Init container does not set runAsNonRoot or readOnlyRootFilesystem while + the pod-level securityContext does. Init containers can run as root even + when main containers are hardened. +remediation: >- + Apply the same securityContext restrictions to init containers as to + regular containers. +scope: + objectKinds: + - DeploymentLike +template: cel-expression +params: + check: > + has(object.spec.template.spec.securityContext) && + has(object.spec.template.spec.securityContext.runAsNonRoot) && + object.spec.template.spec.securityContext.runAsNonRoot == true && + has(object.spec.template.spec.initContainers) && + object.spec.template.spec.initContainers.exists(c, + !has(c.securityContext) || + !has(c.securityContext.runAsNonRoot) || + c.securityContext.runAsNonRoot != true + ) ? 'init container may run as root despite pod-level runAsNonRoot — apply securityContext to init containers' : '' diff --git a/pkg/builtinchecks/yamls/projected-token-long-expiration.yaml b/pkg/builtinchecks/yamls/projected-token-long-expiration.yaml new file mode 100644 index 000000000..95603d2b3 --- /dev/null +++ b/pkg/builtinchecks/yamls/projected-token-long-expiration.yaml @@ -0,0 +1,23 @@ +name: projected-token-long-expiration +description: >- + Projected service account token volume with expirationSeconds > 24h or + without explicit expiration. Long-lived tokens extracted from compromised + pods remain valid indefinitely. +remediation: >- + Set expirationSeconds to 3600 (1 hour) or less unless the workload + specifically requires longer token lifetimes. +scope: + objectKinds: + - DeploymentLike +template: cel-expression +params: + check: > + has(object.spec.template.spec.volumes) && + object.spec.template.spec.volumes.exists(v, + has(v.projected) && + v.projected.sources.exists(s, + has(s.serviceAccountToken) && + has(s.serviceAccountToken.expirationSeconds) && + s.serviceAccountToken.expirationSeconds > 86400 + ) + ) ? 'projected SA token with expirationSeconds > 24h — stolen tokens persist' : '' diff --git a/tests/checks/automount-service-account-token.yml b/tests/checks/automount-service-account-token.yml new file mode 100644 index 000000000..85d9eabbd --- /dev/null +++ b/tests/checks/automount-service-account-token.yml @@ -0,0 +1,35 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dont-fire +spec: + template: + spec: + automountServiceAccountToken: false + containers: + - name: app + image: app:v1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: fire-default-mount +spec: + template: + spec: + containers: + - name: app + image: app:v1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: fire-explicit-true +spec: + template: + spec: + automountServiceAccountToken: true + containers: + - name: app + image: app:v1 diff --git a/tests/checks/init-container-security-gap.yml b/tests/checks/init-container-security-gap.yml new file mode 100644 index 000000000..921de6513 --- /dev/null +++ b/tests/checks/init-container-security-gap.yml @@ -0,0 +1,47 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dont-fire-no-init +spec: + template: + spec: + securityContext: + runAsNonRoot: true + containers: + - name: app + image: app:v1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dont-fire-init-hardened +spec: + template: + spec: + securityContext: + runAsNonRoot: true + initContainers: + - name: init + image: init:v1 + securityContext: + runAsNonRoot: true + containers: + - name: app + image: app:v1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: fire-init-no-security-context +spec: + template: + spec: + securityContext: + runAsNonRoot: true + initContainers: + - name: init + image: init:v1 + containers: + - name: app + image: app:v1 diff --git a/tests/checks/projected-token-long-expiration.yml b/tests/checks/projected-token-long-expiration.yml new file mode 100644 index 000000000..e7d45a93e --- /dev/null +++ b/tests/checks/projected-token-long-expiration.yml @@ -0,0 +1,36 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dont-fire-short-token +spec: + template: + spec: + containers: + - name: app + image: app:v1 + volumes: + - name: token + projected: + sources: + - serviceAccountToken: + expirationSeconds: 3600 + path: token +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: fire-long-token +spec: + template: + spec: + containers: + - name: app + image: app:v1 + volumes: + - name: token + projected: + sources: + - serviceAccountToken: + expirationSeconds: 604800 + path: token From 7641681602b9e1069796f996d294b58705dd0fe5 Mon Sep 17 00:00:00 2001 From: Sukumar Yethadka Date: Fri, 26 Jun 2026 09:06:41 +0000 Subject: [PATCH 2/3] Address review findings and regenerate docs - Fix init-container-security-gap description to match CEL (runAsNonRoot only) - Fix projected-token-long-expiration description to remove missing-expiration claim - Clarify automount-service-account-token checks pod spec only - Regenerate docs/generated/checks.md and templates.md --- docs/generated/checks.md | 65 ++++++++++++++++++- .../automount-service-account-token.yaml | 8 ++- .../yamls/init-container-security-gap.yaml | 10 +-- .../projected-token-long-expiration.yaml | 6 +- 4 files changed, 76 insertions(+), 13 deletions(-) diff --git a/docs/generated/checks.md b/docs/generated/checks.md index 772475bc0..69816475c 100644 --- a/docs/generated/checks.md +++ b/docs/generated/checks.md @@ -49,6 +49,24 @@ verbs: - ^watch$ - ^*$ ``` +## automount-service-account-token + +**Enabled by default**: No + +**Description**: Pod spec does not explicitly set automountServiceAccountToken: false. If the workload does not call the Kubernetes API, consider disabling automatic token mounting to reduce attack surface. Note: this checks the pod spec only — a ServiceAccount-level setting may also control this behavior. + +**Remediation**: Set automountServiceAccountToken: false in the pod spec if the workload does not need to interact with the Kubernetes API. + +**Template**: [cel-expression](templates.md#cel) + +**Parameters**: + +```yaml +check: | + (!has(object.spec.template.spec.automountServiceAccountToken) || + object.spec.template.spec.automountServiceAccountToken == true) + ? 'service account token is auto-mounted — set automountServiceAccountToken: false if not needed' : '' +``` ## cluster-admin-role-binding **Enabled by default**: No @@ -255,9 +273,9 @@ forbiddenServiceTypes: **Enabled by default**: Yes -**Description**: Alert on pods/deployment-likes with sharing host's network namespace +**Description**: Alert on pods/deployment-likes sharing the host's network namespace. Pods with hostNetwork: true bypass service mesh mTLS, silently disabling encryption for inter-service traffic. They also gain access to network-level attacks against other pods on the same node. -**Remediation**: Ensure the host's network namespace is not shared. +**Remediation**: Ensure the host's network namespace is not shared. If host networking is required, be aware that service mesh mTLS is bypassed and consider alternative encryption for inter-service communication. **Template**: [host-network](templates.md#host-network) ## host-pid @@ -284,6 +302,26 @@ forbiddenServiceTypes: ```yaml minReplicas: 3 ``` +## init-container-security-gap + +**Enabled by default**: No + +**Description**: Init container does not set runAsNonRoot: true while the pod-level securityContext does. Init containers can run as root even when main containers are hardened via the pod-level security context. + +**Remediation**: Set runAsNonRoot: true in each init container's securityContext to match the pod-level restriction. + +**Template**: [cel-expression](templates.md#cel) + +**Parameters**: + +```yaml +check: | + has(object.spec.template.spec.securityContext) && has(object.spec.template.spec.securityContext.runAsNonRoot) && object.spec.template.spec.securityContext.runAsNonRoot == true && has(object.spec.template.spec.initContainers) && object.spec.template.spec.initContainers.exists(c, + !has(c.securityContext) || + !has(c.securityContext.runAsNonRoot) || + c.securityContext.runAsNonRoot != true + ) ? 'init container may run as root despite pod-level runAsNonRoot — apply securityContext to init containers' : '' +``` ## invalid-target-ports **Enabled by default**: Yes @@ -524,6 +562,29 @@ acceptedPriorityClassNames: **Remediation**: Ensure privileged ports [0, 1024] are not mapped within containers. **Template**: [privileged-ports](templates.md#privileged-ports) +## projected-token-long-expiration + +**Enabled by default**: No + +**Description**: Projected service account token volume with expirationSeconds greater than 24 hours. Long-lived tokens extracted from compromised pods remain valid for extended periods. + +**Remediation**: Set expirationSeconds to 3600 (1 hour) or less unless the workload specifically requires longer token lifetimes. + +**Template**: [cel-expression](templates.md#cel) + +**Parameters**: + +```yaml +check: | + has(object.spec.template.spec.volumes) && object.spec.template.spec.volumes.exists(v, + has(v.projected) && + v.projected.sources.exists(s, + has(s.serviceAccountToken) && + has(s.serviceAccountToken.expirationSeconds) && + s.serviceAccountToken.expirationSeconds > 86400 + ) + ) ? 'projected SA token with expirationSeconds > 24h — stolen tokens persist' : '' +``` ## read-secret-from-env-var **Enabled by default**: No diff --git a/pkg/builtinchecks/yamls/automount-service-account-token.yaml b/pkg/builtinchecks/yamls/automount-service-account-token.yaml index a84ac457b..93ce56eb8 100644 --- a/pkg/builtinchecks/yamls/automount-service-account-token.yaml +++ b/pkg/builtinchecks/yamls/automount-service-account-token.yaml @@ -1,8 +1,10 @@ name: automount-service-account-token description: >- - Pod does not explicitly disable automatic mounting of the service account - token. If the workload does not call the Kubernetes API, the mounted token - is unnecessary attack surface. + Pod spec does not explicitly set automountServiceAccountToken: false. + If the workload does not call the Kubernetes API, consider disabling + automatic token mounting to reduce attack surface. Note: this checks + the pod spec only — a ServiceAccount-level setting may also control + this behavior. remediation: >- Set automountServiceAccountToken: false in the pod spec if the workload does not need to interact with the Kubernetes API. diff --git a/pkg/builtinchecks/yamls/init-container-security-gap.yaml b/pkg/builtinchecks/yamls/init-container-security-gap.yaml index 008e5cf3c..7e6c525a3 100644 --- a/pkg/builtinchecks/yamls/init-container-security-gap.yaml +++ b/pkg/builtinchecks/yamls/init-container-security-gap.yaml @@ -1,11 +1,11 @@ name: init-container-security-gap description: >- - Init container does not set runAsNonRoot or readOnlyRootFilesystem while - the pod-level securityContext does. Init containers can run as root even - when main containers are hardened. + Init container does not set runAsNonRoot: true while the pod-level + securityContext does. Init containers can run as root even when main + containers are hardened via the pod-level security context. remediation: >- - Apply the same securityContext restrictions to init containers as to - regular containers. + Set runAsNonRoot: true in each init container's securityContext to match + the pod-level restriction. scope: objectKinds: - DeploymentLike diff --git a/pkg/builtinchecks/yamls/projected-token-long-expiration.yaml b/pkg/builtinchecks/yamls/projected-token-long-expiration.yaml index 95603d2b3..c50ef9c6d 100644 --- a/pkg/builtinchecks/yamls/projected-token-long-expiration.yaml +++ b/pkg/builtinchecks/yamls/projected-token-long-expiration.yaml @@ -1,8 +1,8 @@ name: projected-token-long-expiration description: >- - Projected service account token volume with expirationSeconds > 24h or - without explicit expiration. Long-lived tokens extracted from compromised - pods remain valid indefinitely. + Projected service account token volume with expirationSeconds greater + than 24 hours. Long-lived tokens extracted from compromised pods remain + valid for extended periods. remediation: >- Set expirationSeconds to 3600 (1 hour) or less unless the workload specifically requires longer token lifetimes. From 736b4d1cbb73b0aa6fd2716310e0e85ed30e4265 Mon Sep 17 00:00:00 2001 From: Sukumar Yethadka Date: Fri, 26 Jun 2026 10:57:45 +0000 Subject: [PATCH 3/3] Add bats tests for workload hardening checks Add bats test entries for automount-service-account-token, init-container-security-gap, and projected-token-long-expiration in correct alphabetical order. --- e2etests/bats-tests.sh | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/e2etests/bats-tests.sh b/e2etests/bats-tests.sh index 56dbeb06a..072f2cc43 100755 --- a/e2etests/bats-tests.sh +++ b/e2etests/bats-tests.sh @@ -98,6 +98,18 @@ get_value_from() { [[ "${count}" == "1" ]] } +@test "automount-service-account-token" { + tmp="tests/checks/automount-service-account-token.yml" + cmd="${KUBE_LINTER_BIN} lint --include automount-service-account-token --do-not-auto-add-defaults --format json ${tmp}" + run ${cmd} + + print_info "${status}" "${output}" "${cmd}" "${tmp}" + [ "$status" -eq 1 ] + + count=$(get_value_from "${lines[0]}" '.Reports | length') + [[ "${count}" == "2" ]] +} + @test "cluster-admin-role-binding" { tmp="tests/checks/cluster-admin-role-binding.yml" cmd="${KUBE_LINTER_BIN} lint --include cluster-admin-role-binding --do-not-auto-add-defaults --format json ${tmp}" @@ -435,6 +447,18 @@ get_value_from() { [[ "${count}" == "1" ]] } +@test "init-container-security-gap" { + tmp="tests/checks/init-container-security-gap.yml" + cmd="${KUBE_LINTER_BIN} lint --include init-container-security-gap --do-not-auto-add-defaults --format json ${tmp}" + run ${cmd} + + print_info "${status}" "${output}" "${cmd}" "${tmp}" + [ "$status" -eq 1 ] + + count=$(get_value_from "${lines[0]}" '.Reports | length') + [[ "${count}" == "1" ]] +} + @test "invalid-target-ports" { tmp="tests/checks/invalid-target-ports.yaml" cmd="${KUBE_LINTER_BIN} lint --include invalid-target-ports --do-not-auto-add-defaults --format json ${tmp}" @@ -815,6 +839,18 @@ get_value_from() { [[ "${count}" == "2" ]] } +@test "projected-token-long-expiration" { + tmp="tests/checks/projected-token-long-expiration.yml" + cmd="${KUBE_LINTER_BIN} lint --include projected-token-long-expiration --do-not-auto-add-defaults --format json ${tmp}" + run ${cmd} + + print_info "${status}" "${output}" "${cmd}" "${tmp}" + [ "$status" -eq 1 ] + + count=$(get_value_from "${lines[0]}" '.Reports | length') + [[ "${count}" == "1" ]] +} + @test "read-secret-from-env-var" { tmp="tests/checks/read-secret-from-env-var.yml" cmd="${KUBE_LINTER_BIN} lint --include read-secret-from-env-var --do-not-auto-add-defaults --format json ${tmp}"