1426개 테스트가 전부 통과하고, 타입체크 클린이고, 린트도 통과했다. 로컬 터미널에서 mock 서버를 붙여 돌리면 깔끔하게 동작한다.
이제 진짜 MCP 서버에 붙여보자.
Claude Desktop 연동
Claude Desktop에서 MCP 서버를 쓰려면 claude_desktop_config.json에 설정을 넣는다. mcp-fence로 감싸는 건 이렇게 생겼다.
{
"mcpServers": {
"nworks": {
"command": "npx",
"args": [
"mcp-fence",
"start",
"--",
"npx", "@modelcontextprotocol/server-filesystem"
]
}
}
}
nworks는 내가 만든 NAVER WORKS MCP 서버다. 이걸 mcp-fence로 감싸서 테스트하기로 했다. 설정 파일 없이 이 한 줄이면 끝이다 — 1편에서 말한 zero-config가 실전에서 어떻게 작동하는지 보자.
이론상으로는 이게 전부다. 실제로는 아니었다.
CWD가 루트라서 SQLite가 죽는다
Claude Desktop을 재시작하니 로그에 이게 찍혔다.
SqliteError: unable to open database file처음엔 의아했다. 로컬에서는 잘 됐었으니까.
원인은 CWD(current working directory)였다. 터미널에서 직접 실행하면 CWD가 내 프로젝트 디렉토리다. 하지만 Claude Desktop이 MCP 서버를 spawn할 때 CWD가 /(루트 디렉토리)로 설정된다. 초기 구현에서 감사 DB 경로가 상대 경로(./mcp-fence-audit.db)였다. 루트 디렉토리에는 파일 생성 권한이 없다.
변경 전: ./mcp-fence-audit.db (상대 경로, CWD 의존)
변경 후: ~/.mcp-fence/audit.db (홈 디렉토리 기반 절대 경로)~/.mcp-fence/ 디렉토리가 없으면 자동 생성한다. ~/.npm, ~/.docker, ~/.aws 전부 이 구조를 따르는 CLI 도구의 표준 패턴이다.
MCP 도구를 만들 때 "터미널에서 직접 실행"과 "MCP 클라이언트가 spawn"은 환경이 다르다는 걸 염두에 둬야 한다. CWD, 환경변수, stdin/stdout 전부 다를 수 있다.
OAuth callback이 데이터 유출로 탐지된다
DB 문제를 해결하고 나니, mcp-fence가 프록시 역할을 하기 시작했다. nworks 서버가 연결되고, tools/list가 통과하고, 도구 목록이 Claude Desktop에 표시됐다.
그런데 nworks의 OAuth 인증 과정에서 이게 터졌다.
[warn] EXF-001: Data exfiltration via URL embedding — score: 0.60, decision: warnnworks는 NAVER WORKS API를 쓰는데, OAuth 인증을 위해 callback URL을 주고받는다. mcp-fence의 EXF-001 패턴은 응답에 외부 URL이 포함되면서 특정 키워드(callback, token, redirect 등)가 함께 있으면 데이터 유출 시도로 판단한다. OAuth 흐름에서 callback URL은 정상적인 동작인데, 패턴 입장에서는 "응답에 URL + callback = 유출 의심"이다.
전형적인 오탐이다.
monitor 모드라서 실제 차단은 안 됐지만, enforce 모드였으면 OAuth 인증 자체가 깨졌을 것이다. callback 키워드를 EXF-001 패턴에서 제거하는 것으로 해결했다. OAuth callback은 MCP 서버 생태계에서 너무 흔한 패턴이라 이걸 유출로 잡으면 오탐이 끝없이 나온다.
오탐은 보안 도구의 숙명이다. 패턴을 넓게 잡으면 오탐이 늘고, 좁게 잡으면 미탐이 늘고. mcp-fence의 기본 모드가 monitor인 이유가 여기에 있다.
npm publish에서 GitHub push protection에 걸림
테스트 코드에는 시크릿 탐지 패턴을 테스트하기 위한 가짜 토큰들이 있다. AWS 키 형식(AKIAIOSFODNN7EXAMPLE), GitHub 토큰 형식(ghp_xxxx...), OpenAI API 키 형식(sk-proj-...) 등.
GitHub에 push하려니 push protection이 동작했다.
remote: error: GH009 -- Secret scanning found a GitHub Token
remote: in tests/unit/secrets.test.ts가짜 토큰인데 진짜로 인식됐다. GitHub push protection은 형식만 보니까 당연한 결과다.
push protection을 끄면 진짜 시크릿도 push될 수 있다. 해결은 런타임 문자열 조합이었다.
// 변경 전 (push protection에 걸림)
const fakeToken = 'ghp_ABCDEFGHijklmnop1234567890abcdef';
// 변경 후 (런타임 조합, push protection 통과)
const fakeToken = ['ghp', 'ABCDEFGH', 'ijklmnop', '1234567890abcdef'].join('_');
GitHub push protection은 정적 텍스트 매칭이라 런타임 조합은 잡지 못한다. 보안 도구를 만들면서 보안 검사에 걸리는 아이러니. 보안 테스트 코드에는 항상 "공격 페이로드"가 포함되어 있으니까.
감사 로그를 열어봤다
모든 문제를 해결하고, nworks를 통해 메시지 전송, 캘린더 조회, 조직도 검색을 테스트했다. 13개 이벤트가 쌓였고, 전부 allow였다. OAuth 오탐도 수정 확인.
그런데 감사 로그를 JSON으로 뽑아봤다.
mcp-fence logs --format json
{
"params": {
"name": "send_message",
"arguments": {
"channelId": "12345678",
"content": "안녕하세요",
"clientSecret": "a1b2c3d4e5f6g7h8i9j0"
}
}
}
clientSecret이 평문으로 저장되어 있다.
한동안 화면을 들여다봤다. 방화벽을 만들었는데, 방화벽의 로그가 공격 대상이 되는 구조다. 감사 DB를 탈취하면 거기에 기록된 모든 시크릿을 한 번에 얻을 수 있다. 시크릿 탐지 엔진이 24개 패턴으로 시크릿을 잡아내는데, 잡아낸 시크릿을 평문으로 기록하고 있으니 아이러니다.
모바일 보안 SDK에서도 이런 적이 있었다. 보안 SDK가 탐지 결과를 서버에 리포팅하는데, 리포팅 데이터에 사용자의 OAuth 토큰이 포함되어 있었다. 보안 시스템 자체가 보안 취약점이 되는 패턴이다.
이걸 수정하는 게 v0.2가 됐다.
시크릿 마스킹 — 우에서 좌로 치환하는 이유
src/audit/masker.ts에서 구현한 마스킹 전략은 이렇다.
12자 이상: 앞 4자 + **** + 뒤 4자
AKIAIOSFODNN7EXAMPLE → AKIA************MPLE
12자 미만: [REDACTED] (전체 치환)왜 12자 기준인가. 8자 패스워드에서 앞 4자 + 뒤 4자를 보여주면 전체가 노출된다. 짧은 건 아예 전부 가린다. 앞 4자를 보여주는 건 디버깅 때문이다. AWS 키가 여러 개 있으면 "어떤 키가 유출됐는가"를 알아야 한다. 접두사(AKIA)와 마지막 4자를 보면 어떤 키인지 특정할 수 있다.
export function maskValue(value: string): string {
if (value.length < 12) return '[REDACTED]';
const prefix = value.slice(0, 4);
const suffix = value.slice(-4);
return `${prefix}${'*'.repeat(value.length - 8)}${suffix}`;
}
maskSecrets 함수는 텍스트 전체를 스캔해서 시크릿 패턴에 매칭되는 부분을 전부 마스킹한다. 24개 시크릿 패턴을 전부 돌리면 하나의 시크릿이 여러 패턴에 매칭될 수 있다. 중복 치환하면 마스킹된 결과를 다시 마스킹해서 깨진다. 그래서 겹치는 매칭을 먼저 제거한다.
핵심은 매칭된 위치들을 뒤에서부터 치환한다는 것이다. 앞에서부터 하면 치환 결과의 길이 차이 때문에 뒤쪽 매치의 offset이 밀린다. 뒤에서부터 치환하면 앞쪽 offset은 영향을 받지 않는다. matches.sort((a, b) => b.start - a.start)로 정렬하고 순서대로 치환한다.
v0.2 적용 후 같은 로그를 뽑으면:
{
"params": {
"name": "send_message",
"arguments": {
"channelId": "12345678",
"content": "안녕하세요",
"clientSecret": "a1b2****j0"
}
}
}
어떤 시크릿인지 식별은 가능하지만 원본 복원은 불가능하다.
HMAC 해시 체인 — 로그가 변조되면?
시크릿 마스킹으로 로그 유출 문제는 해결했다. 그런데 감사 로그 자체가 변조되면?
SQLite 파일은 그냥 파일이다. 파일 접근 권한만 있으면 누구든 열어서 수정할 수 있다. 공격자가 악성 행위를 했는데 로그에서 해당 이벤트를 삭제하면, 포렌식 분석 시 "로그에 없으니 공격이 없었다"가 된다.
블록체인의 해시 체인과 같은 원리를 적용했다. 각 이벤트의 HMAC이 이전 이벤트의 HMAC에 의존한다.
이벤트 1: hmac = SHA256(key, "genesis|ts1|direction1|decision1|...")
이벤트 2: hmac = SHA256(key, "hmac1|ts2|direction2|decision2|...")
이벤트 3: hmac = SHA256(key, "hmac2|ts3|direction3|decision3|...")이벤트 2를 삭제하면 이벤트 3의 prev_hmac이 존재하지 않는 HMAC을 가리킨다. 체인이 끊긴다. 이벤트 2의 내용을 수정해도 HMAC 재계산 값이 저장된 값과 다르다. 모바일 앱의 코드 사이닝과 같은 원리다. APK가 변조되면 서명이 깨지듯, 감사 로그가 변조되면 HMAC 체인이 깨진다.
HMAC 키는 ~/.mcp-fence/hmac.key에 자동 생성된다. 파일 권한은 0o600(owner만 읽기/쓰기). 다른 사용자가 키를 읽으면 체인을 위조할 수 있으니까.

$ mcp-fence verify
Chain integrity: VALID
Events verified: 13
체인이 끊기면 복구하지 않는다. 이게 의도된 동작이다. 체인을 "고칠 수 있으면" 공격자도 고칠 수 있다.
감사 로그는 계속 쌓이니까 크기 제한도 필요하다. 기본 100MB, 초과하면 오래된 이벤트부터 삭제한다. 이때 prune marker라는 synthetic 이벤트를 삽입해서, 검증 시 "여기서 pruning이 있었다"고 인정하고 마커부터 새 체인을 시작한다. 의도된 끊김과 변조에 의한 끊김을 구분하는 장치다.
실제 v0.2 테스트 결과

v0.2를 적용하고 nworks를 다시 돌렸다.
$ mcp-fence logs --since 1h
Timestamp Direction Method Decision Score Tool
--------------------------------------------------------------------------------
2026-03-26 10:15:01.112 request tools/list allow 0.00 -
2026-03-26 10:15:02.334 response tools/list allow 0.00 -
2026-03-26 10:17:15.667 request tools/call allow 0.00 send_message
2026-03-26 10:17:16.891 response tools/call allow 0.12 send_message
...
13 event(s), 0 warn(s)
13개 이벤트 전부 allow. OAuth 오탐이 사라진 거 확인.
$ mcp-fence verify
Chain integrity: VALID
Events verified: 13
JSON으로 뽑아보면 clientSecret이 마스킹되어 있다.
다음 편에서는 v0.3에서 v1.0까지의 이야기를 다룬다. stdio에서 HTTP로 전송 계층을 바꾸면서 생긴 인증 문제, PII 탐지, 그리고 테스트 1426개가 놓친 CRITICAL 취약점을 AI가 잡아낸 과정.
참고 자료:
'개발기' 카테고리의 다른 글
| NAVER WORKS CLI + MCP 서버 개발기 (3) — MCP 서버 보안 설계, AI가 만드는 입력을 검증해야 하는 이유 (0) | 2026.04.14 |
|---|---|
| MCP AI 에이전트 보안 프록시 개발기 (4) — YAML 정책 엔진과 SARIF 감사 로깅 (0) | 2026.04.14 |
| MCP AI 에이전트 보안 프록시 개발기 (3) — 유니코드 호모글리프와 ReDoS로 보안 감사하기 (0) | 2026.04.14 |
| MCP AI 에이전트 보안 프록시 개발기 (2) — regex로 프롬프트 인젝션 탐지하기 (1) | 2026.04.14 |
| MCP AI 에이전트 보안 프록시 개발기 (1) — MCP 서버 응답은 검증 없이 AI에 전달된다 (0) | 2026.04.14 |
