Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions development/examples/kgateway/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.run/
41 changes: 41 additions & 0 deletions development/examples/kgateway/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# kgateway with mesh authorization

kgateway keeps routing; an `agentmesh-node` decides each request over Envoy
gRPC `ext_authz`. Two routes: `/mcp/greeter` goes to a server in the cluster,
`/mcp/calc` goes through the mesh to a server in another namespace. The
guide is `site/content/docs/guides/kgateway.md`.

```sh
./run.sh # create a kind cluster and run every case
./run.sh down # delete the cluster and .run/
```

Needs kind, kubectl, helm, docker and make. The agentmesh images are built
from this checkout (the published control plane and router images are amd64
only).

```
== kgateway routes, the node decides
PASS alice -> /mcp/calc, through the mesh (calc-user) 200
PASS alice calls add 200 5.0
PASS alice -> /mcp/greeter (no greeter-user) 403
PASS bob -> /mcp/greeter, a local backend (greeter-user) 200
PASS bob calls hello 200 Hello, bob!
PASS bob -> /mcp/calc (no calc-user) 403

== Refused by the node's ext_authz
PASS no token 401
PASS alice, token for client other-app (audience) 403
PASS the node's own API token 403

== Fail closed: the node goes away
PASS the greeter itself, from inside the cluster 200
PASS bob -> /mcp/greeter, node unreachable 503
PASS alice -> /mcp/calc, node unreachable 503
PASS bob -> /mcp/greeter, node back 200
PASS alice -> /mcp/calc, node back 200
```

For local testing only: plain http, Keycloak `start-dev`, test passwords in
the realm file, and auto-approved enrollment. `.run/` holds the generated
tokens and is not committed.
51 changes: 51 additions & 0 deletions development/examples/kgateway/apps.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# A local MCP server and the client pod run.sh sends requests from.
apiVersion: v1
kind: Namespace
metadata:
name: apps
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: greeter
namespace: apps
spec:
selector:
matchLabels: { app: greeter }
template:
metadata:
labels: { app: greeter }
spec:
containers:
- name: greeter
image: agentmesh-kgateway/greeter-mcp:local
imagePullPolicy: Never
ports:
- containerPort: 7778
---
apiVersion: v1
kind: Service
metadata:
name: greeter
namespace: apps
spec:
selector: { app: greeter }
ports:
- { name: http, port: 7778 }
---
# ID tokens are fetched into this pod's /tmp and stay there.
apiVersion: v1
kind: Pod
metadata:
name: client
namespace: apps
spec:
containers:
- name: curl
image: curlimages/curl:8.11.1
command: ["sleep", "infinity"]
volumeMounts:
- { name: admin, mountPath: /run/example, readOnly: true }
volumes:
- name: admin
secret: { secretName: client-admin }
95 changes: 95 additions & 0 deletions development/examples/kgateway/gateway.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# The kgateway side. Plain Gateway API routes, plus an ext_authz extension
# pointing at the node and a policy attaching it. run.sh fills in
# __PROVIDER__.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: apps-gateway
namespace: apps
spec:
gatewayClassName: kgateway
listeners:
- name: http
protocol: HTTP
port: 8080
---
# Local server: kgateway routes, the node only decides.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: greeter
namespace: apps
spec:
parentRefs:
- name: apps-gateway
rules:
- matches:
- path: { type: PathPrefix, value: /mcp/greeter }
filters:
- type: URLRewrite
urlRewrite:
path: { type: ReplaceFullPath, replaceFullPath: /mcp }
# Runs after ext_authz, so the backend never gets the token.
- type: RequestHeaderModifier
requestHeaderModifier:
remove: ["Authorization"]
backendRefs:
- name: greeter
port: 7778
---
# Remote server: kgateway routes to the node, the node goes over the mesh.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: calc
namespace: apps
spec:
parentRefs:
- name: apps-gateway
rules:
- matches:
- path: { type: PathPrefix, value: /mcp/calc }
filters:
- type: URLRewrite
urlRewrite:
path: { type: ReplaceFullPath, replaceFullPath: /mesh/__PROVIDER__/mcp/calc }
- type: RequestHeaderModifier
requestHeaderModifier:
remove: ["Authorization"]
backendRefs:
- name: mesh-node
namespace: mesh
port: 8080
---
apiVersion: gateway.kgateway.dev/v1alpha1
kind: GatewayExtension
metadata:
name: mesh-authz
namespace: apps
spec:
type: ExtAuth
extAuth:
grpcService:
backendRef:
name: mesh-node-authz
namespace: mesh
port: 8080
# The node only needs the token; the service comes from the path.
headersToForward: ["authorization"]
# Default, but worth being explicit: no answer, no request.
failOpen: false
# 503 instead of 403, so a node outage doesn't look like a denial.
statusOnError: 503
---
apiVersion: gateway.kgateway.dev/v1alpha1
kind: TrafficPolicy
metadata:
name: mesh-authz
namespace: apps
spec:
targetRefs:
- { group: gateway.networking.k8s.io, kind: HTTPRoute, name: greeter }
- { group: gateway.networking.k8s.io, kind: HTTPRoute, name: calc }
extAuth:
extensionRef:
name: mesh-authz
202 changes: 202 additions & 0 deletions development/examples/kgateway/mesh.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
# Keycloak, the control plane, a router and the node kgateway asks over
# ext_authz. run.sh creates the secrets and the realm ConfigMap.
apiVersion: v1
kind: Namespace
metadata:
name: mesh
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: keycloak
namespace: mesh
spec:
selector:
matchLabels: { app: keycloak }
template:
metadata:
labels: { app: keycloak }
spec:
containers:
- name: keycloak
image: quay.io/keycloak/keycloak:26.4.0
# start-dev: plain http, no database. Local testing only.
args: ["start-dev", "--import-realm"]
env:
- { name: KC_HEALTH_ENABLED, value: "true" }
ports:
- containerPort: 8080
readinessProbe:
httpGet: { path: /health/ready, port: 9000 }
periodSeconds: 5
failureThreshold: 60
volumeMounts:
- { name: realm, mountPath: /opt/keycloak/data/import, readOnly: true }
volumes:
- name: realm
configMap: { name: keycloak-realm }
---
apiVersion: v1
kind: Service
metadata:
name: keycloak
namespace: mesh
spec:
selector: { app: keycloak }
ports:
- { name: http, port: 8080 }
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: control-plane
namespace: mesh
spec:
selector:
matchLabels: { app: control-plane }
template:
metadata:
labels: { app: control-plane }
spec:
containers:
- name: control-plane
image: agentmesh-control-plane:local
imagePullPolicy: Never
args:
- --issuer=http://keycloak.mesh:8080/realms/mesh
- --allowed-audiences=agentmesh
- --admin-token-path=/run/example/admin.token
- --db-dsn=/data/control-plane.db
# So the router and nodes enroll without a manual approval.
# User access is still decided by the policy bindings.
- --auto-approve-enrollment
ports:
- containerPort: 8080
readinessProbe:
httpGet: { path: /info, port: 8080 }
periodSeconds: 2
volumeMounts:
- { name: admin, mountPath: /run/example, readOnly: true }
- { name: data, mountPath: /data }
volumes:
- name: admin
secret: { secretName: control-plane-admin }
- name: data
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: control-plane
namespace: mesh
spec:
selector: { app: control-plane }
ports:
- { name: http, port: 8080 }
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: router
namespace: mesh
spec:
selector:
matchLabels: { app: router }
template:
metadata:
labels: { app: router }
spec:
containers:
- name: router
image: agentmesh-router:local
imagePullPolicy: Never
args:
- --control-plane=http://control-plane.mesh:8080
- --insecure-control-plane
- --bootstrap-token-path=/run/example/bootstrap.token
- --keys-path=/data/router.key
- --control-plane-sync-interval=10s
volumeMounts:
- { name: bootstrap, mountPath: /run/example, readOnly: true }
- { name: data, mountPath: /data }
volumes:
- name: bootstrap
secret: { secretName: router-bootstrap }
- name: data
emptyDir: {}
---
# Answers kgateway's ext_authz checks, and is the backend for routes into
# the mesh. Its own credential grants no service.
apiVersion: apps/v1
kind: Deployment
metadata:
name: mesh-node
namespace: mesh
spec:
selector:
matchLabels: { app: mesh-node }
template:
metadata:
labels: { app: mesh-node }
spec:
containers:
- name: mesh-node
image: agentmesh-node:local
imagePullPolicy: Never
args:
- run
- --control-plane=http://control-plane.mesh:8080
- --insecure-control-plane
- --bootstrap-token-path=/run/example/bootstrap.token
- --data-dir=/data
- --bind-addr=0.0.0.0:8080
- --api-token-path=/run/example/api.token
- --control-plane-sync-interval=10s
ports:
- containerPort: 8080
readinessProbe:
httpGet: { path: /readyz, port: 8080 }
periodSeconds: 2
volumeMounts:
- { name: tokens, mountPath: /run/example, readOnly: true }
- { name: data, mountPath: /data }
volumes:
- name: tokens
secret: { secretName: mesh-node-tokens }
- name: data
emptyDir: {}
---
# Same port, two Services: h2c for ext_authz, plain HTTP for /mesh/.
apiVersion: v1
kind: Service
metadata:
name: mesh-node-authz
namespace: mesh
spec:
selector: { app: mesh-node }
ports:
- { name: grpc, port: 8080, appProtocol: kubernetes.io/h2c }
---
apiVersion: v1
kind: Service
metadata:
name: mesh-node
namespace: mesh
spec:
selector: { app: mesh-node }
ports:
- { name: http, port: 8080 }
---
# Lets the gateway in apps use these Services.
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: apps-gateway
namespace: mesh
spec:
from:
- { group: gateway.networking.k8s.io, kind: HTTPRoute, namespace: apps }
- { group: gateway.kgateway.dev, kind: GatewayExtension, namespace: apps }
to:
- { group: "", kind: Service, name: mesh-node }
- { group: "", kind: Service, name: mesh-node-authz }
Loading
Loading