Gate: 데이터와 AI 에이전트 사이의 결정론적 PII 경계
요약
AI 에이전트가 데이터 도구를 사용할 때 발생하는 개인정보(PII) 유출 문제를 해결하기 위한 Rust 기반 도구 'gate'를 소개합니다. LLM을 사용하지 않는 결정론적 방식으로 데이터를 스캔하고 플레이스홀더로 치환하여 보안을 강화합니다.
핵심 포인트
- AI 에이전트의 컨텍스트 내 PII 유출 위험 방지
- Rust 기반의 빠르고 결정론적인 데이터 비식별화
- LLM 없이 작동하여 네트워크 독립성 및 속도 보장
- JSON 구조를 유지하며 타입화된 플레이스홀더로 치환
새벽 2시에 해결했어야 할 문제
데이터베이스 도구를 코딩 에이전트(coding agent)에 연결합니다. PostgreSQL, Databricks, 내부 HTTP API 등 무엇이든 상관없습니다. 에이전트는 유용합니다. 존재조차 잊고 있었던 테이블을 조인하고, 마이그레이션(migration) 초안을 작성하며, 보고서를 씁니다. 생산성이 올라갑니다.
2주 후, 대화 기록(transcript)을 스크롤하다가 다음과 같은 내용을 발견합니다:
> select * from users where signup_at > '2026-04-01' limit 5;
[{
...
이 데이터는 이제 모델의 컨텍스트(context)에 포함되었습니다. 거기서부터 대화 로그, 에이전트가 생성하는 모든 요약본, 방금 /tmp에 작성한 파일, 그리고 — 만약 하네스(harness)에 어떤 종류의 메모리나 "세션 공유(share session)" 기능이 있다면 — 잠재적으로 다른 누군가의 머신 어딘가에 남게 됩니다. 에이전트는 잘못한 것이 없습니다. 당신도 잘못하지 않았습니다. 도구는 요청한 것을 반환했고, 모델은 이를 섭취(ingest)했으며, 삶은 계속되었습니다.
이것이 기본값(default)입니다. 모든 CLI 클라이언트, MCP 서버, 그리고 curl | jq 파이프라인은 사람이 보는 것과 동일한 바이트(bytes)를 반환합니다. 그리고 에이전트의 경우, 모델의 윈도우(window)에 무엇이 들어가는지 분류(triage)할 인간(human in the loop)이 없습니다.
이 포스트는 결정론적(deterministically)으로 해결 가능한 계층에서 이 누출을 해결하는 도구에 관한 것입니다.
gate란 무엇인가
gate는 AI 에이전트와 에이전트가 호출하는 데이터 도구 사이에 위치하는 단일 Rust 바이너리입니다. 이 도구는 구성된 명령의 _출력(output)_을 가로채고, PII(개인정보)를 스캔하며, 바이트가 모델에 도달하기 전에 값을 타입화된 플레이스홀더(typed placeholders)로 다시 작성합니다:
- {"id": 1, "email": "alice@example.com", "ssn": "123-45-6789", "status": "active"}
+ {"id": 1, "email": "[PII:email]", "ssn": "[PII:ssn]", "status": "active", "_gate_summary": {"redacted": 2, "types": ["email", "ssn"]}}
원래의 JSON 구조는 보존됩니다. 에이전트는 여전히 행(rows)을 반복(iterate)하고, 계산하고, 추론할 수 있습니다. 단지 필요하지 않은 값을 보지 못할 뿐입니다.
우선순위에 따른 설계 제약 조건은 다음과 같습니다:
- 결정론적 (Deterministic). LLM-in-the-loop 방식의 비식별화(redaction)를 사용하지 않습니다. 네트워크가 없는 환경에서도 동일한 입력에 대해 매 실행마다 동일한 출력을 보장합니다.
- 하네스(harness)의 위협 모델 내에서 우회 저항성 (Bypass-resistant) 보유. 에이전트가 정중하게 요청하거나, 교묘한 방식으로 도구를 호출하거나, 다른 동사를 통해 셸(shell)을 실행함으로써 필터를 비활성화할 수 없어야 합니다.
- 핫 패스(hot path)에서의 빠른 속도. 에이전트가 호출하는 모든 Bash 명령에서 실행됩니다. 속도가 느리면 사람들은 이를 꺼버릴 것입니다.
- 한계에 대한 정직함. 오탐(false-positive)보다 미탐(false-negative)이 더 위험합니다. 실패 모드는 시끄러운 차단이 아니라 조용한 데이터 노출이기 때문입니다.
핫 패스에서 10ms 미만의 오버헤드를 가진 작은 Rust 도구로, MIT 라이선스이며, cargo build로 빌드되고 Homebrew를 통해 배포됩니다.
두 가지 액세스 경로
현대적인 에이전트 하네스(agent harnesses)는 두 개의 문을 통해 모델에 데이터를 제공합니다. Model Context Protocol (MCP)은 Postgres, Snowflake, GitHub, Linear 및 내부 API를 위한 사실상의 표준(de-facto) 통합 계층이 되었지만, 이에 관한 대부분의 공개 자료는 서버를 구축하는 것에 관한 것이지, 서버가 반환하는 내용을 감사(auditing)하는 것에 관한 것이 아닙니다. 모델은 서버가 돌려주는 값을 신뢰합니다. 아무도 바이트(bytes)를 읽지 않습니다.
gate는 두 문을 모두 커버합니다: stdio 프록시를 통한 MCP 서버, 그리고 하네스 훅(harness hook)을 통한 Bash/CLI 도구입니다.
1. MCP 서버 (stdio 프록시를 통해)
gate mcp는 아주 작은 stdio JSON-RPC 프록시입니다. 이를 하네스 내의 MCP 서버로 등록하면, 내부적으로 실제 서버를 생성하고 모든 메시지를 있는 그대로 전달합니다. 단, tools/call 응답은 바이트가 모델로 돌아가기 전에 값 스캐너(value-scanner)를 거치게 됩니다.
AI ──tools/call──> gate mcp ──forward──> upstream MCP server
│
│ <── PII가 포함된 tools/call response
...
이 프록시는 투명합니다. 업스트림(upstream) 서버는 변경 없이 실행되며, 전체 플릿(fleet)을 한 번에 마이그레이션할 수 있습니다:
gate init --wrap-mcp # dry-run: 래핑될 모든 서버 목록을 표시합니다
gate init --wrap-mcp --yes # 적용
gate init --wrap-mcp --servers postgres,github --yes # 선택적 부분 집합 적용
이 명령은 ~/.claude.json에 있는 모든 서버(또는 프로젝트 범위의 ./.mcp.json, 혹은 OpenCode / Copilot CLI의 상응하는 파일)를 한 번에 gate mcp <original-command> 프록시(proxy)로 변환합니다. 이미 프록시가 적용된 서버는 건너뛰므로, 재실행해도 멱등성(idempotent)이 보장됩니다. 나중에 새로운 MCP 서버를 추가하면 다시 실행하면 됩니다.
이것이 구체적으로 의미하는 바는 다음과 같습니다: 여러분이 제어할 수 없는 제3자 MCP 서버(예: 벤더의 Postgres 커넥터, 내부 팀의 CRM 브리지 등)를 채택하더라도, 해당 서버가 반환하는 데이터와 모델이 섭취(ingest)하는 데이터 사이에 결정론적인 PII(개인정보) 경계를 유지할 수 있다는 것입니다. 서버를 변경할 필요도 없고, 작성자가 비식별화(redaction)를 고려했을 것이라고 신뢰할 필요도 없습니다.
2. Bash 도구 (하네스 훅을 통해)
에이전트가 실행하려는 모든 명령 — tkpsql query ..., psql -c ..., databricks api post ..., curl https://internal/... — 은 먼저 gate hook을 통과합니다. 훅(hook)은 해당 명령이 설정에 나열된 도구와 일치하는지 확인합니다. 일치할 경우, 명령은 다음과 같이 조용히 재작성(rewritten)됩니다:
gate run -- <original command>
gate run은 원래의 서브프로세스(subprocess)를 생성하고, 그 표준 출력(stdout)을 캡처하며, 바이트(bytes)에 대해 2단계 비식별화 파이프라인(redaction pipeline)을 실행한 뒤, 정화된(sanitized) 결과를 다시 내보냅니다. 에이전트는 기존과 동일한 JSON 구조를 보게 되지만, 값들은 [PII:<type>]으로 대체되어 있습니다.
재작성은 하네스(harness)의 도구 실행 전 훅(pre-tool-execution hook)에서 발생하므로, 이는 권고 사항이 아니라 강제(enforcing) 사항입니다.
- Claude Code —
~/.claude/settings.json내의PreToolUse훅(hook); Claude Code는 프로세스를 생성(spawning)하기 전에updatedInput을 통해 재작성된 명령어를 대체합니다. - OpenCode — TypeScript 플러그인의
tool.execute.before핸들러가 실행 중에output.args.command를 변형(mutate)합니다. - Cursor —
.cursor/mcp.json내의PreToolUse훅(hook); Cursor는 프로세스를 생성하기 전에 재작성된 명령어를 대체합니다. - GitHub Copilot CLI —
.github/hooks/PreToolUse.json내의PreToolUse훅(hook)이modifiedArgs를 반환합니다. - Codex CLI — 권한 UI(Permissions UI)를 통해 신뢰 및 활성화된
PreToolUse훅(hook); 프로세스를 생성하기 전에 재작성된 명령어를 대체합니다. - Gemini CLI —
PreToolUse훅(hook); Gemini CLI는gate init --harness gemini실행 및 세션 재시작 후에 재작성된 명령어를 대체합니다.
에이전트는 gate가 존재한다는 사실을 알지 못하며, 일반 터미널에서 동일한 명령어를 실행하는 인간은 영향을 받지 않습니다. PATH에 래퍼 스크립트(wrapper script)가 존재하지 않기 때문입니다.
두 단계의 게이트 탐지 파이프라인 (The two-gate detection pipeline)
이름에도 불구하고, gate는 서로 매우 다른 역할을 수행하는 두 개의 필터가 순차적으로 적용되는 구조입니다.
게이트 1: SQL 의도 분석 (SQL intent analysis, 최선 노력 방식)
가로챈 명령어에 sql_arg가 설정되어 있는 경우(예: tkpsql --sql, psql -c, databricks --json statement), gate는 SQL 문자열을 추출하여 직접 작성된 토크나이저(tokenizer)로 실행합니다. 목표는 겸손합니다. 쿼리가 어떤 _컬럼(columns)_을 선택하는지 파악하여, 반환되는 값이 무엇이든 상관없이 확실한 비식별화(redaction)를 위해 표시해 두는 것입니다.
SELECT u.first_name, u.email AS contact, p.phone
FROM users u JOIN profiles p ON u.id = p.user_id
WHERE u.signup_at > NOW() - INTERVAL '30 days'
게이트 1은 first_name, email (contact로 별칭 지정됨), 그리고 phone을 추출합니다. PII 휴리스틱(heuristic)과 일치하는 항목은 forced_columns 맵에 추가됩니다. 그러면 게이트 2는 해당 필드들이 설령 NULL이거나 `
sqlparser-rs대신 직접 작성한 토크나이저(tokenizer)를 사용하는 이유는 무엇인가요?
Gate 1은 컬럼 참조(column references)를 찾는 작업만 필요하기 때문입니다. 전체 SQL 파서(parser)를 도입하는 것은 결과적으로 손해였습니다. 의존성(dependencies)이 늘어나고, 방언(dialect) 버그가 많아지며, 파싱 실패 시 실행 계획(plan)에서 컬럼이 조용히 누락될 수 있는 코드 경로가 늘어납니다. 이 토크나이저는 약 300줄 정도로 구성되어 있으며, 방언에 구애받지 않고(dialect-agnostic), 파싱 실패 시 "어떤 컬럼인지 모르겠다"는 방향으로 오류를 발생시킵니다. 이는 괜찮은 선택인데, 왜냐하면 Gate 2가 모든 필드에 대해 실행되기 때문입니다.
Gate 1은 명시적으로 최선(best-effort)을 다하는 방식입니다. 그렇게 문서화되어 있습니다. 와일드카드(Wildcards), CTE(Common Table Expressions), 컬럼 주변의 함수 호출(function calls), 그리고 특이한 방언(dialects)들이 나타나더라도 성능이 점진적으로 저하될 뿐(degrade gracefully) 시스템이 무너지지는 않습니다.
| 패턴 | Gate 1 동작 | 안전망 (Safety net) |
|---|---|---|
SELECT email, name FROM u | 컬럼 추출됨 ✓ | — |
| ... |
이것이 gate의 핵심적인 설계 선택(load-bearing design choice)입니다. Gate 1은 틀릴 수 있도록 허용되며, Gate 2가 안전망 역할을 하기 때문입니다.
Gate 2: 값 스캐닝(value scanning) + 컬럼명 휴리스틱(heuristics)
Gate 2는 서브프로세스(subprocess)가 반환한 후의 JSON 응답에 대해 실행됩니다. 각 필드에 대해 다음 세 가지 검사를 적용합니다:
- Gate 1에서 지정된 강제 컬럼(Forced columns) → 값에 관계없이 항상 비식별화(redact)합니다.
- 컬럼명 휴리스틱(Column-name heuristics) → JSON 키를 토큰화(tokenise)하고(
snake_case,camelCase,PascalCase,UPPER_CASE처리), 약 50개의 PII(개인정보) 카테고리와 매칭합니다.userEmail,user_email,USER_EMAIL은 모두 동일한 규칙으로 해석됩니다. - 값 패턴(Value patterns) → 이메일, 미국/호주/뉴질랜드 전화번호, 미국 SSN, 호주 ABN, 호주 Medicare, 호주/뉴질랜드 TFN/IRD(형식 지정됨), 뉴질랜드 NHI, 뉴질랜드 은행 계좌 번호에 대한 정규 표현식(regex) 매칭과 결제 카드에 대한 Luhn 검사를 수행합니다.
컬럼명 매칭은 동일한 필드 내의 모든 값 매칭에 대한 신뢰도(confidence)를 높여줍니다. Gate 2는 매칭되는 즉시 항상 비식별화(redact)를 수행하며, 통과를 위한 별도의 임계값(threshold)은 없습니다. 신뢰도가 낮은 매칭(예: tax_id라는 이름의 컬럼에 있는 9자리 문자열)은 비식별화 처리됨과 동시에 _gate_summary에 low-confidence 경고가 표시됩니다. gate retro를 통해 경고된 컬럼을 검토한 후, gate allowlist add <column> 명령어로 오탐(false positive)을 차단할 수 있습니다.
출력값은 도구가 생성했던 것과 동일한 JSON 형식으로 반환되며, 값은 제자리에서(in-place) 다시 작성되고, 에이전트가 무엇이 삭제되었는지 추론할 수 있도록 _gate_summary 블록이 추가됩니다:
{
"rows": [{"id": 1, "email": "[PII:email]", "ssn": "[PII:ssn]"}],
"count": 1,
...
한계점에 대한 솔직한 고백
전체 위협 모델(threat model)은 repo에 명시되어 있으나, 주요 내용은 다음과 같습니다:
-
gate는 샌드박스(sandbox)가 아닙니다.gate는tools:에 명시적으로 나열된 명령만 필터링합니다. 그 외의 모든 것은 통과됩니다. -
적대적 모델(adversary model)은 악의적인 에이전트가 아닌, 부주의한 에이전트입니다.
sudo gate protect(Unix)는 설정을 root 소유로 변경하여 탈취된 에이전트가 설정 편집을 통해gate를 비활성화할 수 없도록 하지만, 데이터를 의도적으로 base64로 인코딩하거나, CSV 출력을 요청하거나, 가로채지 않은 도구를 통해 데이터를 유출하는 탈옥(jailbroken)된 에이전트는 여전히 범위 외(out of scope)입니다. 이러한 경계가 필요하다면gate를 하네스(harness) 수준의 도구 제한 및 읽기 전용 데이터베이스 역할과 결합하여 사용하십시오. -
값 정규식(Value regex)은 일반적인 사례와 호주/뉴질랜드(AU/NZ)를 지원합니다. 이메일, 미국 사회보장번호(US SSN, 대시 필수 —
123456789는 통과됨), 결제 카드(Luhn 알고리즘 사용), 그리고 호주/뉴질랜드의 휴대전화 및 유선전화를 포함한 전화번호가 지원됩니다. 전화번호는 현지 형식(04XX/02X,0[2378]/0[34679])과 국제 형식(+61/+64접두사)을 모두 지원하며, 국제 접두사가 붙은 번호는 어떤 컬럼에서든 자동으로 삭제(auto-redact)되지만, 현지 형식의 번호는 PII 이름이 지정된 컬럼이 필요합니다. 값 검출을 통해 식별되는 호주/뉴질랜드 전용 식별자는 다음과 같습니다: ABN (mod-89 체크섬), Medicare 카드 (mod-10), 형식이 지정된 TFN 및 IRD (mod-11, 구분자 필수), NZ NHI, 그리고 NZ 은행 계좌 번호입니다. 구분자가 없는 순수 TFN/IRD 문자열은 값만으로는 검출되지 않으며, 이 경우 컬럼 이름 매칭이 안전망 역할을 합니다. IBAN, 여권, NHS, Aadhaar 및 기타 호주/뉴질랜드 이외의 형식은 컬럼 이름 매칭에만 의존하므로, 해당 지역에 맞게pii.patterns를 확장하십시오. -
MCP
resources/read및prompts/get은 삭제되지 않습니다. 오직tools/call응답만이 스캐너를 통과합니다. -
JSON이 아닌 출력은 마스킹(redacted)되지 않습니다. 만약 도구(tool)가 CSV 또는 일반 텍스트를 출력한다면, 이를 변환하기 위해
pipe:를 설정하십시오 (예시 설정에서는curl을 위해jq -c .을 사용하고,psql --csv를 위해 3줄짜리 Pythoncsv.DictReader를 사용합니다). -
비활성화 메커니즘이 존재합니다. 설정에서
enabled: false로 지정하거나, 설정 파일을 삭제하거나, 하네스(harness) 설정에서 훅(hook) 항목을 제거할 수 있습니다.sudo gate protect(Unix) 명령은 에이전트 내부에서 앞의 두 가지 방법을 차단하기 위해 설정 파일의 소유권을 root로 변경(chown)하지만, 하네스 설정 파일은 여전히 사용자가 수정할 수 있습니다.
만약 이러한 사항들이 수용 불가능한 결격 사유라면, 이 도구는 사후 분석(post-mortem) 단계에서 발견하는 것보다 훨씬 나은 방식인 사전에 이를 솔직하게 밝힙니다.
왜 소스 단계에서 데이터를 마스킹하지 않나요?
데이터베이스 수준의 마스킹 — 정적 익명화 복사본(static anonymised copies), 동적 데이터 마스킹 (DDM), 행 보안 정책 (row security policies) — 은 소스를 제어할 수 있고 이를 구성할 권한이 있을 때 정답입니다. Gate는 사용자가 소스를 제어할 수 없을 때 그 간극을 메우며, 마스킹이 도달할 수 없는 경로를 커버합니다.
| gate | 데이터베이스 마스킹 (Database masking) | |
|---|---|---|
| DB 관리자 권한 필요 | ✅ 데이터베이스 변경 없음 | ❌ DBA에 의한 컬럼 수준 설정 필요 |
| ... |
이들은 상호 보완적입니다. 만약 DDM이 구성되어 있다면, gate는 DDM이 놓치는 경로와 패턴에 대한 안전망 역할을 합니다.
왜 "그냥 모델에게 물어보기"가 아니라 결정론적 CLI인가
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기