22에서 23으로: 회피 탐지, 유도 및 231가지 새로운 테스트
요약
AegisGate MCP 프레임워크가 v1.5.0으로 업데이트되며, 23번째 보안 레이어인 '회피 탐지(Evasion Detection)' 기능이 추가되었습니다. 이 레이어는 Base64 인코딩, 유니코드 동형 문자 등 다양한 공격 패턴을 스캔하여 보안성을 강화합니다. 또한, 서버가 도구 실행 중 클라이언트에게 정보를 요청할 수 있는 'Elicitation' 기능도 구현되어 통신 방식별로 지원됩니다.
핵심 포인트
- 23번째 레이어 추가: 회피 탐지(Evasion Detection)를 통해 난독화된 공격 패턴을 차단합니다.
- 다양한 공격 벡터 대응: Base64, 유니코드 동형 문자 등 15가지 회피 패턴을 스캔합니다.
- Elicitation 기능 구현: 서버가 도구 실행 중 클라이언트에게 정보를 요청할 수 있게 합니다.
- 통신 방식 지원 확대: HTTP/SSE, TCP, stdio 세 가지 전송 방식으로 Elicitation 기능을 제공합니다.
AegisGate MCP의 업데이트: 23번째 보안 레이어와 Elicitation 기능 추가
이전 게시물에서 저는 AegisGate MCP의 아키텍처를 설명했습니다. AegisGate MCP는 22개의 보안 레이어를 갖춘 독립형 MCP 서버 프레임워크이며, 외부 Go 의존성이 없고, 번들링된 신경망 ML 파이프라인을 사용합니다.
당시 버전은 v1.3.0이었고, 이번 버전은 v1.5.0입니다. 변경된 내용은 다음과 같습니다.
23번째 레이어: 회피 탐지 (Evasion Detection)
가장 큰 추가 사항은 L1 정규식 스캐닝과 L3 신경망 감지 사이에 라이브 요청 파이프라인에 연결된 새로운 보안 레이어입니다. 우리는 이를 **L2.5: 회피 탐지(Evasion Detection)**라고 부릅니다.
이것이 해결하는 문제점: 공격자들은 원시 프롬프트(raw prompts)를 보내지 않습니다. 그들은 인코딩하고, 분할하고, 난독화하며, 재구성합니다. Base64로 인코딩된 페이로드, 유니코드 동형 문자(Unicode homoglyphs), "이전 지침 무시"라는 문구를 "ignore previous instructions."와 같이 공백으로 분할하는 제로 너비 문자(Zero-width characters) 사용, 리트 스피크(Leet speak), 카멜 케이스(CamelCase), 역할극 프레이밍 등이 있습니다.
EvasionDetector는 4가지 범주에 걸쳐 15가지 회피 패턴을 스캔합니다:
| 범주 | 패턴 | 예시 |
|---|---|---|
| 인코딩 (Encoding) | base64, URL-encoding, unicode, HTML entities | aWdub3JlIHByZXZpb3Vz |
| ... |
점수(Score)가 0.8 이상일 경우 독립적으로 차단됩니다. 점수가 0.3 이상일 경우 경고를 기록합니다. 순수 Go 언어로 작성되었으며 CGO가 필요하지 않아 휴리스틱 전용 빌드에서도 실행 가능합니다.
이로써 보안 레이어 개수는 22개에서 23개로 늘어났습니다. 이제 시작 로그에는 전체 체인이 출력됩니다:
Security chain (23 layers): auth → rbac → regex → evasion → neural → toolPoison → ...
Elicitation: 질문하는 서버
MCP의 elicitation 기능은 서버가 툴 실행 도중에 클라이언트에게 정보를 요청할 수 있게 합니다. "진행하려면 API 키가 필요합니다. 제공해 주시겠습니까?"라고 요청하고, 클라이언트는 elicitation/create를 통해 응답합니다.
우리는 이 기능을 세 가지 전송 방식(transports) 모두에 구현했습니다:
- HTTP/SSE: 대기 요청 레지스트리(pending-request registry)를 통한 비동기 상관관계(Async correlation). 서버는 SSE 이벤트로
elicitation/create를 전송하고, 클라이언트의 응답은 일치하는 요청 ID가 포함된 별도의 POST 요청으로 도착합니다. - TCP: 양방향 연결(bidirectional connection)에서 동기식 요청-응답(Synchronous request-response). 서버는
elicitation/create를 전송하고, 동일한 conn에서 응답이 도착할 때까지 블록(blocks)됩니다. - stdio: stdin/stdout 파이프를 통해 동일한 동기 패턴을 사용합니다.
ElicitationRegistry는 30초의 기본 타임아웃, 단조 증가 ID(monotonic IDs), 그리고 동시성 안전 해소(concurrent-safe resolution) 기능을 갖춘 대기 요청들을 관리합니다. 도구 핸들러(Tool handlers)는 ElicitInput()을 호출하여 사용자 응답(또는 타임아웃 오류)을 받습니다.
resp, err := server.ElicitInput(ctx, &ElicitRequest{
Message: "Enter your API key:",
Schema: map[string]any{"type": "string"},
...
initialize 응답은 이제 기능 목록(capabilities)에 elicitation을 광고합니다. 이를 지원하는 클라이언트는 이 흐름을 처리할 수 있으며, 그렇지 않은 클라이언트는 method not found를 반환하고 도구 핸들러는 타임아웃이 발생합니다.
SSE의 발전: 다중화 및 재연결(Multiplexing and Reconnection)
원문에서는 다음과 같은 한계를 지적했습니다.
JSON-RPC 2.0은 배치 요청(batch requests)을 정의합니다. 이는 단일 메시지로 전송되는 요청 배열입니다. 이제 세 가지 전송 방식(TCP, stdio, HTTP) 모두 이를 처리합니다:
[
{"jsonrpc":"2.0","id":1,"method":"tools/list"},
{"jsonrpc":"2.0","id":2,"method":"ping"},
...
응답은 수집되어 배열로 반환됩니다. 모든 알림(All-notification) 배치 요청은 응답을 생성하지 않습니다 (규격에 따름). 배치 내의 각 요청은 여전히 23개의 보안 계층을 독립적으로 통과합니다.
도구별 호출 제한 (Per-Tool Rate Limiting)
이전에는 전역 및 세션별 호출 제한만 있었습니다. 이제 도구별 호출 제한도 추가되었습니다:
config := ServerConfigV2{
MaxCallsPerToolPerSession: 50, // 세션당 단일 도구에 대한 최대 호출 횟수 50회
ToolRateOverrides: map[string]int{
...
이는 도구별로 세션당 원자 카운터(Atomic counter)를 사용하며, 세션이 만료되면 초기화됩니다.
테스트: 412 → 643
| 지표 (Metric) | v1.3.0 | v1.5.0 |
|---|---|---|
| 단위/통합 테스트 (Unit/integration tests) | 412 | 611 |
| ... |
k6 스타일 부하 테스트
7가지 카테고리에 걸쳐 32개의 부하 테스트가 빌드 태그 //go:build load를 통해 추가되었습니다:
| 카테고리 (Category) | 테스트 내용 (What it tests) |
|---|---|
| Stress | 최대 500개 동시 연결까지 점진적 증가(Ramp-up), 지속 부하 |
| ... | |
이 테스트들은 푸시(push)가 발생할 때마다 CI에서 실행됩니다. Load / Stress / Crush 작업은 약 1분 25초가 소요되며, 메인 브랜치에 도달하기 전에 회귀를 포착합니다. |
CI의 Race Detector (경쟁 감지기)
새로운 CI 작업이 푸시와 PR(Pull Request)마다 go test -race를 실행합니다. 이 과정에서 mockResponseWriter의 테스트 전용 경쟁 조건(test-only race condition)을 발견했습니다. 이는 테스트 고루틴(test goroutine)이 buf.String()을 호출하는 동안 다른 고루틴이 fmt.Fprintf를 통해 쓰기 작업을 수행했기 때문입니다. 프로덕션 코드는 이미 안전했습니다 (실제 sseConn은 뮤텍스(mutex)를 가지고 있음), 하지만 테스트 헬퍼는 그렇지 않았습니다. 이는 모의 객체에 sync.Mutex를 적용하여 수정되었습니다.
CI는 이제 27개 검사로 늘어났습니다 (기존 17개). 여기에는 benchstat을 통한 벤치마크 회귀 추적 및 go mod tidy 검사가 포함됩니다.
미묘한 버그: HTTP 세션 ID 불일치
이 부분은 교묘했습니다. OnAuthSuccess가 설정된 경우(기본값), 인증 미들웨어는 conn.Session.ID를 RBAC 세션 ID로 대체합니다. 그러면 HTTP 전송 계층(transport)이 이 RBAC ID를 Mcp-Session-Id 헤더에 반환했지만, 세션 자체는 내부 맵(map)에는 원래의 HTTP 세션 ID로 저장했습니다.
결과: RBAC ID가 헤더와 함께 오는 모든 후속 요청에서 "세션을 찾을 수 없거나 만료됨"이라는 메시지가 발생했습니다.
이 문제는 단일 요청 경로와 배치(batch) 요청 경로 모두에서 HTTP 세션 ID를 conn.Session.ID와 별도로 추적하여 수정되었습니다. 이제 Mcp-Session-Id 헤더는 일관되게 HTTP 세션 ID를 반환하며, 세션 맵 조회도 정상적으로 작동합니다.
MCP 레지스트리 (MCP Registry)
이제 서버가 MCP 레지스트리의 io.github.aegisgatesecurity/aegisgate-mcp에 게시되었습니다.
게시 과정이 순탄치 않았습니다. mcp-publisher CLI의 GitHub 디바이스 플로우는 read:org 범위를 요청하지 않기 때문에, 조직 멤버임에도 불구하고 관리자 권한을 가진 사용자가 403 오류를 받았습니다. 저는 레지스트리의 GitHub에 근본 원인과 해결 방법(workaround)을 문서화했습니다. 수정 사항은 read:org 범위의 토큰을 레지스트리의 인증 엔드포인트와 직접 교환하는 것입니다.
빠른 시작 (Quick Start) (v1.5.0)
# Docker (전체 ML)
docker pull ghcr.io/aegisgatesecurity/aegisgate-mcp:1.5.0
docker run -p 8081:8081 ghcr.io/aegisgatesecurity/aegisgate-mcp:1.5.0
...
MCP 레지스트리: io.github.aegisgatesecurity/aegisgate-mcp
GitHub: aegisgatesecurity/aegisgate-mcp
라이선스: Apache 2.0
다음 계획 (What's Next)
-
P2: 서버 시작 알림 (
notifications/tools/list_changed) -
P4:
resources/templates/list
로드맵은 docs/roadmap.md에서 확인할 수 있습니다.
모든 AI 상호작용을 안전하게 보호하세요.
Josh Colvin은 오픈 소스 기반의 자체 호스팅(self-hosted) AI 보안 솔루션인 AegisGate Security를 구축한 창립자입니다. Apache 2.0 라이선스를 따르며, 원격 측정(telemetry)이나 데이터 외부 전송(data egress)이 없습니다. GitHub.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기