
AI 에이전트의 공격만을 단 2줄로 차단하기 — 행동 탐지를 버리고 '자백'을 취하는 설계
요약
AI 에이전트의 공격을 탐지하기 위해 행동 패턴 분석 대신, 서버가 응답에 심어둔 특정 값을 클라이언트가 다시 제출하는 '자백' 방식을 활용한 보안 설계법을 소개합니다. 기존의 속도나 패턴 기반 차단 방식이 가진 오탐 문제를 해결하는 새로운 접근법을 다룹니다.
핵심 포인트
- 행동 기반 탐지는 인간의 행동과 구별이 어려워 오탐 가능성이 높음
- AI 에이전트의 자율적 공격 능력이 강화됨에 따라 새로운 보안 대책 필요
- 서버 응답에 포함된 특정 값을 재사용하는 방식으로 에이전트 여부를 확정
- Proofwire 라이브러리를 통해 Express 환경에서 간편하게 적용 가능
결론부터 말씀드리면
Express라면, 이 2줄로 AI 에이전트에 의한 공격 세션을 차단할 수 있습니다.
const proofwire = require("./proofwire")({ block: true });
app.use(proofwire.express());
단, "수상하니까 차단"하는 것이 아닙니다. 차단하는 것은, 이쪽에서 심어둔 값을 실제로 사용한 세션뿐입니다. 속도가 빠르다거나, 스크립트 같다거나, User-Agent가 이상하다는 이유로는 차단하지 않습니다. 이 기사는 왜 그런 설계를 했는지에 대한 이야기입니다.
만든 것은 Proofwire로서 공개하고 있습니다. 월 10만 이벤트까지 무료입니다.
왜 지금 이것이 필요한가
2025년 11월, Anthropic이 국가 지원형 스파이 활동을 차단했다고 공표했습니다. Claude Code가 정찰·침입·횡적 이동(Lateral Movement)·데이터 유출까지, 침입 작업의 80~90%를 자율적으로 실행했다는 내용입니다. 표적은 약 30개 조직이었습니다.
이 작전이 저지된 이유는 단 하나입니다. 상용 API 위에서 동작하고 있었기 때문입니다. 제공처가 탐지하여 계정을 삭제할 수 있었습니다.
그리고 2026년 7월 27일, Moonshot이 Kimi K3의 가중치(Weights)를 공개했습니다. 2.8T 파라미터의 MoE (Active 104B), 100만 토큰 컨텍스트, 수정 MIT 라이선스. GPU를 빌려 거부(Refusal)를 파인튜닝(Fine-tuning)으로 제거하여 원하는 곳으로 향하게 할 수 있습니다. 삭제할 계정도, 연락할 제공처도 없습니다.
공격의 "조사하는" 공정이, 어떤 사이트를 향해 계속 시도해도 수지타산이 맞는 가격까지 떨어진 것이 현재의 상황입니다.
행동 시그널로는 원리적으로 불가능
가장 먼저 생각하는 것은 "자동화된 액세스를 막는 것"일 것입니다. 저도 처음에는 그렇게 만들었습니다. 그리고 망가졌습니다.
실제로 스코어링(Scoring)을 구현하여 수중에 있는 앱에 적용한 결과입니다.
| 입력 | 기대값 | 단순한 구현의 결과 |
|---|---|---|
| 인간이 천천히 5페이지 열람 | 아무것도 나오지 않음 | OK |
| 인간이 새로고침을 15번 연타 | 아무것도 나오지 않음 | 탐지 (오탐) |
| 에이전트가 25개 경로 스캔 | 탐지 | OK |
새로고침 연타는 burst_rate도, no_think_time도 발생시킵니다. 인간이 짜증이 나서 F5를 연타한 것만으로 공격자가 됩니다. 이것은 임계값(Threshold) 조정으로는 해결되지 않습니다. "빠르다", "간격이 짧다"는 인간도 낼 수 있기 때문입니다.
다른 재료들도 마찬가지입니다.
| 판정 재료 | 왜 효과가 없는가 |
|---|---|
| 요청 빈도 | 인간의 연타로 발생함. 반대로 4초에 한 번으로 낮추면 숨을 수 있음 |
허니팟, 가짜 /admin | 존재를 광고하고 있음. 우회되거나 "감시당하고 있음"을 알려주게 됨 |
| WAF 시그니처 | 알려진 페이로드(Payload) 대상임. 모델은 그 자리에서 새로운 것을 조합함 |
특히 3번째 항목이 치명적입니다. 당신의 고객이 자사의 에이전트를 통해 보내는 요청은, 공격자가 보내는 것과 구별할 수 없습니다. 같은 라이브러리, 같은 헤더, 같은 속도. 여기서 차단하면 잃는 것은 거래처입니다.
발상을 전환하기: 정규 클라이언트가 "원리적으로 낼 수 없는" 것은 무엇인가
관측 가능한 성질은 전부 위장할 수 있습니다. 그렇다면, 정규 클라이언트가 절대 낼 수 없는 입력은 무엇일까요?
답은 "이쪽에서만 전달할 수 있는 값을 사용했다는 것"입니다.
이쪽에서 응답(Response) 안에 자격 증명(Credentials) 형태의 값을 섞어 둡니다. 그 값이 요청(Request)으로서 돌아온다면, 그 세션은 틀림없이 응답 본문을 읽고, 내용을 해석하여, 사용했다는 뜻이 됩니다. 브라우저는 이것을 하지 않습니다. 크롤러도 하지 않습니다. 연동된 API 클라이언트도 하지 않습니다.
추측이 아니라 자백입니다. 그리고 자백에는 오탐률이 없습니다.
허니팟과의 차이점
"canarytokens과 같지 않나"라는 질문이 나올 수 있어, 차이점 3가지를 적습니다.
1. 아무것도 광고하지 않음
robots.txt에 표식을 남기지 않습니다. 가짜 /admin도 만들지 않습니다. 추가 호스트명도 세우지 않습니다. 스캐너가 보기에 표면은 오리진(Origin)과 바이트 단위로 동일하며, 404도 앱 본체가 반환하는 진짜입니다. 눈치챌 단서가 없으므로 우회할 방법도 없습니다. 광고형 디코이(Decoy)는 능력이 높은 에이전트에게 "앱 본체와 표면이 다르다"는 점 때문에 간파당합니다.
2. 값은 기존 응답 안에 들어감
별도의 엔드포인트를 만드는 것이 아니라, 원래 반환하고 있는 HTML과 JSON 안에 섞어 넣습니다. HTML 주석 하나, JSON 필드 하나를 활용합니다.
3. 차단이 로컬에서 완결됨
클라이언트는 심어둔 값을 수중에 가지고 있으므로, 해당 값을 포함하는 요청이 오면 API에 문의하지 않고 그 자리에서 즉시 거부할 수 있습니다. '자백'으로부터 차단까지의 간극이 제로입니다.
구현
값의 생성
값은 테넌트(Tenant)의 secret으로부터 HMAC을 사용하여 결정론적(Deterministic)으로 유도합니다. 저장하지 않습니다. 재계산이 가능하므로 대조할 수 있습니다.
const BAIT_SLOTS = [
{
id: "aws", placement: "html-comment", fmt: (h) => `AKIA${h.slice(0, 16).toUpperCase()}`
},
{
id: "api", placement: "json-field", fmt: (h) => `sk_live_${h.slice(0, 32)}`
},
...
고객마다 secret이 다르기 때문에 값이 충돌할 일도 없습니다.
스코어링 (Scoring)
이 부분이 설계의 핵심입니다. 행동 시그널(Behavioral signal)만으로는 절대로 확정 짓지 않습니다.
function score(s) {
const ev = [], beh = [];
const win = s.times.filter((t) => Date.now() - t < 10000).length;
...
포인트는 두 가지입니다.
breadth(폭)를 필수 조건으로 만들었습니다. 업데이트를 연타하여 발생하는 오탐(False Positive) 문제를 이 방식으로 해결했습니다. 연사(Rapid fire)와 사고 시간(Thinking time) 제로 패턴은 인간도 보일 수 있지만,
10개 이상의 서로 다른 경로를 9초 만에 훑는 것은 인간의 조작으로는 일어날 수 없습니다.
likely(가능성 높음) 단계에서는 차단하지 않습니다. 0.6 점수는 알림만 보냅니다. 차단은 0.9 이상, 즉 '자백'이 있었을 경우에만 수행합니다. 행동 시그널에 기반한 추측을 실제 사용자의 경로에 개입시키지 않는 것이 우리가 그은 선입니다.
차단
if (opts.block && (hit || isBlocked(sid))) {
if (hit) markBlocked(sid);
enqueue({ session: sid, path, ua, dt, status: 403, bait: hit });
...
hit은 "수신된 요청 안에 심어둔 값이 나타났다"는 플래그입니다. 클라이언트가 값을 가지고 있으므로 그 자리에서 판정할 수 있으며, API 왕복을 기다리지 않습니다. 이후 30분 동안 동일한 세션으로부터 오는 요청은 모두 거부합니다.
검증
실제로 Express 앱을 구축하고, 운영 API에 연결하여 측정했습니다.
오탐이 발생하지 않는가
$ 인간 (천천히 5페이지, 사고 시간 3~12초)
{"accepted":5,"alerts":[]}
$ 인간 (동일 경로를 15회 연타, 간격 120ms)
...
연타 15회에도 0건. 이 부분이 가장 공들여 다시 만든 부분입니다.
실제로 차단되는가
1. 일반 방문자
GET / -> 200
GET /api/me -> 200
...
JSON 응답에도 _k 필드로 포함됩니다.
{"user":"alice","_k":"sk_live_40566148f8d70fad5ad8c10a452b95a8"}
장애 시에도 시스템을 망가뜨리지 않음
보안 제품이 앱을 다운시키는 것이 가장 치명적이므로, 그 부분은 철저히 했습니다.
- fail-open: API가 다운되거나, 할당량을 다 쓰거나, 설정을 가져오지 못하더라도
next()를 호출하여 응답을 그대로 내보냅니다. - 비동기·배치 전송 (Asynchronous/Batch sending): 측정 데이터는 응답을 보낸 후 백그라운드에서 최대 200건씩 모아서 보냅니다. 응답 시간에 영향을 주지 않습니다.
- 정적 자산(Static assets)은 계산하지 않음: 페이지 한 번 로드 시 CSS/JS/이미지가 수십 개씩 날아가므로, 이를 계산하면 사고 시간 제로 패턴이 대량 생산되어 오탐의 원인이 됩니다.
if (/\.(css|js|mjs|png|jpe?g|gif|svg|webp|ico|woff2?|ttf|map)$/i.test(req.path || "")) return next();
할 수 없는 것
솔직하게 적겠습니다.
- 인간처럼 행동하는 인내심 강한 공격자는 막을 수 없습니다. 이것은 포기한 부분입니다. 대신, 알림이 떠도 선별해야 할 대상이 없게 됩니다.
- WAF가 아닙니다.
block
를 설정하면 아무것도 차단하지 않습니다 -
차단하면 상대방이 탐지되었다는 사실을 눈치챕니다. 상대를 계속 방치(泳がせたい)하고 싶다면 알림(Notification) 모드로만 운영하십시오 -
값이 어디까지 도달하느냐에 따라 정밀도가 결정됩니다. 자격 증명(Credentials) 형태의 데이터가 공격자의 손에 전혀 넘어가지 않는다면, 자백을 받아낼 방법이 없습니다 -
응답 본문(Response Body)을 수정합니다 (HTML 주석 1개, JSON _k 1개). 서명(Signature)이나 해시(Hash)를 사용 중이라면 대상 경로(Route)를 좁히십시오 -
세션 식별은 IP + User-Agent입니다. 공유 NAT 내부에서는 정밀도가 떨어집니다. 다만 자백 그 자체는 세션 식별의 정밀도에 의존하지 않습니다 -
v0입니다. API 형태는 변경될 수 있으며, 제3자에 의한 보안 리뷰(Security Review)는 아직 받지 않았습니다
사용법
curl -o proofwire.js https://layersecurity.jp/proofwire/express.js
const proofwire = require("./proofwire")({ block: true });
app.use(proofwire.express());
키(Key)는 PROOFWIRE_KEY에서 읽어옵니다. 계정 생성은 콘솔(Console)에서 하거나, 다음 명령어를 사용하십시오.
curl -sX POST https://api.layersecurity.jp/v1/signup \-
-H 'content-type: application/json' -d '{"email":"you@company.com"}'
Node 이외의 환경이라면 Cloudflare Worker 버전이 동일한 역할을 수행합니다. PHP, Rails, Java, 정적 사이트(Static Site)에서도 애플리케이션 본체를 수정하지 않고 전단(Front-end)에 배치할 수 있습니다.
저장하는 데이터는 경로(Path), 상태(Status), User-Agent, 해싱된 세션 ID(Hashed Session ID), 그리고 심어둔 값이 나타났는지 여부뿐입니다. 요청 본문(Request Body)이나 응답 본문(Response Body), 헤더(Header)는 저장하지 않습니다.
월 10만 이벤트까지 무료입니다. 초과하더라도 429를 반환할 뿐, 클라이언트는 계속해서 통과(Pass-through)됩니다.
문서: https://layersecurity.jp/proofwire/ja/
마치며
"수상한 동작을 탐지한다"를 버리고 "정상적인 클라이언트는 원리적으로 내보낼 수 없는 입력만을 본다"로 방향을 틀면, 오탐(False Positive)에 대한 논의 자체가 사라집니다. 대신 탐지 범위는 좁아지겠지만, 알림을 확인했을 때 모든 것이 진짜(공격)라는 상태는 운영 측면에서 매우 강력한 효과를 발휘합니다.
설계상의 허점을 발견하면 알려주세요. 특히 "이 경로라면 정상적인 클라이언트도 값을 보낼 수 있다"라는 패턴이 있다면, 이는 전제가 무너지는 것이므로 꼭 알고 싶습니다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기