조직에서 사용하는 데이터베이스는 사용 목적과 규모에 따라 데이터의 양과 객체 종류는 다양하다. 또한 서비스 운영에 있어 중요도가 높은 데이터베이스 객체와 민감한 정보(개인정보 및 금융 정보 등)가 저장되어 있을 수 있다.
내가 속한 조직만 봐도 200개 이상의 테이블과 100개 이상의 프로시저와 함수 등이 있다.
이 외에도 인덱스, 트리거 등 다양한 데이터베이스 객체가 존재한다.
만약 조직 내 데이터베이스를 모든 사람에게 전체 공개하게 되면 아래와 같은 크고 작은 문제가 발생할 수 있다.
- 민감 데이터 도용
- 데이터 임의 삭제 및 변경
- 조직 내 데이터 대내/외 유출
이러한 이유로 조직 내 각 사용자에게 맞는 데이터베이스 설정과 권한 정책이 필요하다.
[운영서버를 기준으로 시나리오 설계]
각 부서별로 사용할 데이터베이스와 테이블이 다르기 때문에 각 부서별로 어떤 데이터베이스, 데이터베이스 객체를 사용할지 조사가 필요하다.
[아래는 운영 DB 사용을 전제로 수립한 계획입니다.]
mysql> show databases; +--------------------+ | Database | +--------------------+ | fn_db | | hr_db | | information_schema | | mysql | | nba_db | | performance_schema | | sakila | | sys | | world | +--------------------+
- 개발 1팀은
sakila와world를 사용한다. (추후 ERP 작업 때문에hr_db사용 예정)
- DML의
SELECT만 사용할 수 있다. 단 부서장의 경우INSERT와UPDATE,DELETE를 사용할 수 있으며,EXECUTE권한 또한 가지고 있다.- 추후 부여받을
hr_db에서 급여 관련 정보는 부서장만 확인할 수 있다. (monthly_salary : 월급, annual_salary : 연봉)
- 개발 2팀은
sakila와world를 사용한다.
- DML의 SELECT만 사용할 수 있다. 단 부서장의 경우
INSERT와UPDATE,DELETE를 사용할 수 있으며,EXECUTE권한 또한 가지고 있다.
- 데이터 분석팀은
hr_db와fn_db,nba_db를 사용한다.
- 데이터 분석팀은 DML의
SELECT만 사용할 수 있다.- 테이터 분석팀 전원
hr_db에서 급여 관련 정보 조회가 불가능하다. (monthly_salary : 월급, annual_salary : 연봉)
- 데이터 엔지니어링팀은
hr_db,fn_db,nba_db,sakila,world를 사용한다.
- DML의
SELECT만 사용할 수 있다. 단 부서장의 경우INSERT와UPDATE,DELETE를 사용할 수 있으며,EXECUTE권한 또한 가지고 있다.- 데이터 엔지니어링팀 전원
hr_db에서 급여 관련 정보 조회가 불가능하다. (monthly_salary : 월급, annual_salary : 연봉)
- 재무/회계팀은
fn_db를 사용한다.
- 재무/회계팀은 DML의
SELECT만 사용할 수 있다.
요약
팀 부서장 일반 팀원 사용 DB 특이사항 개발 1팀 SELECT, INSERT, UPDATE, DELETE, EXECUTE SELECT sakila, world, hr_db 부서장만 급여 정보 확인 가능 개발 2팀 SELECT, INSERT, UPDATE, DELETE, EXECUTE SELECT sakila, world - 데이터 분석팀 SELECT SELECT hr_db, fn_db, nba_db 데이터 분석팀은 1개의 계정을 공용으로 사용,
SELECT만 사용 가능,
급여 정보 확인 불가데이터 엔지니어링팀 SELECT, INSERT, UPDATE, DELETE, EXECUTE SELECT hr_db, fn_db, nba_db, sakila, world 급여 정보 확인 불가 재무/회계팀 SELECT SELECT fn_db 재무/회계팀은 1개의 계정을 공용으로 사용,
SELECT만 사용 가능
각 부서에 대한 계정은 아래처럼 생성할 예정이다.
- 실습 편의를 위해 비밀번호는 "password"로 통일하여 진행.
- 실습 환경이 집, 카페, 스터디룸에 따라 IP가 달라지기 때문에 IP 접근 허용을 "%"으로 설정.
-- 개발 1팀 (10명) CREATE USER 'dev1_admin'@'%' IDENTIFIED BY 'password'; -- 부서장 CREATE USER 'dev1_user001'@'%' IDENTIFIED BY 'password'; ... CREATE USER 'dev1_user009'@'%' IDENTIFIED BY 'password'; -- 개발 2팀 (10명) CREATE USER 'dev2_admin'@'%' IDENTIFIED BY 'password'; -- 부서장 CREATE USER 'dev2_user001'@'%' IDENTIFIED BY 'password'; ... CREATE USER 'dev2_user009'@'%' IDENTIFIED BY 'password'; -- 데이터 분석팀 (4명) CREATE USER 'da_team001'@'%' IDENTIFIED BY 'password'; -- 팀 공용 -- 데이터 엔지니어링팀 (3명) CREATE USER 'de_admin'@'%' IDENTIFIED BY 'password'; -- 부서장 CREATE USER 'de_user001'@'%' IDENTIFIED BY 'password'; CREATE USER 'de_user002'@'%' IDENTIFIED BY 'password'; -- 재무/회계팀 (3명) CREATE USER 'fin_team001'@'%' IDENTIFIED BY 'password'; -- 팀 공용
계정을 생성하는 과정에서 실패하는 경우가 발생할 수 있다. 실패 원인을 보면 보통 명령문이 틀리거나 비밀번호 정책 문제일 가능성이 높다.
[비밀번호 정책 확인 및 재설정]
아래 설정을 보면
policy가 MEDIUM으로 되어 있다. 이는 대/소문자, 특수문자가 포함된 비밀번호를 사용해야 한다는 의미를 가지고 있다.추가로
length가 8로 되어 있다. 즉, 비밀번호는 최소 8자 이상이어야 한다.mysql> show variables like 'validate_password%'; +-------------------------------------------------+--------+ | Variable_name | Value | +-------------------------------------------------+--------+ | validate_password.changed_characters_percentage | 0 | | validate_password.check_user_name | ON | | validate_password.dictionary_file | | | validate_password.length | 8 | | validate_password.mixed_case_count | 1 | | validate_password.number_count | 1 | | validate_password.policy | MEDIUM | | validate_password.special_char_count | 1 | +-------------------------------------------------+--------+ 8 rows in set (0.00 sec)"password"처럼 단순한 비밀번호를 사용하려면 validate_password.policy를 'LOW'로 설정해야 한다.
mysql> set global validate_password.policy = 'LOW'; Query OK, 0 rows affected (0.00 sec)
아래 명령문를 통해 MySQL 서버에 생성된 모든 계정을 확인할 수 있다.
mysql> select user, host from mysql.user; +------------------+-----------+ | user | host | +------------------+-----------+ | admin | % | | da_team001 | % | | de_admin | % | | de_user001 | % | | de_user002 | % | | dev1_admin | % | | dev1_user001 | % | ... | dev2_user009 | % | | fin_team001 | % | | mysql.infoschema | localhost | | mysql.session | localhost | | mysql.sys | localhost | | root | localhost | +------------------+-----------+ 30 rows in set (0.00 sec)
모든 계정에 일일이 권한을 부여하여 관리할 수 있지만, 역할(ROLE)에 특정 권한을 부여하고 해당 역할을 사용자에게 부여하는 방식으로도 권한을 할당할 수 있다.
[읽기 전용(SELECT) 권한 생성]
※ 민감 정보의 경우 아래 방식처럼 일일이 권한을 부여하는 것보다
VIEW를 만들어 권한을 허용하는 게 일반적이다.-- hr_db 읽기 전용(SELECT) 권한 생성 CREATE ROLE role_hr_db_readonly; -- hr_db의 경우 테이블이 1개이기 때문에 아래처럼 권한 설정 -- 급여 관련 컬럼(monthly_salary : 월급, annual_salary : 연봉) 조회 항목에서 제외 GRANT SELECT (no,first_name,last_name,gender,start_date, years,department,country,center,job_rate, sick_leaves,unpaid_leaves,overtime_hours) ON hr_db.hr_information TO role_hr_db_readonly; -- fn_db 읽기 전용(SELECT) 권한 생성 CREATE ROLE role_fn_db_readonly; -- fn_db에 있는 모든 테이블 조회 가능 GRANT SELECT ON fn_db.* TO role_fn_db_readonly; CREATE ROLE role_nba_db_readonly; GRANT SELECT ON nba_db.* TO role_nba_db_readonly; CREATE ROLE role_sakila_readonly; GRANT SELECT ON sakila.* TO role_sakila_readonly; CREATE ROLE role_world_readonly; GRANT SELECT ON world.* TO role_world_readonly;
[부서장용(SELECT, INSERT, DELETE, UPDATE, EXECUTE) 권한 생성]
CREATE ROLE role_hr_db_dept; GRANT SELECT, INSERT, DELETE, UPDATE, EXECUTE ON hr_db.* TO role_hr_db_dept; CREATE ROLE role_fn_db_dept; GRANT SELECT, INSERT, DELETE, UPDATE, EXECUTE ON hr_db.* TO role_fn_db_dept; CREATE ROLE role_nba_db_dept; GRANT SELECT, INSERT, DELETE, UPDATE, EXECUTE ON sakila.* TO role_hr_db_dept; CREATE ROLE role_sakila_dept; GRANT SELECT, INSERT, DELETE, UPDATE, EXECUTE ON sakila.* TO role_sakila_dept; CREATE ROLE role_world_dept; GRANT SELECT, INSERT, DELETE, UPDATE, EXECUTE ON world.* TO role_world_dept;
[부서장용2(SELECT, INSERT, DELETE, UPDATE, EXECUTE) 권한 생성]
데이터엔지니어링팀 부서장은 인사 급여 정보를 확인할 수 없다.CREATE ROLE role_hr_db_dept_is; GRANT SELECT (no,first_name,last_name,gender,start_date, years,department,country,center,job_rate, sick_leaves,unpaid_leaves,overtime_hours) , INSERT , UPDATE , DELETE ON hr_db.hr_information TO role_hr_db_dept_is;
[읽기 전용 역할 권한 부여]
GRANT role_hr_db_readonly TO 'dev1_user001'@'%'; ... GRANT role_hr_db_readonly TO 'dev1_user009'@'%'; GRANT role_hr_db_readonly TO 'da_team001'@'%'; GRANT role_hr_db_readonly TO 'de_user001'@'%'; GRANT role_hr_db_readonly TO 'de_user002'@'%';인사 정보 외 다른 DB의 역할 부여는 생략
[부서장 전용 역할 권한 부여]
-- 개발1팀 부서장 GRANT role_sakila_dept TO 'dev1_admin'@'%'; GRANT role_world_dept TO 'dev1_admin'@'%'; -- 개발2팀 부서장 GRANT role_sakila_dept TO 'dev2_admin'@'%'; GRANT role_world_dept TO 'dev2_admin'@'%'; -- 데이터엔지니어링팀 부서장 GRANT role_hr_db_dept_is TO 'de_admin'@'%'; GRANT role_fn_db_dept TO 'de_admin'@'%'; GRANT role_nba_db_dept TO 'de_admin'@'%'; GRANT role_sakila_dept TO 'de_admin'@'%'; GRANT role_world_dept TO 'de_admin'@'%';
역할 및 권한을 부여 받으면 해당 역할을 바로 사용할 수 없다. 별도의 역할 활성화 명령문을 통해 부여받은 역할을 활성화해야 한다.
역할(ROLE)이 아닌 권한(SELECT, INSERT 등)의 경우 별도의 활성화 과정 없이 즉시 사용 가능하다.
일일이 모든 역할을 활성화하는 과정은 시간적 소비가 크기 때문에 사용자마다 부여받은 모든 역할을 기본 역할로 설정한다.
-- 개발 1팀 역할 활성화 SET DEFAULT ROLE ALL TO 'dev1_admin'; ... SET DEFAULT ROLE ALL TO 'dev1_user009';개발 1팀 외 다른 부서/사용자의 역할 활성화 명령문은 생략
위에서 사용한 방법은 역할을 기본 역할로 설정하는 방법이다. 기본 역할로 설정하는 방법 외 현재 세션에서만 역할을 활성화하는 방법도 있다. 하지만 이는 추천하는 방법이 아니다.
DEFAULT 키워드를 사용하여 역할이 자동으로 활성화 되도록 설정할 수 있다. 이를 통해 DB서버가 재시작되더라도 DEFAULT로 활성화된 역할은 자동 활성화된다.
DEFAULT 키워드를 사용한 활성화는 자기 자신 또는 역할/권한 부여가 가능한 계정(root, DBA)만 활성화가 가능하다.
-- 부여받은 모든 역할을 기본 역할로 활성화 SET DEFAULT ROLE ALL TO 'dev1_user001'@'%' -- 특정 역할만 기본 역할로 활성화 SET DEFAULT ROLE 'role_world_readonly' TO 'dev1_user001'@'%' -- 기본 역할 없애기 SET DEFAULT ROLE NONE TO 'dev1_user001'@'%'
현재 DB서버 세션에서만 활성화 되도록 설정할 수 있다. DB서버가 종료될 때 활성화된 역할은 비활성화된다.
부여받은 역할을 1회성(현재 세션)으로 활성화할 수 도 있다. 현재 로그인한 사용자가 직접 부여받은 ROLE만 활성화할 수 있다.
SET ROLE 'role_world_readonly';
MySQL에서 역할(ROLE)은 USER 테이블에 저장된다. ROLE을 특정하는 컬럼이 별도로 없기 때문에 아래와 같은 명령문으로 확인할 수 있다.
아래 쿼리는 MySQL에서 공식적으로 제공하는 조회 방식이 아니기 때문에 조회 결과가 틀릴 수 있다.
SELECT DISTINCT user AS role_name, host FROM mysql.user WHERE 1=1 AND account_locked = 'Y' AND authentication_string = '' ;
SHOW GRANTS FOR '<역할(ROLE)명>'@'%';
-- 특정 사용자에게 부여된 역할/권한 확인 -- DBA, root 계정으로만 확인 가능하다. SHOW GRANTS FOR '<사용자명>'@'%'; -- 현재 접속자에게 부여된 역할/권한 확인 SHOW GRANTS FOR CURRENT_USER();
아래 명령문을 통해 어떤 사용자에게 어떤 권한이 부여되었는지 확인할 수 있다.
(DBA, root 계정으로만 확인할 수 있다)
-- 어떤 사용자에게 어떤 역할이 부여되었는지 확인 SELECT * FROM mysql.role_edges;
아래 명령문를 통해 특정 사용자가 어떤 기본 역할을 설정했는지 확인 할 수 있다.
(DBA, root 계정으로만 확인할 수 있다)
SELECT * FROM mysql.default_roles;
현재 로그인한 계정에서 활성화된 역할을 조회할 수 있다.
다른 사용자의 역할 활성화는 확인 할 수 없다. (세션 격리 때문에 확인 불가)
SELECT CURRENT_ROLE();
MySQL Enterprise 버전에는 Audit Plugin 기능이 있어 권한 부여와 테이블 접근 등의 감사를 기록할 수 있지만, MySQL Community 버전에는 Audit Plugin을 사용할 수 없기 때문에 권한에 대한 로그기록을 다른 방법으로 작성/확인해야 한다.
MySQL Enterprise 버전이라면 MySQL 전용 Audit Plugin 기능을 사용하는 게 좋지만, MySQL Community 버전에서는 직접적으로 Audit Plugin 기능을 사용할 수 없다.
Mariadb의 Audit Plugin 기능 설치하여 간접적으로 사용할 순 있지만, 권장하진 않는다. 호환성 문제 및 보안 문제 때문에 적절한 방법은 아니다.
(MySQL 내부에 무리하게 설치하여 사용할 때 보안 문제 및 누락, 충돌이 발생할 수 있다)
general_log을 ON으로 설정하여 모든 명령문에 대한 로그기록을 작성 및 확인할 수 있지만, 모든 SQL 명령문이 기록되기 때문에 사용 환경에 따라 성능 저하가 발생할 수 있다.
-- 설정 활성화 SET GLOBAL general_log = 'ON'; -- 로그 저장 위치 확인 SHOW VARIABLES LIKE 'general_log_file';아래 localhost(general_log_file)로그를 확인하면 admin 계정이 dev1_user009 및 dev2_user009 계정의 역할을 변경한 내역을 확인할 수 있다.
- admin계정이 세션 ID 31로 연결(Connect)
- 세션 ID 31이 dev1_user009 계정의 전체 역할을 NONE으로 처리
- 세션 ID 31이 dev2_user009 계정의 전체 역할을 기본 역할로 활성화 처리
[root@localhost mysql]# cat localhost.log /usr/libexec/mysqld, Version: 8.0.41 (Source distribution). started with: Tcp port: 3306 Unix socket: /var/lib/mysql/mysql.sock Time Id Command Argument 2025-07-19T13:22:18.076585Z 31 Connect admin@localhost on using Socket 2025-07-19T13:22:18.077079Z 31 Query select @@version_comment limit 1 2025-07-19T13:23:22.111796Z 31 Query set default role NONE TO 'dev1_user009' 2025-07-19T13:23:33.647180Z 31 Query set default role ALL TO 'dev2_user009'
binary_log는 기본적으로 ON으로 설정되어 있으며, MySQL의 대부분 명령문이 기록되어 있다.
권한 관련 명령문은 기록되어 있지만, 누가 해당 권한을 수정/설정했는지는 확인할 수 없다.
사실 binary_log는 주로 복제 및 복구 용도로 사용되며, 감사(audit) 용도로는 사용되지 않는다.
# 바이너리파일 조회 예시 mysqlbinlog --base64-output=DECODE-ROWS binlog.000038 | grep -i -B 3 'SET DEFAULT ROLE ALL TO'
조직 내 부서 이동 또는 신규 프로젝트 등의 이유로 기존에 사용할 수 없던 DB, 테이블 등을 사용해야 하는 경우가 발생할 수 있다. 영구적으로 사용하는 경우라면 GRANT와 SET 명령문을 통해 기본 역할 및 권한으로 설정하면 되지만, 특정 기간동안 사용해야 하는 경우 이벤트(EVENT) 기능을 사용하여 사용 기간에 대한 타이머를 설정할 수 있다.
개발 1팀(dev1)은 ERP 개발 때문에 90일 동안 hr_db(인사 정보)를 사용한다는 전제로 아래처럼 권한/역할을 90일 동안 부여하는 작업을 수행해 볼 예정이다.
개발 1팀에게 인사 관련 권한을 부여한다.
-- 개발 1팀 직원에게 hr_db관련 역할 부여 GRANT role_hr_db_readonly TO 'dev1_admin'; GRANT role_hr_db_readonly TO 'dev1_user001'; ... GRANT role_hr_db_readonly TO 'dev1_user009'; -- 부여받은 hr_db관련 역할을 기본 역할로 지정 및 역할 활성화 SET DEFAULT ROLE ALL TO 'dev1_admin'; SET DEFAULT ROLE ALL TO 'dev1_user001'; ... SET DEFAULT ROLE ALL TO 'dev1_user009';
90일 이후 특정 작업(event)이 이루어져야 하므로 별도의 EVENT 스케줄러를 만들어 사용하는 게 가장 이상적이라 생각된다.
물론 수기로 DBA가 90일 이후 권한을 회수하는 방법도 있다...
EVENT 스케줄러 사용과 생성은 아래와 같은 작업을 통해 사용한다.
[이벤트 스케줄러 활성화]
-- 이벤트 활성화 SET GLOBAL event_scheduler = ON; -- 이벤트 활성화 확인 SHOW VARIABLES LIKE 'event_scheduler';[이벤트 스케줄러 생성 및 실행]
아래 이벤트 스케줄러는 1회성 스케줄러이다. 만약 아래 이벤트를 다시 사용하려면 삭제 후 재생성해야 한다.-- 기존 동명으로 작성된 이벤트가 있을 수 있기 때문에 삭제 후 생성/실행 DROP EVENT event_revoke_hr_db_dev1;-- ON SCHEDULE : 이벤트 예약어 -- AT : 단일 시점에 한 번 실행(스케줄러 X) -- CURRENT_TIMESTAMP : 현재 시각 -- + INTERVAL 90 DAY : 현재 시각에서 90일을 더한 시점 DELIMITER $$ CREATE EVENT event_revoke_hr_db_dev1 ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 MINUTE DO BEGIN REVOKE role_hr_db_dept FROM 'dev1_admin'@'%'; REVOKE role_hr_db_readonly FROM 'dev1_user001'@'%'; ... REVOKE role_hr_db_readonly FROM 'dev1_user009'@'%'; END$$ DELIMITER ;위 코드처럼 명령문을 실행하면 90일 뒤 개발 1팀은 인사 정보(hr_db)에 대한 권한을 박탈할 수 있다.
조직 내 장기간 DB를 사용할 일이 없는 사용자의 경우 계정을 잠금 처리하여 관리/유지하는 게 보안 측면에서 좋다. 일정 기간 계정을 잠금 처리하는 방법은 EVENT 스케줄러를 통해 설정할 수 있지만, DBA가 인사 정보(휴직 및 휴가 등)를 확인하여 상황에 따라 처리하는 방법이 제일 정확할 수 있다.
가령 육아휴직을 1년 신청했지만, 회사 사정으로 9개월 만에 복직해야 하는 경우 인사 정보를 통해 수기로 잠금 처리를 해지해야 한다.
EVENT 스케줄러를 통해 설정하면 처음 1년 신청과 다르기 때문에 결국 DBA가 수기로 잠금 처리를 해지해야 한다.
잠금 처리 해지 시점은 상황에 따라 달라질 수 있지만, EVENT 스케줄러 등록을 통해 1차적으로 관리하고 상황에 맞게 DBA가 수기로 작업하는 방법이 가장 이상적이라고 생각된다.
[계정 잠금 처리]
-- dev1_user001 사용자 계정 잠금 ALTER USER 'dev1_user001'@'%' ACCOUNT LOCK;
[계정 잠금 해지 이벤트]
-- 현재 시간을 기준으로 1년 뒤 계정 잠금 해지
DELIMITER $$
CREATE EVENT event_unlock_365
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 YEAR
DO
BEGIN
ALTER USER 'dev1_user001'@'%' ACCOUNT UNLOCK;
END $$
DELIMITER ;
[계정 잠금 수기 해지]
-- dev1_user001 사용자 계정 잠금 해지 ALTER USER 'dev1_user001'@'%' ACCOUNT UNLOCK;