
주말 동안 AI 에이전트를 위한 보안 스캐너를 만들었습니다. 이를 정직하게 만드는 데는 훨씬 더 오랜 시간이 걸렸습니다.
요약
AI 에이전트의 보안 취약점을 테스트하기 위한 로컬 플랫폼 'AgentGuard' 개발 과정을 다룹니다. 엔진 중심의 설계와 점진적 빌드 방식을 통해 에이전트가 공격 시나리오에 어떻게 반응하는지 검증하는 도구의 중요성을 설명합니다.
핵심 포인트
- AI 에이전트의 보안을 검증할 구조화된 공격 시나리오 도구 필요성
- 사용자 경험(UX)보다 핵심 테스트 엔진을 우선 구축하는 설계 전략
- 자유 형식 텍스트 외에 구조화된 파라미터 기반 인터페이스 대응의 중요성
- 실제 에이전트 테스트를 통한 점진적이고 반복적인 개발 프로세스
아이디어는 토요일 하루 안에 끝낼 수 있을 만큼 작게 시작되었습니다. 저는 AI 에이전트를 구축했지만, 이를 사람들에게 공개하기 전에 안전한지 확인할 실질적인 방법이 없었습니다. "작동하는가"의 문제가 아니었습니다. 그건 눈으로 확인할 수 있었으니까요. "누군가가 에이전트에게 해서는 안 될 일을 하도록 유도할 수 있는가"가 문제였습니다. 이 질문에는 명확한 답이 없었고, 제가 알기로는 명확한 도구도 없었습니다. 그래서 저 자신을 위한 작은 도구를 만들기로 결심했습니다.
그 결과 탄생한 것이 AgentGuard입니다. 이는 로컬에서 실행되는 테스트 플랫폼으로, AI 에이전트에 구조화된 일련의 공격 시나리오를 투입하고 평이한 언어로 된 보고서를 제공합니다. 보고서에는 성적(letter grade), 심각도별로 분류된 발견 사항, 증거, 그리고 구체적인 수정 방법이 포함됩니다. 이를 침투 테스트(penetration-testing) 스위트보다는 바이러스 스캐너에 더 가까운 것으로 생각하십시오. n8n, Langflow, LangGraph와 같은 도구로 에이전트를 조립하고 있지만, 보안 배경 지식이 없고 실제 사용자를 만나기 전에 자신의 작업물을 확인할 방법이 없는 점점 늘어나는 사람들을 위해 만들어졌습니다.
저는 이 글을 출시 포스트라기보다는 이 프로젝트가 실제로 저에게 가르쳐준 것들에 대한 기록으로서 작성하고 있습니다. 왜냐하면 흥미로운 부분은 결코 아이디어 자체가 아니었기 때문입니다. 아이디어가 실제 에이전트와 만났을 때 부딪힌 모든 것들이 핵심이었습니다.
엔진 우선, 인터페이스는 나중에
여기서는 구축 순서가 중요합니다. 제가 처음에 예상했던 순서와 달랐기 때문입니다. 저는 친숙한 위저드(wizard)부터 시작해서 나중에 테스트 엔진을 덧붙이는 방식으로 진행하지 않았습니다. 저는 핵심 테스트 엔진과 보고서 생성기, 즉 실제로 공격 시나리오를 실행하고 무엇이 좋은 결과이고 나쁜 결과인지를 결정하는 부분을 먼저 구축했습니다. 그리고 그것이 제대로 작동한 후에야 스택 트레이스(stack trace)를 읽지 못하는 사람들도 쉽게 접근할 수 있게 해주는 프론트엔드 위저드를 구축했습니다.
그러한 순서로 작업한 것은 제가 예상하지 못했던 이유로 옳은 결정이 되었습니다. 엔진이야말로 실제 제품 결정이 이루어지는 핵심이기 때문입니다. 프론트엔드(frontend)는 오후 한나절 만에도 다시 설계할 수 있습니다. 하지만 무엇을 보안 실패(security failure)로 간주할 것인지, 그리고 도구가 그 결과에 대해 어느 정도의 확신을 가지고 말하도록 허용할 것인지에 대한 문제는, 실제 사용자들이 실제 보고서를 읽기 시작한 이후에는 되돌리기가 훨씬 더 어렵습니다. 이 부분을 먼저 제대로 설정했다는 것은, 이후에 진행된 UX 작업이 정직하게 표현할 수 있는 무언가를 갖게 되었음을 의미했습니다.
범위가 지정된 점진적 빌드(scoped, incremental build)는 미리 세워진 계획에서 나온 것이 아닙니다. 엔진을 제 자신의 에이전트(agent)에 대해 테스트하고, 두 번째 에이전트, 세 번째 에이전트로 이어지는 과정에서, 각각의 에이전트가 제가 생각하지 못했던 무언가를 망가뜨리면서 만들어진 결과였습니다. 이것이 이 이야기 전체의 진정한 모습입니다.
그러다 망가졌다
첫 번째 실제 테스트는 제 자신의 에이전트가 아니었습니다. 핵심 엔진이 안정화된 후, 제가 도구를 겨냥했던 Langflow의 보험 청구 처리(claims-processing) 에이전트였습니다. AgentGuard의 전체 테스트 접근 방식은 하나의 조용한 가정 위에 구축되어 있었습니다. 바로 모든 에이전트가 챗봇(chatbot)처럼 자유 형식의 텍스트 채팅 입력(free-text chat input)을 받는다는 가정이었습니다. 하지만 보험 청구 처리 에이전트는 그렇지 않았습니다. 그것은 구조화된 파라미터 기반 인터페이스(parameter-based interface)를 가지고 있었으며, 채팅창도, 프롬프트(prompt)를 주입할 자유 형식의 텍스트 필드도 없었습니다.
그 순간의 본능은 임시방편으로 해결하는 것입니다. 이 특정 프레임워크에 대해서만 예외 처리를 하거나, "Langflow처럼 보이면 X를 수행하라"는 식의 if-문(if-statement)을 추가하는 식이죠. 저는 그렇게 하지 않았습니다. 실제 문제는 Langflow가 아니었기 때문입니다. 문제는 도구가 대화를 시작하기 전에 자신이 무엇과 대화하고 있는지 알지 못했다는 점이었습니다. 그래서 패치(patch) 대신, 제대로 된 입력 탐색 계층(input-discovery layer)을 구축했습니다. 즉, 테스트가 실행되기 전에 에이전트의 실제 인터페이스를 조사하는 단계를 만들어, 도구가 입력 공간의 형태를 가정하는 대신 실제 형태를 이해하도록 만든 것입니다. 이 결정은 패치를 하는 것보다 더 많은 시간을 소모했습니다. 하지만 이것이 바로 이 도구가 제가 특별히 제작하지 않은 에이전트들에서도 작동하는 이유이기도 합니다.
채팅창처럼 보였던 버그
모든 문제가 구조적인 것은 아니었습니다. 어떤 것들은 당혹스러울 정도로 문자 그대로의 문제였습니다. 실제 비즈니스 웹사이트를 대상으로 테스트하던 중, 도구의 가시성 탐지 (visibility-detection) 단계에서 채팅창이라고 판단한 것을 발견했습니다. 하지만 그것은 채팅창이 아니었습니다. 그것은 페이지의 마크업 (markup) 어딘가에 숨겨져 있던, 제가 테스트하려던 에이전트와는 아무런 관련이 없는 숨겨진 무관한 폼 필드 (form field)였습니다. 도구는 마침내 포기하고 실패하기 전까지, 죽은 HTML 조각과 대화하려고 30분 넘게 인내심 있게 시도했습니다.
30분 동안 조용하고 자신만만하게 틀린 상태를 유지하는 것은 어떤 도구에게도 좋지 않은 실패 모드 (failure mode)이지만, 보안 제품에게는 특히나 최악의 모드입니다. 보안 제품의 전제 조건 자체가 "당신의 리스크에 대해 내가 말하는 것을 믿으라"이기 때문입니다. 해결책은 탐지 로직 (detection logic)을 강화하는 것뿐만이 아니었습니다 (물론 그것도 중요했습니다). 도구가 찾고 있는 것을 찾을 수 없을 때, 잘못된 추측을 붙들고 계속 나아가는 대신 즉시 명확하게 알리도록 만드는 것이었습니다. 이제 동일한 유형의 실패는 무엇을 찾지 못했는지와 그 이유에 대한 명확한 설명과 함께 1초도 안 되어 나타납니다. 결과만 들으면 더 나빠 보일 수 있지만, 중요한 모든 면에서 훨씬 더 나은 결과입니다.
도구가 의도치 않게 거짓말을 했을 때
저에게 가장 기억에 남는 버그는 찾으려고 애쓰지 않는 한 전혀 보이지 않는 것이었습니다. 파이프라인 (pipeline) 어딘가에서, 누구에게도 보여줄 의도가 없었던 내부 기록용 데이터인 폴백 세션 식별자 (fallback session identifier)가 특정 조건 하에서 테스트 중인 에이전트의 실제 답변을 조용히 대신하고 있었습니다. 도구는 에러를 발생시키지도 않았고, 이상한 점을 감지하지도 않았습니다. 그저 그 폴백 값을 가져와서 에이전트가 말한 실제 콘텐츠인 것처럼 취급했고, 실제 응답인 것처럼 채점했습니다.
외부에서 보기에 그 실패는 버그처럼 보이지 않았습니다. 도구는 끝까지 실행되었고, 보고서를 생성했으며, 점수를 부여했습니다. 바로 그 점이 이 글을 쓸 가치가 있게 만드는 부분입니다. "도구가 성공적으로 실행되었다"와 "도구가 진실을 말했다"는 동일한 주장이 아니며, 첫 번째를 만족하면서 두 번째는 실패하는 것이 충분히 가능하기 때문입니다. 대부분의 소프트웨어에서 이러한 차이는 짜증스러운 일일 뿐입니다. 하지만 누군가에게 무엇을 믿어야 할지 알려주는 것이 업무의 전부인 보안 도구(security tool)에서, 이는 자격 미달의 문제입니다. 그 간극을 찾아 메우는 것은 제가 추가한 그 어떤 기능보다 이 프로젝트에 더 중요했습니다.
제가 넘지 않기로 결정한 선
그런 버그들을 수정하던 도중, 저는 AgentGuard가 수행한다고 주장하는 범위를 좁히기로 의도적인 결정을 내렸습니다. 더 명백하고 흥미로운 홍보 문구는 "어떤 AI 에이전트든 테스트하세요"일 것입니다. 하지만 실제 제품은 더 좁은 범위를 약속합니다. 즉, 당신이 직접 구축했거나 관리하는 에이전트를 테스트하는 것입니다.
이것은 제가 사과해야 할 제한 사항이 아니라, 제가 의도적으로 선택한 경계입니다. 대상에 대해 적대적 공격 시나리오(adversarial attack scenarios)를 실행하는 도구가 URL이 연결된 무엇이든 겨냥할 수 있다면, 명백한 이중 용도(dual-use) 문제를 야기합니다. 범위를 당신이 소유하거나 관리하는 에이전트로 한정하는 것은 제가 맞닥뜨린 기술적 제약이 아닙니다. 그것은 누군가 요청하기 전에 제가 스스로 그은 동의와 정직함의 선입니다. 헤드라인에서 더 멋지게 들릴 법한 약속보다는 규모가 작지만, 저는 이것이 올바른 방향이라고 생각합니다.
제품의 실제 모습
이야기 이면에서 제품의 형태는 실제 고민이 많이 반영된 부분이기에, 이를 솔직하게 짚어볼 가치가 있습니다.
위저드(wizard)는 사용자에게 연결 프로토콜을 선택하라고 묻는 것으로 시작하지 않습니다. 대신 아주 일상적인 언어로 한 가지 질문을 던집니다. "현재 에이전트와 어떻게 대화하시나요?" 그리고 네 가지 일상적인 답변을 제공합니다. 웹페이지의 채팅창, API 엔드포인트(endpoint) 및 키(key), 본인의 코드, 또는 아직 라이브 에이전트는 없지만 시스템 프롬프트(system prompt)만 있는 상태 중 하나를 선택하면 됩니다. 이 각각의 답변은 적절한 기저 테스트 방법으로 보이지 않게 매핑됩니다. 사용자는 그중 어떤 것이 사용되는지 알 필요도, 신경 쓸 필요도 없습니다.
실제 테스트를 실행하기 전에, 빠른 무결성 검사 (sanity check) 단계가 있습니다. 이는 도달할 수 없거나 잘못 설정된 대상을 몇 초 만에 잡아내는 저렴한 사전 점검 (pre-flight) 단계입니다. 응답할 리가 없는 대상을 상대로 전체 테스트 스위트 (test suite)가 몇 분 동안이나 돌아가는 것을 지켜보는 것만큼 최악인 일은 없습니다. 따라서 이 도구는 비용이 많이 드는 단계에 도달하기 전에, 빠르게 실패를 알리고 그 이유를 설명합니다.
대상이 해당 검사를 통과하면, 도구는 내성 검사 (introspection) 단계를 실행하고 사용자가 실제로 무엇을 발견했는지 평이한 언어로 보여줍니다. 즉, 에이전트의 실제 입력값과, 웹페이지의 인터페이스가 전송할 수 있다고 암시하는 내용과 실제 기반 API (underlying API)가 수락하는 내용 사이의 모든 격차를 보여줍니다. 그 격차가 바로 Langflow 에이전트가 노출했던 것이며, 이제는 도구가 조용히 우회하는 대상이 아니라 사용자가 직접 확인할 수 있는 일급 객체 (first-class thing)가 되었습니다.
그다음에는 짧고 선택적이며 범위가 제한된 인터뷰가 이어집니다. 이 특정 에이전트에게 나쁜 결과란 실제로 어떤 모습일지에 대한 몇 가지 평이한 질문을 던지며, 이는 나중에 모호한 결과에 대한 판단을 날카롭게 다듬는 데 사용됩니다. 이는 의도적으로 작고 범위가 고정되어 있습니다. 즉, 개방형 심문이 아니라 몇 가지 질문으로 구성됩니다. 목표는 더 나은 컨텍스트 (context)를 얻는 것이지, 두 번째 온보딩 (onboarding) 흐름을 만드는 것이 아니기 때문입니다.
테스트된 모든 에이전트는 실행 중인 대시보드 (dashboard)에 기록되므로, 보고서가 닫힌다고 해서 아무것도 사라지지 않습니다. 무엇이 스캔되었는지, 점수가 어떻게 매겨졌는지, 그리고 무엇이 여전히 실패하고 있는지에 대한 명확한 이력을 확인할 수 있습니다:
테스트 실행 자체는 다음과 같은 보고서를 생성합니다: 어떻게 계산되었는지에 대한 정직한 설명이 포함된 종합 등급, 실제 증거와 구체적인 수정 제안이 포함된 심각도별 결과 그룹, 그리고 — 하단에 숨겨두지 않고 시각적으로 동일한 비중을 두어 — 무엇을 테스트할 수 없었는지와 그 이유를 설명하는 섹션입니다. "모른다"는 상태를 거짓으로 깨끗한 점수 뒤에 숨기는 대신, 명확하게 라벨링된 정당한 제품 상태로 취급하는 것은 전체 빌드 과정에서 가장 어려운 UX (User Experience) 결정 중 하나였으며, 제가 가장 옹호하고 싶은 부분이기도 합니다.
이것이 실제로 증명하는 것
제가 이를 직접적으로 명시하는 이유는, 단순히 기술이 드러나기를 바라는 것이 아니라 기술을 명확하게 보여주기 위해 기록하는 것이 목적이기 때문입니다.
AI 제품 개발 (AI product development). 이 작업의 대부분은 AI 코딩 도구를 단순한 코드 생성 단축키가 아닌 진정한 엔지니어링 파트너로 활용하며 구축되었습니다. 즉, 모호한 요청 대신 정밀하고 범위가 지정된 수정 요청을 작성하고, "괜찮아 보인다"는 답변을 수용하는 대신 실제 수치를 통한 검증을 고집하며, 배포 전 회귀(regression)를 포착하고, 명세서의 문자 그대로의 표현이 더 나은 현실적인 답변을 위해 유연하게 적용되어야 할 때를 아는 과정입니다 (Langflow 패치 위의 입력-탐색 레이어는 그러한 판단력의 가장 명확한 예시입니다).
UX 및 제품 사고 (UX and product thinking). 기술적으로는 정확한 기능이라도 비기술적 사용자에게 혼란을 줄 수 있는 시점을 인식하고, 근거 없는 자신감 대신 쉬운 언어와 정직한 부분적 답변을 중심으로 재설계하는 능력입니다. "테스트할 수 없었던 항목" 섹션은 그러한 직관의 가장 명확한 결과물이며, 제품의 약속을 과장하기보다 오히려 좁히기로 한 결정 또한 마찬가지입니다.
정보 구조 (Information architecture). 제품의 실제 형태 — 네 가지 기술적 연결 방식 (connection methods)을 대신하는 하나의 평이한 언어로 된 입력 질문, 비용이 많이 드는 테스트 실행 전의 저렴한 사전 점검 (pre-flight check), 결함을 숨기는 대신 표면화하는 자기 성찰 (introspection) 단계, 개방형 인터뷰 대신 작고 제한된 인터뷰, 그리고 불확실성을 설계된 상태 (designed state)로 취급하는 보고서 — 그 자체가 증거입니다. 이러한 형태 중 어느 것도 우연히 만들어진 것이 아니며, 이 모든 것은 실제 에이전트(agent)가 실제 가정(assumptions)을 깨뜨리는 것을 관찰한 결과에서 비롯되었습니다.
현재 실제 상태
핵심 엔진, 보고서 생성기(report generator), 그리고 프론트엔드 위저드(frontend wizard)가 구축되어 엔드 투 엔드(end to end)로 작동하고 있습니다. 마지막 작업 단계는 기능을 추가하는 것보다 도구를 정직하고 적응 가능하게 만드는 데 집중되었습니다. 즉, 결과를 조용히 잘못 보고할 수 있는 유형의 버그를 수정하고, 위에서 설명한 입력 발견 레이어(input-discovery layer)를 구축하며, 에이전트의 API가 실제로 인터페이스가 사용자에게 보여주는 것과 동일한 제한 사항을 강제하는지에 대한 첫 번째 기계적 점검을 추가하는 작업이었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기
