Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
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$", "^\\*$"]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
20 changes: 20 additions & 0 deletions pkg/builtinchecks/yamls/configmap-with-credentials.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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')
) ? 'ConfigMap contains credential-like keys — use Secrets instead' : ''

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the target file and any CEL-related implementation/tests.
git ls-files 'pkg/builtinchecks/yamls/*' 'pkg/**' | sed -n '1,200p'
printf '\n--- target file outline ---\n'
ast-grep outline pkg/builtinchecks/yamls/configmap-with-credentials.yaml --view expanded || true
printf '\n--- search for matches()/regex usage in yamls checks ---\n'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
  "matches\(|contains\(|binaryData|api[_-]?key|private[_-]?key|credential" pkg/builtinchecks/yamls pkg 2>/dev/null | sed -n '1,240p'

Repository: stackrox/kube-linter

Length of output: 15758


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the relevant YAML file in a narrow slice with line numbers.
wc -l pkg/builtinchecks/yamls/configmap-with-credentials.yaml
printf '\n--- file excerpt ---\n'
cat -n pkg/builtinchecks/yamls/configmap-with-credentials.yaml | sed -n '1,120p'

Repository: stackrox/kube-linter

Length of output: 1155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect CEL template/runtime wiring and any tests around supported functions.
ast-grep outline pkg/templates/cel/template.go --view expanded || true
printf '\n--- cel template excerpt ---\n'
cat -n pkg/templates/cel/template.go | sed -n '1,260p'

printf '\n--- search for cel-go dependency and matches support references ---\n'
rg -n "cel-go|google/cel|matches\(|startsWith\(|endsWith\(|contains\(" go.mod go.sum pkg test .github 2>/dev/null | sed -n '1,260p'

Repository: stackrox/kube-linter

Length of output: 5375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the CEL template and tests for expression evaluation behavior.
wc -l pkg/templates/cel/template.go pkg/templates/cel/template_test.go
printf '\n--- template.go excerpt ---\n'
cat -n pkg/templates/cel/template.go | sed -n '1,260p'
printf '\n--- template_test.go excerpt ---\n'
cat -n pkg/templates/cel/template_test.go | sed -n '1,260p'

Repository: stackrox/kube-linter

Length of output: 9252


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the CEL environment construction and any custom function declarations.
rg -n "cel\.NewEnv|cel\.EnvOption|functions\.|matches|strings\." pkg go.mod go.sum 2>/dev/null | sed -n '1,260p'

Repository: stackrox/kube-linter

Length of output: 30628


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate CEL environment creation and the exact expression evaluator used by built-in checks.
rg -n "cel\.NewEnv|cel\.Compile|cel\.Program|Evaluate|cel-expression|NewProgram|EnvOption|FunctionDecl|matches\(" pkg | sed -n '1,260p'

Repository: stackrox/kube-linter

Length of output: 662


🌐 Web query:

cel-go v0.28.1 string.matches regex semantics CEL standard library string method matches

💡 Result:

In the Common Expression Language (CEL), the string method matches is part of the standard library and is used to test whether a string matches a specified regular expression [1][2][3]. Regex Semantics: CEL uses RE2 syntax for its regular expressions [4][5][6]. A matches operation succeeds if the provided regular expression matches any substring of the target string [4][5][6]. To perform a full-string match—where the pattern must match the entire input—you must explicitly anchor the regular expression using the ^ (start of string) and $ (end of string) anchors [4][5][6]. Usage in cel-go v0.28.1: The method is invoked using member syntax: targetString.matches(regexPattern) [1]. The targetString is the string being evaluated, and regexPattern is the string containing the RE2-compliant regular expression [7][1]. Key distinctions: 1. Standard Library vs. Extensions: The matches function is a core part of the CEL standard library and is always available [3]. It is distinct from the additional regex operations (such as find or findAll) provided by the optional cel-go regex extension library, which may require explicit registration in your CEL environment [2]. 2. Versioning: As of cel-go v0.28.1, the core behavior of matches remains consistent with the CEL specification, following RE2 syntax and substring matching semantics [4][8]. Recent updates to cel-go (including v0.28.1) have focused on areas such as cost estimation and performance tracking for regex operations, but they have not altered the fundamental substring-matching behavior of the matches method [8][9].

Citations:


🌐 Web query:

cel-go v0.28.1 string.matches regex semantics CEL standard library string method matches

💡 Result:

In cel-go v0.28.1, the string.matches(regex) method is part of the CEL standard library [1][2]. Its semantics are defined by the CEL language specification and implemented in Go using the RE2 regular expression engine [1][2][3]. Key technical details regarding the matches method include: Regex Syntax: CEL uses RE2 syntax for regular expressions [1][3]. You can reference the official RE2 syntax documentation for details on supported patterns. Matching Behavior: A matches call evaluates to true if the regular expression matches anywhere within the target string (a substring match) [1][3]. To perform a full-string match, you must explicitly use anchors such as ^ (start of string) and $ (end of string) in your regex pattern [1][3]. Implementation: In the cel-go runtime, the matches method on a string type is implemented to invoke Go's standard regexp package (which uses the RE2 engine) to validate the pattern against the input string [4]. Best Practices: - Anchors: Use ^ and $ to ensure the regex matches the entire string rather than just a substring [1][3]. - Optimization: In modern cel-go versions, if a regex pattern is provided as a static string constant, it can be type-checked and pre-compiled at compile time rather than being recompiled at runtime for every evaluation [5]. - Error Handling: If an invalid regex pattern is provided, the function will return an error [4][6]. When constructing CEL expressions dynamically in Go, ensure that external input strings used as regex patterns are properly escaped (e.g., using regexp.QuoteMeta) to avoid syntax errors or injection issues [6].

Citations:


Expand the check to inspect binaryData and case-insensitive key names.

object.data only catches lower-case keys; PASSWORD, API_KEY, clientSecret, and any credential-like keys in binaryData still bypass this rule.

Suggested change
-    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')
-    ) ? 'ConfigMap contains credential-like keys — use Secrets instead' : ''
+    (
+      (has(object.data) && object.data.exists(k,
+        k.matches('(?i).*(password|secret|token|api[_-]?key|private[_-]?key|credential).*')
+      )) ||
+      (has(object.binaryData) && object.binaryData.exists(k,
+        k.matches('(?i).*(password|secret|token|api[_-]?key|private[_-]?key|credential).*')
+      ))
+    ) ? 'ConfigMap contains credential-like keys — use Secrets instead' : ''
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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')
) ? 'ConfigMap contains credential-like keys — use Secrets instead' : ''
(
(has(object.data) && object.data.exists(k,
k.matches('(?i).*(password|secret|token|api[_-]?key|private[_-]?key|credential).*')
)) ||
(has(object.binaryData) && object.binaryData.exists(k,
k.matches('(?i).*(password|secret|token|api[_-]?key|private[_-]?key|credential).*')
))
) ? 'ConfigMap contains credential-like keys — use Secrets instead' : ''
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/builtinchecks/yamls/configmap-with-credentials.yaml` around lines 15 -
20, The ConfigMap credential check only scans object.data with lowercase
substring matches, so it misses mixed-case keys and any sensitive entries under
binaryData. Update the rule in the existing yamls/configmap-with-credentials
check to inspect both object.data and object.binaryData, and compare keys in a
case-insensitive way by normalizing the key names before testing for password,
secret, token, api_key, apikey, private_key, credential, and similar variants.
Ensure the logic still returns the same warning message when either map contains
credential-like keys.

17 changes: 17 additions & 0 deletions pkg/builtinchecks/yamls/externalname-service-redirect.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
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.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
}
64 changes: 64 additions & 0 deletions pkg/templates/imagedigest/template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package imagedigest

import (
"fmt"
"regexp"
"strings"

"golang.stackrox.io/kube-linter/pkg/check"
"golang.stackrox.io/kube-linter/pkg/config"
"golang.stackrox.io/kube-linter/pkg/diagnostic"
"golang.stackrox.io/kube-linter/pkg/objectkinds"
"golang.stackrox.io/kube-linter/pkg/templates"
"golang.stackrox.io/kube-linter/pkg/templates/imagedigest/internal/params"
"golang.stackrox.io/kube-linter/pkg/templates/util"
v1 "k8s.io/api/core/v1"
)

const (
templateKey = "image-not-pinned-by-digest"
)

func init() {
templates.Register(check.Template{
HumanName: "Image Not Pinned By Digest",
Key: templateKey,
Description: "Flag container images that are not pinned to a specific digest",
SupportedObjectKinds: config.ObjectKindsDesc{
ObjectKinds: []string{objectkinds.DeploymentLike},
},
Parameters: params.ParamDescs,
ParseAndValidateParams: params.ParseAndValidate,
Instantiate: params.WrapInstantiateFunc(func(p params.Params) (check.Func, error) {
allowedRegexes := make([]*regexp.Regexp, 0, len(p.AllowList))
for _, res := range p.AllowList {
rg, err := regexp.Compile(res)
if err != nil {
return nil, fmt.Errorf("invalid regex %s: %w", res, err)
}
allowedRegexes = append(allowedRegexes, rg)
}

return util.PerContainerCheck(func(container *v1.Container) (results []diagnostic.Diagnostic) {
if isAllowed(allowedRegexes, container.Image) {
return nil
}
if !strings.Contains(container.Image, "@sha256:") {
results = append(results, diagnostic.Diagnostic{
Message: fmt.Sprintf("container %q image %q is not pinned by digest", container.Name, container.Image),
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
return results
}), nil
}),
})
}

func isAllowed(regexlist []*regexp.Regexp, name string) bool {
for _, regex := range regexlist {
if regex.MatchString(name) {
return true
}
}
return false
}
46 changes: 46 additions & 0 deletions tests/checks/cluster-wide-secrets-access.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: secrets-reader
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: fire-cluster-secrets
subjects:
- kind: ServiceAccount
name: operator-sa
namespace: operators
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: secrets-reader
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: ns-secrets-reader
namespace: default
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: dont-fire-ns-scoped
namespace: default
subjects:
- kind: ServiceAccount
name: app-sa
namespace: default
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: ns-secrets-reader
16 changes: 16 additions & 0 deletions tests/checks/configmap-with-credentials.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
apiVersion: v1
kind: ConfigMap
metadata:
name: dont-fire
data:
config.yaml: |
logLevel: info
---
apiVersion: v1
kind: ConfigMap
metadata:
name: fire-password-key
data:
database_password: "hunter2"
connection_string: "postgres://localhost:5432"
17 changes: 17 additions & 0 deletions tests/checks/externalname-service-redirect.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
apiVersion: v1
kind: Service
metadata:
name: dont-fire
spec:
type: ClusterIP
ports:
- port: 80
---
apiVersion: v1
kind: Service
metadata:
name: fire-externalname
spec:
type: ExternalName
externalName: attacker.example.com
33 changes: 33 additions & 0 deletions tests/checks/image-not-pinned-by-digest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: dont-fire
spec:
template:
spec:
containers:
- name: app
image: registry.example.com/app@sha256:abc123def456
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: fire-tag-only
spec:
template:
spec:
containers:
- name: app
image: registry.example.com/app:v1.2.3
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: fire-no-tag
spec:
template:
spec:
containers:
- name: app
image: registry.example.com/app
Loading