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/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}" 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..93ce56eb8 --- /dev/null +++ b/pkg/builtinchecks/yamls/automount-service-account-token.yaml @@ -0,0 +1,19 @@ +name: automount-service-account-token +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. +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..7e6c525a3 --- /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: 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. +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..c50ef9c6d --- /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 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. +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