権限があるからといって見てよいわけではない
한국어 원문으로 표시합니다.
한 줄 요약
FDE 는 고객의 실제 데이터에 손이 닿는 자리입니다. 기술보다 먼저 배워야 할 것은 최소한만 보고, 최소한만 옮기고, 흔적을 남기는 습관입니다.
왜 이게 필요했나
조사하다 보면 자연스럽게 이런 명령을 치게 됩니다.
SELECT * FROM orders WHERE status = 'FAILED' LIMIT 100;
여기에는 주문번호만 있는 게 아닙니다. 이름, 전화번호, 주소, 때로는 결제 정보가 함께 나옵니다. 그리고 그 결과가 터미널 스크롤백에, 캡처 이미지에, 슬랙 메시지에, 여러분의 노트북 임시 파일에 남습니다.
사고는 악의에서보다 편의에서 생깁니다. "빨리 보려고 CSV 로 뽑아서 메일로 보냈다" 가 실제 사고 보고서에 가장 많이 등장하는 문장입니다.
네 가지 원칙
1. 필요한 열만 가져온다
SELECT * 대신 조사에 필요한 열만 지정합니다. 원인 분석에 이름과 주소가
필요한 경우는 거의 없습니다.
-- 나쁨
SELECT * FROM orders WHERE status='FAILED';
-- 좋음
SELECT id, created_at, status, error_code FROM orders WHERE status='FAILED';
2. 식별자는 목적별로 가명처리한다 동일인 연결이 꼭 필요하면 정규화한 식별자에 목적별 비밀 키를 사용한 HMAC-SHA-256을 적용하고 충분한 길이의 출력을 유지합니다.
uid = HMAC_SHA256(secret_from_kms, normalize(email))
이 값은 익명화된 값이 아니라 가명화된 식별자입니다. 비밀 키나 주변 데이터가 노출되면 다시 연결될 수 있으므로 원본과 같은 수준으로 접근을 통제합니다. 시스템 간 연결이 승인된 경우에만 같은 목적 키를 공유하고, 서로 연결되면 안 되는 분석에는 목적별로 다른 키를 사용합니다.
3. 밖으로 내보내지 않는다 데이터는 있던 자리에서 다룹니다. 로컬로 내려받거나 메신저로 보내는 순간 통제 밖입니다. 부득이하면 집계된 형태로만 옮깁니다 — 원본 100행 대신 "실패 사유별 건수" 표 한 장.
4. 남긴다 언제 무엇을 조회했는지 기록합니다. 나중에 "누가 이걸 봤나" 라는 질문이 왔을 때 스스로를 방어하는 유일한 수단입니다. 대부분의 시스템에 감사 로그가 있고, 없다면 여러분의 작업 노트가 그 역할을 합니다.
마스킹은 되돌릴 수 있는지로 나뉜다
| 방식 | 되돌리기 | 언제 |
|---|---|---|
| 삭제 | 불가 | 조사에 불필요한 열 |
| 목적별 키를 쓴 충분히 긴 HMAC | 키 없이는 곤란 | 승인된 범위에서 동일인 묶기가 필요할 때 |
부분 마스킹 (010-****-1234) |
불가하지만 유추 가능 | 육안 확인이 필요할 때 |
| 토큰화(매핑 보관) | 가능 | 나중에 원본이 필요할 때 |
| 암호화 | 가능 | 이동·보관이 불가피할 때 |
일반 해시나 공개 솔트만으로는 안전하지 않습니다. 전화번호와 이메일은 후보를 만들어 전수 대입하기 쉬우며, 8자리처럼 출력을 자르면 충돌 위험도 커집니다. 레코드마다 다른 솔트는 같은 사람을 같은 값으로 묶을 수 없으므로, 반복 가능한 연결이 필요할 때는 접근 통제된 목적별 비밀 키와 충분히 긴 HMAC을 사용합니다.
테스트 데이터를 만들 때
운영 데이터를 복사해 테스트에 쓰는 것은 가장 흔하고 가장 위험한 관행입니다. 꼭 필요하다면 구조는 유지하고 값은 바꾸는 방식으로 만듭니다 — 이름은 사전에서, 이메일은 도메인을 example.com 으로, 금액은 분포를 유지하며 노이즈 추가. 이렇게 하면 재현에 필요한 성질(길이 분포, 중복, 결측)은 남고 개인정보는 사라집니다.
현장에서 만나는 모습
- 장애 채널에 붙인 로그 스크린샷에 고객 이메일이 그대로 → 채널 참여자 전원에게 유출.
- 조사용 CSV 를 노트북 바탕화면에 두고 프로젝트 종료 → 몇 년 뒤 사고.
SELECT *결과를 그대로 티켓에 붙여넣음 → 티켓 시스템에 영구 보존.
로그와 오류 보고가 가장 흔한 유출 경로
개인정보 사고는 데이터베이스가 털려서만 나지 않는다. 정상적으로 동작하는 시스템이 성실하게 남긴 기록에서 더 자주 샌다.
요청 전체를 찍는 습관이 가장 위험하다. 디버깅하려고 넣은 한 줄이 운영에 남으면, 주민등록번호와 카드번호가 로그 저장소로 흘러 들어간다. 로그는 대개 암호화되지 않고, 보존 기간이 길고, 조회 권한이 넓다.
# 이렇게 두면 어느 날 반드시 샌다
log.info("요청: %s", request.json())
# 필요한 것만, 식별자는 해시로
log.info("결제 요청 user=%s amount=%s", hash_id(user.id), amount)
오류 추적 도구는 지역 변수를 함께 보낸다. 예외가 난 프레임의 변수를 담아 보내는 것이 그 도구의 장점인데, 그 프레임에 비밀번호나 토큰이 있으면 그대로 따라간다. 보내기 전에 거르는 자리를 반드시 둔다.
def before_send(event, hint):
for k in ("password", "token", "authorization", "ssn", "card"):
_scrub(event, k)
return event
URL 에 개인정보를 넣지 않는다. 질의 문자열은 접근 로그, 프록시 로그,
브라우저 기록, Referer 헤더에 남는다. 같은 값이라도 본문에 담으면 이 네 곳에
남지 않는다.
세 번째 유출 경로는 사람이다. 장애를 조사하다가 운영 데이터를 노트북으로 내려받고, 사내 메신저에 표를 붙여 넣고, 스크린샷을 문서에 넣는다. 기술적 통제로는 막기 어려우므로 애초에 내려받을 이유를 없애는 쪽이 실효가 있다: 읽기 전용 조회 화면, 마스킹된 관리자 화면, 질의 결과를 자동으로 지우는 분석 환경.
남긴 기록은 언젠가 요청받는다. 어떤 로그에 무엇이 들어가는지 목록으로 관리하고 보존 기간을 정해 둔다. 삭제 요청이 왔을 때 데이터베이스만 지우고 로그와 백업에 남아 있다면 지운 것이 아니다. 처음부터 안 남기는 것이 지우는 것보다 언제나 싸다.
이어지는 실습에서 할 것
개인정보가 섞인 주문 200행을 받아, 밖으로 내보낼 한 장을 만드는 데까지 갑니다. 네 원칙을 각각 파일로 남기고, 마지막에는 반출 전 검사기를 직접 만듭니다.
채점기가 그 검사기를 유출본과 깨끗한 파일에 각각 돌립니다. 전부 막는 검사기도, 전부 통과시키는 검사기도 떨어집니다 — 오탐이 나면 아무도 쓰지 않고, 놓치면 있으나 마나이기 때문입니다.