데이터베이스 보안의 목표
데이터베이스 보안은 공격을 완전히 막겠다는 선언이 아니다. 현실적인 목표는 두 가지다. 첫째, 허가되지 않은 접근을 어렵게 만든다. 둘째, 침해가 발생하더라도 피해 범위를 작게 만든다.
개발자는 보안을 종종 로그인, 방화벽, 비밀번호 정책 정도로 생각한다. 하지만 데이터베이스 관점에서는 질문이 더 구체적이어야 한다.
애플리케이션 계정이 모든 테이블을 삭제할 수 있는가?
분석가가 고객의 전화번호와 주소를 그대로 볼 필요가 있는가?
백업 파일이 유출되면 원본 데이터가 그대로 노출되는가?
운영자가 특정 사용자의 민감 정보를 조회했을 때 기록이 남는가?
SQL 인젝션이 발생해도 읽을 수 있는 범위가 제한되는가?
좋은 보안 설계는 "정상 요청만 들어온다"는 가정에 기대지 않는다. 계정이 탈취되고, 쿼리가 조작되고, 백업 파일이 외부로 나가고, 내부 사용자가 실수할 수 있다고 보고 방어선을 여러 겹 둔다.
AI 생성 이미지
계정은 역할별로 분리한다
가장 기본적인 원칙은 최소 권한이다. 어떤 계정에도 "혹시 필요할지 모르니 전부 허용"하지 않는다. 계정은 보통 다음처럼 나눈다.
애플리케이션 계정은 서비스 코드가 사용하는 계정이다. 이 계정은 필요한 테이블에 대해 필요한 작업만 할 수 있어야 한다. 예를 들어 주문 API가 orders, order_items, payments에 SELECT, INSERT, UPDATE만 필요하다면 DROP TABLE, ALTER TABLE, 전체 스키마 생성 권한은 주지 않는다. OWASP도 애플리케이션별 계정 분리, 관리자 계정 사용 금지, 필요한 권한만 부여하는 방식을 권장한다.1
운영자 계정은 장애 대응이나 데이터 확인에 사용하는 계정이다. 운영자는 강한 권한이 필요할 수 있지만, 상시 관리자 권한을 들고 작업하면 위험하다. 평소에는 읽기 전용 또는 제한된 권한을 쓰고, 스키마 변경이나 긴급 수정이 필요할 때만 승인 절차를 거쳐 임시 권한을 부여하는 편이 안전하다.
분석 계정은 리포트, 대시보드, 데이터 추출 작업에 사용한다. 분석 목적이라면 대부분 쓰기 권한이 필요 없다. 또한 원본 테이블 전체가 아니라 마스킹된 뷰, 집계 테이블, 별도 분석 DB를 제공하는 것이 좋다. 고객 이메일, 전화번호, 주소, 인증 정보, 결제 관련 식별자를 그대로 열어 주면 분석 편의가 보안 위험으로 바뀐다.
DBMS마다 방식은 다르지만 원리는 같다. PostgreSQL에서는 역할과 GRANT로 권한을 나누고, MySQL에서는 사용자와 접속 호스트를 함께 제한할 수 있다. MongoDB도 데이터베이스·컬렉션 단위 역할을 설정할 수 있다. 중요한 것은 "접속에 성공한 뒤 무엇까지 할 수 있는가"를 명확히 제한하는 것이다. 컬럼이나 행 단위까지 제한해야 한다면 단순 테이블 권한만으로 충분하지 않을 수 있으므로, 뷰, 행 수준 보안, 별도 조회 테이블 같은 방법을 함께 검토한다.
네트워크와 접속 경로도 권한이다
데이터베이스 계정 권한만 줄여서는 부족하다. 데이터베이스가 인터넷에서 직접 접근 가능하다면 계정 하나가 약해지는 순간 공격 표면이 커진다. 데이터베이스는 가능한 한 애플리케이션 서버나 내부 네트워크에서만 접근하게 한다. 관리 도구도 VPN, 사내망, 접근 제어, 다중 인증 뒤에 둬야 한다.
전송 구간 암호화도 기본 방어선이다. 애플리케이션과 데이터베이스 사이의 연결이 평문이면 네트워크 구간에서 계정 정보나 쿼리 결과가 노출될 수 있다. 운영 환경에서는 TLS 연결을 사용하고, 클라이언트가 서버 인증서를 검증하도록 설정해야 한다.1
개발 환경에서 편의를 위해 열어 둔 포트, 임시로 만든 관리자 계정, 테스트용 비밀번호가 운영까지 남아 있는 경우가 많다. 보안 사고는 복잡한 해킹보다 이런 방치된 경로에서 시작되는 일이 적지 않다.
암호화는 위치에 따라 목적이 다르다
암호화는 하나의 기능처럼 보이지만 적용 위치에 따라 해결하는 문제가 다르다.
저장 시 암호화는 디스크, 스토리지, 백업 파일이 유출되었을 때 원본 데이터를 바로 읽지 못하게 한다. 클라우드 스토리지 암호화, 디스크 암호화, DBMS의 TDE가 여기에 해당한다. 운영 중인 DB에 정상 계정으로 접속한 공격자는 여전히 데이터를 볼 수 있으므로, 저장 시 암호화만으로 내부 권한 남용이나 SQL 인젝션을 막을 수는 없다.
전송 구간 암호화는 애플리케이션과 데이터베이스 사이, 데이터베이스와 복제 노드 사이, 백업 전송 구간에서 데이터가 도청되는 것을 막는다. TLS를 쓰더라도 인증서 검증을 꺼 두면 중간자 공격에 취약할 수 있다.
애플리케이션 레벨 암호화는 특정 필드를 애플리케이션에서 암호화한 뒤 데이터베이스에 저장하는 방식이다. 예를 들어 주민등록번호에 준하는 식별자, 외부 결제 토큰, 민감한 의료 정보처럼 DB 관리자도 원문을 볼 필요가 없는 데이터에 사용할 수 있다. 이 방식은 데이터베이스가 유출되거나 읽기 권한이 남용되었을 때 피해를 줄인다.
다만 애플리케이션 레벨 암호화에는 비용이 있다. 암호화된 값은 일반적으로 부분 검색, 정렬, 범위 조건, 인덱스 활용이 어렵다. WHERE phone_number LIKE '010%' 같은 쿼리가 불가능하거나 제한된다. 따라서 모든 컬럼을 무작정 암호화하기보다 다음 기준으로 판단한다.
원문 조회가 실제 업무에 필요한가?
검색이 필요한가, 단순 보관만 필요한가?
유출 시 사용자 피해가 큰가?
키를 어디에 저장하고 누가 접근할 수 있는가?
키 회전과 복구 절차가 준비되어 있는가?
암호화의 핵심은 알고리즘 이름보다 키 관리다. 데이터와 암호화 키가 같은 데이터베이스에 함께 있으면 유출 시 함께 털릴 수 있다. 키는 별도 KMS나 비밀 관리 시스템에 두고, 접근 권한과 사용 로그를 관리해야 한다.
비밀번호는 암호화하지 말고 해시한다
사용자 비밀번호는 복호화할 필요가 없다. 로그인할 때 사용자가 입력한 비밀번호가 저장된 값과 일치하는지만 확인하면 된다. 따라서 비밀번호는 되돌릴 수 있는 암호화가 아니라, 되돌리기 어려운 단방향 해시로 저장해야 한다.
일반 해시 함수만 쓰는 것도 충분하지 않다. 공격자는 자주 쓰이는 비밀번호 목록과 빠른 하드웨어를 이용해 대량으로 추측할 수 있다. 그래서 비밀번호 저장에는 Argon2id, bcrypt, scrypt, PBKDF2처럼 의도적으로 계산 비용을 높일 수 있는 비밀번호 해시 알고리즘을 사용한다. 각 비밀번호에는 고유한 솔트를 붙여 같은 비밀번호라도 저장 결과가 달라지게 해야 한다. OWASP도 비밀번호를 평문으로 저장하지 말고, Argon2id, bcrypt, PBKDF2 같은 강하고 느린 해시 알고리즘과 고유한 솔트를 사용하라고 권고한다.2
솔트는 비밀값이 아니다. 보통 해시 결과와 함께 저장한다. 솔트의 역할은 같은 비밀번호가 같은 해시로 보이는 것을 막고, 미리 계산한 공격을 어렵게 만드는 것이다. 추가로 페퍼를 사용할 수도 있다. 페퍼는 사이트 전체에서 쓰는 비밀값이며, 데이터베이스 밖의 안전한 저장소에 둬야 의미가 있다.
나쁜 예는 다음과 같다.
password = AES_ENCRYPT('user-password', 'hardcoded-key')이 방식은 키가 유출되면 모든 비밀번호가 복호화될 수 있다. 더 나은 방향은 다음과 같다.
stored = password_hash(user_password, unique_salt, work_factor)로그인 시에는 입력값에 같은 절차를 적용해 저장된 결과와 비교한다. 비밀번호 원문은 로그, 이벤트, 에러 메시지, 분석 테이블 어디에도 남기지 않는다.
마스킹, 익명화, 감사 로그
개인정보 보호는 "암호화했는가"만으로 끝나지 않는다. 실제 운영에서는 사람이 데이터를 보는 순간이 많다. 고객센터가 주문을 확인하고, 개발자가 장애를 분석하고, 분석가가 리포트를 만든다. 이때 원문이 꼭 필요한지 따져야 한다.
마스킹은 데이터 일부를 가리는 방식이다. 예를 들어 이메일은 ki*@example.com, 전화번호는 010-**-1234처럼 보여 줄 수 있다. 고객 식별에는 충분하지만 전체 값은 노출하지 않는다. 운영 관리자 화면, 로그, 검색 결과, 분석용 뷰에 유용하다.
익명화는 특정 개인을 알아볼 수 없도록 식별 가능성을 제거하는 것이다. 단순히 이름을 지우는 것만으로는 부족할 수 있다. 나이, 지역, 구매 이력, 접속 시간 조합만으로도 개인을 다시 식별할 수 있기 때문이다. 분석 목적이라면 원본 테이블을 직접 제공하기보다 집계, 범주화, 샘플링, 가명 처리 등을 검토해야 한다.
접근 로그와 감사 추적은 민감 데이터 접근을 나중에 설명할 수 있게 만든다. 최소한 다음 정보는 남겨야 한다.
누가 접근했는가
언제 접근했는가
어떤 데이터 범위에 접근했는가
어떤 목적 또는 기능을 통해 접근했는가
조회, 수정, 삭제 중 무엇을 했는가
성공했는가, 실패했는가
감사 로그에는 민감 데이터 원문을 남기지 않는다. "누가 고객 123의 전화번호를 조회했다"는 기록은 필요하지만, 전화번호 전체를 로그에 쓰면 로그 저장소가 또 다른 유출 지점이 된다. 감사 로그는 수정하기 어렵게 보관하고, 관리자 권한으로도 쉽게 삭제할 수 없게 분리하는 것이 좋다.
자주 발생하는 유출 경로
첫 번째는 SQL 인젝션이다. 사용자의 입력이 SQL 구조를 바꾸면 공격자는 의도하지 않은 조건을 추가하거나 다른 테이블을 읽을 수 있다. 기본 방어는 Prepared Statement와 파라미터 바인딩이다. 쿼리 템플릿과 값을 분리해야 한다. 정렬 컬럼, 테이블명처럼 파라미터로 바인딩하기 어려운 부분은 허용 목록으로 제한한다. 문자열 이스케이프만으로 막으려는 방식은 실수하기 쉽다.3
두 번째는 과도한 권한이다. SQL 인젝션이 발생해도 애플리케이션 계정이 필요한 테이블과 컬럼만 읽을 수 있고, 민감한 조회는 뷰나 행 수준 보안으로 제한되어 있다면 피해는 줄어든다. 반대로 애플리케이션 계정이 관리자 권한을 갖고 있으면 하나의 취약점이 전체 데이터베이스 파괴로 이어질 수 있다.
세 번째는 백업과 덤프 파일이다. 운영 DB는 잘 보호하면서 backup.sql, dump.gz, 엑셀 추출 파일을 공유 폴더나 개인 PC에 두는 경우가 있다. 백업은 운영 데이터의 복사본이므로 운영 DB와 같은 수준으로 보호해야 한다. 암호화, 접근 제어, 보관 기간, 삭제 절차, 복구 테스트를 함께 관리한다.
네 번째는 로그와 이벤트다. 디버깅을 위해 요청 본문 전체를 남기면 비밀번호, 토큰, 주소, 결제 식별자가 로그 시스템에 흘러갈 수 있다. 로그는 검색과 공유가 쉬워 유출 범위가 더 커질 수 있다. 민감 필드는 수집 단계에서 제거하거나 마스킹한다.
다섯 번째는 운영 데이터의 개발 환경 복사다. 개발자가 실제 장애를 재현하기 위해 운영 데이터를 내려받는 순간, 보호해야 할 환경이 하나 더 늘어난다. 가능하면 비식별화한 데이터 세트를 만들고, 꼭 필요한 경우에는 기간, 사용자, 목적, 저장 위치를 제한한다.
개발자가 점검할 질문
기능을 만들 때 다음 질문에 답할 수 있어야 한다.
이 API가 사용하는 DB 계정은 어떤 테이블에 어떤 권한을 갖는가?
이 데이터는 원문 저장이 필요한가, 해시나 암호화로 충분한가?
이 컬럼이 로그, 검색 인덱스, 캐시, 백업에도 복사되는가?
운영자나 분석가가 원문을 볼 필요가 있는가?
민감 데이터 접근 기록이 남는가?
SQL 구조에 사용자 입력이 섞이는 부분은 없는가?
백업 파일과 덤프 파일은 암호화되고 접근이 제한되는가?
데이터베이스 보안은 한 번 설정하고 끝나는 작업이 아니다. 스키마가 바뀌고, 계정이 늘고, 분석 요구가 생기고, 백업 경로가 추가될 때마다 다시 점검해야 한다. 좋은 보안 설계는 개발 속도를 무조건 늦추는 장치가 아니라, 사고가 나도 서비스와 사용자를 지킬 수 있게 만드는 운영 설계다.
참고
1OWASP Database Security Cheat Sheet
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.