Skip to content

fix(audit): keep a denied RBAC answer on one line - #19

Closed
nvvqi wants to merge 1 commit into
SemiAnalysisAI:masterfrom
nvvqi:fix-auth-can-i-double-no
Closed

nvvqi wants to merge 1 commit into
SemiAnalysisAI:masterfrom
nvvqi:fix-auth-can-i-double-no

Conversation

@nvvqi

@nvvqi nvvqi commented Sep 18, 2026

Copy link
Copy Markdown

Summary

A read-only Kubernetes audit aborts at check 4 of 41 and writes no report. The RBAC probe builds a two-line value that is then spliced into a JSON string literal, so jq rejects the document.

Found on v0.2.1 and still present at 3f426ba (current master).

What happens

[  4/41] 1. KUBERNETES VERSION & CLUSTER IDENTITY / Client Tooling and RBAC
  ⚠ kubectl auth can-i create pods --all-namespaces: no
no
  ⚠ kubectl auth can-i create jobs.batch --all-namespaces: no
no
jq: parse error: Invalid string: control characters from U+0000 through U+001F must be escaped at line 4, column 3
cmax: error: audit failed with exit 5
ClusterMAX audit · k8s failed: 4/41 checks in 1s

The stray no on its own line after each warning is the tell.

Cause

cluster-audit-k8s.sh:493, and the scope-less variant on 495:

result=$(kubectl auth can-i "$verb" "$resource" "$scope" 2>/dev/null || echo "no")

kubectl auth can-i reports its answer twice: it prints yes/no on stdout and encodes the same answer in its exit status. On a denial it prints no and exits 1, so || echo "no" appends a second line rather than supplying a missing one, and result becomes "no\nno".

Line 505 splices that into a JSON string literal:

KUBECTL_AUTH_JSON_ENTRIES+=("{\"name\":\"${name}\",...,\"result\":\"${result}\"}")

A bare newline is illegal inside a JSON string, so jq rejects the whole document rather than the one field, and the audit stops with 37 checks unrun.

Why it stayed hidden

It cannot happen to an admin. create pods answers yes and exits 0, the || never fires, and result stays on one line. It takes an identity that cannot create pods — which is to say a read-only audit, the case a third-party collector most needs to be safe in.

Fix

Take the first line, so result is single-line whether no was printed once or twice:

result=$({ kubectl auth can-i "$verb" "$resource" "$scope" 2>/dev/null || echo "no"; } | head -1)

No check logic changes: allowed still keys off result == "yes".

Tests

tests/audit/test_k8s_auth_check.py runs the real add_kubectl_auth_check against a stub kubectl, following the bashtest pattern already used for this collector. It asserts the appended entry parses as JSON for a denial at both call sites, that a grant is still recorded as allowed, and that the probe passes through the arguments it was given.

Without the fix the two denial cases fail with JSONDecodeError: Invalid control character. With it, python3 -m pytest -q tests/audit is green (1304 passed, 835 subtests).

One more thing, not in this PR

The entry on line 505 is assembled by string concatenation, so any interpolated value containing a newline, a double quote or a backslash breaks the document. Building it with jq -n --arg would escape those automatically and demote this class of failure to a display bug. Happy to send that separately if you want it.


Note

Low Risk
Narrow change to RBAC probe string handling plus regression tests; no change to permission semantics beyond fixing malformed JSON on denials.

Overview
Fixes a read-only Kubernetes audit crash where denied kubectl auth can-i answers produced invalid RBAC JSON and jq aborted the whole report (often around check 4 of 41).

In add_kubectl_auth_check, the probe now keeps only the first line of the can-i output (head -1) so a denial that prints no and exits 1 no longer becomes no\nno when spliced into the manual JSON string. Allowed/denied logic is unchanged (result == "yes").

Adds tests/audit/test_k8s_auth_check.py, which runs the real shell helper against a stub kubectl (deny/allow and argument passthrough) so denials stay JSON-parseable at both scoped and cluster-wide call sites.

Reviewed by Cursor Bugbot for commit f83640b. Bugbot is set up for automated code reviews on this repo. Configure here.

`kubectl auth can-i` reports a denial twice, on stdout and through exit
status 1, so `|| echo "no"` appended a second line instead of supplying a
missing one. The resulting "no\nno" was spliced into a JSON string
literal, where a bare newline is illegal, and jq rejected the whole
document. The audit aborted at check 4 of 41 and wrote no report.

Only an identity that cannot create pods reaches the fallback, so this
never fires for an admin and always fires for a read-only audit.

Take the first line, so the value is single-line whether "no" was printed
once or twice. No check logic changes: `allowed` still keys off
`result == "yes"`.
@nvvqi

nvvqi commented Sep 18, 2026

Copy link
Copy Markdown
Author

Closing

@nvvqi nvvqi closed this Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant