콘텐츠로 이동
Study Notekagent · kmcp

oauth2-proxy와 trusted-proxy 인증

결론부터
  • kagent의 인증은 “앞단의 proxy를 믿는다”는 모델이다. controller는 로그인 화면도, 토큰 검증도 직접 하지 않는다.
  • trusted-proxy 모드의 controller는 Authorization의 JWT에서 사용자 ID를 읽기만 하고 서명을 검증하지 않는다.
  • 그래서 proxy나 backend를 거치지 않고 controller·Agent Pod에 닿는 길은 NetworkPolicy로 막아야 한다. chart가 만들어 주지 않는다.
  • kagent의 로그인은 “누구인가”까지다. “이 사람이 이 Agent를 써도 되는가”는 우리 backend가 호출 전에 검사한다.
  • oauth2-proxy가 뒤로 넘기는 Authorization은 access token이 아니라 ID token이다.
이 장에서 처음 나오는 말6개
OIDCOpenID Connect
OAuth 2.0 위에 "누가 로그인했는가"를 표준화한 프로토콜이다. Keycloak이 이 방식으로 임직원을 인증한다.
oauth2-proxy
애플리케이션 앞에서 OIDC 로그인을 대신 처리하는 reverse proxy다. 로그인한 요청만 뒤로 넘기고 사용자 정보를 header에 실어 준다.
JWTJSON Web Token
서명된 JSON 토큰이다. 서명을 검증해야 내용을 믿을 수 있다. 검증 없이 읽기만 하면 누구나 만들 수 있는 문자열이다.
ID token
OIDC가 "이 사용자가 로그인했다"는 사실을 client에게 알리는 JWT다. 수신 대상(aud)이 로그인을 요청한 client다.
access token
API를 호출할 권한을 나타내는 토큰이다. 수신 대상이 API 서버다.
NetworkPolicy
Pod 사이의 네트워크 연결을 허용 목록으로 제한하는 Kubernetes 리소스다.

설치 구성에서 기본 설치의 controller가 unsecure 모드라는 것을 봤다. 임직원이 쓰는 환경에서는 “이 요청이 누구의 것인가”가 정해져야 대화 이력이 사람별로 나뉘고, 뒤의 토큰 전파도 성립한다. 이 페이지는 kagent가 사용자를 식별하는 방식과 그 방식이 전제하는 것을 본다.

controller.auth.mode Helm 값으로 고른다(Helm reference). 아래 동작은 v0.10.2 소스에서 확인했다.

unsecure (기본)trusted-proxy
사용자 ID의 출처user_id query 또는 X-User-Id header. 없으면 admin@kagent.devAuthorization: Bearer <JWT>의 claim
어느 claim인가—controller.auth.userIdClaim. 비우면 sub
토큰이 없으면통과한다401
JWT 서명 검증—하지 않는다
전제개발·평가 환경앞단의 proxy가 이미 검증했다

unsecure 모드는 요청이 적어 보낸 이름을 그대로 믿는다. trusted-proxy는 그보다 낫지만 “토큰이 진짜인지”는 여전히 확인하지 않는다. 이름 그대로 proxy를 신뢰하는 모드다.

# kagent chart values
controller:
auth:
mode: trusted-proxy
userIdClaim: "" # sub
브라우저 요청은 oauth2-proxy에서 Keycloak 로그인과 토큰 검증을 거친 뒤 UI와 controller로 넘어가고, controller는 토큰의 claim을 읽기만 한다

kagent의 OIDC proxy 설계 문서가 경계를 이렇게 적는다.

  • 로그인 redirect, session cookie, 토큰 갱신은 전부 oauth2-proxy가 한다.
  • controller는 JWT 서명을 다시 검증하지 않는다. oauth2-proxy가 했다고 믿는다.
  • proxy를 우회한 직접 접근을 막는 NetworkPolicy는 “후속 PR 예정”이다.

마지막 줄이 중요하다. controller의 8083 포트에 직접 닿을 수 있는 Pod는 아무 문자열이나 JWT 모양으로 만들어 원하는 사용자로 행세할 수 있다. trusted-proxy는 네트워크 격리와 함께일 때만 인증이다.

kagent chart는 oauth2-proxy를 선택 subchart로 포함한다. 기본은 꺼져 있다.

# kagent chart values — 발췌
oauth2-proxy:
enabled: true
config:
existingSecret: kagent-oauth2-proxy # client ID · client secret · cookie secret
extraEnv:
- name: OIDC_ISSUER_URL
value: https://keycloak.example.com/realms/corp
- name: OIDC_REDIRECT_URL
value: https://kagent.example.com/oauth2/callback
- name: UPSTREAM_URL
value: http://kagent-ui:8080

chart의 기본 인자 가운데 뒤의 내용과 관계있는 것만 본다.

기본 인자효과
provider: oidcKeycloak을 일반 OIDC provider로 연결한다
pass-authorization-header: trueupstream으로 Authorization: Bearer <ID token>을 넘긴다
scope: openid profile email groupsID token에 이름·이메일·group claim을 요청한다
skip-jwt-bearer-tokens: true검증된 Bearer JWT를 들고 온 API client는 로그인 화면 없이 통과시킨다
sessionStorage.type: cookiesession을 cookie에 저장한다

Keycloak 쪽 client 설정과 realm 구성은 Keycloak 덱의 범위다.

oauth2-proxy 문서에 따르면 --pass-authorization-header는 OIDC ID token을 Authorization header로 넘긴다. access token은 별도 옵션 --pass-access-token을 켜야 X-Forwarded-Access-Token header로 넘어온다.

kagent UI는 Authorization을 항상 controller로 전달한다. 다른 header는 ui.additionalForwardedHeaders에 적은 것만 전달한다. 이 차이는 토큰 전파에서 backend가 어떤 토큰을 받게 되는지를 정한다.

oauth2-proxy의 session cookie는 기본 168시간 유효하지만 ID token은 훨씬 짧다. cookie는 살아 있는데 토큰이 만료된 상태가 생긴다. v0.10의 UI는 이때 /oauth2/start로 보내 다시 로그인시킨다 (release notes). oauth2-proxy가 토큰을 갱신하게 하려면 cookie-refresh와 offline_access scope를 직접 켜야 한다. chart 기본값에는 없다.

임직원 요청이 들어오는 두 경로

섹션 제목: “임직원 요청이 들어오는 두 경로”

kagent를 어디에 두느냐에 따라 controller에 토큰을 넘기는 주체가 다르다.

경로 A — kagent UI경로 B — 우리 portal
흐름브라우저 → oauth2-proxy → kagent UI → controller브라우저 → oauth2-proxy → portal·backend → controller A2A
누가 쓰나운영자임직원
controller에 토큰을 싣는 주체kagent UI(자동)backend(우리 코드)
Agent별 접근 제어없음backend가 호출 전에 검사

임직원에게 여는 것은 경로 B다. kagent UI에는 Agent 생성·수정 화면이 있고 Agent별 접근 제어가 없어서, GitOps 승인 흐름을 우회하는 문이 된다.

경로 B에서 backend가 지킬 것은 둘이다.

  • trusted-proxy 모드의 controller는 모든 A2A 요청에 Bearer JWT를 요구한다. backend가 임직원의 토큰을 Authorization header에 실어 보내야 하고, 없으면 401이다.
  • backend가 그 토큰을 가지려면 backend 앞의 oauth2-proxy가 토큰을 넘겨 줘야 한다. cookie session만 쓰는 구성에서는 backend가 토큰을 받지 못한다. --pass-authorization-header(ID token) 또는 --pass-access-token(access token)을 켠다.
터미널 창
# 설명용 — backend가 controller를 호출하는 요청의 모양
curl -N http://kagent-controller.kagent:8083/api/a2a/agents/hr-helper/ \
-H "Authorization: Bearer $EMPLOYEE_TOKEN" \
-H "Content-Type: application/json" \
-d @a2a-request.json

요청 본문의 형식은 A2A 프로토콜 페이지를 따른다.

controller가 뽑은 사용자 ID는 두 곳에 쓰인다. kagent DB에서 session의 소유자가 되고, Agent Pod로 X-User-Id header에 실려 전달된다.

기본값 sub를 유지한다. sub는 Keycloak이 사용자마다 부여하는 바뀌지 않는 ID다. 이메일은 바뀔 수 있어서 userIdClaim: email로 두면 이메일이 바뀐 임직원이 이전 대화를 잃는다. 이메일이 필요한 곳에서는 토큰의 email claim을 따로 읽는다.

kagent가 해 주는 것은 여기까지다 — 요청에 사용자 ID를 붙인다. 로그인한 사용자는 떠 있는 Agent를 전부 호출할 수 있다. “이 Agent는 인사팀만”이라는 규칙을 kagent에 선언하는 자리는 없다.

그래서 사내 Agent 배포 플랫폼 덱의 세 권한이 그대로 필요하다. backend가 A2A를 호출하기 전에 그 임직원이 그 Agent를 써도 되는지 검사하고, 통과한 요청만 controller로 보낸다. 이 검사가 의미를 가지려면 backend를 거치지 않는 길이 없어야 한다.

우회로왜 열려 있나막는 방법
다른 Pod → controller 8083controller는 서명을 검증하지 않는다NetworkPolicy로 backend와 kagent UI만 허용
다른 Pod → controller 8083/mcp같은 포트에 모든 Agent를 부르는 MCP endpoint가 있다위와 같은 정책으로 함께 막힌다
다른 Pod → Agent Pod 8080Agent마다 Service가 있고 sub-agent 호출이 이 주소를 쓴다controller와 Agent Pod끼리만 허용
임직원 → kagent UIUI에 Agent별 접근 제어가 없다UI 앞 oauth2-proxy에서 운영자 group만 허용
# 설명용 예제 — controller로 들어오는 연결을 backend와 UI로 제한
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: kagent-controller-ingress
namespace: kagent
spec:
podSelector:
matchLabels:
app.kubernetes.io/component: controller
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: portal
podSelector:
matchLabels:
app: portal-backend
- podSelector:
matchLabels:
app.kubernetes.io/component: ui
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: agents
ports:
- port: 8083

마지막 항목은 Agent Pod가 session 저장 등을 위해 controller를 다시 호출하기 때문에 필요하다(소스에서 확인). label 이름은 설치된 chart의 실제 값을 kubectl -n kagent get pods --show-labels로 확인해 맞춘다. NetworkPolicy는 CNI가 지원해야 동작한다.

이 정책으로도 남는 틈이 있다. Agent Pod는 controller에 닿을 수 있어야 하므로, 침해된 Agent Pod가 controller에 위조한 요청을 보내는 것은 네트워크 수준에서 막지 못한다. BYO image와 MCP 서버 image를 PR에서 검토하는 이유 중 하나다.

검증은 실패해야 하는 요청으로 한다. 허용되지 않은 namespace의 Pod에서 controller로 요청을 보내 연결이 거부되는지 확인한다.

터미널 창
# 허용되지 않은 namespace에서 — timeout이 나야 정상
kubectl -n default run probe --rm -it --image=curlimages/curl --restart=Never -- \
curl -m 5 http://kagent-controller.kagent:8083/api/a2a/agents/hr-helper/.well-known/agent.json
  • trusted-proxy 모드인데 NetworkPolicy가 없다. 같은 클러스터의 무관한 Pod 하나가 침해되면 무엇이 가능한가? → 그 Pod가 controller에 임의의 sub를 넣은 JWT 모양 문자열을 보내 다른 임직원으로 Agent를 호출할 수 있다. controller는 서명을 확인하지 않는다.
  • backend 앞의 oauth2-proxy가 cookie session만 쓰고 있다. controller를 trusted-proxy로 바꾸면? → backend가 넘길 토큰이 없어서 모든 A2A 호출이 401이 된다. oauth2-proxy가 backend로 토큰을 넘기게 해야 한다.