Skip to content

feat(#170): tune all seven argocd SBoBs to score 0 - #178

Open
entlein wants to merge 5 commits into
mainfrom
fix/argocd-170
Open

feat(#170): tune all seven argocd SBoBs to score 0#178
entlein wants to merge 5 commits into
mainfrom
fix/argocd-170

Conversation

@entlein

@entlein entlein commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

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

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@entlein, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 52 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1ecfadd8-42e9-4c2c-ade1-f17b6ef2f942

📥 Commits

Reviewing files that changed from the base of the PR and between 0184a4c and 10c03af.

📒 Files selected for processing (6)
  • example/argocd/sbobs/cp-argocd-application-controller.yaml
  • example/argocd/sbobs/cp-argocd-applicationset-controller.yaml
  • example/argocd/sbobs/cp-argocd-dex-server.yaml
  • example/argocd/sbobs/cp-argocd-notifications-controller.yaml
  • example/argocd/sbobs/cp-argocd-redis.yaml
  • example/argocd/sbobs/cp-argocd-server.yaml
📝 Walkthrough

Walkthrough

The 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 \0 sequence. The pkg submodule pointer advances.

Changes

Argo CD configuration updates

Layer / File(s) Summary
Attack-suite detection expectations
example/argocd-*-attacks.yaml
Selected attack cases now use empty expectedDetections lists. The Argo CD server memfd command preserves the intended \0 sequence, while its R1005 assertion remains.
pkg submodule update
pkg
The submodule pointer changes to commit 085f6c3f2fdf032b88563337fef901207e45defa.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies tuning all seven Argo CD SBoBs to achieve a score of zero.
Description check ✅ Passed The description directly explains the profile tuning, removed assertions, corrections, and validation results.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/argocd-170

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between b7d17c0 and c139c9a.

📒 Files selected for processing (15)
  • example/argocd-application-controller-attacks.yaml
  • example/argocd-applicationset-controller-attacks.yaml
  • example/argocd-dex-server-attacks.yaml
  • example/argocd-notifications-controller-attacks.yaml
  • example/argocd-redis-attacks.yaml
  • example/argocd-repo-server-attacks.yaml
  • example/argocd-server-attacks.yaml
  • example/argocd/sbobs/cp-argocd-application-controller.yaml
  • example/argocd/sbobs/cp-argocd-applicationset-controller.yaml
  • example/argocd/sbobs/cp-argocd-dex-server.yaml
  • example/argocd/sbobs/cp-argocd-notifications-controller.yaml
  • example/argocd/sbobs/cp-argocd-redis.yaml
  • example/argocd/sbobs/cp-argocd-repo-server.yaml
  • example/argocd/sbobs/cp-argocd-server.yaml
  • pkg

Comment thread example/argocd-server-attacks.yaml Outdated
entlein added a commit that referenced this pull request Aug 2, 2026
…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
entlein added a commit that referenced this pull request Aug 2, 2026
…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
entlein added a commit that referenced this pull request Aug 2, 2026
…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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between c139c9a and 0184a4c.

📒 Files selected for processing (8)
  • example/argocd-application-controller-attacks.yaml
  • example/argocd-applicationset-controller-attacks.yaml
  • example/argocd-dex-server-attacks.yaml
  • example/argocd-notifications-controller-attacks.yaml
  • example/argocd-redis-attacks.yaml
  • example/argocd-repo-server-attacks.yaml
  • example/argocd-server-attacks.yaml
  • pkg

Comment on lines 36 to +39
- 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: []

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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: retain R0001 for cat.
  • example/argocd-applicationset-controller-attacks.yaml#L36-L39: retain R0001 for cat.
  • example/argocd-dex-server-attacks.yaml#L28-L31: retain R0001 for cat.
  • example/argocd-notifications-controller-attacks.yaml#L36-L39: retain R0001 for cat.
  • example/argocd-repo-server-attacks.yaml#L111-L114: retain R0001 for non-baseline cat.
📍 Affects 5 files
  • example/argocd-application-controller-attacks.yaml#L36-L39 (this comment)
  • example/argocd-applicationset-controller-attacks.yaml#L36-L39
  • example/argocd-dex-server-attacks.yaml#L28-L31
  • example/argocd-notifications-controller-attacks.yaml#L36-L39
  • example/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.

Comment on lines 220 to 225
- 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 }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.

entlein added a commit that referenced this pull request Aug 3, 2026
…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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

check the path prefix for rancher please

entlein added a commit that referenced this pull request Aug 7, 2026
… 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
entlein added a commit that referenced this pull request Aug 7, 2026
…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
entlein and others added 3 commits August 25, 2026 21:51
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>
Comment thread scripts/generalise-sbob.py Dismissed
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.

2 participants