
실제로 삭제될 수 있는 PII — 블라인드 인덱스와 암호화 파쇄를 AI 에이전트 스택에 적용하기
요약
AI 에이전트 스택에서 개인정보(PII)를 안전하게 관리하고 '잊힐 권리'를 보장하기 위한 암호화 파쇄(Crypto-shredding) 아키텍처를 소개합니다. 단일 키 파괴를 통해 백업과 복제본을 포함한 모든 데이터 저장소에서 즉각적인 삭제 효과를 거두는 방식을 제안합니다.
핵심 포인트
- PII는 단일 서비스(party-api)에만 저장하고 나머지는 불투명 참조만 보유
- 암호화 파쇄를 통해 키 하나만 파괴함으로써 모든 복사본의 즉각적 삭제 구현
- LLM 시스템의 데이터 증폭 문제를 방지하기 위한 보안 설계 전략
- OpenBao Transit을 활용한 키 관리 및 데이터 암호화 구조
DELETE는 실제로 데이터를 삭제하지 않습니다. Postgres는 VACUUM을 수행할 때까지 고객의 이름을 힙 페이지(heap pages)에, 모든 WAL 세그먼트에, 모든 백업본에, 모든 복제본에 보관합니다. 여기서는 '잊힐 권리(right to erasure)'가 단 하나의 키 연산으로 이루어지며, AI 계층이 애초에 PII를 유출할 기회조차 없는 아키텍처를 소개합니다.
LLM 시스템은 PII의 증폭기 역할을 합니다. 채팅창에 입력된 이름 하나가 그래프 상태(graph state), 체크포인터 테이블(checkpointer table), 로그, 트레이스, 벡터 스토어 등에 남을 수 있습니다. 그리고 고객이 잊힐 권리를 행사할 때, '사용자 삭제'라는 것이 사실상 일곱 개의 데이터 저장소와 재작성할 수 없는 백업 아카이브 전반에 걸친 포렌식 조사(forensic sweep)를 의미한다는 것을 알게 됩니다.
이 스택은 정반대의 가정을 기반으로 하며, 두 가지 아키텍처적 결정으로 이루어져 있습니다:
1. PII는 오직 하나의 서비스만이 보유합니다. 나머지 모든 것은 불투명한 참조(opaque reference)만을 가지고 있습니다.
2. 그 단일 서비스는 다른 누군가가 관리하는 키 없이는 읽을 수 있는 어떤 것도 저장하지 않습니다 — 따라서 삭제는 하나의 키를 파괴하는 것입니다.
이 서비스는 BIAN의 'Party Reference Data Directory' 서비스 도메인을 모델로 한 party-api입니다. 키 관리 주체(key custodian)는 OpenBao Transit(
- 그 외 모든 구성 요소 (Everyone else) — 오케스트레이터(orchestrator), loans-api, 에이전트(agent), UI 등 다른 모든 서비스는 불투명한 참조(opaque reference)만을 보유합니다. 저장하지 않은 데이터는 유출될 수도 없습니다.
- 금고 (The vault) — party-api는 PII가 존재하는 유일한 장소이며, 그곳에서도 데이터는 암호문(ciphertext)과 단방향 블라인드 인덱스(one-way blind indexes) 형태로 존재합니다. 금고 스스로도 자신의 행(row)을 평문(cleartext)으로 읽을 수 없습니다.
- 키 (The keys) — OpenBao Transit은 개인당 하나의 키를 관리(custody)하며, 키 자재(key material)는 절대 외부로 유출되지 않습니다. 금고가 잠긴 상자를 보유한다면, 다른 누군가가 모든 열쇠를 보유하는 구조입니다.
- 삭제 권리 (Right to erasure) — 따라서 "이 고객을 삭제하라"는 명령은 7개의 저장소를 전수 조사하는 포렌식 작업이 되지 않습니다. 삭제(Erasure) = 하나의 키를 파괴하는 것입니다.
- 모든 복사본의 동시 삭제 (Every copy, at once) — 단 한 번의 삭제로 라이브 행(live rows), WAL 세그먼트(WAL segments), 베이스 백업(base backups), 복제본(replicas) 등 모든 복사본을 즉시 읽을 수 없는 상태로 만듭니다. 삭제를 증명하는 과정은 단 하나의 키 삭제 이벤트가 발생했음을 증명하는 것으로 단순화됩니다.
- 핵심 전략 (The whole bet) — 저장하지 않은 것은 유출할 수 없으며, 단 하나의 키로 잠긴 데이터는 삭제 실패를 방지할 수 있습니다.
원문 기사에는 읽는 단계에 따라 각 단계가 점등되는 대화형 스크롤 기반 다이어그램 버전이 포함되어 있습니다.
다른 모든 구성 요소가 불투명한 ID로 참조하는 단 하나의 금고, 금고가 생존하기 위해 반드시 필요한 단 하나의 키 관리자, 그리고 모든 백업의 모든 복사본을 즉시 읽을 수 없게 만드는 단 하나의 키 파괴로 축소된 삭제 프로세스.
왜 행 삭제(row DELETE)만으로는 삭제를 증명할 수 없는가
스키마의 헤더 주석은 대부분의 컴플라이언스(compliance) 자료보다 위협 모델(threat model)을 더 잘 설명하고 있습니다:
// 행 삭제(Row DELETE)만으로는 삭제를 증명(PROVE)할 수 없습니다. 평문은 VACUUM이 실행될 때까지 Postgres 페이지에 남아 있으며,
// 모든 WAL 세그먼트와 베이스 백업에서도 해당 데이터가 만료될 때까지 남아 있습니다. 따라서 어떤 PII 컬럼도 평문을 저장하지 않습니다.
// PII를 포함하는 각 행은 다음을 저장합니다:
...
전체 설계는 두 가지 핵심 사항으로 요약됩니다: 당사자별 키(per-party keys)는 삭제를 세밀하게 만들고, 키가 적용된 해시(keyed hashes)는 평문 없이도 검색을 가능하게 합니다. 데이터베이스에는 암호문(ciphertext)과 단방향 HMAC이 포함됩니다. party-api의 메모리, 환경 변수 또는 데이터베이스를 덤프하더라도 키는 얻을 수 없습니다. Transit이 네트워크 통신(wire)의 자기 측에서 암호화 및 복호화를 수행하기 때문입니다.
이를 작동하게 하는 세 가지 흐름인 등록(register), 조회(lookup), 삭제(erase)에 대한 상세 내용은 다음과 같습니다:
[
블라인드 인덱스 (Blind indexes): 저장하지 않기로 결정한 데이터에 대한 등가 검색
"alice@example.com에 해당하는 당사자를 찾으시오"라는 요청은 반드시 작동해야 합니다. 이것이 인증된 로그인이 은행 당사자와 연결되는 방식이며, 감사인이 사람을 참조 값으로 식별하는 방식이기 때문입니다. 블라인드 인덱스는 정규화된 값(trim().toLowerCase(), 쓰기 및 검색 시 동일하게 적용됨)에 대한 키가 적용된 HMAC-SHA256이며, 커버링 B-tree 인덱스(covering B-tree index)가 포함된 *_bidx 컬럼에 저장됩니다. 검색 시에는 Transit을 통해 쿼리 용어를 해싱한 후 등가 매칭(equality match)을 수행합니다.
이 설계에는 두 가지 의도적인 선택이 숨어 있습니다:
- HMAC 키는 당사자별이 아닌 서비스 전체(service-wide)에 적용됩니다 — 코드에 그 이유가 명시되어 있습니다: "당사자별 키를 사용하면 '모든 당사자에 걸쳐 이 이메일을 찾기'가 불가능해집니다." 어떤 당사자를 찾고 있는지 모르는 상태에서 조회를 수행하는 것이 목적이기 때문입니다.
- 결정론적 암호화(deterministic encryption)가 아닌 HMAC을 사용합니다 — 즉, 단방향입니다. 설령 컬럼이 유출되더라도 아무것도 드러나지 않으며 역산할 수도 없습니다.
그리고 잔여 위험(residual risk)은 막연한 바람이 아닌 문서로 기록되어 있습니다:
// 블라인드 인덱스를 위한 서비스 전체 HMAC 키. 삭제 시 절대 파기되지 않음: 당사자를 삭제하면 대신 해당 당사자의
// 블라인드 인덱스 행(ROWS)이 삭제됨. 수용되고 문서화된 잔여 위험: 오래된 DB 백업에 남아 있는
// 블라인드 인덱스 해시는 가명 정보(키가 적용된 해시이며, 키는 OpenBao만 보유)이지만, ...
삭제 (Erasure): 키를 먼저, 행을 나중에 — 각 단계의 실패 방식 때문에
삭제 (Erasure) 엔드포인트는 party-api에서 유일하게 되돌릴 수 없는 경로이며, 다음과 같이 정밀한 순서로 실행됩니다:
async erase(partyReference) {
await requireLive(deps.db, partyReference);
await deps.cipher.destroyKey(partyReference); // 1. 암호화 파쇄 (crypto-shred)
...
이 순서는 스타일의 선택이 아니라 실패 모드 (failure-mode)에 근거한 논리입니다. 키를 먼저 파괴하면, 행 삭제 (row-delete) 전에 시스템이 충돌하더라도 _재시도 (retry)_를 통해 상태를 완료할 수 있습니다. 만약 행을 먼저 삭제하고 키 파괴 전에 충돌이 발생한다면, 영구적으로 절반만 삭제된 당사자 (party) 정보가 남게 되며, 이 경우 requireLive 가드(guard)가 재시도를 거부하게 됩니다. 작업이 되돌릴 수 없는 경우, 어디에서 중단될 수 있는지를 중심으로 설계를 해야 합니다.
남겨지는 것은 **툼스톤 (tombstone)**입니다. 해당 항목은 status: 'Erased', erasedAt 날짜와 함께 어떤 식별 정보도 없는 상태로 생존합니다. 이는 의도된 설계입니다. 다른 모든 서비스는 여전히 partyReference를 보유하고 있으며, 공중에 떠 있는 참조 (dangling reference)는 데이터 손실과 구별할 수 없는 상태가 아니라, "2026-07-17에 삭제됨"과 같이 _설명 가능 (explainable)_해야 하기 때문입니다. 모든 곳의 감사 이력 (Audit history)은 당사자, 금액, 상태, 시간과 같은 모든 구조적 사실을 유지하지만, _누구인지_를 말할 수 있는 능력은 영구적으로 상실합니다.
하류 (Downstream) 단계에서는 오케스트레이터 (orchestrator)의 당사자 게이트 (party gate)가 루프를 완성합니다. 삭제된 당사자에 대한 새로운 여정 (journey)은 422 오류와 함께 거부되어 실패 시 폐쇄 (fail-closed) 상태를 유지하는 반면, 과거의 여정들은 설계된 대로 해당 참조를 계속 가리키게 됩니다.
누가 버튼을 누를 수 있는가
삭제 작업은 그것이 파괴하는 데이터와 동일한 엄격함으로 인증됩니다. 해당 경로는 Keycloak을 통해 검증된 베어러 토큰 (bearer token)을 요구하며 (RS256, 고정된 발행자 (pinned issuer), email_verified, 권한 부여된 당사자 확인), 그 후 두 가지 근거 규칙 (two-ground rule)을 적용합니다:
if (principal.realmRoles.includes(complianceRole)) {
return; // 근거 1: 컴플라이언스 담당자 (compliance-officer)
}
...
토큰 없음 → 401. 검증 불가능한 토큰 → 401 (이유는 로그에 남지만, 네트워크 메시지는 의도적으로 거칠게 처리됨). 유효한 토큰이지만 권한이 없는 사용자 → 403. 엔드투엔드 (e2e) 테스트 스위트는 이 세 가지 결과 모두를 curl로 호출한 뒤, 데이터베이스에서 툼스톤 (tombstone)을 직접 확인하여 엔트리가 존재하고, 상태가 Erased이며, PII 행이 0개임을 검증합니다.
우아한 자기 참조(self-reference)에 주목하십시오: 근거 2는 블라인드 인덱스 (blind index) 자체를 사용합니다 — 검증된 이메일이 해싱되어 일치 여부를 확인함으로써 "이것이 귀하의 당사자임을" 증명합니다. 즉, 프라이버시 메커니즘이 권한 부여 (authorization) 메커니즘의 역할도 겸하는 것입니다.
AI 플레인 (AI plane): 저장하지 않은 것은 파쇄하지 마라
암호화 파쇄 (Crypto-shredding)는 기록 시스템 (system of record)을 보호합니다. 대화 플레인 (conversational plane) — LLM 프롬프트, LangGraph 체크포인트, 자기 개선형 RAG — 은 별도의 제어 방식이 적용되는 다른 데이터 플레인이며, 단일 수집 접점 (ingestion seam)에서 적용됩니다:
def _redact_and_log(text: str) -> str:
"""
단일 수집 접점에서 유입되는 사용자 (USER) 텍스트로부터 PII를 제거합니다 — 데이터가 되기 전에
...```
리댁터 (redactor)는 순수 정규 표현식 (regex) + Luhn 알고리즘을 사용합니다: 사회보장번호 (SSN), 결제 카드 (Luhn 검증을 통해 20,000달러 대출 금액이 카드 번호로 오인되지 않도록 함), 키워드 기반 계좌 번호, 비밀번호 및 PIN 번호 등이 대상입니다. 어떤 데이터가 pgvector 지식 베이스 (knowledge base)에 도달하기 전에, 두 번째 패스 (pass)를 통해 텍스트를 일반화합니다 — 모든 숫자는 `<num>`으로 축소됩니다 — 따라서 지식 베이스는 사람이나 금액이 아닌 *문제와 해결책*만을 저장합니다. 그리고 Bedrock Guardrails가 모델 경계의 안전장치 역할을 하여, 양방향 모두에서 이메일과 전화번호를 익명화하고 카드 및 SSN을 차단합니다.
이 두 플레인은 깔끔한 주장을 구성합니다: **에이전트는 모델이나 벡터 스토어에 원본 식별자를 절대 보내지 않으며, 기록 시스템은 평문 신원 정보를 전혀 저장하지 않습니다.** 따라서 삭제 (erasure) 작업은 정확히 하나의 서비스에만 영향을 미치며, 이를 증명한다는 것은 단 하나의 키 삭제 (key-deletion) 이벤트를 생성하는 것을 의미합니다.
## 결론
대부분의 "GDPR 준비 완료(GDPR-ready)" 아키텍처는 데이터 삭제 요청이 들어왔을 때 데이터를 찾아보겠다는 약속에 불과합니다. 반면, 이 방식은 데이터를 퍼뜨리지 않겠다는 거부입니다. 즉, 단 하나의 PII (개인정보) 서비스, 그 외 모든 곳에서의 불투명한 참조 (opaque references), 각 당사자별 키 (per-party keys)로 관리되는 암호문 (ciphertext), 단방향 해시 (one-way hashes)를 통한 검색, 키 파괴 (key destruction)와 툼스톤 (tombstone)을 결합한 삭제 방식, 그리고 저장하기 전에 데이터를 세척하는 AI 레이어를 구축하는 것입니다.
이것이 주는 시사점은 금융업을 넘어 일반화될 수 있습니다: **시스템을 구축하기 전에 PII가 존재할 수 있는 위치를 결정하십시오. 그리고 그 위치가 외부 키 없이는 자신의 데이터베이스를 읽을 수 없도록 만드십시오. 그렇게 하면 잊힐 권리 (right to be forgotten)는 고고학 프로젝트가 아닌 하나의 API 호출이 됩니다.**
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기