본문으로 건너뛰기

사용자 개인정보 삭제

사용자를 익명화하면 계정에서 이름과 이메일 주소가 지워지고, 로그인 연결과 세션이 제거되며, 그 사람의 감사 기록에 남은 접속 정보가 지워집니다. 되돌릴 수 없고, 한 사람이 다른 사람에 대해 요청하는 일입니다.

이 문서는 그 절차입니다. 이 작업이 지우지 않는 것도 함께 적습니다. 무언가를 남긴 채 성공했다고 보고하는 삭제는 무엇을 남겼는지 밝히는 삭제보다 나쁘기 때문입니다.

대상 독자

요청과 승인은 super_admin, 이행은 데이터베이스 소유자 자격증명을 가진 운영자입니다. 두 역할은 서로 다른 사람이 서로 다른 일을 하는 것이고, 그것이 의도입니다.

버튼 하나로 만들지 않은 이유

계정 행 자체를 지울 수는 없습니다. 감사 기록이 그 행을 참조하고 있고, 그 참조가 "누가 무엇을 했는가"를 증거로 만듭니다. 사용자를 지우면 그 기록이 함께 사라지거나, 이름이 있던 자리가 "누군가"로 바뀝니다. 그래서 계정은 내용만 비운 채 남습니다.

감사 기록 자체는 추가만 가능하고, 데이터베이스 트리거가 그것을 강제합니다. 거기 남은 접속 정보를 지우려면 그 규칙에 예외가 필요한데, 그 예외는 데이터베이스 소유자만 실행할 수 있는 SECURITY DEFINER 함수입니다. 애플리케이션은 그 자격증명을 갖지 않습니다. docker-compose.yml이 런타임 컨테이너에 넘기지 않기 때문에, 포털이 침해되더라도 자기가 한 일의 기록을 고쳐 쓸 수 없습니다.

여기서 두 가지가 따라 나오고, 둘 다 불편이 아니라 요점입니다.

  • 승인은 제품 안에서, 이행은 서버에서 사람이 직접 합니다.
  • 그 사이에는 아무것도 고장 나지 않은 채 누군가가 기다리는 구간이 생깁니다.

세 단계

1. 요청 열기

최고 관리자 누구나 대상자에 대해 요청을 엽니다. 이 단계에서 지워지는 것은 없습니다.

POST /v1/user-anonymisation
{ "subject_user_id": "...", "reason": "..." }

최고 관리자가 자기 자신에 대해 요청할 수는 없습니다. 이 통제의 핵심이 서로 다른 두 사람의 동의인데, 자기 요청은 설득할 사람을 하나로 줄입니다. reason은 적은 그대로 저장되므로 연락처를 적지 마십시오.

요청은 7일 동안 승인할 수 있고, 만료를 알리는 통지는 없습니다. 알림이 가지 않고 만료된 요청은 어느 화면에도 나타나지 않습니다. 요청을 연 사람은 아직 결정을 기다리는 중이라고 믿는 동안 삭제는 이행되지 않은 채 남고, 그에 걸린 기한이 지나갑니다. 아직 결정되지 않은 요청을 주기적으로 확인하는 일을 조직의 운영 절차에 넣으십시오. 포털이 알려 주지 않습니다.

같은 대상자에 대해 새 요청을 열면 만료된 요청은 정리되므로, 아무도 결정하지 않은 요청 때문에 대상자가 영구히 막히지는 않습니다.

2. 다른 최고 관리자가 승인

POST /v1/user-anonymisation/{request_id}/approval

승인자는 요청자와도 대상자와도 달라야 합니다. 서비스에서 확인하고 데이터베이스 제약으로 한 번 더 막습니다. 나중에 다른 호출 지점이 생겨도 잊어버려서 건너뛸 수 없게 하기 위해서입니다.

승인해도 아직 지워지는 것은 없습니다.

3. 운영자가 명령 실행

승인은 아무것도 예약하지 않습니다. 삭제는 사람이 이 명령을 실행할 때만 이루어지고, 대신 실행해 주는 것은 없습니다.

이 단계를 조직의 운영 절차에 넣으십시오. 포털이 시간이 되었다고 실행하지 않습니다.

docker-compose -f docker-compose.yml exec -T \
-e DATABASE_URL_OWNER="$(grep ^DATABASE_URL_OWNER= .env | cut -d= -f2-)" \
-e SUBJECT_USER_ID=... \
-e CONFIRM=yes \
backend python -m scripts.anonymise_user

승인된 요청이 없으면 명령이 거부하고, 데이터베이스도 따로 거부합니다. CONFIRM=yes를 요구하는 이유는, 두 사람이 대상자에 대해 동의한 것과 운영자가 지금 이 배포에서 이 식별자를 대상으로 실행할 뜻이 있는 것은 다른 문제이기 때문입니다.

명령이 하는 일은 모두 한 트랜잭션 안에서 이루어집니다. 거부되면 절반만 지워진 상태로 남지 않습니다.

그 사이를 보는 화면

관리 → 상태 화면에 "이행을 기다리는 익명화" 패널이 있습니다. 승인됐지만 아직 실행되지 않은 요청을 오래된 순으로, 대기 일수와 함께 보여 줍니다. 7일이 지나면 기한 초과로 표시합니다. 그 시점이면 결정을 기다릴 수 있었던 기간보다 이행을 기다린 기간이 더 길어진 것입니다.

비어 있을 때 이 패널은 초록이 아니라 무채색입니다. 무언가를 확인해서 이상이 없는 것이 아니라, 갚을 것이 없는 것이기 때문입니다.

2인 승인이 막는 것과 막지 못하는 것

다른 곳의 표현이 설계보다 강한 약속으로 읽힐 수 있어 정확히 적습니다.

요청 행은 데이터베이스가 확인합니다. 그 대상자에 대한 승인된 요청이 없으면 스크럽을 거부합니다. 이것은 행이 존재하는지를 보는 것이지 두 사람이 동의했음을 증명하는 것이 아닙니다. 절차가 동작하려면 애플리케이션이 그 테이블에 쓸 수 있어야 하고, 따라서 포털을 통해 SQL 실행에 도달한 것은 무엇이든 approved라고 적힌 행을 넣고 실재하는 최고 관리자 두 명의 이름을 적을 수 있습니다.

그 뒤를 받치는 것이 셋입니다. 제품을 거친 요청은 추가만 가능한 감사 로그에 createupdate를 남기고, 운영자 명령은 둘 다 없는 요청을 거부하므로 직접 SQL로 만든 행은 실행되지 않습니다. 미이행 목록이 요청자와 승인자를 표시하므로 운영자는 되돌릴 수 없는 일을 하기 전에 물어볼 사람이 둘 있습니다. 그리고 그 명령은 실행 중인 포털이 결코 받지 않는 자격증명을 가진 사람이 실행합니다.

요청 행에 애플리케이션 비밀키로 서명하는 방안은 검토했고 채택하지 않았습니다. 그 방식이 방어하는 것은 데이터베이스에는 쓸 수 있지만 애플리케이션 설정에는 닿지 못하는 공격자인데, 운영자 명령이 그 키를 같은 호스트에서 읽어야 하므로 키가 결국 그으려던 경계 위에 놓입니다. 또 행을 위조하는 비용만 올릴 뿐 그 행이 뜻하는 바를 바꾸지 못하고, 이 기능이 막으려는 상황을 새로 만듭니다. 키를 회전하면 이전에 승인된 삭제가 실행되지 않은 채 미이행 목록에 남아 기한을 넘깁니다.

지워지는 것

위치지워지는 내용
users이메일(연락 불가능한 대체 값으로 교체), 이름, 비밀번호 해시. 계정은 비활성화됩니다.
oauth_identities연결된 모든 공급자. 공급자가 갖고 있던 주소 사본도 함께 지워집니다.
refresh_tokens, password_reset_tokens전부. 사용 중인 세션이 끊깁니다.
saved_searches대상자가 직접 작성한 저장된 검색.
audit_logs대상자가 수행자로 기록된 행의 ipuser_agent.
개인 팀개인 조직에 속하고 대상자가 구성원인 팀의 이름을 Personal team (<식별자 앞부분>)으로 바꾸고 설명을 비웁니다.

계정은 남지만 로그인은 남지 않습니다

감사 기록이 이 행을 참조하고 그 참조가 누구의 행위인지를 지키기 때문에 계정 행은 남습니다. 기록으로 남는 것이지 들어오는 길로 남는 것이 아닙니다. 어떤 경로로도 인증되지 않습니다.

  • 비밀번호. 비밀번호 변경이 지나는 것과 같은 단일 지점을 통해 아무도 모르는 값으로 바뀌고, 계정은 비활성화됩니다. 옛 비밀번호를 알던 사람이 알고 있는 것은 쓸모가 없습니다.
  • 비밀번호 재설정. 옛 주소가 더 이상 어떤 계정에도 연결되지 않으므로, 그 주소로 온 재설정 요청은 등록된 적 없는 주소와 똑같이 처리됩니다. 토큰이 발급되지 않고 메일도 가지 않습니다.
  • OAuth. 공급자 연결이 삭제되어 있어 다시 찾아온 공급자가 연결로 계정을 찾을 수 없습니다. 주소로도 찾을 수 없습니다. 대조할 주소가 더 이상 저장되어 있지 않기 때문입니다. 같은 공급자에 같은 주소로 다시 로그인하면 옛 계정이 다시 열리는 것이 아니라 비어 있는 새 계정이 만들어집니다.

남는 것

누군가에게 개인정보를 지웠다고 알리기 전에 이 절을 읽으십시오.

감사 기록의 diff 내용. 감사 행은 변경의 내용을 담고 있고, 이 버전 이전에 쓰인 행에는 대상자의 옛 주소가 그 안에 들어 있을 수 있습니다. 이 작업이 쓰는 트리거 예외는 diff를 건드리는 것 자체를 금지합니다. 의도한 것입니다. diff를 고쳐 쓸 수 있을 만큼 넓은 예외는 감사 기록의 불변성을 없애고, 그 불변성이 감사 기록을 증거로 만드는 성질입니다. 마스킹이 도입된 뒤에 쓰인 행은 주소 대신 단방향 해시를 담습니다. 다만 그 값이 email 또는 full_name이라는 이름의 컬럼에 있었을 때만 그렇습니다. 마스킹이 컬럼 이름을 기준으로 동작하기 때문입니다. 알림 수신자 목록 같은 JSON 안에 주소가 들어간 경우처럼 다른 경로로 diff에 들어간 주소는 평문 그대로 남습니다. 그리고 해시로는 주소를 복원할 수 없지만 특정 주소를 대조해 확인할 수는 있습니다. 이것은 익명화가 아니라 가명처리이고, 그렇게 설명해야 합니다.

공용 팀의 이름과 설명. 누군가 공용 팀 이름이나 설명에 대상자의 주소를 적어 두었다면 그대로 남습니다. 다른 사람들이 그 이름으로 팀을 찾고 있고, 한 사람에 대한 삭제가 여러 사람이 쓰는 기록을 고쳐 쓸 근거가 되지는 않습니다. 바꿔야 한다고 판단하면 직접 바꾸십시오.

알림 전달 규칙. 전달 규칙에 설정된 수신 이메일은 누군가 목적지로 입력한 주소이지 계정의 속성이 아니어서 이 작업의 대상이 아닙니다. 별도로 검토하십시오.

이 버전 이전에 만들어진 개인 팀. 그 팀들은 사람 이름으로 지어졌습니다. 이 작업은 대상자 본인의 팀만 바꾸고, 나머지는 그 팀의 주인이 익명화될 때까지 이름을 유지합니다.

삭제 이전에 뜬 백업. 백업에는 계정이 그대로 들어 있습니다. 복원하면 이메일과 이름이 되살아나고 요청 행도 executed 상태로 함께 돌아오므로 명령을 그냥 다시 돌릴 수도 없습니다. 자동 백업은 BACKUP_RETENTION_DAYS(기본 7일)만큼 보관하고, 오프사이트 사본과 시점 복구 구간은 별도로 관리됩니다. 영향받는 백업을 폐기할지, 복원 후 삭제를 다시 수행할지를 조직의 절차로 정해 두십시오. 다시 수행하려면 새 요청을 열고 다시 승인받으십시오. 이행이 끝난 요청은 대상자의 슬롯을 잡지 않으므로 같은 사람에 대해 새 요청을 열 수 있고, 명령은 그 요청으로 실행됩니다. 옛 요청을 다시 돌리려 하지 마십시오. 이행 완료로 표시되어 있어 거부됩니다.

복원만이 옛 값이 되살아나는 경로는 아닙니다. PostgreSQL이 정리하기 전까지 데이터베이스 내부 페이지에도 남습니다. 백업과 복원을 참고하십시오.

대상자가 만든 API 키. 남는 것은 아니지만 미리 대비해야 합니다. 계정이 비활성화되는 순간 인증에 실패합니다. API 키 인증이 키를 만든 사용자를 조회해 비활성 계정을 거부하기 때문입니다. 퇴사자가 발급한 CI 키가 삭제와 함께 죽습니다. 파이프라인이 의존하는 키는 미리 다른 발급자로 재발급하십시오.

감사 기록 자체. 누가 언제 무엇을 했는지는 남습니다. 이 작업이 파괴하지 않으려고 조심하는 기록이 바로 그것입니다.

자기 데이터 사본 받기

로그인한 사용자는 누구나 자기에 대해 보관된 내용을 내려받을 수 있습니다.

GET /v1/users/me/export

응답은 호출자의 토큰을 기준으로 만들어집니다. 경로 어디에도 사용자 식별자가 없어서 다른 사람을 겨냥할 수 없습니다.

계정, 알림 설정, 로그인 수단, 팀 소속, 저장된 검색, 본인의 활동 기록이 들어 있습니다. 업무 산출물은 들어 있지 않습니다. 프로젝트·스캔·발견·정책은 조직을 위해 수행된 업무의 기록이지 그 일을 한 사람에 대한 개인정보가 아닙니다.

두 가지 한계는 숨기지 않고 응답 안에 적습니다.

  • 활동 기록에서 변경 내용은 뺍니다. 본인이 다른 사용자에 대해 수행한 변경 기록에는 그 사람의 정보가 들어 있어, 한 사람의 권리를 명분으로 다른 사람의 정보를 넘기게 됩니다.
  • 활동 기록은 최근 5,000건까지입니다. 잘린 경우 activity.truncatedtrue가 되고 activity.total에 실제 총계가 들어갑니다. 전체 기록이 필요한 요청이면 최고 관리자가 감사 로그에서 직접 추출할 수 있습니다. 추출 경로와 필터는 감사 로그를 참고하십시오.