Skip to content

pin kagent 0.1.22: the per-user tool path leaked the user's JWT on a failed call - #829

Merged
braghettos merged 1 commit into
mainfrom
kagent-0122-token-fix
Nov 11, 2026
Merged

braghettos merged 1 commit into
mainfrom
kagent-0122-token-fix

Conversation

@braghettos

Copy link
Copy Markdown
Contributor

Security priority. On a failed kubectl call the per-user tool path returned the user's JWT in the tool result ([kagent-dev/tools#107]). All 98 stored tokens were already expired; its owner redacts the session store separately.

What changes

  • kagent 0.1.20 → 0.1.22. The per-user tool server now comes from a private krateo-agentiko package pinned by digest only — ghcr.io/krateo-agentiko/kagent-tools@sha256:6308035d…230c — under the new key perUserRbac.toolImage. There is no tag field at all, so no stale tag can win over the digest, which is the trap that defeated the ClickHouse digest pin.
  • inst.privateImageComponents gains {name: kagent, path: [perUserRbac, toolImagePullSecrets]}, so the existing registryAuth-derived dockerconfigjson pulls the private image with no manual ServiceAccount patch.

repository and digest are deliberately not set in componentValues: the chart default is the pin, and a second source would recreate the stale-value hazard this design removes.

Why 0.1.21 is skipped — the point of this commit

0.1.21 carried the pull secret inside toolImage, whose schema requires [repository, digest]. crdgen preserves chart required into the composition CRD — verified with krateo-audit-worm as a control, whose 5 chart-level blocks appear verbatim as .spec.* required in both served versions — and the image-pull injection creates each parent object while setting only the leaf:

{{- range $seg := (initial $ip.path) }} ... set $cur $seg (index $cur $seg | default dict) ...
{{- if not (hasKey $cur $leaf) }}{{- $_ := set $cur $leaf (list (dict "name" $ipsName)) }}

So the installer would have written toolImage: {pullSecrets: [...]} with neither required sibling, and the apiserver would have rejected the Kagent CR outright on any cluster with registryAuth enabled — which includes 057. Its owner moved the key to the sibling toolImagePullSecrets in 0.1.22 so the injection never touches toolImage.

Checked before pinning

check result
injected shape vs 0.1.22's schema {enabled: true, toolImagePullSecrets: [{name: krateo-registry-image-pull}]} — no unknown keys, satisfies item required: [name], does not construct toolImage
root required / perUserRbac required none / none — cannot wedge the umbrella
credential coverage 057's krateo-registry-image-pull already covers host ghcr.io, the same host as the new package; inst.imagePullAuths dedupes one auths entry per host
componentValues widening not needed — the injection writes onto the composition spec after the merge, validated by the component's own crdgen CRD
0.1.22 present at the registry yes (0.1.21 present as control, 9.9.9 absent as negative)
other pins 85 asserted byte-identical
privateImageComponents parses to exactly 8 entries, every pre-existing path unchanged

One mistake worth recording: the first attempt at the helper edit matched a prefix of the agentgateway-policies uiTokenHop entry and spliced the new block in front of its - image/- pullSecrets lines, orphaning them onto kagent and corrupting both entries. Caught by parsing the rendered YAML rather than trusting the diff, then re-anchored on the following entry.

🤖 Generated with Claude Code

https://claude.ai/code/session_016RAxvmbdtWAFW9j2vWsv7a

…failed call

Security priority. On a failed kubectl call the per-user tool path returned the
user's JWT in the tool result (kagent-dev/tools#107). All 98 stored tokens were
already expired; its owner redacts the session store separately.

The per-user tool server now comes from a PRIVATE krateo-agentiko package pinned
BY DIGEST ONLY, ghcr.io/krateo-agentiko/kagent-tools@sha256:6308035d...230c,
under the new key perUserRbac.toolImage. There is no tag field at all, so no
stale tag can win over the digest -- the trap that defeated the ClickHouse
digest pin. repository and digest are deliberately NOT set in componentValues:
the chart default IS the pin, and a second source would re-create that hazard.

inst.privateImageComponents gains {name: kagent, path: [perUserRbac,
toolImagePullSecrets]} so the existing registryAuth-derived dockerconfigjson
pulls the private image with no manual ServiceAccount patch. 057 already has
it: krateo-registry-image-pull, covering host ghcr.io, which is the same host as
the new package, and inst.imagePullAuths dedupes one auths entry per host.

0.1.21 IS DELIBERATELY SKIPPED, and the reason is the point of this commit.
0.1.21 carried the pull secret INSIDE toolImage, whose schema requires
[repository, digest]. crdgen preserves chart `required` into the composition CRD
-- verified with krateo-audit-worm as a control, whose 5 chart-level blocks
appear verbatim as .spec.* required in both its served versions -- and the
image-pull injection CREATES each parent object while setting only the leaf. So
the installer would have written toolImage: {pullSecrets: [...]} with neither
required sibling, and the apiserver would have REJECTED the Kagent CR outright
on any cluster with registryAuth enabled, which includes 057. Its owner moved
the key to the sibling toolImagePullSecrets in 0.1.22 so the injection never
touches toolImage.

The injected shape was checked against 0.1.22's own schema before pinning:
{enabled: true, toolImagePullSecrets: [{name: krateo-registry-image-pull}]} has
no unknown keys, satisfies the item's required [name], and does not construct
toolImage. perUserRbac itself carries no `required`, and the chart has no root
`required`, so nothing here can wedge the umbrella.

No componentValues widening is needed: the injection writes onto the composition
spec AFTER the componentValues merge, so it is validated by the component's own
crdgen CRD and never passes through the installer's componentValues schema.

Asserted: 85 other pins byte-identical; the privateImageComponents list parses
to exactly 8 entries with every pre-existing path unchanged. The first attempt
at that edit matched a PREFIX of the agentgateway-policies uiTokenHop entry and
spliced the new block in front of its `- image`/`- pullSecrets` lines, orphaning
them onto kagent; caught by parsing the rendered YAML rather than trusting the
diff, and re-anchored on the following entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RAxvmbdtWAFW9j2vWsv7a
@braghettos
braghettos merged commit 58b233f into main Nov 11, 2026
36 checks passed
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