diff --git a/CHANGELOG.md b/CHANGELOG.md index c95a36d..50fe7e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed +- **M2M agent impersonation RBAC.** The `agents..m2m` block (and the `impersonation.yaml` / `_agent-impersonation.tpl` templates it rendered) is removed from `agentic-platform-connectivity`. Agents no longer get a deputy `Role`/`ClusterRole` letting mcp-kubernetes impersonate the agent's own identity or bind its groups to ClusterRoles. The only supported flow is on-behalf-of: muster mints a token with the human as `sub` and the agent SA in the RFC 8693 `act` claim, and the mcp-kubernetes chart's `*-obo-impersonate` ClusterRole authorizes impersonating the human. Requires mcp-kubernetes with M2M removed and muster with the matching broker config. - **Retired the `agentic-platform-crds` bundle chart** — every component now owns its CRDs (app-owned CRDs). The standalone chart (`helm/agentic-platform-crds/`), its CircleCI build/test/push jobs, and the now-dead Renovate `helmv3` lockstep `packageRules` are deleted. There is no longer a `components.agentic-platform-crds` entry in the meta-package. ### Changed diff --git a/docs/authentication.md b/docs/authentication.md index ff99998..1f18825 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -173,32 +173,22 @@ agentic-platform-mcps: clientSecretKey: client-secret ``` -### Machine-to-machine (M2M): local mint + impersonation - -A kagent agent reaching a downstream server is a machine, not a human with a Dex -token. muster validates the agent's inbound ServiceAccount token, then -**local-mints** (`auth.mode: localMint`, `enableJWTMode: true`) a token carrying a -configured subject and groups for the downstream audience. The downstream server -(e.g. mcp-kubernetes) cannot present that token to the kube-apiserver as a bearer -(wrong issuer/audience), so it authenticates with its own SA token and sets -`Impersonate-User`/`Impersonate-Group` headers to act *as* the agent. Kubernetes RBAC -and the audit log then reflect the agent, not the proxy. - -The same identity is configured in three places, which must agree: - -- **muster** — the `tokenExchangeBroker.workloadGroupGrants` entry (which inbound - subject to mint for, and the groups to inject), set in giantswarm-config next to - the rest of the muster broker config; -- **mcp-kubernetes** — its `trustedIssuers` (`impersonateUser`/`impersonateGroups`), - set in its own app values; -- **the RBAC** — rendered by agentic-platform-connectivity from `agents..m2m`: - the impersonation `Role`/`ClusterRole` letting each `m2m.impersonators` SA - impersonate `m2m.granted`, plus a `ClusterRoleBinding` per `m2m.granted.clusterRoles` - for the agent's actual access. - -A `system:serviceaccount:…` granted user yields a namespaced `serviceaccounts` Role -plus a groups `ClusterRole`; a plain username (e.g. `agent:sre`) yields a single -cluster-scoped `users`+`groups` ClusterRole. +### On-behalf-of (OBO): local mint + human impersonation + +A kagent agent reaching a downstream server forwards the human's muster token as +`Authorization` and its own ServiceAccount token as `X-Actor-Token`. muster +validates both and **local-mints** (`auth.mode: localMint`, `enableJWTMode: true`) a +token for the downstream audience carrying the human as `sub` and the agent SA in +the RFC 8693 `act` claim. The downstream server (e.g. mcp-kubernetes) cannot present +that token to the kube-apiserver as a bearer (wrong issuer/audience), so it +authenticates with its own SA token and sets `Impersonate-User` to the human plus +`Impersonate-Extra-actor` to the agent SA. Kubernetes RBAC and the audit log then +reflect the human, with the acting agent recorded. + +A trusted-issuer token with no `act` claim is rejected: only on-behalf-of is +accepted, and any cryptographically validated actor is allowed (the impersonated +human's downstream RBAC governs access). The impersonation `ClusterRole` is rendered +by the mcp-kubernetes chart (`*-obo-impersonate`), not by agentic-platform. --- diff --git a/helm/agentic-platform-connectivity/templates/kagent/agents/_agent-impersonation.tpl b/helm/agentic-platform-connectivity/templates/kagent/agents/_agent-impersonation.tpl deleted file mode 100644 index 6d7afcd..0000000 --- a/helm/agentic-platform-connectivity/templates/kagent/agents/_agent-impersonation.tpl +++ /dev/null @@ -1,124 +0,0 @@ -{{/* vim: set filetype=mustache: */}} -{{/* -RBAC letting a set of deputy ServiceAccounts (e.g. mcp-kubernetes) impersonate one -agent's M2M identity at the local apiserver. The deputy authenticates with its own -SA token and adds Impersonate-* headers; this RBAC authorises those headers. - -Kubernetes maps Impersonate-User: system:serviceaccount:NS:NAME to a namespaced -"serviceaccounts" check in NS, not a "users" check at cluster scope. So when the -granted user is SA-prefixed we render a namespaced Role on serviceaccounts; a plain -username instead takes a cluster-scoped "users" rule. Group impersonation is always -cluster-scoped, and system:authenticated must be impersonable because the deputy -appends it to the impersonated group set. - -Input dict: - ctx — root context (.), for name/labels helpers - agentName — agent identifier, used in object names - user — granted impersonated user (SA-prefixed or plain) - groups — list of granted impersonated groups - impersonators — list of {name, namespace} SAs permitted to impersonate - clusterRoles — list of ClusterRole names to bind the granted groups to (authz, - e.g. read-all); one ClusterRoleBinding per role. May be empty. -*/}} -{{- define "agentic-platform.agentImpersonation" -}} -{{- $ctx := .ctx -}} -{{- $base := printf "%s-%s-impersonate" (include "name" $ctx) .agentName -}} -{{- $isSA := hasPrefix "system:serviceaccount:" .user -}} -{{- $groupNames := .groups | default list -}} -{{- if not (has "system:authenticated" $groupNames) -}} -{{- $groupNames = append $groupNames "system:authenticated" -}} -{{- end -}} -{{- if $isSA -}} -{{- $parts := splitList ":" .user -}} -{{- $saNs := index $parts 2 -}} -{{- $saName := index $parts 3 -}} ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: {{ $base }} - namespace: {{ $saNs }} - labels: - {{- include "labels.common" $ctx | nindent 4 }} -rules: - - apiGroups: [""] - resources: ["serviceaccounts"] - verbs: ["impersonate"] - resourceNames: [{{ $saName | quote }}] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: {{ $base }} - namespace: {{ $saNs }} - labels: - {{- include "labels.common" $ctx | nindent 4 }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: {{ $base }} -subjects: - {{- range .impersonators }} - - kind: ServiceAccount - name: {{ .name }} - namespace: {{ .namespace }} - {{- end }} -{{- end }} ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: {{ $base }} - labels: - {{- include "labels.common" $ctx | nindent 4 }} -rules: - {{- if not $isSA }} - - apiGroups: [""] - resources: ["users"] - verbs: ["impersonate"] - resourceNames: [{{ .user | quote }}] - {{- end }} - - apiGroups: [""] - resources: ["groups"] - verbs: ["impersonate"] - resourceNames: - {{- range $groupNames }} - - {{ . | quote }} - {{- end }} ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: {{ $base }} - labels: - {{- include "labels.common" $ctx | nindent 4 }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: {{ $base }} -subjects: - {{- range .impersonators }} - - kind: ServiceAccount - name: {{ .name }} - namespace: {{ .namespace }} - {{- end }} -{{- range .clusterRoles }} ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: {{ printf "%s-%s" $base . }} - labels: - {{- include "labels.common" $ctx | nindent 4 }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: {{ . | quote }} -subjects: - {{- range $.groups }} - - kind: Group - name: {{ . | quote }} - apiGroup: rbac.authorization.k8s.io - {{- end }} -{{- end }} -{{- end -}} diff --git a/helm/agentic-platform-connectivity/templates/kagent/agents/impersonation.yaml b/helm/agentic-platform-connectivity/templates/kagent/agents/impersonation.yaml deleted file mode 100644 index 0afeb7e..0000000 --- a/helm/agentic-platform-connectivity/templates/kagent/agents/impersonation.yaml +++ /dev/null @@ -1,9 +0,0 @@ -{{- if .Values.kagent.enabled }} -{{- range $name, $cfg := .Values.agents.definitions }} -{{- $agent := merge (dict "name" $name) $cfg }} -{{- if and $agent.enabled $agent.m2m.enabled }} -{{- $imp := $agent.m2m }} -{{- include "agentic-platform.agentImpersonation" (dict "ctx" $ "agentName" $agent.name "user" $imp.granted.user "groups" $imp.granted.groups "impersonators" $imp.impersonators "clusterRoles" $imp.granted.clusterRoles) }} -{{- end }} -{{- end }} -{{- end }} diff --git a/helm/agentic-platform-connectivity/values.yaml b/helm/agentic-platform-connectivity/values.yaml index efecdd5..1cec0e9 100644 --- a/helm/agentic-platform-connectivity/values.yaml +++ b/helm/agentic-platform-connectivity/values.yaml @@ -733,22 +733,20 @@ agents: # kagent agents, keyed by agent name. Each enabled entry renders: the Agent CR # (prompt, model, tools); a per-agent muster RemoteMCPServer named "muster-" - # whose headersFrom Secret carries the agent's OWN SA Bearer token (so muster - # authenticates tool calls as system:serviceaccount::, - # the M2M subject); the mint Job + refresh CronJob + Secret RBAC that run as that - # SA; and, when m2m.enabled, the impersonation RBAC. Add an agent = add a key. + # whose headersFrom Secret carries the agent's OWN SA Bearer token (the + # on-behalf-of actor presented to muster as X-Actor-Token); the mint Job + + # refresh CronJob + Secret RBAC that run as that SA. Add an agent = add a key. # Keyed by name (not a list) so per-cluster values deep-merge field-by-field — a # cluster can flip enabled or tweak the prompt without restating the whole entry. - # The kagent controller auto-creates the SA named after the agent, which is both - # the M2M subject and the on-behalf-of actor. Agents run the Python ADK. + # The kagent controller auto-creates the SA named after the agent, which is the + # on-behalf-of actor. Agents run the Python ADK. # # Disabled by default; enable per cluster in giantswarm-config. definitions: sre-agent: enabled: false - # SA whose token is minted and presented to muster. Defaults to the agent - # name (the SA the kagent controller creates for it). Must match the muster - # workloadGroupGrant subject and mcp-kubernetes impersonateUser. + # SA whose token is minted and presented to muster as the on-behalf-of actor. + # Defaults to the agent name (the SA the kagent controller creates for it). serviceAccount: sre-agent # ModelConfig CR the agent uses. Defaults to the one kagent's sub-chart # creates from kagent.providers.anthropic. @@ -831,30 +829,6 @@ agents: resources: {} # Optional extra env for the agent Deployment. env: [] - # Impersonation RBAC for this agent's M2M identity: lets the deputy SAs - # (mcp-kubernetes) impersonate the agent at the apiserver, and binds the - # agent's groups to ClusterRoles for its actual access. The matching muster - # workloadGroupGrant is configured in giantswarm-config; it and - # mcp-kubernetes' impersonateUser/impersonateGroups must agree with `granted`. - m2m: - enabled: false - # Identity the deputy impersonates; must equal mcp-kubernetes' impersonateUser. - # An SA-prefixed user renders a namespaced serviceaccounts Role; a plain - # username (e.g. "agent:sre") renders a cluster-scoped users rule instead. - granted: - user: "system:serviceaccount:kagent:sre-agent" - # Groups the deputy impersonates (muster injects them; the deputy also - # appends system:authenticated, which the ClusterRole therefore allows). - groups: - - agent:sre - # ClusterRoles to bind the granted groups to (the agent's actual K8s authz); - # one ClusterRoleBinding per role. Empty = no binding (granting cluster-wide - # read is left an explicit per-cluster opt-in). Set e.g. [read-all]. - clusterRoles: [] - # ServiceAccounts permitted to impersonate `granted` at the apiserver. - impersonators: - - name: mcp-kubernetes - namespace: mcp-kubernetes # Agentgateway route — exposes this agent's A2A endpoint at /agents/ # on the agentgateway inner Gateway (agentgateway-* ingress modes only). # Rendered automatically when the agent is enabled. Only in-cluster callers diff --git a/helm/agentic-platform/values.yaml b/helm/agentic-platform/values.yaml index 6b71df8..e260f61 100644 --- a/helm/agentic-platform/values.yaml +++ b/helm/agentic-platform/values.yaml @@ -910,25 +910,23 @@ agents: # kagent agents, keyed by agent name. Each enabled entry renders: the Agent CR # (prompt, model, tools); a per-agent muster RemoteMCPServer named "muster-" - # whose headersFrom Secret carries the agent's OWN SA Bearer token (so muster - # authenticates tool calls as system:serviceaccount::, - # the M2M subject); the mint Job + refresh CronJob + Secret RBAC that run as that - # SA; and, when m2m.enabled, the impersonation RBAC. Add an agent = add a key. + # whose headersFrom Secret carries the agent's OWN SA Bearer token (the + # on-behalf-of actor presented to muster as X-Actor-Token); the mint Job + + # refresh CronJob + Secret RBAC that run as that SA. Add an agent = add a key. # An agent that forwards the human token (allowedHeaders includes "authorization", # OBO mode) drops the headersFrom injection AND the mint Job/CronJob/RBAC — it # presents the propagated user muster token, not an SA token. # Keyed by name (not a list) so per-cluster values deep-merge field-by-field — a # cluster can flip enabled or tweak the prompt without restating the whole entry. - # The kagent controller auto-creates the SA named after the agent, which is both - # the M2M subject and the on-behalf-of actor. Agents run the Python ADK. + # The kagent controller auto-creates the SA named after the agent, which is the + # on-behalf-of actor. Agents run the Python ADK. # # Disabled by default; enable per cluster in giantswarm-config. definitions: sre-agent: enabled: false - # SA whose token is minted and presented to muster. Defaults to the agent - # name (the SA the kagent controller creates for it). Must match the muster - # workloadGroupGrant subject and mcp-kubernetes impersonateUser. + # SA whose token is minted and presented to muster as the on-behalf-of actor. + # Defaults to the agent name (the SA the kagent controller creates for it). serviceAccount: sre-agent # ModelConfig CR the agent uses. Defaults to the one kagent's sub-chart # creates from kagent.providers.anthropic. @@ -1010,30 +1008,6 @@ agents: resources: {} # Optional extra env for the agent Deployment. env: [] - # Impersonation RBAC for this agent's M2M identity: lets the deputy SAs - # (mcp-kubernetes) impersonate the agent at the apiserver, and binds the - # agent's groups to ClusterRoles for its actual access. The matching muster - # workloadGroupGrant is configured in giantswarm-config; it and - # mcp-kubernetes' impersonateUser/impersonateGroups must agree with `granted`. - m2m: - enabled: false - # Identity the deputy impersonates; must equal mcp-kubernetes' impersonateUser. - # An SA-prefixed user renders a namespaced serviceaccounts Role; a plain - # username (e.g. "agent:sre") renders a cluster-scoped users rule instead. - granted: - user: "system:serviceaccount:kagent:sre-agent" - # Groups the deputy impersonates (muster injects them; the deputy also - # appends system:authenticated, which the ClusterRole therefore allows). - groups: - - agent:sre - # ClusterRoles to bind the granted groups to (the agent's actual K8s authz); - # one ClusterRoleBinding per role. Empty = no binding (granting cluster-wide - # read is left an explicit per-cluster opt-in). Set e.g. [read-all]. - clusterRoles: [] - # ServiceAccounts permitted to impersonate `granted` at the apiserver. - impersonators: - - name: mcp-kubernetes - namespace: mcp-kubernetes # List of additional RemoteMCPServer CRs to create in the kagent namespace. # Use for MCP servers other than the built-in muster. Each entry creates one