콘텐츠로 이동
Study Notekagent 실습

8. Backend 권한과 직접 우회 차단

결론부터
관리 RBAC·Agent 호출 Grant·tool action 권한은 서로 다른 문이며, backend가 허용해도 controller A2A port 직접 접근이 열려 있으면 invocation authorization은 실패한 설계다
이 장에서 처음 나오는 말4개
Grant
어떤 회사 principal이 어떤 Agent를 조회·호출·편집할 수 있는지 포털 DB에 저장한 제품 중립 권한이다.
session ownership
대화 session ID를 만든 principal과 이후 요청 principal이 같은지 backend가 확인하는 경계다.
direct bypass
사용자가 backend의 Grant 검사 없이 controller나 Agent endpoint를 직접 호출하는 우회 경로다.
negative test
허용 사례가 아니라 금지해야 할 요청이 실제로 거부되는지 확인하는 시험이다.

7장의 probe는 개발자 kubeconfig로 모든 단계를 실행했다. production backend에는 그 권한을 주지 않는다. 또 kagent v0.9 계열의 OIDC 로그인은 사용자가 누구인지 확인하는 인증이지 Agent별 application access control이 아니다. 이번 장은 서로 다른 세 문을 따로 잠근다.

문판단 주체막을 대상
control planeKubernetes RBACbackend가 허용되지 않은 namespace·Secret·cluster resource를 변경
invocation포털 DB의 Grant + gateway/network금지 사용자가 Agent를 호출하거나 controller 8083으로 우회
actiontool gateway·MCP server허용된 Agent 호출 안에서 금지된 write action 실행

실습에서는 kagent namespace에 ServiceAccount를 만들지만 Agent 관리 권한만 준다. Secret, ModelConfig, RemoteMCPServer, SandboxAgent와 cluster-scoped resource는 허용하지 않는다. Substrate를 실제 채택한 뒤에만 SandboxAgent 권한을 별도 검토한다.

터미널 창
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: ServiceAccount
metadata:
name: agent-backend
namespace: kagent
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: agent-backend
namespace: kagent
rules:
- apiGroups: ["kagent.dev"]
resources: ["agents"]
verbs: ["get", "list", "watch", "create", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: agent-backend
namespace: kagent
subjects:
- kind: ServiceAccount
name: agent-backend
namespace: kagent
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: agent-backend
EOF

권한이 있는지보다 없는 권한이 실제로 없는지가 더 중요하다.

터미널 창
BACKEND_AS="system:serviceaccount:kagent:agent-backend"
kubectl auth can-i --as="$BACKEND_AS" patch agents.kagent.dev -n kagent
kubectl auth can-i --as="$BACKEND_AS" watch agents.kagent.dev -n kagent
kubectl auth can-i --as="$BACKEND_AS" get secrets -n kagent
kubectl auth can-i --as="$BACKEND_AS" patch sandboxagents.kagent.dev -n kagent
kubectl auth can-i --as="$BACKEND_AS" create clusterroles.rbac.authorization.k8s.io

기대 결과는 앞의 둘만 yes, 뒤의 셋은 no다. kubectl auth can-i의 --as는 kind 관리자 신원으로 ServiceAccount authorization을 모의한다. 온프렘에서 impersonation 권한이 없다면 플랫폼 관리자가 같은 검증을 수행하고 결과를 증거로 남긴다.

Grant와 session ownership을 실패부터 시험하기

섹션 제목: “Grant와 session ownership을 실패부터 시험하기”

/tmp/kagent-backend-probe/policy.test.ts를 만들고 backend가 지켜야 할 최소 불변조건을 executable test로 남긴다. 실제 구현에서는 in-memory Set 대신 DB transaction과 IdP principal을 사용한다.

import assert from 'node:assert/strict';
import test from 'node:test';
type Principal = { issuer: string; subject: string };
const key = (p: Principal) => `${p.issuer}\u0000${p.subject}`;
const allowed: Principal = { issuer: 'https://idp.example', subject: 'user-a' };
const denied: Principal = { issuer: 'https://idp.example', subject: 'user-b' };
const sameSubOtherIssuer: Principal = { issuer: 'https://other-idp.example', subject: 'user-a' };
const invokeGrants = new Set([`${key(allowed)}\u0000backend-reader`]);
const sessions = new Map<string, string>();
function authorizeInvoke(principal: Principal, agent: string) {
if (!invokeGrants.has(`${key(principal)}\u0000${agent}`)) {
throw new Error('invoke denied');
}
}
function assertSessionOwner(principal: Principal, sessionId: string) {
const owner = sessions.get(sessionId);
if (owner && owner !== key(principal)) throw new Error('session owner mismatch');
sessions.set(sessionId, key(principal));
}
test('허용 principal만 backend-reader를 호출한다', () => {
assert.doesNotThrow(() => authorizeInvoke(allowed, 'backend-reader'));
assert.throws(() => authorizeInvoke(denied, 'backend-reader'));
});
test('같은 sub라도 issuer가 다르면 다른 principal이다', () => {
assert.throws(() => authorizeInvoke(sameSubOtherIssuer, 'backend-reader'));
});
test('다른 사용자의 session ID를 재사용하지 못한다', () => {
assertSessionOwner(allowed, 'session-1');
assert.throws(() => assertSessionOwner(denied, 'session-1'));
});
터미널 창
cd /tmp/kagent-backend-probe
node --import tsx --test policy.test.ts

세 test가 모두 통과해야 한다. 실제 HTTP handler에서는 Agent Card 조회, invoke, stream 재연결, approval 응답 각각에서 같은 authorization 함수를 다시 호출한다. frontend에서 Agent를 숨기는 것은 이 test를 대신하지 못한다.

7장의 정상 호출은 다음 URL로 갔다.

http://kagent-controller.kagent.svc.cluster.local:8083/api/a2a/kagent/backend-reader/

따라서 막아야 할 대상은 임의의 Agent Pod Service가 아니라 controller Pod의 8083 ingress다. production에서는 portal backend와 운영자 UI·승인된 gateway만 이 port에 닿게 한다. 다음 policy는 release 이름이 kagent이고 backend가 agent-portal namespace의 app.kubernetes.io/name=agent-backend Pod라는 전제의 staging 예시다.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: controller-approved-callers
namespace: kagent
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: kagent
app.kubernetes.io/instance: kagent
app.kubernetes.io/component: controller
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: agent-portal
podSelector:
matchLabels:
app.kubernetes.io/name: agent-backend
ports:
- protocol: TCP
port: 8083
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: kagent
app.kubernetes.io/instance: kagent
app.kubernetes.io/component: ui
ports:
- protocol: TCP
port: 8083
- protocol: TCP
port: 8084

적용 전 kubectl -n kagent get pod --show-labels와 실제 backend label을 비교한다. Gateway·Prometheus·service mesh가 controller에 닿는 환경이면 필요한 source와 port를 명시적으로 더한다. 빈칸을 추측해서 운영에 적용하지 않는다.

Staging에서 실행할 허용·거부 probe

섹션 제목: “Staging에서 실행할 허용·거부 probe”
  1. backend Pod에서 Agent Card와 message/send가 성공하는지 확인한다.
  2. label이 없는 Pod와 다른 namespace Pod에서 같은 controller Service의 8083 연결이 실패하는지 확인한다.
  3. 외부 ingress·NodePort·service mesh route에 8083 우회 노출이 없는지 조회한다.
  4. 금지 사용자가 portal API에서 403을 받고, 같은 request ID가 deny audit event에 남는지 확인한다.
  5. 다른 사용자의 session ID와 approval ID를 제출했을 때 모두 거부되는지 확인한다.

timeout은 곧바로 성공으로 기록하지 않는다. DNS 실패인지 NetworkPolicy deny인지 CNI flow log와 backend log로 구분한다. 허용 probe까지 함께 성공해야 policy가 의도한 source만 남겼다고 판단할 수 있다.

HITL과 identity propagation의 추가 gate

섹션 제목: “HITL과 identity propagation의 추가 gate”

이번 필수 Agent는 read-only라 Human-in-the-Loop(HITL) approval을 만들지 않는다. write tool을 여는 단계에서는 다음 항목을 별도 acceptance test로 추가한다.

시험통과 조건
approval streambackend가 A2A stream의 승인 대기 event를 우리 UI 형식으로 번역하고 연결이 끊겨도 재조회
approval ownership요청자·승인자 policy를 서버가 검사하고 다른 사용자의 approval ID를 거부
identity propagationtool에는 승인된 OBO token 또는 workload credential만 전달하고 원본 browser token을 무차별 전달하지 않음
action denymodel prompt 밖의 tool gateway/MCP server가 금지 action을 거부하고 decision log 기록
retryside effect에 idempotency key를 전달해 재시도 중 중복 실행 방지

OIDC 로그인, Kubernetes RBAC, NetworkPolicy 중 하나만으로 이 표를 통과했다고 간주하지 않는다.

  • backend ServiceAccount는 Agent get/list/watch/create/patch만 가능하고 Secret·cluster resource는 거부된다.
  • Grant는 (issuer, subject) 복합 principal을 사용하고 금지 사용자·session 탈취 test가 통과했다.
  • 직접 우회 대상이 Agent Pod가 아니라 controller 8083이라는 것을 설명할 수 있다.
  • NetworkPolicy manifest 적용과 실제 packet deny 검증을 구분해 staging 할 일로 기록했다.
  • write tool 전에 필요한 HITL·OBO·action deny acceptance test를 남겼다.