logo
|
Blog
  • 회사 소개
  • Alarmy
  • DARO
  • Delighthub
  • ENKO
딜라이트룸 채용
엔지니어링

AI 에이전트가 사람의 권한을 넘지 못하게 만드는 법

누구나 시킬 수 있지만 아무거나 할 수 없게
Dan Min's avatar
Dan Min
Sep 28, 2026
AI 에이전트가 사람의 권한을 넘지 못하게 만드는 법
Contents
사람보다 더 많은 권한을 가진 AI 에이전트의 문제는?AI 에이전트의 권한을 어디에서 통제할 것인가?인하우스 MCP 게이트웨이, 왜 이 구조인가게이트웨이를 우회하지 못하게 크리덴셜 회수하기권한을 통제하고 나서야 보이기 시작한 것들

저는 딜라이트룸의 파운데이션(Foundation) 그룹에서 SRE(Site Reliability Engineer)로 일하는 Dan(민선홍)입니다. 파운데이션 그룹은 인프라나 데이터 파이프라인처럼 딜라이트룸의 모든 제품에 기초가 되는 영역을 책임지는 조직입니다. 최근에는 사내 곳곳에서 쓰이는 AI 에이전트(AI Agent)들의 공통 기반을 만드는 일도 맡고 있습니다.

딜라이트룸에서는 사내 슬랙(Slack) 채널에서 누구나 AI 에이전트에게 일을 시킬 수 있습니다. AI 에이전트는 사람이 말로 일을 시키면 필요한 도구를 스스로 골라 실행하는 프로그램입니다. 예를 들어 슬랙 채널에 ‘알라미 최근 DAU 추이 알려줘’라고 쓰면 에이전트가 데이터베이스에서 일간 활성 사용자 수(DAU)를 조회해 답해 주는 식입니다.

배포를 담당하는 에이전트, 인프라를 다루는 에이전트, 외부 서비스와 연동된 에이전트가 이런 식으로 여러 팀에서 쓰이고 있습니다. 개발자가 아니어도 슬랙에 글을 올릴 수 있는 사람이면 누구나 이용할 수 있고 데이터 팀이나 인프라 담당자에게 따로 부탁하지 않아도 되니 편리합니다.

문제는 에이전트가 그 일을 시켜도 되는 사람인지 확인하지 않는다는 것이었습니다.

그래서 이론상으로는 누구나 에이전트를 통해 자기 권한 밖의 일까지 시킬 수 있었습니다. 다른 팀이 관리하는 서비스의 설정을 바꾸거나 자기에게는 열람 권한이 없는 데이터를 조회하는 요청도 걸러지지 않았습니다. 코드 어디에도 '이 요청을 한 사람이 이 작업을 할 권한이 있는가'를 확인하는 단계가 없었기 때문입니다.

이 글에서는 이 문제를 어떻게 인식했는지부터 시작해 에이전트의 권한을 어디에서 어떤 기준으로 통제하기로 했는지를 살펴봤습니다. 이후에는 이 기준을 어떤 구조로 구현했는지와 그 구조를 우회하지 못하도록 에이전트 쪽에 남아 있던 키를 어떻게 회수했는지를 순서대로 정리했습니다.

사람보다 더 많은 권한을 가진 AI 에이전트의 문제는?

딜라이트룸의 AI 에이전트는 어느 한 팀이 계획을 세워 만든 것이 아닙니다. 올해 들어 팀마다 필요한 에이전트를 하나둘씩 직접 만들어 슬랙에 연결했습니다. 배포를 도와주는 에이전트, 인프라 상태를 확인하고 조치하는 에이전트, 외부 서비스의 데이터를 가져오는 에이전트가 이렇게 생겨났지만 각 에이전트가 무슨 권한을 가지고 있는지 한눈에 볼 수 있는 목록은 없었습니다.

그래서 이 문제를 살펴보기 시작하면서 가장 먼저 한 일은 에이전트 전수 조사였습니다. 조사하는 동안에도 새 에이전트가 하나 생겼고 조사를 하면서 처음 존재를 알게 된 에이전트도 있었습니다. 조사해 보니 5~6개의 에이전트가 각 팀에서 따로 만들어져 돌아가고 있었습니다. 에이전트가 많아진 것 자체보다는 이 에이전트들이 공통으로 가진 구조가 문제였습니다.

사진 1: 슬랙 요청이 처리되는 경로. 사람이 슬랙에 글을 쓰면 에이전트가 자기 머신에 저장된 키로 연동된 서비스를 호출한다 (GPT-6 Astra 모델로 제작)
사진 1: 슬랙 요청이 처리되는 경로. 사람이 슬랙에 글을 쓰면 에이전트가 자기 머신에 저장된 키로 연동된 서비스를 호출한다 (GPT-6 Astra 모델로 제작)

터미널에서 클로드 코드(Claude Code) 같은 도구를 써 보신 분이라면 익숙한 장면이 있습니다. 파일을 지우거나 배포하는 것처럼 위험한 명령을 실행하기 전에 도구가 잠시 멈추고 ‘이 명령을 실행할까요?’라고 사람에게 묻고 사람이 승인해야 다음으로 넘어가는 화면입니다.

그런데 슬랙은 메시지를 주고받는 곳이지 터미널이 아니고 사람이 옆에 앉아 매번 승인해 줄 수도 없기 때문에 슬랙 봇에는 이 화면이 없습니다.

그래서 사내 에이전트는 모두 이 확인 단계를 끈 채로 시키는 대로 바로 실행하도록 운영되고 있었습니다. 보통 에이전트가 실제로 서비스를 호출할 때 사용하는 키는 에이전트가 실행되는 컴퓨터에 저장되어 있습니다. 이 글에서는 이 컴퓨터를 ‘에이전트 머신’이라고 부르겠습니다. 이 키에는 읽기는 물론 쓰기까지 많은 일을 할 수 있는 권한이 있었고 만료 기한도 없거나 매우 넉넉했습니다.

슬랙에 글을 쓸 수 있는 사람은 누구든 에이전트에게 말을 걸 수 있고 에이전트는 묻지 않고 실행하니 결과적으로 슬랙에 글을 쓸 수 있는 사람이라면 누구나 에이전트가 가진 권한 전체를 그대로 쓸 수 있었습니다.

문제는 에이전트마다 맡은 일이 다르니 가진 권한도 달랐다는 점입니다. 배포를 돕는 에이전트는 배포 파이프라인(Deployment Pipeline, 코드를 서비스에 반영하는 자동화된 절차)을 실행할 수 있고 데이터를 다루는 에이전트는 데이터베이스를 읽을 수 있으며 인프라를 다루는 에이전트는 클러스터(Cluster, 서비스를 실제로 실행하는 서버 묶음)에 명령을 내릴 수 있습니다.

그중에는 운영 환경에 직접 영향을 줄 수 있는 권한을 가진 에이전트도 있었습니다. 어느 에이전트든 요청을 처리하는 경로 어디에도 '누가 시켰는지'를 확인하는 단계는 없었고 기록에도 에이전트가 무엇을 했는지는 남아도 그 일을 시킨 사람이 누구인지는 남지 않았습니다.

이런 구조가 그대로 유지되고 있었던 데에는 세 가지 이유가 있었습니다.

첫째, 위험을 막는 장치가 프롬프트의 문장뿐이었습니다. 시스템 프롬프트(System Prompt, 에이전트가 따르도록 미리 넣어 두는 지시문)에 ‘운영 환경에 영향을 주는 작업은 하지 마세요’ 같은 문장을 적어 두는 것이 전부였습니다. 동료 한 분의 표현을 빌리면 에이전트에게 서약서를 받아 두는 것과 같았습니다. 서약서는 지키겠다는 약속일 뿐이고 실제로 지키게 만드는 장치는 아닙니다.

둘째, 요청을 거부할 방법이 없었습니다. 권한을 조이려면 특정 사람만 에이전트를 쓰게 하거나 에이전트에서 위험한 기능을 아예 빼야 했습니다. 다만, 이렇게 하면 이미 에이전트로 일하던 사람들의 업무에 바로 지장이 생겼습니다. 거부된 요청을 누군가 확인하고 승인해 주는 중간 단계가 없으니 전부 막거나 전부 여는 양자택일만 남아 있었습니다. 당장은 편리한 쪽이 우선이다 보니 모두 열어 두는 쪽을 택하게 되었습니다.

셋째, 아직 아무 사고도 없었습니다. 에이전트 덕분에 일이 눈에 띄게 빨라지고 있었고 잘 돌아가는 것을 굳이 건드려 불편하게 만들 이유가 없어 보였습니다.

그러나 사고가 없었다는 것과 사고가 날 수 없다는 것은 다른 이야기입니다. 이 구조에서는 누군가 실수로 잘못된 요청을 하거나 에이전트가 읽은 문서에 숨겨진 지시에 속거나 누군가의 슬랙 계정이 탈취되는 순간 에이전트의 모든 권한이 그대로 넘어갑니다. 그래서 이 구조를 바꾸기로 하고 어디에서 무엇을 기준으로 막을지부터 정하기로 했습니다.

즉, 이 프로젝트는 사고가 나서 시작한 것이 아닙니다. 팀원들과 에이전트 코드를 함께 읽으며 논의하다가 '지금 여기까지 할 수 있구나'라는 사실을 발견하면서 시작한 셈입니다.

AI 에이전트의 권한을 어디에서 통제할 것인가?

본격적인 작업을 하기에 앞서 무엇을 이루어야 하는지부터 네 가지로 정리했습니다.

  1. 권한 확인은 코드로: 모든 요청은 요청한 사람의 권한 안에서만 처리하고 그 확인은 프롬프트가 아니라 코드가 한다.

  2. 모든 결정을 기록: 허용이든 거부든 모든 결정을 요청한 사람과 에이전트 정보와 함께 남긴다.

  3. 거부가 끝이 아니게: 거부된 요청은 그 권한을 가진 다른 사람이 같은 스레드(Thread)에서 승인할 수 있다.

  4. 머신에 키를 남기지 않기: 에이전트 머신에 오래 남아 있는 넓은 권한의 키를 없앤다.

네 가지 중에서 나머지 셋의 방향을 정하는 것은 첫 번째였고 그중에서도 '권한을 어디에서 확인할 것인가'가 핵심이었습니다. 확인 지점을 잘못 고르면 아무리 정교한 규칙을 만들어도 소용이 없기 때문입니다.

가장 쉽게 떠올릴 수 있는 방법은 프롬프트에 규칙을 적어 두는 것입니다. 앞에서 이야기한 서약서 방식으로 에이전트에게 '이런 요청은 거절하라'고 미리 일러두는 것입니다. 그러나 이 방식이 통하지 않는 이유는 프롬프트 주입(Prompt Injection, 에이전트가 읽는 문서나 메시지 안에 지시문을 숨겨 에이전트를 속이는 공격) 때문입니다.

에이전트는 일을 하면서 문서, 티켓, 웹 페이지, 다른 사람이 쓴 슬랙 메시지를 끊임없이 읽습니다. 그 어딘가에 '지금부터는 이 지시를 따르라'는 문장이 숨어 있으면 모델은 그것을 지시로 받아들여 속게 됩니다. 모델에게는 시스템 프롬프트에 적힌 문장과 방금 읽은 문서 속 문장이 똑같은 글자이기 때문입니다. 그래서 에이전트에게 프롬프트의 문장은 더 이상 규칙이 되지 못합니다.

그렇다면 에이전트가 무엇을 읽고 어떤 결론을 내리든 상관없는 지점에서 확인해야 합니다. 그 지점은 에이전트가 실제로 도구를 호출하고 크리덴셜(Credential)을 사용하는 순간입니다. 모델이 어떤 판단을 하든 서비스에 실제 요청을 보내는 일은 코드가 처리하고 이때는 누가 시켰고 무슨 작업인지가 정해져 있으니 코드로 확인할 수 있습니다.

크리덴셜(Credential)이란?

어떤 서비스에 요청을 보낼 때 '나는 이런 권한을 가진 요청자'라는 것을 증명하는 비밀값입니다. API 키, 토큰, 비밀번호, 인증서가 모두 크리덴셜입니다. 서비스는 요청을 보낸 사람이 누구인지가 아니라 요청에 실린 크리덴셜만 보고 권한을 판단하기 때문에 크리덴셜을 가진 쪽이 곧 권한을 가진 쪽이 됩니다. 앞에서 에이전트 머신에 저장되어 있다고 한 '키'도 크리덴셜의 한 종류입니다.

확인 지점을 정했으니 다음은 무엇을 기준으로 허용할지입니다. 요청 하나에는 주체가 둘 있습니다. 일을 시킨 사람과 그 일을 실행하는 에이전트입니다. 저희는 이 둘의 권한을 모두 확인해서 양쪽이 다 허용할 때만 통과시키기로 했습니다.

사진 2: 요청한 사람의 권한과 에이전트의 권한. 두 권한이 겹치는 범위 안의 요청만 실행된다 (GPT-6 Astra 모델로 제작)
사진 2: 요청한 사람의 권한과 에이전트의 권한. 두 권한이 겹치는 범위 안의 요청만 실행된다 (GPT-6 Astra 모델로 제작)

둘 다 확인하는 이유는 한쪽만 보면 각각 다른 허점이 생기기 때문입니다. 에이전트의 권한만 보면 앞에서 본 문제가 그대로 남습니다. 슬랙에 글을 쓸 수 있는 사람이면 누구나 에이전트의 권한 전체를 쓸 수 있기 때문입니다. 반대로 사람의 권한만 보면 권한이 큰 사람이 시킬 때 에이전트가 원래 맡지 않은 일까지 하게 됩니다. 데이터를 조회하려고 만든 에이전트가 관리자의 요청이라는 이유로 배포까지 하게 되는 식입니다. 에이전트마다 맡은 일에 맞게 권한을 좁혀 두고 사람의 권한과 겹치는 범위에서만 실행하면 두 허점이 모두 사라집니다.

사람이 시키지 않은 요청도 있습니다. 정해진 시간에 돌아가는 예약 작업이나 알림 같은 이벤트가 에이전트를 깨우는 경우입니다. 이때는 확인할 사람의 권한이 없으니 에이전트의 권한만으로 읽기까지만 허용하고 위험한 작업이 필요하면 승인할 사람을 호출하도록 했습니다.

권한의 등급은 처음부터 잘게 나누지 않고 읽기와 위험한 작업 두 단계로 시작했습니다. 처음부터 세밀하게 나누면 규칙을 만드는 데 시간이 다 가고 정작 아무도 쓰지 못하기 때문입니다. 두 등급의 기본 정책은 서로 반대입니다.

구분

기본 정책

예외

읽기

허용

사람마다 볼 수 있는 범위가 다른 서비스만 차단

위험한 작업

차단

에이전트별로 꼭 필요한 작업만 허용 목록에 추가

읽기를 기본 허용으로 둔 이유는 에이전트 요청의 대부분이 읽기이고 그 대부분이 무해하기 때문입니다. 지표를 조회하거나 배포 상태를 확인하는 요청까지 일일이 막으면 에이전트를 쓰는 이유가 사라집니다. 다만 노션(Notion)이나 구글 드라이브(Google Drive)처럼 문서마다 볼 수 있는 사람이 다른 서비스는 예외로 두어 요청한 사람이 원래 볼 수 있는 문서인지 확인하도록 했습니다.

대신 배포, 설정 변경, 데이터 수정 같은 위험한 작업은 수가 적고 하나하나가 되돌리기 어렵거나 다른 팀에 영향을 주기 때문에 기본 차단으로 두고 에이전트별로 꼭 필요한 작업만 허용 목록에 넣었습니다. 이 허용 목록은 프롬프트가 아니라 설정 파일로 저장소에 두고 코드 리뷰를 거쳐야만 바꿀 수 있습니다.

거부가 그냥 거부로 끝나면 앞에서 본 양자택일로 되돌아갑니다. 요청이 막힌 사람은 에이전트를 포기하고 담당자에게 직접 부탁하게 되고 그러면 에이전트를 둔 의미가 없어집니다. 그래서 거부된 요청은 같은 슬랙 스레드 안에서 바로 승인받을 수 있게 했습니다. 에이전트가 ‘이 작업은 권한이 필요합니다’라는 메시지와 함께 승인 버튼을 스레드에 올리면 그 작업을 직접 할 권한을 가진 다른 사람이 버튼을 눌러 승인하는 방식입니다. 요청한 사람 본인은 승인할 수 없고 승인한 사람이 누구인지도 기록에 남습니다.

사진 3: 슬랙 스레드에 승인 버튼이 올라온 모습
사진 3: 슬랙 스레드에 승인 버튼이 올라온 모습

여기서 한 가지를 분명히 정했는데 승인은 버튼 클릭만 인정하고 ‘승인합니다’라고 쓴 글은 인정하지 않는다는 것입니다. 글은 모델이 읽어서 해석해야 하는데 모델은 없는 승인을 지어낼 수도 있고 다른 뜻으로 쓴 글을 승인으로 잘못 읽을 수도 있습니다. 반면 버튼 클릭은 누가 눌렀는지를 슬랙이 직접 보증해 주기 때문에 모델의 해석이 끼어들 틈이 없습니다.

이 기준을 구현하는 가장 간단한 방법은 에이전트 코드 안에 검사 함수를 넣어서 도구를 호출하기 직전에 사람과 에이전트의 권한을 확인하는 것입니다. 저희도 처음에는 이 방향으로 설계를 시작했지만 곧 부족하다는 결론에 이르렀습니다.

문제는 앞에서 본 것처럼 사내 에이전트가 확인 단계를 끈 채로 셸(Shell, 컴퓨터에 명령을 직접 내리는 창) 명령을 자유롭게 실행한다는 점입니다. 에이전트는 검사 함수를 거치지 않고 머신에 있는 키를 직접 읽어서 서비스를 호출하면 그만입니다. 검사 코드와 에이전트가 같은 프로세스(Process, 실행 중인 프로그램 하나) 안에 있으면 검사는 언제든 건너뛸 수 있는 절차가 됩니다. 크리덴셜이 에이전트 머신에 남아 있는 한 어떤 검사를 붙여도 소용이 없었습니다. 특히 세 후보를 나란히 놓고 보면 차이가 분명했습니다.

확인하는 곳

속은 모델이 우회할 수 있는가

이유

프롬프트의 문장

있음

모델은 문서 속 문장과 프롬프트의 문장을 구분하지 못함

에이전트 코드 안의 검사

있음

셸 명령으로 머신에 있는 키를 직접 사용

별도 서비스(게이트웨이)

없음

머신에 키가 없어 게이트웨이를 거칠 수밖에 없음

방향을 바꿔 에이전트에 검사를 더하는 대신 크리덴셜 자체를 에이전트가 손댈 수 없는 별도의 서비스로 옮겼습니다.

에이전트는 크리덴셜 없이 그 서비스에 '이 작업을 해 달라'는 요청을 전달하기만 하는 단순한 프록시(Proxy, 요청을 대신 전달하는 중개자)가 됩니다. 권한 확인은 크리덴셜을 가진 그 서비스가 하니 에이전트가 무엇을 읽고 무엇에 속든 결과는 달라지지 않습니다.

이렇게 크리덴셜을 대신 맡아 권한을 확인해 주는 서비스를 저희는 게이트웨이(Gateway, 에이전트와 외부 서비스 사이에서 요청을 받아 대신 처리하는 중간 서비스)라고 부르기로 했습니다.

인하우스 MCP 게이트웨이, 왜 이 구조인가

게이트웨이를 만들면서 한 가지 더 정한 것은 MCP(Model Context Protocol, AI 에이전트가 외부 도구를 부를 때 쓰는 표준 규격)로 구현한다는 것이었습니다. MCP를 고른 이유는 에이전트 코드를 거의 고치지 않아도 되기 때문입니다. 사내 에이전트들은 이미 MCP 규격으로 도구를 호출하고 있어서 에이전트 입장에서는 게이트웨이가 새로 추가된 도구 하나로 보일 뿐입니다. 새로운 외부 서비스를 연동할 때도 게이트웨이에 한 번만 추가하면 모든 에이전트가 같은 권한 확인을 거쳐 쓸 수 있습니다.

속은 에이전트가 셸 명령을 마음대로 실행하더라도 이제는 머신에 훔쳐 쓸 키가 없고 게이트웨이로 보내는 요청은 앞에서 정한 기준을 벗어나는 순간 거부됩니다.

사진 4: 요청 하나가 게이트웨이를 거쳐 처리되는 흐름 (GPT-6 Astra 모델로 제작)
사진 4: 요청 하나가 게이트웨이를 거쳐 처리되는 흐름 (GPT-6 Astra 모델로 제작)

요청 하나가 처리되는 흐름은 다음과 같습니다.

  1. 누가 요청했는지 확인한다. 에이전트는 요청을 보낼 때 '슬랙의 어떤 사용자가 이 에이전트에게 시킨 일'인지를 함께 보냅니다. 이 정보는 모델이 아니라 모델을 감싸고 있는 에이전트 프로그램이 붙이고 게이트웨이가 위조되지 않았는지 검증하기 때문에 속은 모델이 다른 사람의 이름을 댈 수는 없습니다.

  2. 그 사람이 할 수 있는 일인지 확인한다. 요청한 사람이 어떤 역할에 속하는지 찾고 그 역할에 허용된 작업인지 대조합니다. 누가 어떤 역할인지는 슬랙 사용자 기준으로 저장소의 설정 파일에 적혀 있습니다.

  3. 에이전트도 그 일을 할 수 있는지 확인한다. 앞에서 언급한 에이전트별 허용 목록과 대조합니다. 사람의 역할과 에이전트의 허용 목록은 같은 저장소에서 코드 리뷰를 거쳐 관리합니다.

  4. 둘 다 허용하는 범위에서만 실행한다. 두 확인을 통과하면 게이트웨이가 크리덴셜로 외부 서비스를 호출하고 결과만 에이전트에 돌려줍니다.

  5. 판단과 실행 결과를 모두 기록한다. 허용이든 거부든 판단 기록이 남고 실제로 실행했다면 실행 기록이 따로 남습니다.

이 흐름 자체는 앞에서 정한 기준을 그대로 옮긴 것이라 특별할 것이 없습니다. 다만 이것을 실제 구조로 옮길 때 왜 그렇게 만들었는지가 궁금하실 수 있습니다. 저희가 내린 결정은 크게 두 가지였습니다.

첫 번째 결정은 게이트웨이를 권한을 판단하는 부분(Auth)과 실제로 서비스를 호출하는 부분(Exec)으로 나눈 것입니다. 하나의 프로그램으로 만들면 더 간단할 텐데 굳이 둘로 나눈 데에는 세 가지 이유가 있습니다.

구분

Auth (권한 판단)

Exec (서비스 호출)

하는 일

요청한 사람과 에이전트의 권한을 확인하고 허가 여부를 정함

허가를 확인하고 크리덴셜로 외부 서비스를 호출

걸리는 시간

밀리초(millisecond, 1000분의 1초) 단위

외부 서비스에 따라 몇 초까지

늘어나는 이유

사람과 역할이 늘어날 때

연동하는 서비스가 늘어날 때

가진 크리덴셜

없음

외부 서비스 크리덴셜 전부

우선 두 작업은 걸리는 시간이 크게 다릅니다. 둘이 한 프로세스에 있으면 외부 호출이 지연될 때 권한 판단까지 함께 지연되고 그 사이 다른 요청들도 기다리게 됩니다. 권한 판단은 언제나 빨리 끝나야 하기 때문에 느린 외부 호출과 분리했습니다.

다음으로 두 부분은 늘어나는 이유가 서로 다릅니다. 둘을 나눠 두면 새 서비스를 연동하는 작업(Exec)이 권한을 판단하는 코드(Auth)를 건드리지 않고 끝납니다.

마지막으로 크리덴셜의 보관 위치가 다릅니다. Exec은 스스로 권한을 판단하지 않고 Auth의 허가가 있어야만 서비스를 호출하고 Auth는 외부 서비스의 크리덴셜을 하나도 갖지 않습니다. 어느 한쪽에 문제가 생겨도 다른 쪽이 가진 권한까지 넘어가지는 않습니다.

두 번째 결정은 Auth의 허가를 한 번만 쓸 수 있는 토큰(Token, 허가증 역할을 하는 짧은 값)으로 전달한 것입니다. Auth가 요청을 허용하면 '이 작업 하나를 이 내용 그대로 실행해도 된다'는 토큰을 발급하고 Exec은 그 토큰을 확인한 뒤에만 서비스를 호출합니다. 토큰은 한 번 확인되는 순간 사용된 것으로 처리되어 다시 쓸 수 없습니다. 예를 들어 '서비스 A를 재시작해도 된다'는 토큰으로는 서비스 B를 재시작할 수 없고 같은 서비스 A를 두 번 재시작할 수도 없습니다.

이렇게 한 이유도 두 가지입니다. 하나는 토큰이 새어 나가는 경우에 대비하기 위해서입니다. 토큰이 중간에 새어 나가더라도 이미 사용된 토큰은 쓸모가 없고 토큰에 적힌 작업과 다른 작업에 돌려쓰려 하면 Exec이 거부합니다.

다른 하나는 규칙을 바꿨을 때 곧바로 적용되게 하기 위해서입니다. 여기서 규칙이란 앞에서 정한 사람별 역할과 에이전트별 허용 목록을 말합니다. 모든 호출이 매번 Auth를 거치기 때문에 이 목록을 고치면 다음 요청부터 바로 새 규칙이 적용됩니다. 만약 한 번 허가를 받으면 한동안 계속 유효한 방식이었다면 권한을 회수당한 사람도 유효 기간이 끝날 때까지는 계속 작업을 시킬 수 있었을 것입니다.

기록은 판단과 실행 두 번 남깁니다. 판단 기록에는 누가 어느 에이전트에게 어떤 작업을 시켰는지, 허용됐는지, 거부됐는지, 그리고 승인이 있었다면 누가 승인했는지가 남고 실행 기록에는 실제로 서비스를 호출한 결과가 남습니다.

둘을 따로 남기는 이유는 허용되었다고 해서 반드시 실행된 것은 아니기 때문입니다. 에이전트가 중간에 멈출 수도 있고 Exec에서 거부될 수도 있고 외부 서비스가 실패할 수도 있습니다. 두 기록을 토큰 번호로 연결해 두면 '허용됐지만 실행되지 않은 요청'과 '실행됐지만 실패한 요청'을 구분할 수 있습니다.

거부된 요청이 스레드 승인으로 이어지는 것도 이 흐름 안에서 처리됩니다. Auth가 요청을 거부하면서 승인이 필요하다고 응답하면 에이전트는 스레드에 승인 버튼을 올리고 권한을 가진 사람이 버튼을 누르면 그 승인을 반영해 Auth가 요청을 다시 확인하고 통과시킵니다.

이렇게 해서 에이전트는 크리덴셜 없이 일하고 모든 권한 판단은 코드에서 이루어지며 모든 결정이 요청한 사람과 에이전트 정보와 함께 기록됩니다.

게이트웨이를 우회하지 못하게 크리덴셜 회수하기

게이트웨이가 제 역할을 하려면 에이전트 머신에 키가 하나도 남아 있지 않아야 한다는 조건이 하나 더 있습니다. 모델이 직접 실행하는 셸 명령은 게이트웨이를 거치지 않기 때문입니다. 머신에 예전 키가 그대로 남아 있으면 속은 에이전트는 게이트웨이에 요청을 보내는 대신 그 키로 서비스를 직접 호출할 수 있고 그러면 게이트웨이의 권한 확인은 아무 의미가 없어집니다.

예를 들어 에이전트가 읽은 문서에 숨겨진 지시에 속아 데이터를 외부로 보내려 할 때 게이트웨이로 요청하면 거부되지만 머신에 있는 키로 직접 호출하면 막을 방법이 없습니다. 게이트웨이를 만든 뒤 바로 이어서 한 일이 에이전트 머신에 남아 있는 키를 모두 회수하는 것이었습니다.

회수의 첫 단계는 전수 조사였습니다. 에이전트 머신마다 '있어야 하는 키'와 '실제로 있는 키'를 나란히 적은 표를 만들었습니다. 있어야 하는 키는 에이전트가 슬랙에 접속하고 게이트웨이에 자기가 누구인지 증명하는 데 필요한 최소한의 것들입니다.

실제로 있는 키는 환경 변수(Environment Variable, 프로그램이 시작할 때 읽어 들이는 설정값), 설정 파일, 머신에 설치된 비밀 관리 도구의 로그인 상태까지 모두 확인해서 찾아낸 것들입니다. 표의 모양은 대략 이랬습니다.

키

있어야 하는가

실제로 있는가

처리

슬랙 접속 토큰

필요

있음

유지

게이트웨이에 자기를 증명하는 키

필요

있음

유지

외부 서비스 키

불필요

있음

게이트웨이로 옮긴 뒤 삭제

클러스터 쓰기 권한

불필요

있음

게이트웨이로 옮긴 뒤 삭제

클러스터 읽기 권한

필요

있음

유지 (사람이 없을 때 조회용)

두 열의 차이가 곧 회수해야 할 목록이었고 키마다 게이트웨이로 옮길지, 그냥 지울지, 이유를 적고 남겨 둘지를 정해 처리 열에 적었습니다. 이 표는 한 번 만들고 끝내지 않고 스크립트로 자동 검사하게 해서 회수한 키가 다시 생기지 않는지 주기적으로 확인했습니다.

키를 삭제할 때는 먼저 슬랙에서 보낸 요청이 게이트웨이를 거쳐 외부 서비스의 응답까지 돌아오는지 확인한 뒤에 비밀 관리 서비스인 도플러(Doppler)에서 에이전트 머신에 배포되던 외부 서비스 키를 삭제했습니다.

삭제한 뒤에는 같은 요청이 여전히 게이트웨이를 통해 처리되는지 다시 확인했습니다. 게이트웨이 경로를 먼저 확인한 것은 게이트웨이 쪽에 문제가 있는 상태에서 키까지 지우면 에이전트가 그 서비스를 쓸 방법이 한동안 없어지기 때문입니다. 또 그때 생기는 문제가 게이트웨이 쪽 문제인지 키를 지워서 생긴 문제인지 구분하기도 어렵습니다.

회수 대상 중 가장 큰 권한은 클러스터 쓰기 권한이었습니다. 인프라를 다루는 에이전트는 쿠버네티스(Kubernetes) 클러스터를 다루는 명령줄 도구인 kubectl로 사실상 무엇이든 할 수 있었습니다. 이 권한을 게이트웨이로 옮기면서 에이전트 환경에서는 kubectl 자체를 없앴습니다. 

대신 미리 정한 여섯 가지 작업(재시작, 규모 조정, 되돌리기, 일시 중지, 재개, 파드(Pod, 쿠버네티스에서 프로그램을 실행하는 가장 작은 단위) 삭제)만 게이트웨이의 도구로 제공했습니다. 이 여섯 가지는 장애 대응에서 실제로 필요했던 작업을 기준으로 골랐습니다. 배포 뒤 문제가 생겼을 때 이전 버전으로 되돌리거나 멈춘 파드를 지워 다시 실행되게 하는 일이 대표적입니다. 무엇이든 할 수 있는 도구 대신 꼭 필요한 작업만 제공한 것이고 이 중에서도 위험한 작업은 여전히 스레드 안에서 사람이 승인해야 실행됩니다.

사진 5: 에이전트 머신이 공격자에게 넘어갔을 때 얻을 수 있는 권한, 회수 전과 후 (GPT-6 Astra 모델로 제작)
사진 5: 에이전트 머신이 공격자에게 넘어갔을 때 얻을 수 있는 권한, 회수 전과 후 (GPT-6 Astra 모델로 제작)

회수를 마치면 에이전트 머신 전체가 공격자에게 넘어가더라도 공격자가 얻는 것은 클러스터를 읽는 권한뿐입니다. 회수 전에는 클러스터에 무엇이든 쓸 수 있는 권한이었습니다. 다만 읽기 권한 하나는 일부러 남겨 두었는데 사람이 없는 새벽에 알림이 왔을 때도 에이전트가 상태를 조회해서 알려 줄 수는 있어야 하기 때문입니다. 이것은 앞에서 정한 '사람이 시키지 않은 요청은 읽기까지만'이라는 기준과도 맞습니다.

회수를 마쳤다고 판단한 뒤에 한 가지 놓친 것을 발견했습니다. 파일과 설정에서 키를 모두 지웠는데도 이전 프로세스 하나가 수일 동안 옛 키를 메모리에 가지고 있었던 것입니다. 회수 전에 시작된 프로세스는 시작할 때 읽어 둔 키를 종료될 때까지 그대로 갖고 있는데 파일만 확인해서는 이것이 보이지 않았습니다. 실행 중인 프로세스 목록까지 확인하고 옛 프로세스를 종료한 뒤에야 회수를 마칠 수 있었습니다.

이렇게 머신의 키를 회수하고 나서야 게이트웨이의 권한 확인이 에이전트가 외부 서비스를 호출하는 유일한 경로가 되었습니다.

권한을 통제하고 나서야 보이기 시작한 것들

권한을 통제하는 구조를 만들고 키를 회수하고 나니 그전에는 보이지 않던 것들이 보이기 시작했습니다.

먼저 에이전트가 접근할 수 있는 서비스가 무엇인지 보이게 되었습니다. 조사 전에는 어떤 에이전트가 어떤 서비스에 접근하는지 누구도 정확히 알지 못했지만 이제는 에이전트가 접근하는 서비스가 모두 목록에 정리되어 있어서 몇 개의 서비스가 연동되어 있는지, 서비스마다 읽기를 열어 둘지, 어떤 쓰기 작업을 허용할지를 바로 알 수 있습니다.

위험도가 높은 서비스는 코드로 차단해 두었기 때문에 어떤 서비스가 막혀 있고 어떤 서비스가 열려 있는지도 이 목록만 보면 알 수 있습니다. 에이전트 목록도 마찬가지로 게이트웨이를 쓰려면 에이전트를 먼저 등록해야 하니 어떤 에이전트가 있고 각각 무엇을 할 수 있는지가 한 곳에 정리됩니다.

누가 어느 에이전트에게 무엇을 시켰는지도 이제는 기록으로 남지만 에이전트를 쓰는 사람 입장에서 달라진 것은 거의 없습니다. 평소처럼 슬랙에서 일을 시키면 되고 자기 권한 밖의 일을 시켰을 때에는 요청이 거부되거나 승인 버튼이 올라옵니다. 에이전트 통제를 강하게 하면서도 쓰는 방식은 그대로 두는 것이 처음부터 목표였습니다.

기록이 뜻밖의 것을 보여 주기도 했습니다. 어느 외부 서비스 연동이 몇 달째 실패하고 있었는데 아무도 모르고 있었던 것입니다. 그동안 수백 건의 요청이 실패했는데 모니터링에도 잡히지 않았고 사용자 제보도 없었습니다. 에이전트는 요청을 보내고 응답도 받았지만 그 응답이 계속 오류였습니다. 이것을 찾아낸 것은 앞에서 이야기한 '허용됐지만 실패한 요청'을 구분하는 실행 기록이었습니다. 요청이 오간다는 것과 기능이 제대로 동작한다는 것은 다른 이야기라는 것을 이때 배웠습니다.

이번 프로젝트를 진행하면서 몇 가지 교훈을 얻었습니다.

프롬프트에 적은 규칙은 강제할 수 없습니다. 무엇이 허용되고 무엇이 막히는지는 어떤 코드가 요청을 확인하는지와 키가 어디에 있는지에 따라 정해집니다.

회수 확인은 머신 전체를 봐야 합니다. 파일에서 키를 지웠다고 끝난 것이 아니었습니다. 환경 변수, 설정 파일, 실행 중인 프로세스까지 확인하고 나서야 회수가 끝났다고 말할 수 있었습니다.

권한 승인을 한 팀이 도맡는 구조는 오래가지 못합니다. 제가 속한 파운데이션 그룹은 네 명뿐이어서 회사 모든 직원의 권한 요청을 이 네 명이 받아서 처리하는 구조였다면 곧 병목이 됐을 것입니다. 그래서 규칙은 코드 리뷰를 거쳐 바꾸고, 승인은 슬랙에서 그 권한을 가진 사람이 하고, 각 팀이 자기 에이전트의 허용 목록을 스스로 관리할 수 있도록 만들었습니다. 파운데이션 그룹의 역할은 권한을 확인하는 구조를 만들고 유지하는 데까지이고 누구에게 어떤 권한을 줄지는 각 팀이 정합니다.

아직 검증하지 못한 부분과 남은 일도 있습니다. 위험한 작업을 승인받아 실행하는 경로는 통제된 시험에서만 확인했고 실제 장애 상황에서 사람이 기다리는 중에 승인이 얼마나 빨리 이루어지는지는 아직 검증하지 못했습니다. 다음 장애 대응 때 이 경로를 실제로 써 볼 계획입니다.

거부가 갑자기 늘어날 때 알려 주는 모니터링도 필요합니다. 지금은 기록을 사람이 들여다봐야 이상을 알 수 있는데 거부 건수가 평소와 달라지면 자동으로 알려 주도록 만들 계획입니다. 그리고 이번에 만든 게이트웨이는 인프라를 다루는 에이전트에 먼저 적용한 것이라 다른 팀의 에이전트들을 같은 구조로 옮기는 일이 남아 있습니다. 에이전트를 하나씩 옮길 때마다 그 에이전트가 가진 키를 다시 전수 조사하고 회수하는 과정을 반복하게 될 것입니다.

AI 에이전트에게 일을 시키는 회사는 앞으로 더 늘어날 것이고 그만큼 에이전트가 사람보다 많은 권한을 갖는 상황도 흔해질 것입니다. 사고가 난 뒤에 고치는 것보다 미리 구조를 바꾸는 편이 비용이 훨씬 적게 듭니다. 비슷한 고민을 하고 계신 분들께 이 글이 참고가 되면 좋겠습니다.

Share article
Contents
사람보다 더 많은 권한을 가진 AI 에이전트의 문제는?AI 에이전트의 권한을 어디에서 통제할 것인가?인하우스 MCP 게이트웨이, 왜 이 구조인가게이트웨이를 우회하지 못하게 크리덴셜 회수하기권한을 통제하고 나서야 보이기 시작한 것들

Delightroom

RSS·Powered by Inblog