가장 안전한 경계는 에이전트가 넘을 수 없는 경계이다
요약
멀티 테넌트 인프라에서 자율 AI 에이전트의 보안을 위해 '권한 제어'가 아닌 '구조적 부재'를 통한 경계 설정을 제안합니다. 에이전트가 잘못된 테넌트의 리소스에 접근하는 것을 방지하기 위해, 접근 권한을 검사하는 방식 대신 해당 리소스의 경로 자체를 에이전트의 환경에서 제거하는 설계 방식을 다룹니다.
핵심 포인트
- 권한(Permission) 기반 제어는 규칙 오류 시 보안 사고로 이어질 수 있음
- 가장 강력한 보안은 리소스가 금지되는 것이 아니라 구조적으로 부재하게 만드는 것
- 경계는 결정 시점이 아닌 존재(Existence) 시점에 강제되어야 함
- 에이전트의 세션은 활성화된 조직에만 엄격하게 범위가 지정되어야 함
헌법적 안전 모델 (constitutional safety model) 하에서 실제 멀티 테넌트 (multi-tenant) 인프라를 운영하는 자율 AI 유기체를 구축하는 시리즈의 두 번째 글입니다. 첫 번째 파트는 두 개의 게이트 — 양심 (conscience)과 의회 (council)에 관한 것이었습니다. 이번 글은 그 뒤에 있는 벽에 관한 것입니다.
제 에이전트는 하나 이상의 조직을 위해 인프라를 운영합니다. 이 문장은 보안 담당자를 불편하게 만들어야 하며, 실제로 그래야 합니다. 왜냐하면 실패 모드 (failure mode)가 미묘하지 않기 때문입니다. 악몽은 에이전트가 영리하지만 잘못된 행동을 하는 것이 아닙니다. 그것은 에이전트가 일상적이고 올바른 행동을 하는 것 — 티켓 작성, 비밀 값 (secret) 순환, 상태 게시 — 을 잘못된 테넌트 (tenant)에게 수행하는 것입니다.
고객 A의 데이터가 고객 B의 시스템으로 들어가는 것은 패치로 해결할 수 있는 버그가 아닙니다. 그것은 공개해야 하는 침해 사고 (breach)입니다.
따라서 제가 답해야 했던 첫 번째 질문은 "어떻게 하면 에이전트가 테넌트 간에 능력을 갖추게 할 것인가"가 아니었습니다. 그것은 바로: 어떻게 하면 테넌트 경계를 넘는 것을 에이전트가 잘못할 수 있는 일이 아닌, 아예 할 수 없는 일로 만들 것인가였습니다.
권한 (Permission)은 약한 버전이다. 부재 (Absence)가 강력한 버전이다.
모두가 가장 먼저 떠올리는 본능은 권한 (permissions)입니다. 에이전트에게 접촉이 허용된 목록을 주고, 모든 동작을 그 목록과 대조하여 나머지는 거부하는 방식입니다. 역할 기반 액세스 제어 (Role-based access), 정책 파일 (policy file), 게이트 (gate) 등이 이에 해당합니다.
권한 게이트는 한 가지 특정한 치명적인 방식으로 실패합니다: 그것들은 요청된 대상이 존재하며, 단지 당신이 '아니오'라고 말해야만 한다고 가정합니다. 에이전트가 고객 B에게 접근하려는 의도를 형성하면, 게이트가 이를 평가하고, 게이트가 이를 거부합니다. 이는 게이트에 버그가 있거나, 오래된 규칙이 있거나, 누락된 케이스가 있기 전까지는 제대로 작동합니다. 하지만 그런 문제가 발생하는 순간, 해당 리소스가 바로 그곳에 있고, 도달 가능하며, '예'라는 대답을 기다리고 있기 때문에 의도는 그대로 통과해 버립니다.
더 강력한 모델은 고객 B의 리소스가 금지된 것이 아니라, 구조적으로 부재한다는 것입니다. 고객 A에게 범위가 지정된 (scoped) 세션 내에서, 에이전트는 고객 B로 향하는 거부된 경로(denied path)를 가진 것이 아닙니다. 아예 경로 자체가 없습니다. 자격 증명(credentials)이 로드되지 않았습니다. 엔드포인트(endpoints)가 에이전트의 맵(map)에 존재하지 않습니다. 요청할 대상이 없으므로 거부할 것도 없으며, 따라서 잘못될 거부 로직(deny-logic)조차 존재하지 않습니다.
'금지됨(Forbidden)'은 규칙에 관한 사실입니다. '부재함(Absent)'은 세상에 관한 사실입니다. 규칙에는 버그가 있지만, 세상에는 버그가 없습니다.
구체적으로 말하면: 권한(capabilities)은 세션별로 발행되며, 활성화된 조직에 범위가 지정(scoped)됩니다. 그리고 여기에는 다른 누구의 권한도 포함되지 않습니다. 경계는 결정 시점(decision time)에 강제되는 것이 아닙니다. 경계는 존재(existence) 시점에 강제됩니다.
함정: 경계는 비밀(secrets)이 아니라 엔드포인트(endpoints)이다.
이 지점은 제가 인정하고 싶지 않을 만큼 오랫동안 틀렸던 부분이며, 많은 사람이 조용히 틀리고 있다고 생각하는 지점이기도 합니다.
저에게는 비밀 관리자(secrets manager)가 있었습니다. 조직별 토큰, 조직 간 경로를 거부하는 정책 등 모든 것이 갖춰져 있었습니다. 저는 스스로에게 말했습니다. '비밀이 격리되어 있으니, 테넌트(tenants)도 격리되어 있다. 깔끔하군. 끝났다.'
하지만 끝나지 않았습니다. 이 설계를 아이디어 게이트(idea-gate) — 1부의 위원회 — 에 통과시켰을 때, 모델 중 하나가 정확히 그 간극을 지적했습니다. 그 지적은 매우 날카로워서 저는 여전히 그것을 인용하곤 합니다.
비밀 관리자는 _비밀(secrets)_을 격리합니다. _엔드포인트(endpoints)_를 격리하는 것이 아닙니다.
세션이 완벽하게 올바른 '고객 A용 토큰'을 보유하고 있더라도 고객 B의 주소로 POST 요청을 보낼 수 있습니다. 만약 그 주소들이 에이전트가 읽는 어떤 병합된 설정(merged config) 안에 존재하고, 에이전트가 잘못된 것을 선택한다면 말입니다. 자격 증명은 맞았습니다. 목적지가 틀렸을 뿐입니다. '비밀이 격리되어 있다'는 논리로는 이를 잡아낼 수 없습니다. 유출이 비밀에서 일어나는 것이 아니라, _라우팅(routing)_에서 일어나기 때문입니다.
게다가 상황은 더 악화됩니다. 라우팅 메타데이터 자체가 민감하기 때문입니다. 어떤 고객이 존재하는지, 그들의 시스템 이름이 무엇인지, 그들의 프로젝트 키가 무엇인지에 대한 목록은 공유 설정(shared config)에 흩뿌려 놓을 수 있는 공개 정보가 아닙니다. 맵(map) 자체가 비밀의 일부입니다.
해결책은 데몬(daemon)이 아니라 불변량(invariant)이다
해결책에 대한 나의 첫 번째 직관은 중앙 디스패처(central dispatcher)였습니다. 모든 액션(action)이 통과하며 테넌트(tenant) 일치 여부를 확인하는 특권화된 단일 서비스 말입니다. 위원회(council)는 이 또한 기각했습니다. 그리고 그 결정은 옳았습니다. 단일 병목 지점(chokepoint)은 병목 현상을 일으킬 뿐만 아니라, 아주 적은 인원이 유지 관리하는 시스템에 있어서는 거대한 공격 표면(attack surface)이 됩니다. (이것이 바로 1부에서 보여준 위원회의 역할입니다. 그럴듯하지만 틀린 해결책이 구축되기 전에 미리 차단하는 것이죠.)
살아남은 것은 더 작고 강력했습니다. 서비스가 아닌, **불변량 (invariant)**이었습니다.
모든 외부 리소스는 분리할 수 없는 하나의 레코드로 결합됩니다: 리소스 → (엔드포인트 (endpoint), 자격 증명 (credential), 소유 테넌트 (owning-tenant)). 소유자를 동시에 얻지 않고서는 주소(address)를 얻을 수 없습니다. 그리고 외부 액션을 수행하는 유일한 라이브러리는 레코드의 테넌트가 세션의 테넌트와 일치하지 않으면 실행을 **거부 (refuses)**합니다.
하드코딩(hardcode)을 통해 이를 우회할 수도 없습니다. 왜냐하면 하드코딩할 수 있는 느슨한 엔드포인트 자체가 존재하지 않기 때문입니다. 주소는 오직 소유자와 용접된 상태로만 존재합니다. 잘못된 테넌트에 대한 쓰기(write)는 거부되는 것이 아닙니다. 그것은 표현 불가능 (unrepresentable) 합니다.
이것이 단 한 번의 조치에 담긴 전체 철학입니다. '안 된다'라고 말하는 체크(check)를 추가하지 마세요. 체크가 필요할 법한 형태(shape) 자체를 제거하십시오.
하나의 벽은 결코 완전한 벽이 될 수 없기에, 그 뒤에는 두 개의 레이어(layer)가 더 존재합니다.
- 도구 수준에서의 우회 불가. 실행 전 훅(pre-execution hook)이 가공되지 않은 외부 호출(raw outbound calls)을 차단합니다. 에이전트(agent)는 일반적인 HTTP 도구로 쉘(shell)을 실행하여 디스패처를 우회할 수 없습니다. 안전한 경로(safe path)는 예의 바른 기본값이 아니라, 유일하게 연결된 경로입니다.
- 목줄이 채워진 이그레스 (Egress on a leash). 각 세션은 자신의 테넌트가 허용하는 주소로만 통신할 수 있습니다. 잘못된 테넌트의 하드코딩된 주소는 애플리케이션 레이어(application layer)에서 연결 거부(connection refused)를 당하는 것이 아니라, 경로(route) 자체가 아예 존재하지 않습니다.
구조적 격리(structural isolation), 그 다음 우회 차단(bypass block), 마지막으로 이그레스 범위 지정(egress scoping). 세 개의 독립적인 레이어가 존재하며, 경계를 넘으려면 이 세 가지를 모두 무력화해야 합니다. 단 하나의 버그로 문이 열리는 일은 없습니다.
반전: 이 벽은 닫힌 상태로 실패 (fails closed) 합니다 — 그리고 이것은 내가 지난번에 말한 모든 것과 모순됩니다.
만약 1부를 읽으셨다면, 제가 에이전트의 양심은 fail-open (오픈 실패) 방식이어야 한다고 주장했던 것을 기억하실 겁니다. 즉, 안전 반사(safety reflex)가 불확실할 때, 시스템이 모든 의구심에 대해 동작을 멈춰버리면 결국 퇴출당하기 때문에 일단 동작을 허용한다는 것입니다. 안전보다 생존 가능성(Viability)이 우선이라는 논리였습니다.
그런데 왜 여기서 저는 fail-closed (클로즈 실패) 방식으로 동작하는 벽을 쌓고 있는 걸까요? 즉, 유기체(organism)가 자신이 어떤 테넌트(tenant)를 위해 행동하고 있는지 긍정적으로 확인할 수 없다면, 아무것도 하지 않도록 만드는 것입니다. 범위가 지정되지 않은 세션(unscoped session)은 외부 쓰기(external writes)를 전혀 수행할 수 없습니다. "아마 괜찮을 테니 진행하세요"가 아니라, 제로(Zero)입니다.
이것은 명백한 모순처럼 보입니다. 하지만 모순이 아닙니다. 그리고 이 모순을 풀어내는 것이 이 글의 핵심 교훈입니다.
이들은 서로 다른 축(axes)이며, 서로 반대되는 기본값(defaults)을 가집니다.
- 행동(actions)에 대해서 — "이 명령을 실행하는 것이 안전한가?" — 기본값은 **"네, 진행하세요"**입니다. 불확실성은 움직임(motion) 쪽으로 해결됩니다. 행동할 수 없는 유기체는 유기체가 아니기 때문입니다.
- 테넌트 경계(tenant boundaries)에 대해서 — "이 데이터는 누구의 것인가?" — 기본값은 **"아니요, 중단하세요"**입니다. 불확실성은 정지(stillness) 쪽으로 해결됩니다. 잘못된 테넌트의 데이터에 대해 행동하는 것은 되돌릴 수 없는 단 하나의 치명적인 실수이기 때문입니다.
Fail-open은 유기체를 살려둡니다. Fail-closed는 유기체가 다른 누군가를 죽이지 않도록 막습니다. 성숙한 시스템은 일률적으로 신중하거나 일률적으로 대담하지 않습니다. 시스템은 자신이 _어떤 차원(dimension) 위에 서 있는지_를 알고, 그 차원이 요구하는 기본값을 선택합니다.
이 원칙이 가장 최근에 적용된 사례는 다음과 같습니다. 저는 에이전트가 단순한 검색(retrieval)으로는 답할 수 없는 질문에 답하기 위해, 자신의 지식 베이스(knowledge base) 위에서 코드를 실행할 수 있게 하는 레이어를 프로토타이핑하고 있습니다. 테넌트별로 분할된 데이터(tenant-partitioned data) 위에서 코드를 실행하는 것은, 새로운 탈을 쓴 전형적인 테넌트 간 침범(cross-tenant)의 악몽입니다. 코드 한 줄을 쓰기 전부터 정해진 타협 불가능한 제약 조건은 다음과 같습니다. 코드는 fail-closed 방식으로 동작하는, 위조 불가능하고(unforgeable), 테넌트 범위가 지정된(tenant-scoped), 읽기 전용 권한(read-only capability) 하에서 실행되어야 합니다. 생성된 코드는 테넌트, ID, 또는 자격 증명(credential)을 명시할 수 없습니다. 이러한 정보들은 서버 측에서 바인딩되며, 모델이 입력한 그 어떤 것에서도 가져오지 않습니다. 같은 벽이지만, 새로운 방인 셈입니다.
이것이 제 설정 너머에서 중요한 이유
멀티 테넌시 (Multi-tenant)는 실제 인프라 작업의 기본 형태입니다. 자율 에이전트 (autonomous agent)가 하나 이상의 고객에게 닿는 순간, "주의하십시오"라는 말은 더 이상 전략이 될 수 없습니다. 주의는 결정의 속성일 뿐이며, 결정에는 버그가 존재하기 때문입니다.
멀티 테넌시 시스템에 풀어놓은 에이전트에 대해 던져야 할 가치 있는 질문들은 그 능력 (capability)에 관한 것이 아닙니다:
- 잘못된 테넌트 (tenant)에 대해 동작할 때, 무엇이 이를 막아주는가 — 올바르게 실행되어야만 하는 규칙인가, 아니면 결코 넘을 수 없었던 벽인가?
- 당신의 경계는 금지된 (forbidden) 것인가 (당신이 유지해야 하는 체크 항목) 아니면 부재하는 (absent) 것인가 (존재하지 않는 형태)?
- 시스템이 "이것이 안전한지 확신할 수 없음" (진행)과 "이것이 누구의 데이터인지 확신할 수 없음" (즉시 중단) 사이의 차이를 알고 있는가?
능력 (Capability)은 모두가 구축하기 위해 경쟁하는 부분입니다. 격리 (Isolation)는 당신이 이 시스템을 실제 운영 환경 (production)에서 켤 수 있을지를 결정하는 부분입니다.
가장 안전한 경계는 에이전트에게 넘지 말라고 말해진 경계가 아닙니다. 에이전트가 넘을 수 없는 경계입니다.
다음: 자율 에이전트를 위한 심층 방어 (defense in depth) — 왜 이 단계를 포함한 그 어떤 단일 계층도 유기체와 실수 사이를 막아주는 유일한 수단이 되어서는 안 되는가.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기