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. 없으면 [email protected] | Authorization: Bearer <JWT>의 claim |
| 어느 claim인가 | — | controller.auth.userIdClaim. 비우면 sub |
| 토큰이 없으면 | 통과한다 | 401 |
| JWT 서명 검증 | — | 하지 않는다 |
| 전제 | 개발·평가 환경 | 앞단의 proxy가 이미 검증했다 |
unsecure 모드는 요청이 적어 보낸 이름을 그대로 믿는다. trusted-proxy는 그보다 낫지만 “토큰이 진짜인지”는
여전히 확인하지 않는다. 이름 그대로 proxy를 신뢰하는 모드다.
# kagent chart valuescontroller: auth: mode: trusted-proxy userIdClaim: "" # subtrusted-proxy가 믿는 것
섹션 제목: “trusted-proxy가 믿는 것”kagent의 OIDC proxy 설계 문서가 경계를 이렇게 적는다.
- 로그인 redirect, session cookie, 토큰 갱신은 전부 oauth2-proxy가 한다.
- controller는 JWT 서명을 다시 검증하지 않는다. oauth2-proxy가 했다고 믿는다.
- proxy를 우회한 직접 접근을 막는 NetworkPolicy는 “후속 PR 예정”이다.
마지막 줄이 중요하다. controller의 8083 포트에 직접 닿을 수 있는 Pod는 아무 문자열이나 JWT 모양으로 만들어
원하는 사용자로 행세할 수 있다. trusted-proxy는 네트워크 격리와 함께일 때만 인증이다.
chart에 포함된 oauth2-proxy
섹션 제목: “chart에 포함된 oauth2-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:8080chart의 기본 인자 가운데 뒤의 내용과 관계있는 것만 본다.
| 기본 인자 | 효과 |
|---|---|
provider: oidc | Keycloak을 일반 OIDC provider로 연결한다 |
pass-authorization-header: true | upstream으로 Authorization: Bearer <ID token>을 넘긴다 |
scope: openid profile email groups | ID token에 이름·이메일·group claim을 요청한다 |
skip-jwt-bearer-tokens: true | 검증된 Bearer JWT를 들고 온 API client는 로그인 화면 없이 통과시킨다 |
sessionStorage.type: cookie | session을 cookie에 저장한다 |
Keycloak 쪽 client 설정과 realm 구성은 Keycloak 덱의 범위다.
넘어오는 것은 ID token이다
섹션 제목: “넘어오는 것은 ID token이다”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가 어떤 토큰을
받게 되는지를 정한다.
session과 토큰의 수명이 다르다
섹션 제목: “session과 토큰의 수명이 다르다”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가 임직원의 토큰을Authorizationheader에 실어 보내야 하고, 없으면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 프로토콜 페이지를 따른다.
사용자 ID로 무엇을 쓸 것인가
섹션 제목: “사용자 ID로 무엇을 쓸 것인가”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 8083 | controller는 서명을 검증하지 않는다 | NetworkPolicy로 backend와 kagent UI만 허용 |
다른 Pod → controller 8083/mcp | 같은 포트에 모든 Agent를 부르는 MCP endpoint가 있다 | 위와 같은 정책으로 함께 막힌다 |
다른 Pod → Agent Pod 8080 | Agent마다 Service가 있고 sub-agent 호출이 이 주소를 쓴다 | controller와 Agent Pod끼리만 허용 |
| 임직원 → kagent UI | UI에 Agent별 접근 제어가 없다 | UI 앞 oauth2-proxy에서 운영자 group만 허용 |
# 설명용 예제 — controller로 들어오는 연결을 backend와 UI로 제한apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: kagent-controller-ingress namespace: kagentspec: 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로 토큰을 넘기게 해야 한다.