← 모든 글
Security

서비스를 멈추지 않고 개인정보를 전부 암호화했다

PG 라이선스 심사를 준비하며 개인정보 영역 전체를 암호화로 옮기는 동안 검색·정렬·중복 확인·조인이 한꺼번에 깨진 과정을 적었다.

PG 라이선스 심사를 준비하면서 개인정보 영역 전체를 암호화해야 했다. 일부 민감한 값만 골라 담는 수준이 아니라, 개인정보로 분류되는 값은 저장된 것까지 전부 대상이었다.

문제는 이미 쌓여 있는 데이터였다. 오래 운영한 결제 시스템이라 평문으로 저장된 이력이 많았고, 그 데이터를 쓰는 화면과 배치가 계속 돌고 있었다. 서비스를 멈추고 한 번에 바꾸는 선택지는 없었다.

멈출 수 없는 시스템을 바꿔야 했다

암호화 자체는 어렵지 않다. 저장 직전에 암호화하고 조회 직후에 복호화하면 된다. 어려운 것은 그 전환을 운영 중에 하는 일이었다. 전환하는 동안에도 결제는 계속 들어오고, 관리자는 조회하고, 정산 배치는 돌아간다.

그래서 전환 기간 동안 평문과 암호문이 같은 테이블에 공존한다는 것을 전제로 잡았다. 어느 시점에 모든 데이터가 한꺼번에 바뀌는 그림이 아니라, 오래 걸리는 이행 상태를 정상으로 다루는 그림이었다.

신규는 암호화해서 쌓고 과거는 배치로 옮겼다

방향은 두 갈래였다. 새로 들어오는 데이터는 그 시점부터 암호화해서 저장했다. 이미 쌓인 데이터는 배치를 돌려 조금씩 옮겼다.

flowchart TD
  A["신규 요청"] --> B["암호화해서 저장"]
  C[("기존 평문 데이터")] --> D["이관 배치"]
  D --> E["암호화해서 갱신"]
  E --> C
  B --> F[("결제 원장")]
  E --> F

이렇게 하면 전환이 끝나지 않아도 새 데이터는 요건을 만족한다. 남은 것은 과거 데이터의 양이고, 그건 시간 문제로 바뀐다. 서비스를 멈추지 않으려면 이 구조 말고는 방법이 없었다.

대신 읽는 쪽이 복잡해졌다. 이관이 끝나지 않은 동안에는 같은 컬럼에 평문과 암호문이 섞여 있으므로, 조회하는 모든 경로가 두 경우를 다 처리할 수 있어야 했다.

암호문이 되는 순간 네 곳이 같이 깨졌다

암호화를 적용하자 예상하지 못한 곳에서 기능이 멈췄다. 하나가 아니라 네 가지가 동시에 나왔고, 원인은 전부 같았다. 암호문은 원문이 가지고 있던 성질을 하나도 유지하지 않는다.

깨진 곳왜 깨지나
검색암호문에는 부분 일치가 성립하지 않는다
정렬암호문의 순서는 원문의 순서와 무관하다
중복 확인같은 원문도 저장할 때마다 다른 암호문이 될 수 있다
조인조인 키가 암호문이면 양쪽 값이 맞아떨어지지 않는다

네 가지를 한꺼번에 푸는 방법은 없었다. 필요한 성질이 서로 달라서 각각 다른 결정이 필요했고, 어떤 것은 기능 요구사항 자체를 다시 봐야 했다.

검색과 정렬은 원문의 순서를 잃는다

이름이나 연락처 일부로 찾는 조회가 여러 화면에 있었다. 암호화하면 이런 조회는 성립하지 않는다. 암호문을 아무리 비교해도 원문의 일부가 포함되어 있는지 알 수 없다.

정렬도 마찬가지다. 암호문을 정렬하면 원문과 아무 관계 없는 순서가 나온다. 목록 화면에서 이름순으로 보던 기능이 그대로 무너졌다.

이 두 가지는 기술로 덮기 전에 그 조회가 정말 필요한지부터 확인해야 했다. 업무상 꼭 필요한 조회와 편의로 만들어 둔 조회를 나누고, 필요한 것만 별도 요구사항으로 떼어 검토했다. 개인정보를 부분 검색할 수 있어야 한다는 요구 자체가 심사 기준과 충돌할 수 있어서, 기능을 유지하는 것이 항상 정답은 아니었다.

중복 확인은 같은 값이 다른 암호문이 된다

가장 조용히 깨진 것이 중복 확인이었다. 같은 값을 두 번 저장하지 못하게 막아 두었는데, 암호문이 매번 달라지면 데이터베이스는 두 행을 서로 다른 값으로 본다. 제약은 걸려 있는데 아무것도 막지 못하는 상태가 된다.

이 문제는 오류를 내지 않아서 더 위험했다. 화면에서는 정상으로 보이고, 중복이 쌓인 뒤에야 드러난다. 암호화를 적용한 컬럼마다 유일성을 어떤 근거로 판단하고 있었는지 다시 확인해야 했다.

암호화 전
  같은 값 저장 시도 → 유일성 제약 위반 → 거부

암호화 후
  같은 값 저장 시도 → 서로 다른 암호문 → 제약이 통과 → 중복 저장

조인은 키가 암호문이 되면 끊긴다

개인정보 컬럼을 기준으로 다른 테이블과 연결하던 쿼리가 있었다. 양쪽이 같은 방식으로 암호화되지 않으면 값이 맞지 않아 결과가 비었다.

여기서 드러난 것은 암호화 문제라기보다 설계 문제였다. 개인정보를 관계의 기준으로 쓰고 있었다는 뜻이기 때문이다. 연결이 필요한 곳은 개인정보가 아닌 식별자로 기준을 옮겨야 했고, 그건 스키마를 건드리는 작업이었다.

이관 배치는 한 번에 끝나지 않았다

과거 데이터를 옮기는 배치는 아주 오래 걸렸다. 양이 많은 것도 이유였지만, 운영 중인 시스템이라 한 번에 많이 처리할 수 없다는 제약이 더 컸다. 배치가 데이터베이스를 오래 붙잡으면 결제 처리가 느려진다.

flowchart TD
  A["대상 조회"] --> B["소량 단위로 자름"]
  B --> C["암호화해서 갱신"]
  C --> D["진행 지점 기록"]
  D --> E{"남은 데이터가 있나?"}
  E -- "있음" --> A
  E -- "없음" --> F["이관 완료"]

그래서 한 번에 조금씩, 오래 돌리는 방식으로 갔다. 어디까지 옮겼는지 기록해 두고 다음 실행이 그 지점부터 이어받게 했다. 중간에 멈춰도 처음부터 다시 하지 않아도 됐다.

끝났다고 선언하기까지도 시간이 걸렸다. 남은 평문이 하나도 없다는 것을 확인해야 이행 상태를 정리할 수 있는데, 그 확인 자체가 전체를 훑는 작업이었다.

보안 심사에서 지적받은 것은 암호화 자체가 아니었다

라이선스 심사를 앞두고 외부 업체에 의뢰한 시스템 보안 심사가 따로 있었다. 정작 거기서 지적받은 것은 암호화한 값이 아니라 그 값을 다루는 방식이었다. 두 가지가 나왔다. 관리자 권한과 로그로 추적하는 부분이었다.

관리자 화면은 업무상 개인정보를 봐야 하는 경우가 있어서 권한이 넓게 열려 있었다. 값을 암호화해도 볼 수 있는 사람의 범위가 넓으면 보호 수준은 그만큼이다. 로그도 마찬가지였다. 누가 언제 무엇을 조회했는지 남지 않으면 사후에 확인할 방법이 없다.

이 두 가지를 어떻게 다시 세웠는지는 PG 시스템 보안 심사에서 마주친 104개의 보안 이슈에 이어서 적었다. 암호화는 시작이었지 결론이 아니었다.

남은 한계

전환은 끝났지만 비용은 남았다. 조회 경로마다 복호화가 붙었고, 검색을 포기한 화면은 다른 방식으로 찾아야 한다. 편의를 위해 만들어 두었던 조회 중 일부는 되살리지 않았다.

배치가 오래 걸린 것도 그대로 기록해 둔다. 운영을 멈추지 않는 대가로 시간을 쓴 것이고, 그 시간 동안 평문과 암호문이 섞여 있는 상태를 계속 관리해야 했다. 처음부터 개인정보를 관계의 기준으로 쓰지 않았다면 그중 상당 부분은 겪지 않았을 일이다.

암호화는 컬럼을 바꾸는 작업이 아니었다. 그 값을 읽는 모든 경로와 그 값에 기대고 있던 모든 규칙을 다시 확인하는 작업이었다.