보안은 '누가 무엇을 할 수 있는가'에서 시작한다
MySQL을 잘 쓰려면 테이블을 설계하고 SQL을 작성하는 능력만큼 보안 감각도 중요합니다. 데이터베이스에는 회원 정보, 주문 내역, 결제 기록, 내부 운영 데이터처럼 민감한 정보가 들어갑니다. 이런 데이터는 아무나 읽거나 바꿀 수 없어야 합니다.
MySQL 보안의 첫 질문은 단순합니다.
누가 접속할 수 있는가?
어디에서 접속할 수 있는가?
어떤 데이터베이스와 테이블을 볼 수 있는가?
INSERT, UPDATE, DELETE 같은 변경 작업을 할 수 있는가?
다른 사용자에게 권한을 줄 수 있는가?
이 질문에 답하는 도구가 계정과 권한입니다. 실무에서는 root 계정 하나로 모든 작업을 처리하지 않습니다. 애플리케이션용 계정, 리포트 조회용 계정, 백업용 계정, 운영자 계정을 목적별로 나누고 필요한 권한만 부여합니다. 이것을 최소 권한 원칙이라고 합니다. 계정이 맡은 일을 수행하는 데 꼭 필요한 권한만 주는 방식입니다.1
AI 생성 이미지
MySQL 계정은 사용자와 호스트의 조합이다
MySQL 계정은 단순히 사용자 이름만으로 구분되지 않습니다. 다음 형식으로 식별됩니다.
'사용자'@'호스트'예를 들어 다음 두 계정은 이름은 같지만 서로 다른 계정입니다.
'app_user'@'localhost'
'app_user'@'10.0.%''app_user'@'localhost'는 MySQL 서버가 설치된 같은 컴퓨터에서 접속하는 계정입니다. 'app_user'@'10.0.%'는 10.0.으로 시작하는 내부 네트워크 대역에서 접속할 수 있는 계정입니다. %는 모든 호스트를 뜻하므로 편리하지만 위험합니다. 특별한 이유가 없다면 %보다 구체적인 IP나 내부 대역을 사용하는 편이 안전합니다.
계정은 CREATE USER로 만듭니다.
CREATE USER 'app_user'@'localhost'
IDENTIFIED BY '강력한_비밀번호';
CREATE USER 'report_user'@'localhost'
IDENTIFIED BY '강력한_비밀번호';root 계정은 MySQL의 최고 관리자 계정입니다. 사용자 생성, 권한 부여, 데이터베이스 삭제 같은 강력한 작업을 모두 할 수 있습니다. 그래서 일상적인 조회나 애플리케이션 연결에 root를 사용하면 안 됩니다. 애플리케이션 코드에 root 비밀번호가 들어가거나, 웹 서버가 탈취되었을 때 데이터베이스 전체가 위험해질 수 있습니다.
GRANT로 필요한 권한만 부여한다
계정만 만들면 아직 아무 작업도 할 수 없습니다. 권한은 GRANT로 부여합니다. 기본 형식은 다음과 같습니다.
GRANT 권한목록 ON 범위 TO '사용자'@'호스트';예를 들어 mysql_book 데이터베이스를 읽기만 하는 리포트 계정을 만들려면 다음처럼 작성합니다.
CREATE USER 'report_user'@'localhost'
IDENTIFIED BY '강력한_비밀번호';
GRANT SELECT ON mysql_book.* TO 'report_user'@'localhost';이 계정은 mysql_book 데이터베이스 안의 테이블을 조회할 수 있지만 데이터를 추가, 수정, 삭제할 수 없습니다. 운영 리포트나 대시보드 계정은 보통 이런 방식으로 시작하는 것이 안전합니다.
애플리케이션 계정은 서비스 기능에 따라 권한을 다르게 줍니다. 게시판 서비스에서 게시글을 생성하고 수정해야 한다면 다음처럼 필요한 권한만 부여할 수 있습니다.
CREATE USER 'board_app'@'10.0.%'
IDENTIFIED BY '강력한_비밀번호';
GRANT SELECT, INSERT, UPDATE, DELETE
ON mysql_book.*
TO 'board_app'@'10.0.%';여기서 중요한 점은 ALL PRIVILEGES를 습관적으로 주지 않는 것입니다. ALL PRIVILEGES는 편하지만 위험합니다. 특히 DROP, ALTER, CREATE USER, GRANT OPTION 같은 권한은 데이터 구조를 바꾸거나 다른 사용자에게 권한을 줄 수 있으므로 신중하게 다뤄야 합니다.
권한 범위는 좁힐수록 안전하다
MySQL 권한은 여러 범위에 적용할 수 있습니다.2
-- 서버 전체
GRANT SELECT ON *.* TO 'global_reader'@'localhost';
-- 특정 데이터베이스 전체
GRANT SELECT ON mysql_book.* TO 'report_user'@'localhost';
-- 특정 테이블
GRANT SELECT, UPDATE ON mysql_book.students TO 'student_manager'@'localhost';
-- 특정 컬럼
GRANT UPDATE (score) ON mysql_book.students TO 'score_manager'@'localhost';*.*는 모든 데이터베이스의 모든 객체를 뜻합니다. 관리 목적이 아니라면 피하는 것이 좋습니다. mysql_book.*는 특정 데이터베이스 전체입니다. mysql_book.students는 특정 테이블입니다. 컬럼 단위 권한은 더 세밀합니다. 예를 들어 성적 관리자는 students.score만 수정할 수 있게 만들 수 있습니다.
실무에서는 먼저 가장 좁은 범위를 생각합니다.
리포트 계정: 특정 데이터베이스에
SELECT애플리케이션 계정: 필요한 DB 또는 테이블에
SELECT, INSERT, UPDATE, DELETE배치 계정: 배치가 처리하는 테이블에만 필요한 권한
백업 계정: 백업에 필요한 읽기 권한과 잠금 관련 권한만 검토
운영 관리자: 구조 변경 권한은 제한적으로 부여
권한은 넓게 주고 나중에 줄이는 것보다, 좁게 주고 필요할 때 추가하는 편이 안전합니다.
SHOW GRANTS와 REVOKE로 권한을 점검한다
권한을 부여한 뒤에는 반드시 확인해야 합니다. 현재 접속한 계정의 권한은 다음 명령으로 확인합니다.
SHOW GRANTS;특정 계정의 권한은 다음처럼 확인합니다.
SHOW GRANTS FOR 'report_user'@'localhost';출력 결과를 보면 해당 계정에 어떤 범위의 어떤 권한이 있는지 알 수 있습니다. 권한 점검은 계정을 만들 때만 하는 일이 아닙니다. 서비스가 오래 운영되면 임시 권한이 남거나, 더 이상 쓰지 않는 계정이 방치되거나, 예전 기능 때문에 과한 권한이 유지될 수 있습니다. 정기적으로 SHOW GRANTS 결과를 검토해야 합니다.
불필요한 권한은 REVOKE로 회수합니다.
REVOKE DELETE ON mysql_book.*
FROM 'board_app'@'10.0.%';이제 board_app 계정은 mysql_book 데이터베이스에서 삭제 작업을 할 수 없습니다. 계정 자체가 필요 없어졌다면 삭제합니다.
DROP USER 'old_user'@'localhost';잠시 막아 두고 싶다면 계정을 잠글 수도 있습니다.
ALTER USER 'report_user'@'localhost' ACCOUNT LOCK;초기 설치 후 기본 보안 상태를 정비한다
MySQL을 설치한 직후에는 기본 보안 상태를 점검해야 합니다. 특히 다음 항목을 확인합니다.3
root 비밀번호를 강력하게 설정했는가?
익명 사용자가 남아 있지 않은가?
테스트 데이터베이스가 남아 있지 않은가?
원격 root 접속이 허용되어 있지 않은가?
사용하지 않는 계정이 방치되어 있지 않은가?
비밀번호 검증 정책을 사용할 수 있는가?
비밀번호는 길고 예측하기 어려워야 합니다. 사람 이름, 서비스명, password123 같은 값은 피해야 합니다. 가능하면 대문자, 소문자, 숫자, 특수문자를 섞고 충분히 긴 비밀번호를 사용합니다. MySQL 8.0에서는 계정 잠금, 비밀번호 만료, 인증 플러그인 같은 기능을 활용해 계정 보안을 강화할 수 있습니다.4
예를 들어 일정 횟수 이상 로그인에 실패하면 계정을 잠그도록 설정할 수 있습니다.
ALTER USER 'app_user'@'10.0.%'
FAILED_LOGIN_ATTEMPTS 5
PASSWORD_LOCK_TIME 1;이 설정은 잘못된 비밀번호 시도가 5번 발생하면 계정을 1일 동안 잠그는 의미입니다. 운영 환경에서는 서비스 특성과 장애 대응 절차를 함께 고려해 값을 정해야 합니다.
원격 접속은 필요한 곳에서만 허용한다
데이터베이스 서버는 외부에 넓게 열어 둘수록 위험합니다. 가장 안전한 방식은 애플리케이션 서버나 관리 서버처럼 필요한 곳에서만 접속을 허용하는 것입니다.
MySQL 접속 제한은 여러 층에서 함께 적용해야 합니다.
첫째, MySQL 계정의 호스트를 제한합니다.
'app_user'@'10.0.%'
'report_user'@'localhost'둘째, MySQL 설정 파일에서 바인딩 주소를 확인합니다. bind-address는 MySQL이 어느 네트워크 인터페이스에서 접속을 받을지 정합니다. 모든 주소에서 접속을 받는 0.0.0.0은 편하지만 위험할 수 있습니다. 필요한 내부 IP로 제한하는 편이 좋습니다.
셋째, 방화벽이나 클라우드 보안 그룹에서 3306 포트를 필요한 IP에만 엽니다. 계정 제한만 믿지 말고 네트워크 단계에서도 막아야 합니다.
넷째, 원격 root 접속은 비활성화합니다. root는 가능하면 localhost에서만 사용하고, 원격 관리가 필요하다면 제한된 관리자 계정을 별도로 만들어 필요한 권한만 부여합니다.
파일 권한도 중요합니다. MySQL 설정 파일에는 접속 설정이나 인증 관련 정보가 들어갈 수 있고, 데이터 디렉터리에는 실제 데이터 파일이 저장됩니다. 운영체제에서 아무 사용자가 이 파일을 읽거나 수정할 수 있으면 MySQL 권한 체계를 우회할 수 있습니다. MySQL 데이터 디렉터리와 설정 파일은 MySQL 전용 사용자와 관리자만 접근할 수 있게 관리해야 합니다.
SQL 인젝션은 권한 관리와 함께 막아야 한다
데이터베이스 계정을 잘 나누어도 애플리케이션이 SQL을 위험하게 만들면 공격을 받을 수 있습니다. 대표적인 문제가 SQL 인젝션입니다. SQL 인젝션은 사용자 입력값이 단순한 값이 아니라 SQL 명령의 일부로 해석될 때 발생합니다.
예를 들어 로그인 쿼리를 문자열 결합으로 만들면 위험합니다.
SELECT *
FROM members
WHERE email = '사용자입력'
AND password = '사용자입력';애플리케이션 코드에서 사용자의 입력을 그대로 이어 붙이면 공격자는 조건을 조작할 수 있습니다. 방어의 핵심은 SQL 구조와 값을 분리하는 것입니다. 이를 준비된 문장, 또는 Prepared Statement라고 합니다.
개념적으로는 다음처럼 작성합니다.
SELECT id, email, name
FROM members
WHERE email = ?
AND password_hash = ?;? 자리에 들어가는 값은 SQL 문법으로 해석되지 않고 값으로만 처리됩니다. 그래서 사용자가 따옴표나 SQL 키워드를 입력해도 쿼리 구조를 바꾸기 어렵습니다.
정렬 컬럼이나 정렬 방향처럼 SQL 구조에 들어가는 값은 바인딩 변수로 처리하기 어렵습니다. 이 경우에는 허용 목록을 사용합니다. 예를 들어 정렬 컬럼은 created_at, name, score 중 하나만 허용하고, 정렬 방향은 ASC, DESC만 허용하는 식입니다.
권한 관리도 SQL 인젝션 피해를 줄이는 방어선입니다. 만약 웹 애플리케이션 계정에 DROP 권한이 없다면 SQL 인젝션이 발생하더라도 테이블 삭제 피해를 막을 가능성이 커집니다. 물론 권한 제한은 마지막 방어선일 뿐입니다. 가장 먼저 해야 할 일은 Prepared Statement와 입력 검증을 사용하는 것입니다.
실습: 조회 전용 계정과 쓰기 계정 만들기
마지막으로 앞에서 배운 내용을 간단히 실습해 보겠습니다. 먼저 조회 전용 계정을 만듭니다.
CREATE USER 'mysql_book_reader'@'localhost'
IDENTIFIED BY '강력한_비밀번호';
GRANT SELECT ON mysql_book.*
TO 'mysql_book_reader'@'localhost';
SHOW GRANTS FOR 'mysql_book_reader'@'localhost';이 계정으로 접속하면 SELECT는 가능하지만 INSERT, UPDATE, DELETE는 실패해야 정상입니다.
다음은 애플리케이션용 쓰기 계정입니다.
CREATE USER 'mysql_book_app'@'localhost'
IDENTIFIED BY '강력한_비밀번호';
GRANT SELECT, INSERT, UPDATE, DELETE
ON mysql_book.*
TO 'mysql_book_app'@'localhost';
SHOW GRANTS FOR 'mysql_book_app'@'localhost';만약 이 계정에 삭제 권한이 필요 없다고 판단되면 회수합니다.
REVOKE DELETE ON mysql_book.*
FROM 'mysql_book_app'@'localhost';
SHOW GRANTS FOR 'mysql_book_app'@'localhost';권한 관리는 한 번 설정하고 끝나는 작업이 아닙니다. 새 기능이 생기면 권한을 추가하고, 기능이 사라지면 권한을 회수해야 합니다. 퇴사자 계정, 테스트 계정, 임시 운영 계정도 주기적으로 정리해야 합니다.
데이터베이스 보안의 기본은 거창하지 않습니다. root를 일상 작업에 쓰지 않고, 계정을 목적별로 나누고, 권한 범위를 좁히고, 접속 위치를 제한하고, 애플리케이션에서 사용자 입력을 안전하게 처리하는 것입니다. 이 기본을 지키는 것만으로도 많은 사고를 예방할 수 있습니다.
참고
1Percona MySQL Database Security Best Practices
2Prisma Data Guide: How to use GRANT and REVOKE to manage privileges in MySQL
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.