6장. 개인정보를 남기지 않고도 재현한다
마스킹은 마지막 필터가 아니다
민감정보를 저장한 뒤 화면에서 가리는 방식은 충분하지 않다. 원본이 이미 수집기, 큐, 저장소, 백업에 퍼졌기 때문이다. 가장 안전한 기본값은 수집 전에 줄이는 것이다. 이름과 연락처 대신 요청 ID와 비가역 지문을 남긴다. 원문이 꼭 필요하다면 별도 권한, 짧은 보존 기간, 목적 제한과 감사 로그를 둔다.
OpenTelemetry 생성형 AI 속성 문서는 도구 인수와 결과에 민감정보가 포함될 수 있음을 명시적으로 경고한다. 따라서 “표준 필드니까 저장해도 안전하다”는 추론은 틀렸다. 표준은 이름을 맞춰 줄 뿐, 개인정보 처리의 적법성과 필요성을 대신 판단하지 않는다.
ClaimOps의 세 단계 방어
첫째, redact 함수가 customer, email, phone 키의 값을 재귀적으로 바꾼다. 둘째, 입력 전문 대신 마스킹한 객체의 SHA-256 일부를 claimFingerprint로 저장한다. 셋째, 테스트가 알려진 이메일과 전화번호 패턴이 트레이스에 없는지 검사한다.
if (['email', 'phone', 'customer'].includes(key)) {
return [key, '[REDACTED]'];
}
키 이름 기반 마스킹만으로 운영 서비스를 보호할 수는 없다. contact, mobile, 자유 텍스트 안의 이메일처럼 스키마 밖 값이 있기 때문이다. 실전에서는 허용 목록 기반 계측을 우선한다. 즉 “금지 키를 제거”하기보다 “보존하기로 승인한 속성만 새 객체에 복사”한다.
실습: 누출 테스트를 깨뜨려 보기
lab/lib.mjs의 Trace.span에서 redact(attributes)를 잠시 제거했다고 가정하자. 현재 fixture는 스팬 속성에 원문 고객 객체를 넣지 않으므로 테스트가 여전히 통과할 수 있다. 이 관찰이 중요하다. 방어 코드를 테스트하려면 실제 누출 경로를 흉내 낸 음성 fixture가 필요하다.
다음 항목을 팀의 보안 테스트에 추가한다.
- 중첩 객체와 배열 속 연락처
- 자유 텍스트 속 이메일·계좌번호
- 도구 오류 메시지에 반사된 입력
- 모델이 응답에 다시 쓴 개인정보
- 첨부 파일 이름과 URL 쿼리 문자열
재현성과 최소 수집의 균형
원문을 없애면 같은 입력을 정확히 재생하기 어렵다. 그래서 운영 환경에서는 원 시스템의 접근 통제된 요청 ID를 남기고, 재생 권한을 사고 대응 역할에만 준다. 평가 환경에는 승인된 비식별 fixture를 별도로 복제한다. 일상 대시보드와 사고 금고를 같은 저장소로 만들지 않는다.
완료 기준
- 수집 전 최소화와 화면 마스킹의 차이를 설명할 수 있다.
- 트레이스에 저장해도 되는 속성의 허용 목록을 만들었다.
- 개인정보 누출 음성 사례를 최소 다섯 개 정의했다.