Skip to content

Commit 46fcf27

Browse files
authored
Merge pull request #418 from rh-dnagornuks/vap-agent-label
Feat: add ValidatingAdmissionPolicy to protect kagenti.io/type label
2 parents f5e4826 + 4230ba6 commit 46fcf27

22 files changed

Lines changed: 478 additions & 1175 deletions
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{{- if .Values.vap.agentLabelProtection.enable }}
2+
apiVersion: admissionregistration.k8s.io/v1
3+
kind: ValidatingAdmissionPolicyBinding
4+
metadata:
5+
name: agent-label-protection
6+
labels:
7+
{{- include "chart.labels" . | nindent 4 }}
8+
app.kubernetes.io/component: admission
9+
spec:
10+
policyName: agent-label-protection
11+
validationActions: [Deny]
12+
{{- end }}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{{- if .Values.vap.agentLabelProtection.enable }}
2+
apiVersion: admissionregistration.k8s.io/v1
3+
kind: ValidatingAdmissionPolicy
4+
metadata:
5+
name: agent-label-protection
6+
labels:
7+
{{- include "chart.labels" . | nindent 4 }}
8+
app.kubernetes.io/component: admission
9+
spec:
10+
failurePolicy: Fail
11+
matchConstraints:
12+
resourceRules:
13+
- apiGroups: ["apps"]
14+
apiVersions: ["v1"]
15+
operations: ["CREATE", "UPDATE"]
16+
resources: ["deployments", "statefulsets"]
17+
18+
matchConditions:
19+
- name: 'has-agent-type-label'
20+
expression: >-
21+
(has(object.metadata.labels) && 'kagenti.io/type' in object.metadata.labels)
22+
|| (has(object.spec.template.metadata)
23+
&& has(object.spec.template.metadata.labels)
24+
&& 'kagenti.io/type' in object.spec.template.metadata.labels)
25+
26+
- name: 'not-exempt-service-account'
27+
expression: >-
28+
!(request.userInfo.username == 'system:serviceaccount:{{ .Release.Namespace }}:{{ .Values.controllerManager.serviceAccountName }}')
29+
{{- range .Values.vap.agentLabelProtection.exemptServiceAccounts }}
30+
&& !(request.userInfo.username == 'system:serviceaccount:{{ . }}')
31+
{{- end }}
32+
33+
variables:
34+
- name: metaLabelUnchanged
35+
expression: >-
36+
!('kagenti.io/type' in object.metadata.labels)
37+
|| (request.operation == 'UPDATE'
38+
&& 'kagenti.io/type' in oldObject.metadata.labels
39+
&& oldObject.metadata.labels['kagenti.io/type'] == object.metadata.labels['kagenti.io/type'])
40+
41+
- name: podTemplateLabelUnchanged
42+
expression: >-
43+
!(has(object.spec.template.metadata)
44+
&& has(object.spec.template.metadata.labels)
45+
&& 'kagenti.io/type' in object.spec.template.metadata.labels)
46+
|| (request.operation == 'UPDATE'
47+
&& has(oldObject.spec.template.metadata)
48+
&& has(oldObject.spec.template.metadata.labels)
49+
&& 'kagenti.io/type' in oldObject.spec.template.metadata.labels
50+
&& oldObject.spec.template.metadata.labels['kagenti.io/type'] == object.spec.template.metadata.labels['kagenti.io/type'])
51+
52+
validations:
53+
- expression: "variables.metaLabelUnchanged"
54+
messageExpression: >-
55+
'The kagenti.io/type label on '
56+
+ object.metadata.namespace + '/' + object.metadata.name
57+
+ ' can only be applied by the kagenti-operator via an AgentRuntime CR.'
58+
+ ' Create an AgentRuntime targeting this workload instead of manually setting the label.'
59+
reason: Forbidden
60+
- expression: "variables.podTemplateLabelUnchanged"
61+
messageExpression: >-
62+
'The kagenti.io/type label in the pod template of '
63+
+ object.metadata.namespace + '/' + object.metadata.name
64+
+ ' can only be applied by the kagenti-operator via an AgentRuntime CR.'
65+
+ ' Create an AgentRuntime targeting this workload instead of manually setting the label.'
66+
reason: Forbidden
67+
{{- end }}

charts/kagenti-operator/values.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,15 @@ metrics:
7676
webhook:
7777
enable: true
7878

79+
# [VAP]: ValidatingAdmissionPolicy for agent label protection.
80+
# Prevents manual application of the kagenti.io/type label on Deployments and
81+
# StatefulSets (both top-level metadata and pod template spec). Only the operator
82+
# SA (derived from controllerManager.serviceAccountName and the release namespace)
83+
# is exempt.
84+
vap:
85+
agentLabelProtection:
86+
enable: true
87+
7988
# [PROMETHEUS]: To enable a ServiceMonitor to export metrics to Prometheus set true
8089
prometheus:
8190
enable: false

kagenti-operator/GETTING_STARTED.md

Lines changed: 19 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,14 @@ This scenario demonstrates the complete lifecycle of an AI agent deployment on t
4040
## Overview
4141
4242
### Kagenti Operator
43-
The Kagenti Operator discovers, indexes, and secures AI agents deployed in Kubernetes. There are two ways to enroll workloads:
43+
The Kagenti Operator discovers, indexes, and secures AI agents deployed in Kubernetes. Enroll workloads by creating an `AgentRuntime` CR:
4444

45-
1. **AgentRuntime CR (Recommended)** — Create a Deployment with a `protocol.kagenti.io/a2a` label and an `AgentRuntime` CR pointing to it. The controller applies `kagenti.io/type` labels and triggers sidecar injection automatically. The protocol label enables automatic AgentCard creation for agent discovery.
46-
2. **Manual labels** — Add the `kagenti.io/type: agent` label directly to your Deployment or StatefulSet. This is simpler for quick tests but does not provide identity or observability configuration.
45+
1. Create a Deployment with a `protocol.kagenti.io/a2a` label
46+
2. Create an `AgentRuntime` CR pointing to it
47+
3. The controller applies `kagenti.io/type` labels and triggers sidecar injection automatically
48+
4. The protocol label enables automatic AgentCard creation for agent discovery
49+
50+
A `ValidatingAdmissionPolicy` prevents the `kagenti.io/type` label from being set directly on Deployments or StatefulSets — it can only be applied by the operator via an AgentRuntime CR.
4751

4852
> **Note:** The `Agent` Custom Resource is deprecated and will be removed in a future release.
4953

@@ -180,86 +184,6 @@ kubectl delete agentruntime weather-agent-runtime -n team1
180184

181185
---
182186

183-
## Deploy an Agent with Manual Labels (Alternative)
184-
185-
Deploy an agent as a standard Kubernetes Deployment with the required `kagenti.io/type: agent` label. The operator will automatically discover the workload and create an AgentCard for it. This approach does not provide AgentRuntime's identity or observability configuration.
186-
187-
### Quick Example Deployment
188-
189-
```yaml
190-
kubectl apply -f - <<EOF
191-
apiVersion: apps/v1
192-
kind: Deployment
193-
metadata:
194-
name: weather-agent
195-
namespace: team1
196-
labels:
197-
app.kubernetes.io/name: weather-agent
198-
kagenti.io/type: agent
199-
protocol.kagenti.io/a2a: ""
200-
kagenti.io/framework: LangGraph
201-
spec:
202-
replicas: 1
203-
selector:
204-
matchLabels:
205-
app.kubernetes.io/name: weather-agent
206-
template:
207-
metadata:
208-
labels:
209-
app.kubernetes.io/name: weather-agent
210-
kagenti.io/type: agent
211-
spec:
212-
containers:
213-
- name: agent
214-
image: "ghcr.io/kagenti/agent-examples/weather_service:v0.0.1-alpha.3"
215-
ports:
216-
- containerPort: 8000
217-
imagePullPolicy: Always
218-
env:
219-
- name: PORT
220-
value: "8000"
221-
- name: UV_CACHE_DIR
222-
value: /app/.cache/uv
223-
- name: MCP_URL
224-
value: http://weather-tool-mcp.team1.svc.cluster.local:8000/mcp
225-
- name: LLM_API_BASE
226-
value: http://host.docker.internal:11434/v1
227-
- name: LLM_API_KEY
228-
value: dummy
229-
- name: LLM_MODEL
230-
value: llama3.2:3b-instruct-fp16
231-
---
232-
apiVersion: v1
233-
kind: Service
234-
metadata:
235-
name: weather-agent
236-
namespace: team1
237-
spec:
238-
selector:
239-
app.kubernetes.io/name: weather-agent
240-
ports:
241-
- name: http
242-
port: 8000
243-
targetPort: 8000
244-
EOF
245-
246-
```
247-
248-
**Check Status**:
249-
```bash
250-
251-
# Check discovered agent cards
252-
kubectl get agentcards -n team1
253-
254-
# Check deployment status
255-
kubectl get deployment weather-agent -n team1
256-
257-
# View logs
258-
kubectl logs -l app.kubernetes.io/name=weather-agent -n team1
259-
```
260-
261-
---
262-
263187
## Deploy an MCP Server
264188

265189
MCP (Model Context Protocol) servers provide tools and resources that your agents can use. Deploy an MCP server after your agent is running.
@@ -273,18 +197,15 @@ metadata:
273197
name: weather-tool
274198
labels:
275199
app.kubernetes.io/name: weather-tool
276-
kagenti.io/type: tool
277200
spec:
278201
replicas: 1
279202
selector:
280203
matchLabels:
281204
app.kubernetes.io/name: weather-tool
282-
kagenti.io/type: tool
283205
template:
284206
metadata:
285207
labels:
286208
app.kubernetes.io/name: weather-tool
287-
kagenti.io/type: tool
288209
spec:
289210
containers:
290211
- name: mcp
@@ -306,6 +227,18 @@ spec:
306227
- name: cache
307228
emptyDir: {}
308229
---
230+
apiVersion: agent.kagenti.dev/v1alpha1
231+
kind: AgentRuntime
232+
metadata:
233+
name: weather-tool
234+
namespace: team1
235+
spec:
236+
type: tool
237+
targetRef:
238+
apiVersion: apps/v1
239+
kind: Deployment
240+
name: weather-tool
241+
---
309242
apiVersion: v1
310243
kind: Service
311244
metadata:
@@ -315,7 +248,6 @@ metadata:
315248
app.kubernetes.io/name: weather-service
316249
kagenti.io/framework: LangGraph
317250
kagenti.io/inject: enabled
318-
kagenti.io/type: agent
319251
kagenti.io/workload-type: deployment
320252
protocol.kagenti.io/a2a: ""
321253
name: weather-service

kagenti-operator/cmd/main.go

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -770,15 +770,6 @@ func main() {
770770
os.Exit(1)
771771
}
772772

773-
// Defaults-only config reconciler: propagates ConfigMap changes to
774-
// workloads that have kagenti.io/type but no AgentRuntime CR.
775-
if err = (&injector.DefaultsConfigReconciler{
776-
Client: mgr.GetClient(),
777-
Scheme: mgr.GetScheme(),
778-
}).SetupWithManager(mgr); err != nil {
779-
setupLog.Error(err, "unable to create controller", "controller", "DefaultsConfig")
780-
os.Exit(1)
781-
}
782773
}
783774
// +kubebuilder:scaffold:builder
784775

kagenti-operator/config/default/kustomization.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ resources:
2323
- ../webhook
2424
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
2525
- ../certmanager
26+
# [VAP] ValidatingAdmissionPolicy that prevents manual application of kagenti.io/type labels.
27+
- ../vap
2628
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
2729
#- ../prometheus
2830
# [METRICS] Expose the controller manager metrics service.

kagenti-operator/config/samples/agent_v1alpha1_agentruntime_skill_discovery.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ metadata:
2929
name: my-agent
3030
labels:
3131
app.kubernetes.io/name: my-agent
32-
kagenti.io/type: agent
3332
protocol.kagenti.io/a2a: ""
3433
annotations:
3534
kagenti.io/skills: '["openshift-review"]'
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
resources:
2+
- validating-admission-policy.yaml
3+
- validating-admission-policy-binding.yaml
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
apiVersion: admissionregistration.k8s.io/v1
2+
kind: ValidatingAdmissionPolicyBinding
3+
metadata:
4+
name: agent-label-protection
5+
labels:
6+
app.kubernetes.io/name: kagenti-operator
7+
app.kubernetes.io/component: admission
8+
spec:
9+
policyName: agent-label-protection
10+
validationActions: [Deny]
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# ValidatingAdmissionPolicy: prevent manual application of the kagenti.io/type
2+
# label on Deployments and StatefulSets. Only the kagenti-operator controller
3+
# (via an AgentRuntime CR) is allowed to set this label. Users who need the
4+
# label must create an AgentRuntime targeting their workload instead.
5+
#
6+
# The policy protects the label in TWO locations:
7+
# 1. object.metadata.labels — top-level workload label
8+
# 2. object.spec.template.metadata.labels — pod template label (propagated
9+
# to every Pod the workload creates)
10+
#
11+
# The policy is split into two layers:
12+
#
13+
# matchConditions — skip evaluation entirely when:
14+
# 1. The label is absent from BOTH locations (irrelevant request), OR
15+
# 2. The request originates from a trusted service account.
16+
# Two operator SA identities are listed because kustomize (make deploy)
17+
# and Helm use different namespaces and SA names:
18+
# - kagenti-operator-system/kagenti-operator-controller-manager (kustomize)
19+
# - kagenti-system/controller-manager (Helm)
20+
#
21+
# validations — for everyone else, allow only UPDATE requests where the
22+
# label already existed on the previous revision with the same value in
23+
# whichever location(s) it appears.
24+
#
25+
# CREATE requests that carry the label are always rejected for non-operator
26+
# callers, because the "label unchanged" check evaluates to false on CREATE
27+
# (there is no oldObject).
28+
apiVersion: admissionregistration.k8s.io/v1
29+
kind: ValidatingAdmissionPolicy
30+
metadata:
31+
name: agent-label-protection
32+
labels:
33+
app.kubernetes.io/name: kagenti-operator
34+
app.kubernetes.io/component: admission
35+
spec:
36+
failurePolicy: Fail
37+
matchConstraints:
38+
resourceRules:
39+
- apiGroups: ["apps"]
40+
apiVersions: ["v1"]
41+
operations: ["CREATE", "UPDATE"]
42+
resources: ["deployments", "statefulsets"]
43+
44+
matchConditions:
45+
- name: 'has-agent-type-label'
46+
expression: >-
47+
(has(object.metadata.labels) && 'kagenti.io/type' in object.metadata.labels)
48+
|| (has(object.spec.template.metadata)
49+
&& has(object.spec.template.metadata.labels)
50+
&& 'kagenti.io/type' in object.spec.template.metadata.labels)
51+
52+
- name: 'not-operator-service-account'
53+
expression: >-
54+
!(request.userInfo.username == 'system:serviceaccount:kagenti-operator-system:kagenti-operator-controller-manager')
55+
&& !(request.userInfo.username == 'system:serviceaccount:kagenti-system:controller-manager')
56+
57+
variables:
58+
- name: metaLabelUnchanged
59+
expression: >-
60+
!('kagenti.io/type' in object.metadata.labels)
61+
|| (request.operation == 'UPDATE'
62+
&& 'kagenti.io/type' in oldObject.metadata.labels
63+
&& oldObject.metadata.labels['kagenti.io/type'] == object.metadata.labels['kagenti.io/type'])
64+
65+
- name: podTemplateLabelUnchanged
66+
expression: >-
67+
!(has(object.spec.template.metadata)
68+
&& has(object.spec.template.metadata.labels)
69+
&& 'kagenti.io/type' in object.spec.template.metadata.labels)
70+
|| (request.operation == 'UPDATE'
71+
&& has(oldObject.spec.template.metadata)
72+
&& has(oldObject.spec.template.metadata.labels)
73+
&& 'kagenti.io/type' in oldObject.spec.template.metadata.labels
74+
&& oldObject.spec.template.metadata.labels['kagenti.io/type'] == object.spec.template.metadata.labels['kagenti.io/type'])
75+
76+
validations:
77+
- expression: "variables.metaLabelUnchanged"
78+
messageExpression: >-
79+
'The kagenti.io/type label on '
80+
+ object.metadata.namespace + '/' + object.metadata.name
81+
+ ' can only be applied by the kagenti-operator via an AgentRuntime CR.'
82+
+ ' Create an AgentRuntime targeting this workload instead of manually setting the label.'
83+
reason: Forbidden
84+
- expression: "variables.podTemplateLabelUnchanged"
85+
messageExpression: >-
86+
'The kagenti.io/type label in the pod template of '
87+
+ object.metadata.namespace + '/' + object.metadata.name
88+
+ ' can only be applied by the kagenti-operator via an AgentRuntime CR.'
89+
+ ' Create an AgentRuntime targeting this workload instead of manually setting the label.'
90+
reason: Forbidden

0 commit comments

Comments
 (0)