
프롬프트 인젝션(Prompt Injections)이 0.1밀리초 만에 사라지는 것을 지켜보기
요약
AI 에이전트를 위한 보안 방화벽 시스템인 SANCTUM 구축 방법을 소개합니다. 이 시스템은 프롬프트 인젝션, 탈옥, 데이터 유출 등 6가지 주요 공격을 모델에 도달하기 전 0.1밀리초 만에 차단합니다.
핵심 포인트
- SANCTUM은 6가지 주요 LLM 공격 클래스를 실시간으로 탐지 및 차단함
- Inspect, Score, Enforce, Egress의 4단계 보안 프로세스 작동
- OWASP LLM Top 10 카테고리에 기반한 실질적인 위협 모델링 제공
- SigNoz를 활용하여 모든 보안 결정 사항을 실시간 트레이싱함
저는 AI 에이전트에게 실제로 작동하는 모습을 지켜볼 수 있는 보디가드를 부여했습니다. 이는 6가지 종류의 공격을 포착(catches six kinds of attack)하는 방화벽이며, 모델이 인지하기도 전에 공격을 차단하고, 모든 결정을 발생하는 즉시 SigNoz로 스트리밍합니다. 제가 일주일 만에 혼자서 SANCTUM을 구축한 방법을 소개합니다.
현재 우리가 AI 에이전트를 구축하는 방식에는 기묘한 사각지대가 있습니다. 우리는 에이전트를 도구(tools)에 연결하고, 메모리(memory)를 부여하며, 현실 세계에서 행동하도록 만듭니다. 그러고 나서 우리는 지루한 웹 서버를 관찰할 때와 똑같은 렌즈를 통해 에이전트를 지켜봅니다. 요청이 들어왔습니다. 응답이 나갔습니다. 상태 코드 200. 모두가 만족합니다.
하지만 AI 에이전트는 웹 서버가 아닙니다. 에이전트는 자연어(natural language)를 읽고 무엇을 할지 결정합니다. 이는 사용자 메시지에 숨겨진
Inspect 단계에서는 메시지를 6가지 공격 클래스(classes of attack)와 대조하여 검사합니다. Score 단계에서는 0에서 100 사이의 실시간 위협 점수(threat score)가 업데이트됩니다. Enforce 단계에서는 위험한 모든 것이 즉각 차단되어 모델에 절대 도달하지 못하게 합니다. 깨끗한 메시지는 Egress를 통해 통과하여 AI로부터 실제 답변을 얻습니다. 이 모든 과정 동안 모든 결정 사항은 SigNoz에 트레이스(trace)로 기록됩니다.
이 시스템이 포착하는 6가지 요소는 사람들이 LLM 앱을 상대로 실제로 사용하는 공격들입니다:
프롬프트 인젝션 (Prompt injection) — 에이전트의 지침을 하이재킹(hijacking)함
탈옥 (Jailbreaks) — "DAN 모드", 개발자 모드 트릭, 역할극 우회 등
데이터 유출 (Data exfiltration) — API 키나 .env 비밀 정보를 유출하도록 유도함
인코딩된 페이로드 (Encoded payloads) — base64 내부에 숨겨진 악성 지침
토큰 비용 급증 (Token cost spikes) — 비용을 소진시키기 위해 설계된 거대한 입력값
비정상적인 도구 호출 (Rogue tool calls) — 에이전트가 허용되지 않은 도구를 실행하도록 유도함
각 항목은 OWASP LLM Top 10의 카테고리에 매핑되므로, 이는 장난스러운 위협 모델이 아니라 실제적인 모델입니다.
내가 어기기를 거부한 설계 원칙: SANCTUM은 알려진 공격 패턴을 완화(mitigate)합니다. 이것은 프롬프트 인젝션을 "해결"하는 것이 아닙니다. 아무도 해결하지 못했으며, 보안 전문가라면 그런 주장에 당연히 눈을 흘길 것입니다. 나는 과대광고된 작동하지 않는 제품보다, 정직하게 작동하는 제품을 출시하고 싶습니다. 이 단 하나의 규칙이 프로젝트의 모든 결정을 형성했습니다.
SigNoz가 단순한 대시보드를 넘어 핵심이 되는 지점
이 프로젝트의 심장이기에 제가 강조하고 싶은 부분입니다. 방화벽을 먼저 구축하고 나중에 차트를 몇 개 붙이는 것은 쉬운 일이었을 것입니다. 하지만 여기서 관측성(observability)은 장식이 아니라 아이디어 그 자체였습니다. 전체적인 핵심 제안은 "공격을 볼 수 있다"는 것이며, SigNoz가 바로 그것을 가능하게 만듭니다.
모든 요청은 단일 OpenTelemetry 스팬(span)인 agent.handle_request를 통해 흐르며, 저는 전체 보안 스토리를 속성(attributes)으로 이 스팬에 매달아 놓았습니다:
*CODE 1 *
with tracer.start_as_current_span("agent.handle_request") as span:
span.set_attribute("security.status", status) # clean / threat_detected
span.set_attribute("threats.types", threat_list) # 예: ["prompt_injection"]
...
이제 SigNoz에서 어떤 트레이스(trace)를 열어도 모든 결정 사항이 바로 그곳에 있습니다. 무엇이 들어왔는지, 무엇이 탐지되었는지, SANCTUM이 그에 대해 무엇을 했는지, 그리고 그 순간의 위협 점수(threat score)가 얼마였는지 말입니다.
저는 총 요청 수, 차단된 공격, 탐지된 위협, 클린 트래픽(clean traffic), 통합 보안 개요, 그리고 실시간 평균 위협 점수를 추적하는 6개 패널로 구성된 "위협 관제실(Threat Control Room)" 대시보드를 구축했습니다. 그다음, 무언가가 차단되는 즉시 발동되는 트레이스 기반 알림(trace-based alert)을 만들었습니다.
하지만 제가 가장 좋아하게 된 발견은 제가 계획했던 것이 아니었습니다. 그것은 지연 시간(latency) 데이터 속에 숨어 있었습니다.
증명은 처음부터 타이밍에 있었습니다. 차단된 공격은 대략 0.1 밀리초 만에 해결됩니다. 정상 요청은 800~1700 밀리초가 걸립니다. 왜 이렇게 큰 차이가 날까요? 차단된 공격은 모델에 도달하지 못하고 Enforce 단계에서 멈추며 즉시 반환되기 때문입니다. 반면, 정상 요청은 LLM까지 전송되었다가 돌아옵니다. 이는 SANCTUM의 보호 기능이 SigNoz의 지연 시간(latency) 차트 자체에서 눈으로 확인 가능하다는 것을 의미합니다. 작동한다고 믿을 필요가 없습니다. 데이터의 모양 속에서 방어 기능을 볼 수 있습니다.
'이것이야말로 관측 가능성(observability)이 단순한 기능이 아니라 논거가 된 순간입니다. 이 대시보드는 방화벽이 작동한다는 것을 단순히 보고하는 것이 아니라, 제가 주관적으로 해석할 필요 없이 수치로 증명합니다.'
제가 조용히 자랑스러워하는 부분들
몇 가지 기능들은 핵심 방화벽을 넘어섰고, 이 시스템에 생동감을 부여하는 디테일들이 되었습니다.
위협 점수(threat score)가 스스로 치유됩니다. 이 점수는 누적 총합이 아닙니다. 2분 간격으로 감쇠(decay)합니다. 따라서 공격 폭주가 발생하면, 점수가 위험(critical) 쪽으로 급락하는 것을 지켜보다가, 공격이 멈추면 스스로 안전(safe) 쪽으로 다시 상승하는 것을 볼 수 있습니다.
이 복구 곡선은 정적인 수치로는 전달할 수 없는 이야기를 들려줍니다. 즉, 시스템에 공격이 가해졌다가, 더 이상 가해지지 않는다는 이야기입니다.
반복적인 공격자를 격리합니다. 동일한 소스에서 세 번의 공격이 방화벽에 가해지면, SANCTUM은 인간의 개입(human in the loop) 없이 자동으로 해당 소스와의 통신을 완전히 중단합니다. 데모 도중에 끈질긴 공격자가 차단되어 얼어붙는 모습을 지켜보는 것은 묘한 만족감을 줍니다.
일상적인 영어로 질문할 수 있습니다. 저는 Sentinel이라는 작은 분석 레이어(analyst layer)를 구축했습니다. "지금 무엇이 나를 공격하고 있는가?"라고 물으면, 시스템은 조작된 사건이 아닌 실시간 원격 측정(telemetry) 데이터와 실제 수치, 실제 소스를 바탕으로 답변합니다. 이는 대시보드를 읽는 것과 동료에게 물어보는 것의 차이와 같습니다.
비용 급증 공격은 거부되는 것이 아니라, 절단(truncated)됩니다. 거대한 악성 입력이 즉시 차단되는 것이 아니라, 안전한 예산(budget) 범위 내로 다듬어져 통과됩니다. 강제적인 차단보다는 우아한 성능 저하(graceful degradation)가 더 정직하게 느껴졌습니다.
탐지가 실제로 결과를 결정하는 핵심 방식은 의도적으로 단순하고 설명 가능합니다:
CODE 2
def detect_threats(message, requested_tool=None):
threats = []
for pattern in INJECTION_PATTERNS: # 시그니처 매칭 (signature match)
...
진행 과정에서 실제로 문제가 되었던 부분들
저는 지저분했던 부분들에 대해 솔직해지고 싶습니다. 상처 하나 없는 빌드 로그는 거짓말을 하는 로그이기 때문입니다.
Foundry를 통해 SigNoz를 셀프 호스팅(self-hosting)하는 데 저녁 시간 전체를 허비했습니다. 첫 실행은 그냥... 멈춰버렸습니다. Docker는 정상이었고, 설정도 문제없어 보였으며, 아무것도 로드되지 않았습니다. 아무도 경고해주지 않는 사실은, Windows를 재부팅한 후에는 SigNoz 컨테이너들이 자동으로 다시 시작되지 않으며, 명확한 원인 없이 OTLP 포트에서 차가운 connection refused 오류가 발생한다는 것입니다. 재부팅할 때마다 Foundry 설정을 다시 적용(re-cast)하는 법을 배우고 나서야 이 유령 같은 문제는 사라졌습니다.
CODE 3
# 재부팅 후, 다른 작업을 하기 전에:
foundryctl cast -f casting.yaml
# 그다음 OTLP 포트가 실제로 공개(published)되었는지 확인합니다:
...
한 시간 동안 저를 속였던 버그: 빌드 후반부에 프론트엔드(frontend)와 백엔드(backend)가 서로 맞지 않기 시작했습니다. 모든 요청이 거부되었고, 파이프라인(pipeline)은 빈 데이터로 움직였으며, 저는 백엔드 전체가 죽었다고 확신했습니다. 원인은 무엇이었을까요? 프론트엔드는 {question: ...}를 보내고 있었고, API는 {text: ...}를 기대하고 있었습니다. 단 한 단어 차이였습니다. 백엔드는 내내 완벽했습니다. 다시 배운 교훈은 이것입니다: 모든 것이 고장 난 것처럼 보일 때, 어느 한쪽을 의심하기 전에 두 정상적인 요소 사이의 계약(contract)을 먼저 의심하십시오.
또한 저는 억지로 덮어쓰기보다는 그대로 남겨두기로 선택한 정직한 한계점들도 있었습니다. 탐지(Detection) 방식은 시그니처 기반(signature-based)이라 빠르고 설명 가능하지만, 공격을 충분히 영리하게 재구성하는 사람에게는 완전히 회피될 수 있습니다. 저는 이 점을 명확하게 문서화했습니다. 이것이 제가 다음에 구축할 것입니다: 철자가 아닌 의도(intent)를 포착하는 시맨틱 레이어(semantic layer)입니다.
궁금해하실 분들을 위한 스택(stack)
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기







