복사되었습니다.

초보 개발자 클릭 금지! Python으로 PostgreSQL DB 개인정보 암호화하기 1편 - 문제정의와 요구사항 분석

Cover Image for 초보 개발자 클릭 금지! Python으로 PostgreSQL DB 개인정보 암호화하기 1편 - 문제정의와 요구사항 분석

AI와 빅데이터가 모든 것의 중심이 되어가는 세상에서 개인정보 보호는 매우 중요하다. 데이터 수집 과정에서 개인정보가 노출되면 심각한 범죄로 이어질 수도 있다. DB 개인정보 암호화를 하려면 어떤 것들을 고려해야 할까? 간단한 예시와 함께 DB 개인정보 암호화를 달성하기 위한 방법들을 살펴보자.

Introduction

작성 배경: DB 암호화해주세요!

참여중인 프로젝트에서 DB에 저장되는 사용자 개인정보를 암호화해달라는 요청사항이 있었다. 소프트웨어 품질인증(GS인증)을 앞두고, 미루고 미루던 DB 암호화를 해야 하는 순간이 온 것이다. 평소 관심이 있는 주제였지만 항상 나중으로 미루고만 있었기 때문에, 이 참에 공부도 할 겸 자신있게 일을 맡겠다고 선언했다.

DB 암호화가 중요하다는 것은 명백하다. 그런데, 기존에는 DB와 소통이 필요한 신규 기능 개발만 급급했지 사실 암호화에 대해 신경쓴 적은 딱히 없었던 것 같다. 생소하지만 중요한 일이었기에, 연속된 야근으로 고통받았음에도 그 끝에는 적지 않은 성취감을 느낄 수 있었다.

최근 다양한 프레임워크와 라이브러리들을 사용하면서, 스스로가 "개발자"가 아니라 "사용자"가 아닐까하는 의문이 들곤 했다. 그런데, DB 개인정보 암호화 업무를 진행하면서 여러 지식들을 흡수하다보니 불안한 마음에 조금은 위안이 된 것 같다. 이렇게 얻은 위안을 조금이나마 나눌 수 있으면 좋겠다고 생각하여 글을 적기 시작했다. 이 글을 읽고 어디가서 DB 암호화에 대해 조금이라도 아는 척을 할 수 있는 순간이 왔으면 좋겠다.

대상 독자 및 주요 내용

이 글은 DB 개인정보 암호화를 해달라는 말을 듣고 당황스러워하는 초보 개발자에게 바치는 글이다. 이 글을 읽고 있다는 것은 애써 태연한 척하며 "까짓거 한 번 해보죠"라고 말한 상황임을 알고 있기 때문이다. 물론 내가 그랬다.

why not give it a shot

DB 개인정보 암호화에 대해 거의 백지 상태에 가깝다는 것을 전제로 하기 때문에, 내용이 다소 장황할 수 있다. 따라서, 최대한 읽는 부담을 덜기 위해 전체 내용을 시리즈로 연재할 예정이다. 이 DB 개인정보 암호화 시리즈는 다음 두 가지 물음에 대한 답이다.

  • DB 개인정보 암호화를 하려면 무엇을 알아야 하지?
  • Python으로 PostgreSQL DB에 암호화를 하려면 어떻게 해야 하지?

다행히 급한 일정은 아니었기에, 생소한 개념들을 차근차근 정리할 기회가 생겼다. 내용은 방대하지만, DB 암호화를 위해 필수적인 것들만 추려서 정리해보고자 한다. 이 시리즈에서 다루는 주요 내용들은 다음과 같다.

  • DB 개인정보 암호화 고려사항
  • 암호화 관련 배경지식
  • Pgcrypto extension을 통한 PostgreSQL DB 암호화 실습

문제정의 및 요구사항

DB 개인정보 암호화라는 task를 구체화해보자. DB에 사용자의 개인정보를 담은 users라는 테이블이 있다. 이 테이블 안에는 세 가지 중요한 column들이 정의되어 있다.

  • name: 사용자의 이름
  • email: 사용자의 이메일 주소
  • password: 사용자 계정의 비밀번호

온라인 게임에 비유해보자. emailpassword는 사용자 계정 로그인에 필요한 정보이고, name은 게임 캐릭터의 닉네임에 해당한다. 이밖에도 사용자의 거주지, 전화번호 등 민감정보가 포함될 수 있다. 하지만, 설명의 편의성을 위해 위 세 가지 값만 있다고 가정한다.

요구사항은 간단하다. 누구든 DB에 접속했을 때, users 테이블에서 평문(plaintext)으로 정보가 노출되면 안된다. 다시 말해서, 인간이 직관적으로 읽을 수 없는 bytes 형식으로 정보들이 암호화되어 저장되어야 한다는 것이다.

pepe with question marks

간단하다고 생각하는가? 문제를 좀 더 복잡하게 만들어보자. 위 세 가지 값에는 저마다의 특징이 있다. 각 특징을 만족시키기 위한 추가적인 요구사항들도 함께 살펴보자.

  • 암호화: name, email, password는 모두 bytes 형식으로 저장되어야 한다.
  • 정렬: nameemailORDER BY 쿼리를 통해 정렬이 가능해야 한다.
  • 검색: nameemailLIKE 쿼리를 통해 검색이 가능해야 한다.
  • 보안: password는 어떤 방법으로도 복호화할 수 없게 만들어야 한다.
  • 고유성: email은 사용자 계정 로그인에 필요한 값이므로, unique해야 한다.

이 다섯 가지 요구사항을 모두 만족하는 DB 개인정보 암호화를 하려면 어떻게 해야 할까? 결론부터 말하자면, 한 가지 방법으로 모든 요구사항을 만족시킬 수는 없다. 다양한 해결 방법들을 찾기 위해 고려해야 할 내용은 크게 세 가지 정도이다.

  • 복호화 가능 여부
  • 암호화와 복호화의 주체
  • 랜덤성 여부

이 세 가지 측면에서 모든 요구사항들을 만족시킬 수 있는 다양한 방법들을 살펴볼 것이다. 아직 혼란스럽겠지만 이어지는 내용을 통해 차근차근 알아보자.

DB 암호화 요구사항 분석

복호화 가능 여부

우선 요구사항들을 천천히 읽어보면, 한 가지 중요한 힌트를 포착할 수 있다. 이를 한 문장으로 표현하면 다음과 같다.

"암호화 방법은 복호화가 가능한 방법과 불가능한 방법으로 나뉜다."

이 한 문장을 머릿속에 입력한 채로 다시 요구사항을 읽어보자. 우선 정렬과 검색이 가능해야 함은 무엇을 의미할까? 당연하게도, 복호화가 가능해야 한다는 뜻이다. 암호화된 상태로도 정렬과 검색이 가능하지 않냐는 의문이 생기는가? 물론 기술적으로는 가능하지만 바람직하지 않은 방법이다.

decrypted sorted encrypted column

왜 바람직하지 않을까? "복호화가 불가능한 방법"을 썼을 때를 생각해보면 된다. 암호화된 상태로 정렬과 검색이 가능하려면 어떤 조건이 필요할까? 우선 정렬 요구사항을 만족하려면 원본 문자열을 암호화하여 bytes로 변환했을 때, 암호화된 bytes 값의 순서와 원본 값의 정렬 순서가 동일해야 한다. 그리고 검색 요구사항을 만족하려면, 원본 문자열을 암호화했을 때 항상 같은 값으로 변환되어야 한다. 이것을 결정적 암호화(deterministic encryption)라고 한다.

정리하면, 복호화가 불가능한 상태에서 정렬과 검색 요구사항을 만족시키기 위해서는 다음 두 가지를 지원하는 암호화 방법을 사용해야 한다.

  • 입력 값이 들어오면 랜덤성 없이 항상 같은 값으로 암호화된다.
  • 암호화되기 이전 값들 사이의 정렬 순서는 암호화가 된 후에도 유지된다.

이 방법은 심지어 email의 unique 제약 조건도 만족시킨다. 입력 값은 항상 같은 값으로 암호화가 되기 때문에, 복호화하지 않더라도 unique한지 검사할 수 있기 때문이다. 그리고 애초에 password는 복호화가 불가능해야 한다는 조건이므로, "복호화가 불가능한 방법"을 사용하면 다섯 가지 요구사항 모두를 만족시킬 수 있다는 결론에 이르게 된다.

그런데, 과연 이것이 정답일까? 이 방법을 사용한 결과물을 들고 가면, 조금이라도 보안에 관심이 있는 선배나 동료들에게 실망을 안겨줄 수도 있다. 물론, 이 방법이 아예 평문으로 개인정보를 저장하는 것보다는 낫다. 그렇지만, 조금만 생각해보면 이 방법이 해킹에 매우 취약하다는 것을 쉽게 알 수 있다.

사실상 이 방법은 암호화라기보다는 단순한 값의 "치환"에 가깝다. Deterministic encryption을 사용했기 때문에, 많은 input과 output 쌍을 생성해보면 쉽게 패턴을 찾아낼 수 있다. 심지어 순서까지 보장되니까 힌트도 충분한 셈이다. 한 번 패턴을 찾아냈다면, 사실상 평문을 사용하는 것과 같이 해커가 마음대로 값을 생성하고 조작할 수 있다. 보안 감사를 통과하기 위해 이 방법을 임시적으로 사용할 수는 있겠지만, 언젠가는 바꿔야 할 날이 올 것이다.

소결론을 하나 내려두자. nameemail같이 정렬과 검색이 가능해야 하는 값들은 랜덤성이 있으면서 복호화가 가능한 암호화 방식을 사용해야 한다. Deterministic encryption의 사용은 되도록이면 피하자.

ColumnDecryptable
nameO
emailO
passwordX

암호화와 복호화의 주체

암호화와 복호화는 어디서 이루어져야 할까? 크게 두 가지 옵션이 있다.

  • Application에서 수행
  • DB에서 수행

두 옵션의 차이가 와닿지 않는다면, 간단하게 "SQL 쿼리로 처리 가능 여부"를 바탕으로 이해하면 된다. DB에서 수행한다는 것은 SQL 문법을 통해 암호화와 복호화를 처리할 수 있다는 뜻이다.

개발자라면 application에서 수행하도록 하는 것이 먼저 떠오를 것이다. 코드로 다양한 암호화 방식을 자체적으로 개발하는 등 유연성이 있기 때문이다. 그렇다면, application에서 암호화와 복호화를 수행하는 것으로 모든 요구사항을 만족시킬 수 있을까? 정답은 no다.

일단 검색과 정렬 기능부터 생각해보자. 값을 저장할 때 application에서 암호화 알고리즘을 실행시킨 뒤, 결과 bytes 값을 DB에 저장했다고 치자. 이 값들을 대상으로 특정 값을 검색하려면 어떻게 해야 할까? DB에 저장된 모든 값들을 application에 불러온 뒤, 하나씩 복호화해서 값이 일치하는지 비교해야 한다. 정렬도 마찬가지다. 모든 값을 application에서 복호화한 뒤에 정렬해서 사용자에게 반환해야 한다. 다시 말해서, LIKEORDER BY같은 쿼리를 사용할 수 없다는 얘기다.

search encrypted value

따라서, nameemail은 DB에서 암호화와 복호화를 수행하는 것이 좋다. 그렇지 않으면, application은 단순한 SELECT 쿼리만을 사용해야 하며, 사용자의 요청이 올 때마다 DB에 있는 모든 값을 application에 불러와야 한다. 데이터가 많다면 메모리의 한계 때문에 애초에 적용이 불가능한 방법이다.

그렇다면, 정렬과 검색이 필요없는 password는 어떨까? 사용자가 로그인을 요청했을 때, 사용자가 입력한 password 값과 DB에 저장되어 있는 값이 일치하는지만 확인하면 된다. 만약 DB가 encryption을 수행한다면, 비교 로직도 DB에서 이루어져야 한다. 다시 말해서, 사용자가 입력한 문자열에 encryption을 수행한 뒤, 저장된 값과 일치하는지 여부를 반환하는 SQL문을 작성해야 한다는 뜻이다. DB에서 제공하는 몇 가지 암호화 또는 해시 알고리즘을 사용하여 bytes로 변환한 뒤 단순 일치여부만 확인하는 것이라면, DB에서 수행하는 것도 고려해볼 만 하다.

그런데, 비밀번호는 다른 값들보다는 좀 더 특별 관리가 필요하지 않을까? DB에서 제공하지 않는 커스텀 암호화 방법을 사용할 수도 있고, validation 로직도 더욱 복잡하게 구성해서 보안을 강화할 수도 있다. 이는 application에서 암호화를 수행해야만 달성할 수 있다. 그리고 애초에 값에 대한 validation 역할은 application에서 수행하도록 설계하는 것이 대부분이다. Password에 대한 validation 로직만 따로 DB에서 수행하도록 할 필요가 있을까? 코드 통합을 위해, 그리고 안그래도 바쁜 DB의 부담을 덜어주기 위해, 비밀번호 암호화는 application에서 수행하도록 하는 것이 어떨까?

참고로, 추후 구현 방법을 소개하는 후속편에서 살펴보겠지만, password에 대해 DB에서 암호화와 validation을 쉽게 수행하도록 돕는 방법들이 있다. 따라서, 이러한 방법들을 채택하고 application 단에서의 구현을 최소화하고자 한다면, DB가 주체가 되도록 구성해도 무방하다. 장단점을 고민해보고 상황에 맞게 선택하도록 하자. 정답이 있는 것은 아니다.

Don't reinvent the wheel.

이 글에서는 다채로운 예시를 선보이기 위해, password의 암호화 주체를 application으로 결정했다는 가정 하에 진행하겠다. 따라서, 소결론을 또 하나 내리고 가자. password는 암호화 방식 및 validation 로직의 유연성을 위해 application에서 암호화를 진행하도록 하고, 나머지 개인정보는 DB에서 암호화 및 복호화하도록 구현한다.

ColumnEncryptor
nameDB
emailDB
passwordApp?

랜덤성 여부

다섯 가지 요구사항 중 가장 까다로운 한 가지가 아직 커버되지 않았다. 바로 email이 unique해야 한다는 것이다. 앞서 deterministic encryption을 사용하는 것은 바람직하지 않다고 언급했다. 다시 말해서, 암호화하고자 하는 모든 값에는 랜덤성이 있어야 한다는 것이다. 그런데, 두 개의 동일한 값을 각각 암호화했을 때 서로 다른 값이 된다면, DB에서 두 값이 같은지 어떻게 비교할 수 있을까? 사실상 랜덤성이 있는 암호화 방식을 사용하면 DB에 unique 제약 조건을 걸어도 무의미하다.

encryption with randomness

진퇴양난이다. email에 deterministic encryption을 적용하자고 번복할 수도 없는 노릇이다. 어떻게 해야 할까? 사실, 주어진 column들만으로는 이 문제를 해결할 수 없다. 추가적인 column을 정의해야 한다.

email_hash라는 별도의 column을 추가하자. 이름 그대로, 암호화하기 전 email의 plaintext를 hash 값으로 변환한 결과를 담는 곳이다. Hash 값은 복호화가 불가능하다는 특징이 있다. 따라서, 값이 노출되더라도 사용자의 개인정보인 이메일은 확인할 수 없다. 한 가지 주의할 점은, hash 알고리즘 중에서 랜덤성이 없는 것을 선택해야 한다는 것이다. 다시 말해서, email_hash에는 deterministic encryption을 적용해야 한다.

이제 email이 아닌 email_hash에 unique 제약 조건을 걸면 된다. 정리하자면, 요구사항들을 만족하기 위한 해결 방법은 다음과 같다.

  • email: 정렬, 검색을 위해 복호화가 가능한 암호화 방식을 사용한다. 이때, 보안성 향상을 위해 랜덤성이 있는 암호화 방식을 선택한다. Unique 제약 조건은 걸지 않는다.
  • email_hash: Unique 여부를 검사하기 위해 deterministic encryption 방식을 선택한다. Unique 제약 조건을 걸어준다.

이렇게 구현하면, 암호화된 email이 마치 랜덤성이 있으면서 정렬과 검색이 가능하고 unique 제약 조건까지 만족하는 것처럼 동작하게 만들 수 있다.

그런데, 뭔가 이상하다는 것을 눈치챘는가? 이 솔루션은 완벽하지 않은 방법이다. 보안성 강화를 위해 랜덤성을 포함시키는 것인데, email_hash는 랜덤성이 결여되어 있다. 즉, 해커가 email 대신 email_hash를 집요하게 파고들면 결국 사용자의 이메일을 탈취할 수 있다. 다행인 것은 emailpassword에 비해 상대적으로 덜 민감한 정보라는 것이다. 사용할 해시 알고리즘의 보안 성능을 굳게 믿는 수 밖에 없다.

아쉽지만 너무 낙심하지 말자. 요구사항이 까다로운 것이다. 애초에 랜덤성과 unique 제약 조건은 모순 관계이다. 보안에 매우 민감한 값이라면, unique 제약 조건을 걸지 않아야 한다. 따라서, 엔지니어로서 요구사항을 만족시키기 위한 최선의 노력의 결과물이라고 이해하고 넘어가자. 누군가 태클을 건다면, 오히려 뻔뻔하게 대응하면 된다. 완벽한 방법이 있는지 물어봐라. 있다고 대답한다면, 그 사람을 조심하라!

세 번째 소결론을 내리자. 랜덤성이 있는 암호화 방식을 사용하면서 동시에 unique 제약 조건이 필요할 때의 해결 방법은 없다. 두 가지 column을 정의하여 둘 중 하나에 deterministic encryption을 적용하는 식으로 우회해야 한다.

ColumnRandomness
nameO
emailO
passwordO
email_hashX

마무리

해결 방법 요약

앞서 살펴본 내용들 중 소결론들만 취합하면 다음과 같다.

ColumnDecryptableEncryptorRandomness
nameODBO
emailODBO
passwordXApp?O
email_hashXDBX
  • nameemail같이 정렬과 검색이 가능해야 하는 값들은 랜덤성이 있으면서 복호화가 가능한 암호화 방식을 사용해야 한다.
  • password는 암호화 방식 및 validation 로직의 유연성을 위해 application에서 암호화를 진행하도록 하고, 나머지 개인정보는 DB에서 암호화 및 복호화하도록 구현한다.
  • 랜덤성이 있는 암호화 방식을 사용하면서 동시에 unique 제약 조건이 필요할 때의 해결 방법은 없다. 두 가지 column을 정의하여 둘 중 하나에 deterministic encryption을 적용하는 식으로 우회해야 한다.

해결 방법 평가

제안한 해결 방법들이 다섯 가지 요구사항을 모두 만족시킬 수 있는지 점검해보자.

  • 암호화: name, email, password는 모두 bytes 형식으로 저장되어야 한다.

모든 column에 각자의 암호화 알고리즘을 사용할 것이기 때문에, 첫 번째 요구사항은 자연스럽게 만족시킬 수 있다.

  • 정렬: nameemailORDER BY 쿼리를 통해 정렬이 가능해야 한다.
  • 검색: nameemailLIKE 쿼리를 통해 검색이 가능해야 한다.

nameemail의 경우, 복호화가 가능한 암호화 방식을 사용한다. 직접 암호화와 복호화를 수행하는 주체는 DB다. 따라서, SQL문을 통해 복호화된 값들을 바탕으로 정렬 및 검색을 하도록 명령할 수 있다.

  • 보안: password는 어떤 방법으로도 복호화할 수 없게 만들어야 한다.

passwordname, email과 다른 암호화 방식을 사용하기로 한다. 사실 답은 어느정도 정해져 있다. 복호화가 불가능한 hash 알고리즘을 사용하면 된다. 단, 보안성 강화를 위해 랜덤성이 있는 hash 방식을 사용하자. 암호화 알고리즘 구현 방법이나 validation 로직의 유연성을 위해 application에서 암호화를 처리하기로 했다는 것을 기억하자.

  • 고유성: email은 사용자 계정 로그인에 필요한 값이므로, unique해야 한다.

이 요구사항은 애초에 모순이기 때문에 완벽하게 만족시킬 수 없다. 그래도 고객을 화나지 않게 하기 위한 우회 방법은 있다. email_hash라는 별도의 column을 만들어서 email의 hash 값을 저장하도록 하고, unique 제약 조건을 email이 아니라 email_hash column에 설정하는 것이다. 엄밀히 따지면 email column에 unique 제약 조건이 걸린 것은 아니지만, 사용자 입장에서는 이 사실을 꿈에도 모르게 만들 수 있다.

예고편

이제 다양한 속성이 포함된 DB 개인정보를 암호화하기 위해 대략적인 방향은 잡았다. 그런데 DB 개인정보 암호화가 익숙하지 않은 개발자라면, 아직 실제로 구현하기에는 다소 어려움이 남아있을 것이다. 걱정할 필요는 없다. 조금의 추가적인 지식만 있다면, 요구사항들을 만족시키는 결과물을 쉽게 만들어낼 수 있다.

후속편에서는 구현에 앞서 알아두면 좋을 배경지식들을 소개하도록 하겠다. PGP, 암호화 알고리즘 등 여러 개념과 도구들을 살펴보고, 어려운 요구사항을 빠른 시간 내에 쉽게 만족시킬 수 있는 현명한 개발자가 되어보자. 만약 python과 postgresql을 활용하는 환경에서 동작하는 결과물을 바로 확인해보고 싶다면, 3편으로 건너뛰어도 무방하다.

Comments

    More Posts

    자가 위임, 자가 캡슐화 - DDD에서 생성자를 정의하는 방법

    DDD에서는 생성자를 정의할 때 자가 위임(self-delegation)과 자가 캡슐화(self-encapsulation)를 사용할 것을 권장한다. 이 두 가지 개념을 통해 어떻게 생성자를 정의해야 하는지 알아보자.

    FastAPI에서 사용자가 연결을 끊으면 어떻게 될까? - Middleware disconnection check 문제

    FastAPI에서 middleware를 사용하면 클라이언트가 연결을 끊었다는 사실을 알 수 없게 된다. 사용자의 disconnection이 요청 취소를 의미하는 경우, 이는 여러가지 문제로 이어질 수 있다. 연결이 끊겼다는 것을 어떻게 감지해야 할까?

    FastAPI로 다양한 input을 받는 웹 서버 개발 방법 - Path parameter, query parameter, request body

    Python의 FastAPI는 간단한 코드로 REST API를 빠르게 개발하도록 해준다. 복잡한 코드 없이 FastAPI로 path parameter, query parameter, JSON 타입의 request body 등 다양한 종류의 input을 받는 python backend 서버를 개발해보자.

    Font Size