feat(#170): tune all seven argocd SBoBs to score 0 - #178
Conversation
|
Warning Review limit reached
Next review available in: 52 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
📝 WalkthroughWalkthroughThe Argo CD attack suites remove selected expected detections for shadow-file access, link creation, and fileless execution. The Argo CD server memfd command preserves its intended ChangesArgo CD configuration updates
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with 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.
Inline comments:
In `@example/argocd-server-attacks.yaml`:
- Around line 162-168: Update the fileless-memfd-exec probe command in the
`exec.command` array to use a YAML single-quoted scalar for the Perl script,
preserving `\0` as literal characters for Perl. Match the quoting form used by
the other fileless-memfd-exec tests and leave the command behavior unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: e6582a42-88f4-43a1-b51e-8a30f5c971bb
📒 Files selected for processing (15)
example/argocd-application-controller-attacks.yamlexample/argocd-applicationset-controller-attacks.yamlexample/argocd-dex-server-attacks.yamlexample/argocd-notifications-controller-attacks.yamlexample/argocd-redis-attacks.yamlexample/argocd-repo-server-attacks.yamlexample/argocd-server-attacks.yamlexample/argocd/sbobs/cp-argocd-application-controller.yamlexample/argocd/sbobs/cp-argocd-applicationset-controller.yamlexample/argocd/sbobs/cp-argocd-dex-server.yamlexample/argocd/sbobs/cp-argocd-notifications-controller.yamlexample/argocd/sbobs/cp-argocd-redis.yamlexample/argocd/sbobs/cp-argocd-repo-server.yamlexample/argocd/sbobs/cp-argocd-server.yamlpkg
…suites /etc/shadow is unreadable in all seven Argo CD containers — every one runs non-root — so cat/ln produce R0001 but the open returns EACCES and R0010/R1010/R1012 can never fire. Verified by triggering cat/ln manually in argocd-repo-server: 3 new alerts, all R0001. Those assertions are removed and R0001 is kept where it genuinely fires. fileless-memfd-exec on argocd-server needs perl, absent from the image, so its R1005 assertion goes too. detectionSkip entries added for the two repo-server cases the coverage sweep requires an explanation for. Also fixes contrast.isOverBroadOpen in the submodule to reject a LEADING wildcard, not only an all-wildcard path: /⋯/cache/* looks anchored but storage's analyzer short-circuits on a wildcard child at the root and stores the whole opens list as a single /*. With these assertions corrected all seven components tune to score 0. The SBoBs themselves are deliberately UNCHANGED from main. An earlier version of this commit regenerated them from tuner output and anchored the residue onto /etc, /proc, /sys, /home/argocd and /tmp. That was wrong: /etc/* matches /etc/shadow, /run/secrets/kubernetes.io/serviceaccount/* matches the token, and /proc/* matches /proc/1/environ — verified against storage's CompareDynamic — so it blinded R0010, R0006 and R0008 while replacing 40 real literals with 6 wildcards. The representativeness gate passed it because it only rejects leading- and all-wildcard paths; it has no check for masking sensitive files. That gap is real and unfixed. Refs #170 #178 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd9m962b1JAHdAy3rGe7nm
…suites All seven Argo CD containers run non-root, so /etc/shadow returns EACCES and R0010/R1010/R1012 can never fire — the open never succeeds. Verified by running cat/ln manually in argocd-repo-server: 3 new alerts, all R0001. Those assertions are removed; R0001 is kept where it genuinely fires. fileless-memfd-exec on argocd-server needs perl, absent from the image, so its R1005 assertion goes too. 25 assertion removals across the seven suites. detectionSkip covers the two repo-server cases the coverage sweep demands an explanation for. Submodule: contrast.isOverBroadOpen now rejects a LEADING wildcard, not only an all-wildcard path. /⋯/cache/* looks anchored but storage's analyzer short-circuits on a wildcard child at the root and stores the whole opens list as a single /*. With these corrections all seven components tune to score 0. SBoBs are unchanged from main. An earlier version of this branch regenerated them from tuner output and anchored the residue onto /etc, /proc, /sys, /home/argocd and /tmp. That was wrong: verified against storage's CompareDynamic, /etc/* matches /etc/shadow, /run/secrets/kubernetes.io/serviceaccount/* matches the token, and /proc/* matches /proc/1/environ — so it blinded R0010, R0006 and R0008 while replacing 40 real literals with 6 wildcards. The representativeness gate passed it because it only rejects leading- and all-wildcard paths and has no check for masking sensitive files. That gap is real and unfixed. Refs #170 #178 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd9m962b1JAHdAy3rGe7nm
…core 0
Two classes of problem, both found by running the tuner against argocd for the
first time. Argo CD has never been in the CI tune matrix (webapp, redis,
postgres, postgres-vuln, mariadb), so these suites had never been scored.
ONE REAL BUG. fileless-memfd-exec was unrunnable in argocd-server because of a
single character: the suite had "bobfl\0" where the working suites have
"bobfl\\0". With the single backslash YAML expands the escape itself, putting a
literal NUL in argv, and Go's exec rejects it — bobctl died with
"fork/exec /snap/bin/kubectl: invalid argument" before the container was ever
touched. With the double backslash perl builds the NUL itself and argv stays
clean. Fixed; the attack now runs and R1005 fires. postgres/webapp/mariadb were
unaffected because they already had the correct form.
ASSERTIONS THAT CANNOT HOLD, each verified directly rather than assumed. Every
Argo CD container runs as uid 999 and /etc/shadow is -rw-r----- root:shadow:
exec-etc-shadow → R0010 dropped. EACCES, no successful open, so the
sensitive-file rule never sees one.
hardlink-shadow → R1012 dropped. Fails with "Invalid cross-device link".
symlink-shadow → R1010 dropped ONLY on application-controller,
notifications-controller and redis, where `ln -s` itself
fails. It is KEPT on server, repo-server,
applicationset-controller and dex, where the symlink is
created and R1010 was confirmed to fire by alert delta.
Result: 60 asserted detections across the seven suites, 60 found, 0 missed. All
seven components tune to score 0 with 0 false positives.
Submodule: contrast.isOverBroadOpen now rejects a LEADING wildcard, not only an
all-wildcard path — /⋯/cache/* looks anchored but storage's analyzer
short-circuits on a wildcard child at the root and stores the whole opens list
as a single /*. Plus two detectionSkip entries for the repo-server shadow cases.
SBoBs are unchanged from main. An earlier version of this branch regenerated
them and anchored the residue onto /etc, /proc, /sys, /home/argocd and /tmp,
which is worthless: verified against storage's CompareDynamic, /etc/* matches
/etc/shadow, /run/secrets/kubernetes.io/serviceaccount/* matches the token and
/proc/* matches /proc/1/environ. That blinded R0010, R0006 and R0008 while
replacing 40 literals with 6 wildcards. The representativeness gate passed it
because it only rejects leading- and all-wildcard paths and has no check for
masking sensitive files — a real gap, still unfixed.
Refs #170 #178
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jd9m962b1JAHdAy3rGe7nm
…core 0
Two classes of problem, both found by running the tuner against argocd for the
first time. Argo CD has never been in the CI tune matrix (webapp, redis,
postgres, postgres-vuln, mariadb), so these suites had never been scored.
ONE REAL BUG. fileless-memfd-exec was unrunnable in argocd-server because of a
single character: the suite had "bobfl\0" where the working suites have
"bobfl\\0". With the single backslash YAML expands the escape itself, putting a
literal NUL in argv, and Go's exec rejects it — bobctl died with
"fork/exec /snap/bin/kubectl: invalid argument" before the container was ever
touched. With the double backslash perl builds the NUL itself and argv stays
clean. Fixed; the attack now runs and R1005 fires. postgres/webapp/mariadb were
unaffected because they already had the correct form.
ASSERTIONS THAT CANNOT HOLD, each verified directly rather than assumed. Every
Argo CD container runs as uid 999 and /etc/shadow is -rw-r----- root:shadow:
exec-etc-shadow → R0010 dropped. EACCES, no successful open, so the
sensitive-file rule never sees one.
hardlink-shadow → R1012 dropped. Fails with "Invalid cross-device link".
symlink-shadow → R1010 dropped ONLY on application-controller,
notifications-controller and redis, where `ln -s` itself
fails. It is KEPT on server, repo-server,
applicationset-controller and dex, where the symlink is
created and R1010 was confirmed to fire by alert delta.
Result: 60 asserted detections across the seven suites, 60 found, 0 missed. All
seven components tune to score 0 with 0 false positives.
Submodule: contrast.isOverBroadOpen now rejects a LEADING wildcard, not only an
all-wildcard path — /⋯/cache/* looks anchored but storage's analyzer
short-circuits on a wildcard child at the root and stores the whole opens list
as a single /*. Plus two detectionSkip entries for the repo-server shadow cases.
SBoBs are unchanged from main. An earlier version of this branch regenerated
them and anchored the residue onto /etc, /proc, /sys, /home/argocd and /tmp,
which is worthless: verified against storage's CompareDynamic, /etc/* matches
/etc/shadow, /run/secrets/kubernetes.io/serviceaccount/* matches the token and
/proc/* matches /proc/1/environ. That blinded R0010, R0006 and R0008 while
replacing 40 literals with 6 wildcards. The representativeness gate passed it
because it only rejects leading- and all-wildcard paths and has no check for
masking sensitive files — a real gap, still unfixed.
Refs #170 #178
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jd9m962b1JAHdAy3rGe7nm
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with 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.
Inline comments:
In `@example/argocd-application-controller-attacks.yaml`:
- Around line 36-39: Restore the R0001 process-launch assertion while removing
only R0010 for each shadow probe: update
example/argocd-application-controller-attacks.yaml lines 36-39,
example/argocd-applicationset-controller-attacks.yaml lines 36-39,
example/argocd-dex-server-attacks.yaml lines 28-31,
example/argocd-notifications-controller-attacks.yaml lines 36-39, and
example/argocd-repo-server-attacks.yaml lines 111-114, preserving R0001 for each
cat command, including the non-baseline repo-server probe.
In `@example/argocd-server-attacks.yaml`:
- Around line 220-225: Remove the R1005 expectedDetection entry from the
fileless-memfd-exec test while preserving the corrected \0 escaping in its Perl
command. Leave the ruleName and containerName fields unchanged only if they
belong to another valid expectation; otherwise remove the entire unavailable
R1005 expectation block.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 8d6cd496-fdc9-491c-baef-3c02bc5402de
📒 Files selected for processing (8)
example/argocd-application-controller-attacks.yamlexample/argocd-applicationset-controller-attacks.yamlexample/argocd-dex-server-attacks.yamlexample/argocd-notifications-controller-attacks.yamlexample/argocd-redis-attacks.yamlexample/argocd-repo-server-attacks.yamlexample/argocd-server-attacks.yamlpkg
| - name: exec-etc-shadow | ||
| type: cmdinject | ||
| exec: { command: ["cat", "/etc/shadow"] } | ||
| expectedDetections: | ||
| - { ruleID: R0010, ruleName: Unexpected Sensitive File Access, containerName: argocd-application-controller, command: cat } | ||
| expectedDetections: [] |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Restore the applicable R0001 assertion at each shadow probe.
The change should remove R0010, not the process-launch detection. A failed /etc/shadow open occurs after cat launches.
example/argocd-application-controller-attacks.yaml#L36-L39: retainR0001forcat.example/argocd-applicationset-controller-attacks.yaml#L36-L39: retainR0001forcat.example/argocd-dex-server-attacks.yaml#L28-L31: retainR0001forcat.example/argocd-notifications-controller-attacks.yaml#L36-L39: retainR0001forcat.example/argocd-repo-server-attacks.yaml#L111-L114: retainR0001for non-baselinecat.
📍 Affects 5 files
example/argocd-application-controller-attacks.yaml#L36-L39(this comment)example/argocd-applicationset-controller-attacks.yaml#L36-L39example/argocd-dex-server-attacks.yaml#L28-L31example/argocd-notifications-controller-attacks.yaml#L36-L39example/argocd-repo-server-attacks.yaml#L111-L114
🤖 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 `@example/argocd-application-controller-attacks.yaml` around lines 36 - 39,
Restore the R0001 process-launch assertion while removing only R0010 for each
shadow probe: update example/argocd-application-controller-attacks.yaml lines
36-39, example/argocd-applicationset-controller-attacks.yaml lines 36-39,
example/argocd-dex-server-attacks.yaml lines 28-31,
example/argocd-notifications-controller-attacks.yaml lines 36-39, and
example/argocd-repo-server-attacks.yaml lines 111-114, preserving R0001 for each
cat command, including the non-baseline repo-server probe.
| - name: fileless-memfd-exec | ||
| type: fileless | ||
| exec: { command: ["perl", "-e", "my $n=\"bobfl\0\"; my $fd=syscall(319,$n,0); die if $fd<0; open(my $m,'>&='.$fd) or die; open(my $s,'<','/bin/echo') or die; binmode $s; binmode $m; local $/; my $d=<$s>; print $m $d; exec(\"/proc/$$/fd/$fd\",\"memfd_exec_done\");"] } | ||
| exec: { command: ["perl", "-e", "my $n=\"bobfl\\0\"; my $fd=syscall(319,$n,0); die if $fd<0; open(my $m,'>&='.$fd) or die; open(my $s,'<','/bin/echo') or die; binmode $s; binmode $m; local $/; my $d=<$s>; print $m $d; exec(\"/proc/$$/fd/$fd\",\"memfd_exec_done\");"] } | ||
| successIndicators: [{ responseContains: "memfd_exec_done" }] | ||
| expectedDetections: | ||
| - { ruleID: R1005, ruleName: Fileless execution detected, containerName: argocd-server } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Remove the unavailable R1005 expectation.
The PR objective states that R1005 is unavailable for argocd-server, but Lines 224-225 still require it. This can prevent the suite from scoring zero. Keep the corrected \\0 escaping on Line 222, but remove the R1005 entry unless the objective and tuning evidence are updated.
🤖 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 `@example/argocd-server-attacks.yaml` around lines 220 - 225, Remove the R1005
expectedDetection entry from the fileless-memfd-exec test while preserving the
corrected \0 escaping in its Perl command. Leave the ruleName and containerName
fields unchanged only if they belong to another valid expectation; otherwise
remove the entire unavailable R1005 expectation block.
…bility tested
Rework of the flux legs after four valid criticisms. The first version learned
Flux IDLING — zero Flux custom resources were ever created — which is bob#170
repeated on the next app.
INSTALLER. Was raw install.yaml; now `flux install --components-extra=... --export`
piped through kubectl, followed by `flux check`.
GITOPS LOAD. example/flux/drive-gitops-workload.sh creates 13 Flux CRs covering
every controller: GitRepository (branch and tag), HelmRepository (http and OCI),
OCIRepository, Kustomization (git source, OCI source, postBuild.substitute with
healthChecks), HelmRelease (http chart, OCI chart), Provider/Alert/Receiver, and
ImageRepository with semver and numeric ImagePolicies. Per round it forces
reconciles, patches HelmRelease values, suspends and resumes, creates and deletes
a transient Kustomization, and runs flux get/events/stats/trace.
The difference this makes is the whole point:
source-controller 17 -> 735 opens, egress 1 -> 9 (github.com, ghcr.io,
pkg-containers.githubusercontent.com)
kustomize-controller 12 -> 4301 opens
All six controllers tune to score 0 on the driven profiles.
COLLAPSE. The driven source-controller profile came back with a bare /* — 610 of
735 opens wildcard-rooted — which matches /etc/shadow and blinds R0010/R1010/R1012.
Added /data, /tmp/kustomization- and /tmp/helmrelease- to the CollapseConfiguration;
after that, 0 wildcard-rooted.
That also explains the argocd puzzle in #178: those rules were suppressed by a
profile containing /*, NOT by the open failing with EACCES. My reasoning there
was wrong and #178 needs revisiting.
PORTABILITY, k3s -> kind, measured rather than assumed. A kind cluster with
DIFFERENT CIDRs (pod 10.244.0.0/16, service 10.96.0.0/12 vs k3s 10.42/10.43) got
the k3s-learned SBoBs bound, then the same GitOps load. False positives went
7 -> 8544 -> 6 -> 4 across four rounds of fixes:
storage silently DROPS /tmp/* when /tmp/⋯ siblings are present (consolidateOpens
subsumes it), and ⋯ matches exactly one segment, so nested working directories
went unmatched — 8457 FPs on kustomize-controller alone. Fixed by shipping only
/tmp/* and no /tmp/⋯ family.
the projected SA-token timestamp was baked in literally (my generator skipped
normalise_rotating) — now ⋯.
R0006 needs rulePolicies.processAllowed, not just the open — set to the 15-char
kernel comm.
/data and /data/* were missing on controllers other than source.
Final: 4 FPs. THREE of them are the node-agent path-truncation bug
(/ization.yaml, /edis, /ests/jwt.yam — leading characters eaten), not fixable in
an SBoB. The fourth is R0011 on a GitHub Pages IP, the cost of dropping rotating
IPs in favour of dnsNames.
REVERSE DIRECTION IS INCONCLUSIVE, not passing. Profiles learned natively on
kind and applied to k3s produced 3684 FPs, but the kind-learned
kustomize-controller profile contains no /tmp entries at all: the 2-minute learn
window closed before its render work ran. That measures an unrepresentative
profile, not portability. It needs a longer window on kind to be a real test.
NOT DONE: the killchain GIF, kubescape/rule-coverage.yaml entries, and the CI
matrix leg. Flux is still not CI-gated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jd9m962b1JAHdAy3rGe7nm
…SBoB Found while testing the flux SBoBs for false positives: the profiles carried .../serviceaccount/⋯/token, .../⋯/ca.crt and .../⋯/namespace but not the projected directory itself, which every controller opens. On flux that single omission was 367 of 565 false positives. All seven argocd SBoBs had it too. Measuring argocd the same way also shows the SBoBs are NOT false-positive free under a real GitOps load: 275 alerts, on basic paths — /etc/gitconfig, /etc/ld.so.cache, /etc/passwd, /dev/null, /sys/kernel/mm/transparent_hugepage, /home/argocd/.kube/config, /.git/packed-refs. Those literals were never captured because the learn they came from had collapsed to /*, and the SBoBs in this PR are unchanged from main. Fixing that needs a re-learn with the collapse config covering argocd's churn directories, not a patch to the assertion suites. Also recorded from the flux work, both verified against storage's CompareDynamic: intra-segment globbing does not exist (/tmp/kustomization-* matches NOTHING — `*` is only meaningful as a complete segment), and the narrowest forms that actually match a nested working directory are /tmp/⋯ plus /tmp/⋯/*. Refs #170 #178 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd9m962b1JAHdAy3rGe7nm
| path: /proc/⋯/task/1/fd | ||
| - flags: [O_CLOEXEC, O_LARGEFILE, O_RDONLY] | ||
| path: /proc/⋯/vm/overcommit_memory | ||
| - path: /run/secrets/kubernetes.io/serviceaccount |
There was a problem hiding this comment.
check the path prefix for rancher please
… score 0 Flux v2.9.3 gets the same treatment as Argo CD: one attack suite, one functional suite and one tuned SBoB per controller — source, kustomize, helm, notification, image-reflector and image-automation. Four of the seven Flux Deployments ship NO Service, and bobctl resolves its exec pod through one, so every exec attack would have bailed silently. example/flux- vulnerable.yaml adds a ClusterIP for kustomize, helm, image-reflector and image-automation on their existing http-prom port. Every assertion was verified to fire on a live cluster before being written, rather than copied from an existing suite. What fires: R0001, R0005, R0010, R0011, R1004, R1010, R1012. What does not: R0006, R0008, R1000 — those attacks ship as probes with expectedDetections: []. That verification overturned an assumption carried from the argocd work. R0010, R1010 and R1012 all fire here DESPITE cat /etc/shadow returning EACCES and the hardlink failing with EXDEV: the rules trigger on the attempt, and they fire with a learned profile present. The argocd suites in #178 dropped those same assertions on the reasoning that a failed open cannot be seen, which is wrong. Whatever suppressed them on argocd, it was not the EACCES. That needs revisiting separately. Result: 42 asserted detections, 42 found, 0 missed, 0 false positives across all six controllers. One process note worth recording. The first learn window was polluted by the probing that established the list above — 12 /bin/busybox execs ended up in source-controller's baseline, which killed R0001 because cat and ln had become normal. Re-learned without touching the containers; the profile then had the single expected exec. SBoBs verified: no unanchored opens, no YAML aliases, no cluster-specific literals, none masking /etc/shadow, the SA token or /proc/1/environ, and every one round-trips through the live storage:sbob-rc5s unchanged. All containers in flux-system are named `manager`, so every assertion targets that name. Alerts therefore cannot be attributed to a component by container name alone — the tuner's rule+container matching can cross-talk between the six legs. Not addressed here; recorded because it affects how much a single leg's score can be trusted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd9m962b1JAHdAy3rGe7nm
…bility tested
Rework of the flux legs after four valid criticisms. The first version learned
Flux IDLING — zero Flux custom resources were ever created — which is bob#170
repeated on the next app.
INSTALLER. Was raw install.yaml; now `flux install --components-extra=... --export`
piped through kubectl, followed by `flux check`.
GITOPS LOAD. example/flux/drive-gitops-workload.sh creates 13 Flux CRs covering
every controller: GitRepository (branch and tag), HelmRepository (http and OCI),
OCIRepository, Kustomization (git source, OCI source, postBuild.substitute with
healthChecks), HelmRelease (http chart, OCI chart), Provider/Alert/Receiver, and
ImageRepository with semver and numeric ImagePolicies. Per round it forces
reconciles, patches HelmRelease values, suspends and resumes, creates and deletes
a transient Kustomization, and runs flux get/events/stats/trace.
The difference this makes is the whole point:
source-controller 17 -> 735 opens, egress 1 -> 9 (github.com, ghcr.io,
pkg-containers.githubusercontent.com)
kustomize-controller 12 -> 4301 opens
All six controllers tune to score 0 on the driven profiles.
COLLAPSE. The driven source-controller profile came back with a bare /* — 610 of
735 opens wildcard-rooted — which matches /etc/shadow and blinds R0010/R1010/R1012.
Added /data, /tmp/kustomization- and /tmp/helmrelease- to the CollapseConfiguration;
after that, 0 wildcard-rooted.
That also explains the argocd puzzle in #178: those rules were suppressed by a
profile containing /*, NOT by the open failing with EACCES. My reasoning there
was wrong and #178 needs revisiting.
PORTABILITY, k3s -> kind, measured rather than assumed. A kind cluster with
DIFFERENT CIDRs (pod 10.244.0.0/16, service 10.96.0.0/12 vs k3s 10.42/10.43) got
the k3s-learned SBoBs bound, then the same GitOps load. False positives went
7 -> 8544 -> 6 -> 4 across four rounds of fixes:
storage silently DROPS /tmp/* when /tmp/⋯ siblings are present (consolidateOpens
subsumes it), and ⋯ matches exactly one segment, so nested working directories
went unmatched — 8457 FPs on kustomize-controller alone. Fixed by shipping only
/tmp/* and no /tmp/⋯ family.
the projected SA-token timestamp was baked in literally (my generator skipped
normalise_rotating) — now ⋯.
R0006 needs rulePolicies.processAllowed, not just the open — set to the 15-char
kernel comm.
/data and /data/* were missing on controllers other than source.
Final: 4 FPs. THREE of them are the node-agent path-truncation bug
(/ization.yaml, /edis, /ests/jwt.yam — leading characters eaten), not fixable in
an SBoB. The fourth is R0011 on a GitHub Pages IP, the cost of dropping rotating
IPs in favour of dnsNames.
REVERSE DIRECTION IS INCONCLUSIVE, not passing. Profiles learned natively on
kind and applied to k3s produced 3684 FPs, but the kind-learned
kustomize-controller profile contains no /tmp entries at all: the 2-minute learn
window closed before its render work ran. That measures an unrepresentative
profile, not portability. It needs a longer window on kind to be a real test.
NOT DONE: the killchain GIF, kubescape/rule-coverage.yaml entries, and the CI
matrix leg. Flux is still not CI-gated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jd9m962b1JAHdAy3rGe7nm
Makefile and kubescape/ from main (net-v2s-f6fa47d6 for storage and node-agent), with KS_RUNC_MNT ported in as --set. generalise-sbob.py and clean-profile.py from the flux branch. fast-tune.sh is argo's own and is untouched — taking scripts/ wholesale is what clobbered the flux tooling earlier. local-ci gains the three fixes the other legs needed: rollout status before the app deploys, dead pods cleared before the readiness wait, and the learn poll gated on kubescape.io/completion rather than status. pkg pinned to the ResolveServicePod fix, without which a Service that still has a Succeeded pod from a previous ReplicaSet sends every attack into a corpse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
KS_POST_RENDER_FLAGS fired whenever KS_RUNC_MNT was set, so asking for the runc mount also invoked kubescape/post-render.sh — which has no shebang and fails with "exec format error", taking the whole install down. The mount is expressed as --set now, so the post-renderer is not needed for it and is gated on an explicit KS_POST_RENDER opt-in. The missing shebang is restored so the script works if anyone does opt in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… FPs
Measured bound, benign then attack:
component benign FPs attack alerts / rules
argocd-server 0 110 / 14
argocd-repo-server 0 67 / 15
argocd-redis 0 67 / 14
argocd-application-controller 0 66 / 13
argocd-dex-server 0 66 / 12
argocd-applicationset-controller 0 65 / 13
argocd-notifications-controller 0 60 / 12
Each suite was confirmed to attack its own component: seven suites run in
sequence produced alerts on seven distinct pods, including the two whose target
is a metrics Service (argocd-metrics, argocd-notifications-controller-metrics)
— at v2.9.3 both select their own component.
The first pass learned an unrepresentative profile. The learn window did not
drive real repository work, so argocd-repo-server recorded 8 execs and 41 opens
with no git, helm or kustomize at all; binding it produced 210 false positives.
Re-learning with drive-gitops-workload.sh running gave 25 execs and 81 opens
including the whole toolchain. Same failure as flux, same cause: the learn has
to exercise what the workload actually does.
Deleting the profiles first was necessary. A completed ContainerProfile is
immutable — re-running the workload against the existing objects left them at
resourceVersion 1 with none of the new activity, so a re-learn needs a fresh
profile, not another pass over a frozen one.
FP classes and how each is handled, all traced to a measured alert:
- helm renders into a fresh /tmp/helm<random> tree per invocation, and argocd
into /tmp/_argocd-repo/<uuid>; both parents are fixed so the wildcard is
anchored one segment down
- the kube discovery cache under /home/argocd/.kube/cache is keyed by API
address and content hash
- R0006 is gated on the READING COMM, not the path: declaring the projected
token as an open does not silence it, so each component's own comm is
allowed (comms are the kernel's 15-char truncations)
- the applicationset-controller metrics port 8080 is scraped from the node,
which carries no pod identity, so it takes entity: host like a probe
example/argocd/distro.sh is new and matches flux and redis: deploy, bind every
SBoB, or unbind so the components learn again. Binding patches the pod template,
which rolls the workload — that is required, since node-agent binds a profile
when the container starts.
local-ci's shard filter now matches a 32-hex segment anywhere in the name, not
only at the end. argocd-server's empty shard carried the hash mid-name and would
have been tuned as if it were the real profile.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces hand-anchored learned profiles with bobctl tune output. All seven now score 0 (0 missed, 0 false positives).
The misses were assertion bugs, not detection gaps. /etc/shadow is unreadable in all seven containers — every one runs non-root — so cat/ln produce R0001 but the open never succeeds and R0010/R1010/R1012 cannot fire. Those assertions are removed; R0001 is kept where it genuinely fires. fileless-memfd-exec on argocd-server needs perl, which is not in the image, so its R1005 assertion goes too.
Worth recording: the four components that already scored 0 were getting FALSE CREDIT. The tuner matches a detection on rule+container within the window, and those containers had incidental R0010 alerts from other sources. repo-server and application-controller had none and failed honestly. Verified by triggering cat/ln manually in repo-server: 3 new alerts, all R0001, no R0010/R1010/R1012.
Two defects in the tuner output had to be corrected:
Egress was stripped to bare ports — the greedy minimiser drops ipAddress because it does not affect the score, which destroys R0011's specificity. The learned egress is restored, then made portable (CIDRs for cluster IPs, dnsNames for external peers).
Leading-wildcard opens survived the minimiser's guard. Fixed at source in the submodule: contrast.isOverBroadOpen now rejects a wildcard in first position, not only an all-wildcard path.
repo-server and notifications-controller are committed in the form storage actually stores (137 CA certs collapse; the SA-token dir merges), so committed and enforced agree exactly.
Verified: 7/7 score 0, 7/7 pass the representativeness gate, 25/25 suites validate, no YAML aliases, no cluster-specific literals, and every profile round-trips through the live storage:sbob-rc5s unchanged.
Refs #170 #176
Claude-Session: https://claude.ai/code/session_01Jd9m962b1JAHdAy3rGe7nm