Give the agent a door a human can reach

Nothing outside the cluster could talk to it: every conversation went through
kubectl run curl, which makes an agent that works and nobody can use.

The same authentication subrequest that guards the n8n endpoints guards this
one. The agent forwards and exchanges the caller's token, so a forged one
fails at the realm — but failing downstream is not refusing, and an
unauthenticated caller could still make it do inference.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Stéphane Tailland
2026-08-31 09:00:25 +02:00
parent b04c5cbb11
commit a64f5026e3
2 changed files with 71 additions and 0 deletions
+70
View File
@@ -0,0 +1,70 @@
# Reaching this agent from outside the cluster.
#
# Until now nothing did: an agent answered only what could call its Service, so
# every conversation with it went through `kubectl run curl`. An orchestrator
# nobody can talk to is not finished.
#
# Peer traffic does **not** come through here — approvisionnement calls, and is called by,
# other agents on their in-cluster Service names. This is the human door.
#
# ## It is not an open door
#
# A2A carries the end user's token in `Authorization`, and this agent forwards it
# and exchanges it; a forged one fails at the realm. But "fails downstream" is
# not the same as "refused", and an unauthenticated caller could still make the
# agent do work — inference costs money and a request that reaches an LLM is a
# prompt-injection surface. So the same authentication subrequest that guards the
# n8n endpoints guards this one: Keycloak's userinfo answers 200 for a valid
# bearer in the realm and 401 otherwise, which is the auth-request contract
# exactly.
#
# It authenticates, it does not authorise. What the caller may actually reach is
# decided further down, by the exchange and by the roles in their token.
#
# **The agent card is behind it too.** A2A discovery is normally public; here it
# is not, because a client that cannot send an `Authorization` header cannot use
# the agent anyway, and the card names the skills.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: approvisionnement-agent-tls
namespace: agents
spec:
secretName: approvisionnement-agent-tls
dnsNames:
- approvisionnement-agent.sttlab.local
issuerRef:
name: sttlab-local-ca
kind: ClusterIssuer
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: approvisionnement-agent
namespace: agents
annotations:
nginx.ingress.kubernetes.io/auth-url: http://keycloak.keycloak.svc.cluster.local:8080/realms/classic-models/protocol/openid-connect/userinfo
# A2A streams a task's progress; buffering it defeats that.
nginx.ingress.kubernetes.io/proxy-buffering: "off"
nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
# An agent turn is a model call and several hops. The nginx default of 60s
# cuts a working answer off mid-flight.
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
spec:
ingressClassName: nginx
tls:
- hosts:
- approvisionnement-agent.sttlab.local
secretName: approvisionnement-agent-tls
rules:
- host: approvisionnement-agent.sttlab.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: approvisionnement-agent
port:
number: 8080
+1
View File
@@ -2,3 +2,4 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization kind: Kustomization
resources: resources:
- approvisionnement.yaml - approvisionnement.yaml
- ingress.yaml