From a64f5026e37af604ae87fce0794abcc8f8b6b056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Tailland?= Date: Mon, 31 Aug 2026 09:00:25 +0200 Subject: [PATCH] Give the agent a door a human can reach MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- deploy/agents/ingress.yaml | 70 ++++++++++++++++++++++++++++++++ deploy/agents/kustomization.yaml | 1 + 2 files changed, 71 insertions(+) create mode 100644 deploy/agents/ingress.yaml diff --git a/deploy/agents/ingress.yaml b/deploy/agents/ingress.yaml new file mode 100644 index 0000000..4b0a53c --- /dev/null +++ b/deploy/agents/ingress.yaml @@ -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 diff --git a/deploy/agents/kustomization.yaml b/deploy/agents/kustomization.yaml index e678c90..d366fc5 100644 --- a/deploy/agents/kustomization.yaml +++ b/deploy/agents/kustomization.yaml @@ -2,3 +2,4 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - approvisionnement.yaml + - ingress.yaml