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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions docs/generated/checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,61 @@ verbs:
**Remediation**: Create and assign a separate role that has access to specific resources/actions needed for the service account.

**Template**: [cluster-admin-role-binding](templates.md#cluster-admin-role-binding)
## cluster-wide-secrets-access

**Enabled by default**: No

**Description**: ClusterRoleBinding granting secrets access (get, list, watch, create, update, patch, delete) across all namespaces. When bound to an operator service account that reconciles tenant-controlled custom resources, this enables cross-namespace secret exfiltration (confused deputy).

**Remediation**: Use namespaced RoleBindings to limit secrets access to required namespaces, or restrict the ClusterRole to specific resource names if cluster-wide access is necessary.

**Template**: [access-to-resources](templates.md#access-to-resources)

**Parameters**:

```yaml
resources:
- ^secrets$
- ^\*$
verbs:
- ^get$
- ^list$
- ^watch$
- ^create$
- ^update$
- ^patch$
- ^delete$
- ^\*$
```
## configmap-with-credentials

**Enabled by default**: No

**Description**: ConfigMap contains keys with credential-like names (password, secret, token, api_key). Credentials should be stored in Secrets, not ConfigMaps.

**Remediation**: Move credential values to a Secret resource. Reference the Secret from pod specs using secretKeyRef instead of configMapKeyRef.

**Template**: [cel-expression](templates.md#cel)

**Parameters**:

```yaml
check: |
object.kind == 'ConfigMap' && (
(has(object.data) &&
object.data.exists(k,
k.contains('password') || k.contains('secret') || k.contains('token') ||
k.contains('api_key') || k.contains('apikey') || k.contains('private_key') ||
k.contains('credential')
)) ||
(has(object.binaryData) &&
object.binaryData.exists(k,
k.contains('password') || k.contains('secret') || k.contains('token') ||
k.contains('api_key') || k.contains('apikey') || k.contains('private_key') ||
k.contains('credential')
))
) ? 'ConfigMap contains credential-like keys — use Secrets instead' : ''
```
## dangling-horizontalpodautoscaler

**Enabled by default**: No
Expand Down Expand Up @@ -242,6 +297,22 @@ forbiddenServiceTypes:
- NodePort
- LoadBalancer
```
## externalname-service-redirect

**Enabled by default**: No

**Description**: Service with type: ExternalName redirects internal traffic to an external DNS name. If the external name resolves to an attacker-controlled host, this enables SSRF at the infrastructure level.

**Remediation**: Use type: ClusterIP with an Endpoints object pointing to a known-safe IP, or validate that the externalName resolves to a trusted destination.

**Template**: [cel-expression](templates.md#cel)

**Parameters**:

```yaml
check: |
object.kind == 'Service' && has(object.spec) && object.spec.type == 'ExternalName' && has(object.spec.externalName) ? 'ExternalName service redirects cluster traffic to external DNS — potential SSRF vector' : ''
```
## host-ipc

**Enabled by default**: Yes
Expand Down Expand Up @@ -284,6 +355,15 @@ forbiddenServiceTypes:
```yaml
minReplicas: 3
```
## image-not-pinned-by-digest

**Enabled by default**: No

**Description**: Container images referenced by tag are mutable — a tag can be moved to point to a different image at any time. Pinning by digest guarantees the exact image content that will run.

**Remediation**: Reference container images by digest (e.g. image@sha256:abc123...) instead of by tag.

**Template**: [image-not-pinned-by-digest](templates.md#image-not-pinned-by-digest)
## invalid-target-ports

**Enabled by default**: Yes
Expand Down
22 changes: 22 additions & 0 deletions docs/generated/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,28 @@ KubeLinter supports the following templates:
type: integer
```

## Image Not Pinned By Digest

**Key**: `image-not-pinned-by-digest`

**Description**: Flag container images that are not pinned to a specific digest

**Supported Objects**: DeploymentLike


**Parameters**:

```yaml
- arrayElemType: string
description: list of regular expressions specifying pattern(s) for container images
to exempt from the digest check.
name: allowList
negationAllowed: true
regexAllowed: true
required: false
type: array
```

## Image Pull Policy

**Key**: `image-pull-policy`
Expand Down
48 changes: 48 additions & 0 deletions e2etests/bats-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,30 @@ get_value_from() {
[[ "${count}" == "1" ]]
}

@test "cluster-wide-secrets-access" {
tmp="tests/checks/cluster-wide-secrets-access.yml"
cmd="${KUBE_LINTER_BIN} lint --include cluster-wide-secrets-access --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 "configmap-with-credentials" {
tmp="tests/checks/configmap-with-credentials.yml"
cmd="${KUBE_LINTER_BIN} lint --include configmap-with-credentials --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 "dangling-horizontalpodautoscaler" {
tmp="tests/checks/dangling-hpa.yml"
cmd="${KUBE_LINTER_BIN} lint --include dangling-horizontalpodautoscaler --do-not-auto-add-defaults --format json ${tmp}"
Expand Down Expand Up @@ -369,6 +393,18 @@ get_value_from() {
[[ "${count}" == "1" ]]
}

@test "externalname-service-redirect" {
tmp="tests/checks/externalname-service-redirect.yml"
cmd="${KUBE_LINTER_BIN} lint --include externalname-service-redirect --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 "host-ipc" {
tmp="tests/checks/host-ipc.yml"
cmd="${KUBE_LINTER_BIN} lint --include host-ipc --do-not-auto-add-defaults --format json ${tmp}"
Expand Down Expand Up @@ -435,6 +471,18 @@ get_value_from() {
[[ "${count}" == "1" ]]
}

@test "image-not-pinned-by-digest" {
tmp="tests/checks/image-not-pinned-by-digest.yml"
cmd="${KUBE_LINTER_BIN} lint --include image-not-pinned-by-digest --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 "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}"
Expand Down
17 changes: 17 additions & 0 deletions pkg/builtinchecks/yamls/cluster-wide-secrets-access.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: cluster-wide-secrets-access
description: >-
ClusterRoleBinding granting secrets access (get, list, watch, create,
update, patch, delete) across all namespaces. When bound to an operator
service account that reconciles tenant-controlled custom resources, this
enables cross-namespace secret exfiltration (confused deputy).
remediation: >-
Use namespaced RoleBindings to limit secrets access to required namespaces,
or restrict the ClusterRole to specific resource names if cluster-wide
access is necessary.
scope:
objectKinds:
- ClusterRoleBinding
template: "access-to-resources"
params:
resources: ["^secrets$", "^\\*$"]
verbs: ["^get$", "^list$", "^watch$", "^create$", "^update$", "^patch$", "^delete$", "^\\*$"]
28 changes: 28 additions & 0 deletions pkg/builtinchecks/yamls/configmap-with-credentials.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: configmap-with-credentials
description: >-
ConfigMap contains keys with credential-like names (password, secret,
token, api_key). Credentials should be stored in Secrets, not ConfigMaps.
remediation: >-
Move credential values to a Secret resource. Reference the Secret from
pod specs using secretKeyRef instead of configMapKeyRef.
scope:
objectKinds:
- Any
template: cel-expression
params:
check: >
object.kind == 'ConfigMap' &&
(
(has(object.data) &&
object.data.exists(k,
k.contains('password') || k.contains('secret') || k.contains('token') ||
k.contains('api_key') || k.contains('apikey') || k.contains('private_key') ||
k.contains('credential')
)) ||
(has(object.binaryData) &&
object.binaryData.exists(k,
k.contains('password') || k.contains('secret') || k.contains('token') ||
k.contains('api_key') || k.contains('apikey') || k.contains('private_key') ||
k.contains('credential')
))
) ? 'ConfigMap contains credential-like keys — use Secrets instead' : ''
19 changes: 19 additions & 0 deletions pkg/builtinchecks/yamls/externalname-service-redirect.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: externalname-service-redirect
description: >-
Service with type: ExternalName redirects internal traffic to an external
DNS name. If the external name resolves to an attacker-controlled host,
this enables SSRF at the infrastructure level.
remediation: >-
Use type: ClusterIP with an Endpoints object pointing to a known-safe IP,
or validate that the externalName resolves to a trusted destination.
scope:
objectKinds:
- Any
template: cel-expression
params:
check: >
object.kind == 'Service' &&
has(object.spec) &&
object.spec.type == 'ExternalName' &&
has(object.spec.externalName)
? 'ExternalName service redirects cluster traffic to external DNS — potential SSRF vector' : ''
Comment thread
coderabbitai[bot] marked this conversation as resolved.
12 changes: 12 additions & 0 deletions pkg/builtinchecks/yamls/image-not-pinned-by-digest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: "image-not-pinned-by-digest"
description: >-
Container images referenced by tag are mutable — a tag can be moved to
point to a different image at any time. Pinning by digest guarantees
the exact image content that will run.
remediation: >-
Reference container images by digest (e.g. image@sha256:abc123...)
instead of by tag.
scope:
objectKinds:
- DeploymentLike
template: "image-not-pinned-by-digest"
1 change: 1 addition & 0 deletions pkg/templates/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
_ "golang.stackrox.io/kube-linter/pkg/templates/hostnetwork"
_ "golang.stackrox.io/kube-linter/pkg/templates/hostpid"
_ "golang.stackrox.io/kube-linter/pkg/templates/hpareplicas"
_ "golang.stackrox.io/kube-linter/pkg/templates/imagedigest"
_ "golang.stackrox.io/kube-linter/pkg/templates/imagepullpolicy"
_ "golang.stackrox.io/kube-linter/pkg/templates/jobttlsecondsafterfinished"
_ "golang.stackrox.io/kube-linter/pkg/templates/kubeconform"
Expand Down
68 changes: 68 additions & 0 deletions pkg/templates/imagedigest/internal/params/gen-params.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions pkg/templates/imagedigest/internal/params/params.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package params

// Params represents the params accepted by this template.
type Params struct {

// list of regular expressions specifying pattern(s) for container images to exempt from the digest check.
AllowList []string
}
Loading
Loading