카테고리 : MySQL/기술노트

MySQL 계정과 권한 모델: user, host, role, privilege resolution의 동작 방식

MySQL 계정 식별, host 매칭, role 활성화, 권한 해석 순서를 운영 관점에서 정리한다.

저자: MySQL 기술 노트 작성: 2026.05.18 약 14분 8,319자
다운로드

MySQL 계정과 권한 모델: user, host, role, privilege resolution의 동작 방식

MySQL에서 계정과 권한은 단순히 “사용자에게 SELECT를 줄 것인가”의 문제가 아니다. 계정 이름은 user만으로 식별되지 않고 userhost의 조합으로 식별되며, 인증 플러그인, TLS 요구 조건, 계정 잠금, password policy, role 활성화, 전역 권한과 객체 권한의 합산 결과가 한 연결의 실제 권한을 결정한다. 운영 환경에서 이 구조를 잘못 이해하면 애플리케이션 접속 장애, 과도한 권한 부여, 배포 후 권한 누락, 복제 계정 오남용, 감사 실패로 이어진다.

특히 MySQL 8.0 이후에는 role 기반 권한 관리가 일반화되었고, mysql.user 테이블을 직접 수정하는 방식은 더 이상 안전한 관리 방식이 아니다. 운영자는 CREATE USER, GRANT, REVOKE, CREATE ROLE, SET DEFAULT ROLE 같은 SQL 문으로 권한 모델을 다루어야 하며, 실제 연결에서 어떤 계정 행이 선택되고 어떤 role이 활성화되는지까지 확인해야 한다.

이 글은 MySQL 계정 모델을 내부 동작 순서에 맞추어 설명하고, DBA가 실무에서 권한을 설계·진단·감사할 때 확인해야 할 기준을 정리한다.

1. MySQL 계정은 user@host로 식별된다

MySQL 계정은 보통 app_user처럼 사용자명만으로 부르는 경우가 많지만, 서버 내부에서는 계정이 다음 두 요소의 조합으로 식별된다.

'user_name'@'host_name'

예를 들어 다음 세 계정은 서로 다른 계정이다.

CREATE USER IF NOT EXISTS 'app'@'%' IDENTIFIED BY '비밀번호는_예시에서_생략';
CREATE USER IF NOT EXISTS 'app'@'app-server.example.com' IDENTIFIED BY '비밀번호는_예시에서_생략';
CREATE USER IF NOT EXISTS 'app'@'localhost' IDENTIFIED BY '비밀번호는_예시에서_생략';

운영자가 “app 계정에 권한을 주었다”고 말하더라도 실제로는 어느 host 패턴의 app 계정에 권한을 주었는지가 중요하다. 같은 사용자명이라도 접속 원본이 다르면 다른 계정 행이 매칭될 수 있고, 그 결과 인증 방식과 권한도 달라질 수 있다.

1.1 host는 네트워크 보안 경계가 아니라 MySQL의 계정 매칭 조건이다

host 값은 MySQL 서버가 연결 요청을 계정 행과 매칭할 때 사용하는 조건이다. 방화벽, 보안 그룹, 네트워크 ACL을 대체하지 않는다. app@%는 모든 원본에서 접속을 허용한다는 의미처럼 보이지만, 실제 접속 가능 여부는 다음 계층을 모두 통과해야 한다.

  1. 네트워크 경로와 방화벽
  2. MySQL 서버의 bind address와 listener
  3. TLS 요구 조건
  4. MySQL 계정의 user@host 매칭
  5. 인증 플러그인과 비밀번호 검증
  6. 계정 잠금·만료 상태
  7. 권한 해석 결과

따라서 host 제한은 계정 관리의 일부일 뿐이며, 운영 보안은 네트워크 제한과 함께 설계해야 한다.

2. 접속 시 계정 행이 선택되는 과정

클라이언트가 MySQL에 접속하면 서버는 먼저 클라이언트가 제시한 사용자명과 접속 원본을 기준으로 계정 행을 찾는다. 이때 핵심은 “가장 먼저 조건에 맞는 행 하나”가 선택된다는 점이다. 계정 테이블의 정렬과 host 패턴의 구체성이 결과에 영향을 준다.

MySQL은 계정 정보를 메모리에 로드한 뒤, 접속 시 다음과 같은 관점으로 계정을 찾는다.

  • 사용자명이 일치하는가?
  • 접속 원본 host가 계정의 host 패턴에 매칭되는가?
  • 더 구체적인 host 조건이 있는가?
  • 익명 사용자 계정이 의도치 않게 먼저 매칭되지 않는가?

예를 들어 app@db-client.example.comapp@%가 함께 존재하면 특정 host 계정이 일반 wildcard 계정보다 우선할 수 있다. 이 동작은 세밀한 권한 분리에 유용하지만, 운영자가 계정 목록을 충분히 확인하지 않으면 “왜 같은 app 계정인데 어떤 서버에서는 권한이 다르게 보이는가”라는 장애로 나타난다.

2.1 익명 사용자 계정의 위험

MySQL 계정에서 사용자명은 빈 문자열일 수 있다. 예를 들어 ''@'localhost'는 익명 사용자 계정이다. 익명 계정은 학습용 설치나 오래된 환경에서 남아 있을 수 있고, 의도하지 않은 계정 매칭을 만들 수 있다.

운영 환경에서는 익명 계정이 없어야 한다.

SELECT user, host, account_locked
FROM mysql.user
WHERE user = '';

조회 결과가 있다면 삭제 여부를 검토한다.

DROP USER IF EXISTS ''@'localhost';

실제 삭제 전에는 해당 계정이 사용 중인지 감사 로그, general log, 접속 통계, 애플리케이션 설정을 확인해야 한다. 다만 일반적인 운영 기준에서는 익명 계정이 남아 있는 것 자체가 위험 신호다.

3. 인증과 권한은 다른 단계다

MySQL 접속은 크게 인증(authentication)과 권한 부여(authorization)로 나뉜다. 인증은 “이 클라이언트가 이 계정의 소유자인가”를 확인하는 단계이고, 권한 부여는 “이 계정이 요청한 작업을 수행할 수 있는가”를 판단하는 단계다.

인증 단계에서는 다음 정보가 중요하다.

  • plugin: caching_sha2_password, mysql_native_password 등 인증 플러그인
  • password hash와 password expiration 상태
  • account_locked
  • ssl_type, x509_issuer, x509_subject 같은 TLS 관련 조건
  • failed login tracking 관련 설정

권한 단계에서는 다음 정보가 중요하다.

  • 전역 권한(global privilege)
  • 데이터베이스 단위 권한(database privilege)
  • 테이블·컬럼·루틴 권한
  • 동적 권한(dynamic privilege)
  • role을 통해 상속되는 권한
  • 현재 세션에서 활성화된 role

운영 장애에서 흔한 오해는 “로그인이 되므로 권한도 정상일 것” 또는 “GRANT를 했으니 현재 세션에 바로 반영될 것”이라고 보는 것이다. 로그인 성공은 인증 통과만 의미한다. 실제 SQL 실행 권한은 별도의 해석 과정을 거친다.

4. 권한 저장 위치와 관리 방식

MySQL 권한 정보는 내부적으로 mysql 시스템 스키마의 grant table에 저장된다. 대표적으로 다음 테이블이 있다.

테이블 의미
mysql.user 계정, 인증, 전역 권한, 계정 상태
mysql.db 데이터베이스 단위 권한
mysql.tables_priv 테이블 단위 권한
mysql.columns_priv 컬럼 단위 권한
mysql.procs_priv stored routine 권한
mysql.global_grants MySQL 8.0 dynamic privilege
mysql.role_edges role 부여 관계
mysql.default_roles 계정의 기본 role

하지만 운영자가 이 테이블을 직접 INSERT, UPDATE, DELETE하는 방식은 피해야 한다. MySQL 8.0에서는 계정과 권한 DDL이 더 엄격해졌고, password hash, 인증 플러그인, dynamic privilege, role metadata가 함께 관리된다. 직접 수정은 메모리 캐시와 불일치하거나 업그레이드 호환성을 해칠 수 있다.

권장 방식은 다음과 같다.

CREATE USER IF NOT EXISTS 'report_app'@'%' IDENTIFIED BY '비밀번호는_안전한_방식으로_관리';
CREATE ROLE IF NOT EXISTS 'role_report_reader';
GRANT SELECT ON analytics.* TO 'role_report_reader';
GRANT 'role_report_reader' TO 'report_app'@'%';
SET DEFAULT ROLE 'role_report_reader' TO 'report_app'@'%';

비밀번호는 문서나 스크립트에 평문으로 남기지 말고, 배포 시스템의 secret 저장소나 DBA 승인 절차를 통해 주입해야 한다. 위 SQL은 구조 설명을 위한 예시이며 실제 비밀번호 값은 운영 문서에 기록하지 않는 것이 원칙이다.

5. 권한 해석은 “허용 권한의 합집합”에 가깝다

MySQL 권한 모델은 일반적으로 명시적 deny보다 grant된 권한의 합산으로 이해하는 것이 적절하다. 어떤 세션이 특정 작업을 수행할 수 있는지는 다음 권한들의 조합으로 결정된다.

  1. 계정 자체에 부여된 전역 권한
  2. 계정 자체에 부여된 스키마·테이블·컬럼·루틴 권한
  3. 현재 세션에서 활성화된 role의 권한
  4. dynamic privilege
  5. 객체 소유자나 DEFINER 문맥이 개입되는 경우의 실행 문맥

예를 들어 어떤 계정에 SELECT 권한이 role로 부여되어 있고, 해당 role이 현재 세션에서 활성화되어 있으면 조회가 가능하다. 반대로 role이 부여되어 있어도 활성화되지 않으면 현재 세션에서는 권한이 없는 것처럼 보일 수 있다.

SELECT CURRENT_USER(), USER();
SELECT CURRENT_ROLE();
SHOW GRANTS FOR CURRENT_USER();

USER()는 클라이언트가 요청한 사용자와 접속 원본을 보여주고, CURRENT_USER()는 인증 후 실제로 매칭된 MySQL 계정을 보여준다. 권한 문제를 진단할 때는 USER()보다 CURRENT_USER()가 더 중요하다. CURRENT_ROLE()은 현재 세션에서 활성화된 role을 확인하는 데 사용한다.

6. Role 모델: 권한 묶음과 활성화 상태를 분리한다

Role은 권한을 직접 사용자에게 반복해서 부여하지 않고, 직무나 애플리케이션 기능 단위로 묶어 관리하기 위한 계정 객체다. MySQL의 role은 내부적으로 계정과 유사하게 취급되지만 로그인 용도로 사용하지 않고 권한 상속에 사용한다.

예를 들어 읽기 전용 애플리케이션, 배치 적재 작업, 운영자 조회 계정을 다음처럼 분리할 수 있다.

CREATE DATABASE IF NOT EXISTS service_db;
CREATE TABLE IF NOT EXISTS service_db.staging_table (
  id BIGINT PRIMARY KEY,
  payload JSON,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

CREATE ROLE IF NOT EXISTS 'role_app_readonly';
CREATE ROLE IF NOT EXISTS 'role_batch_writer';
CREATE ROLE IF NOT EXISTS 'role_dba_observer';

GRANT SELECT ON service_db.* TO 'role_app_readonly';
GRANT SELECT, INSERT, UPDATE, DELETE ON service_db.staging_table TO 'role_batch_writer';
GRANT PROCESS, SHOW DATABASES ON *.* TO 'role_dba_observer';
GRANT SELECT ON performance_schema.* TO 'role_dba_observer';

사용자 계정에는 role을 부여한다.

CREATE USER IF NOT EXISTS 'app_reader'@'%' IDENTIFIED BY '비밀번호는_안전한_방식으로_관리';
GRANT 'role_app_readonly' TO 'app_reader'@'%';
SET DEFAULT ROLE 'role_app_readonly' TO 'app_reader'@'%';

여기서 중요한 점은 role 부여와 role 활성화가 다르다는 것이다. role을 계정에 부여해도 기본 role로 설정하지 않으면 새 세션에서 자동 활성화되지 않을 수 있다. 세션에서 수동으로 role을 활성화할 수도 있다.

GRANT 'role_app_readonly' TO CURRENT_USER();
SET ROLE 'role_app_readonly';
SELECT CURRENT_ROLE();
SET ROLE NONE;

실행 결과(MySQL 8.0.46):

+-------------------------+
| CURRENT_ROLE()          |
+-------------------------+
| `role_app_readonly`@`%` |
+-------------------------+

운영에서는 애플리케이션 계정에 필요한 role을 SET DEFAULT ROLE로 명시하는 것이 좋다. 애플리케이션 코드가 매 연결마다 SET ROLE을 실행하도록 의존하면 connection pool, 장애 복구, 재시도 경로에서 누락될 가능성이 있다.

7. Dynamic privilege와 SUPER 권한 축소

오래된 MySQL 운영에서는 SUPER 권한이 여러 관리 작업에 광범위하게 사용되었다. 그러나 SUPER는 과도하게 강한 권한이며, MySQL 8.0에서는 많은 관리 기능이 dynamic privilege로 분리되었다. 예를 들어 다음과 같은 권한들이 있다.

  • BACKUP_ADMIN
  • CONNECTION_ADMIN
  • REPLICATION_APPLIER
  • REPLICATION_SLAVE_ADMIN
  • SYSTEM_VARIABLES_ADMIN
  • ROLE_ADMIN
  • CLONE_ADMIN

운영 관점에서는 SUPER 하나로 모든 것을 해결하는 방식보다 필요한 dynamic privilege를 좁게 부여하는 방식이 안전하다.

CREATE USER IF NOT EXISTS 'ops_admin'@'%' IDENTIFIED BY '비밀번호는_안전한_방식으로_관리';
GRANT SYSTEM_VARIABLES_ADMIN ON *.* TO 'ops_admin'@'%';
GRANT CONNECTION_ADMIN ON *.* TO 'ops_admin'@'%';

다만 dynamic privilege는 버전과 배포판에 따라 지원 범위가 달라질 수 있다. MySQL Community Server, Percona Server, Aurora MySQL의 지원 권한과 동작이 완전히 같다고 가정하지 말고, 실제 환경에서 SHOW PRIVILEGES와 공식 호환성 정보를 확인해야 한다.

SHOW PRIVILEGES;
SELECT * FROM mysql.global_grants ORDER BY user, host, priv;

8. 객체 권한과 실행 문맥: DEFINER를 주의한다

View, stored procedure, stored function, trigger, event는 DEFINER와 실행 문맥 때문에 권한 해석이 복잡해질 수 있다. 예를 들어 SQL SECURITY DEFINER인 view나 procedure는 호출자의 권한이 아니라 definer 계정의 권한으로 실행될 수 있다.

이 구조는 공통 조회 인터페이스나 제한된 procedure 실행을 제공할 때 유용하지만, 다음 위험이 있다.

  • definer 계정이 삭제되어 객체 실행이 실패한다.
  • definer 계정에 과도한 권한이 있어 호출자가 간접적으로 강한 작업을 수행한다.
  • dump와 restore 후 대상 환경에 같은 definer가 없어 배포가 실패한다.
  • 운영자 계정으로 생성한 객체가 개인 계정 definer를 갖고 남는다.

진단 쿼리는 다음과 같다.

SELECT table_schema, table_name, definer, security_type
FROM information_schema.views
WHERE table_schema NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')
ORDER BY table_schema, table_name;

SELECT routine_schema, routine_name, routine_type, definer, security_type
FROM information_schema.routines
WHERE routine_schema NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')
ORDER BY routine_schema, routine_name;

운영 표준에서는 애플리케이션 객체의 definer를 개인 DBA 계정이 아니라 관리되는 서비스 계정으로 통일하는 것이 좋다. 또한 migration 도구가 DEFINER를 어떻게 처리하는지 사전에 확인해야 한다.

9. 권한 진단의 기본 순서

권한 문제가 발생했을 때는 “GRANT가 있는가”만 확인하면 부족하다. 다음 순서로 접근하는 것이 안전하다.

9.1 실제 매칭 계정 확인

애플리케이션 계정으로 접속 가능한 경우 세션에서 확인한다.

SELECT USER() AS requested_user,
       CURRENT_USER() AS matched_account,
       CURRENT_ROLE() AS active_roles;

실행 결과(MySQL 8.0.46):

+----------------+-----------------+--------------+
| requested_user | matched_account | active_roles |
+----------------+-----------------+--------------+
| root@localhost | root@localhost  | NONE         |
+----------------+-----------------+--------------+

USER()CURRENT_USER()가 다르면, 운영자가 생각한 계정과 실제 매칭 계정이 다를 수 있다.

9.2 계정 상태 확인

관리 계정으로 다음을 확인한다.

SELECT user, host, plugin, account_locked, password_expired
FROM mysql.user
WHERE user = 'app_reader'
ORDER BY host;

실행 결과(MySQL 8.0.46):

+------------+------+-----------------------+----------------+------------------+
| user       | host | plugin                | account_locked | password_expired |
+------------+------+-----------------------+----------------+------------------+
| app_reader | %    | caching_sha2_password | N              | N                |
+------------+------+-----------------------+----------------+------------------+

여러 host 행이 있으면 어느 행이 실제 접속에 매칭되는지 확인해야 한다.

9.3 부여 권한 확인

SHOW GRANTS FOR 'app_reader'@'%';

실행 결과(MySQL 8.0.46):

+---------------------------------------------------+
| Grants for app_reader@%                           |
+---------------------------------------------------+
| GRANT USAGE ON *.* TO `app_reader`@`%`            |
| GRANT `role_app_readonly`@`%` TO `app_reader`@`%` |
+---------------------------------------------------+

role이 사용 중이면 role 자체의 권한도 별도로 확인한다.

SHOW GRANTS FOR 'role_app_readonly';

실행 결과(MySQL 8.0.46):

+-----------------------------------------------------------+
| Grants for role_app_readonly@%                            |
+-----------------------------------------------------------+
| GRANT USAGE ON *.* TO `role_app_readonly`@`%`             |
| GRANT SELECT ON `service_db`.* TO `role_app_readonly`@`%` |
+-----------------------------------------------------------+

9.4 기본 role 확인

SELECT user, host, default_role_user, default_role_host
FROM mysql.default_roles
WHERE user = 'app_reader'
ORDER BY host, default_role_user;

실행 결과(MySQL 8.0.46):

+------------+------+-------------------+-------------------+
| user       | host | default_role_user | default_role_host |
+------------+------+-------------------+-------------------+
| app_reader | %    | role_app_readonly | %                 |
+------------+------+-------------------+-------------------+

role은 부여되어 있지만 기본 role이 아니면 애플리케이션 세션에서 권한이 없을 수 있다.

9.5 객체 존재와 스키마명을 확인

권한 오류가 실제로는 잘못된 스키마명, 대소문자, view definer 문제, synonym처럼 보이는 잘못된 객체 참조에서 비롯될 때도 있다. MySQL에는 Oracle식 synonym이 없으므로 애플리케이션이 어떤 스키마를 참조하는지 명확히 확인해야 한다.

SELECT table_schema, table_name, table_type
FROM information_schema.tables
WHERE table_schema = 'service_db'
  AND table_name = 'target_table';

10. 운영 설계 원칙: 최소 권한과 계정 분리

MySQL 권한 설계의 기본 원칙은 최소 권한이다. 하지만 최소 권한은 단순히 권한을 적게 주는 것이 아니다. 계정 목적과 장애 대응 경로를 분리하고, 변경 가능성을 고려하여 검증 가능한 구조로 만드는 것이다.

10.1 애플리케이션 계정과 운영 계정 분리

다음 계정은 분리하는 것이 좋다.

  • 애플리케이션 읽기 계정
  • 애플리케이션 쓰기 계정
  • 배치·마이그레이션 계정
  • 복제 계정
  • 백업 계정
  • 모니터링 계정
  • DBA 긴급 운영 계정

예를 들어 모니터링 계정에는 데이터 변경 권한이 필요하지 않다.

CREATE ROLE IF NOT EXISTS 'role_monitoring';
GRANT PROCESS, REPLICATION CLIENT ON *.* TO 'role_monitoring';
GRANT SELECT ON performance_schema.* TO 'role_monitoring';
GRANT SELECT ON sys.* TO 'role_monitoring';

반대로 애플리케이션 쓰기 계정에 PROCESS, SYSTEM_VARIABLES_ADMIN, CREATE USER 같은 운영 권한이 있어서는 안 된다.

10.2 스키마 단위 권한과 테이블 단위 권한의 균형

스키마 단위 권한은 관리가 쉽지만 범위가 넓다. 테이블 단위 권한은 세밀하지만 운영 비용이 커진다. 일반적인 기준은 다음과 같다.

상황 권장 방식
애플리케이션이 한 스키마 전체를 소유 스키마 단위 권한
리포팅 계정이 일부 테이블만 조회 테이블 또는 view 단위 권한
개인정보·결제 정보 등 민감 테이블 별도 role과 제한된 view
배치가 특정 staging 테이블만 변경 테이블 단위 DML 권한
마이그레이션 도구 별도 계정과 제한된 기간의 DDL 권한

운영 편의만 고려하여 *.*에 권한을 부여하면 나중에 감사와 장애 분석이 어려워진다.

11. Aurora MySQL에서의 차이와 주의점

Aurora MySQL은 MySQL 호환 엔진이지만 운영 제어면에서 일부 차이가 있다. 특히 managed service 특성상 OS 접근, 일부 SUPER급 작업, 백업·복구·복제 제어 방식이 일반 MySQL과 다르다.

Aurora MySQL에서 계정과 권한을 다룰 때는 다음을 고려한다.

  1. 최상위 관리자 권한이 제한적이다.
    RDS/Aurora의 master user는 강한 권한을 갖지만 self-managed MySQL의 root와 동일하지 않다. 일부 파일 시스템 접근, 플러그인 설치, low-level replication 제어는 제한된다.

  2. 파라미터 변경은 DB parameter group을 따른다.
    SYSTEM_VARIABLES_ADMIN 성격의 작업이라도 모든 변수를 SQL로 자유롭게 바꿀 수 있는 것은 아니다. 정적 파라미터는 parameter group 변경과 재시작이 필요할 수 있다.

  3. 백업과 복구는 서비스 기능과 결합된다.
    계정에 MySQL 내부 백업 권한을 부여하는 것만으로 Aurora snapshot, point-in-time recovery, cluster clone 권한이 생기는 것은 아니다. AWS IAM 권한과 RDS API 권한이 별도로 필요하다.

  4. 모니터링 권한은 Performance Insights, CloudWatch와 함께 설계한다.
    MySQL 내부 performance_schema 조회 권한과 AWS 콘솔/API 권한은 별도다. DBA 역할을 설계할 때 두 권한 체계를 함께 정의해야 한다.

  5. 복제·고가용성은 서비스가 관리하는 영역이 많다.
    일반 MySQL에서 사용하는 복제 관리 권한이 Aurora cluster 내부의 writer/reader failover 제어와 직접 일치하지 않는다.

따라서 Aurora에서는 MySQL 계정 권한만 보지 말고, AWS IAM, parameter group, cluster role, monitoring 권한을 함께 점검해야 한다.

12. 감사와 정기 점검 쿼리

권한은 한 번 설계하고 끝나는 항목이 아니다. 배포, 장애 대응, 임시 권한 부여, 인력 변경, 스키마 변경을 거치며 쉽게 누적된다. 정기 점검에서는 적어도 다음 항목을 확인해야 한다.

12.1 계정 목록과 인증 상태

SELECT user, host, plugin, account_locked, password_expired,
       password_last_changed
FROM mysql.user
ORDER BY user, host;

12.2 과도한 전역 권한 후보

SELECT user, host,
       Select_priv, Insert_priv, Update_priv, Delete_priv,
       Create_priv, Drop_priv, Grant_priv, Super_priv
FROM mysql.user
WHERE Grant_priv = 'Y'
   OR Super_priv = 'Y'
   OR Create_user_priv = 'Y'
ORDER BY user, host;

실행 결과(MySQL 8.0.46):

+---------------+-----------+-------------+-------------+-------------+-------------+-------------+-----------+------------+------------+
| user          | host      | Select_priv | Insert_priv | Update_priv | Delete_priv | Create_priv | Drop_priv | Grant_priv | Super_priv |
+---------------+-----------+-------------+-------------+-------------+-------------+-------------+-----------+------------+------------+
| mysql.session | localhost | N           | N           | N           | N           | N           | N         | N          | Y          |
| root          | %         | Y           | Y           | Y           | Y           | Y           | Y         | Y          | Y          |
| root          | localhost | Y           | Y           | Y           | Y           | Y           | Y         | Y          | Y          |
+---------------+-----------+-------------+-------------+-------------+-------------+-------------+-----------+------------+------------+

MySQL 8.0에서 SUPER 사용은 줄이는 방향으로 검토해야 하며, 필요한 dynamic privilege로 대체할 수 있는지 확인한다.

12.3 Dynamic privilege 목록

SELECT user, host, priv, with_grant_option
FROM mysql.global_grants
ORDER BY user, host, priv;

12.4 Role 부여 관계

SELECT from_user, from_host, to_user, to_host
FROM mysql.role_edges
ORDER BY to_user, to_host, from_user, from_host;

실행 결과(MySQL 8.0.46):

+--------------------+-----------+------------+-----------+
| from_user          | from_host | to_user    | to_host   |
+--------------------+-----------+------------+-----------+
| role_app_readonly  | %         | app_reader | %         |
| role_report_reader | %         | report_app | %         |
| role_app_readonly  | %         | root       | localhost |
+--------------------+-----------+------------+-----------+

12.5 기본 role 누락 확인

SELECT re.to_user AS user_name,
       re.to_host AS host_name,
       re.from_user AS granted_role,
       dr.default_role_user AS default_role
FROM mysql.role_edges AS re
LEFT JOIN mysql.default_roles AS dr
  ON dr.user = re.to_user
 AND dr.host = re.to_host
 AND dr.default_role_user = re.from_user
 AND dr.default_role_host = re.from_host
ORDER BY re.to_user, re.to_host, re.from_user;

실행 결과(MySQL 8.0.46):

+------------+-----------+--------------------+--------------------+
| user_name  | host_name | granted_role       | default_role       |
+------------+-----------+--------------------+--------------------+
| app_reader | %         | role_app_readonly  | role_app_readonly  |
| report_app | %         | role_report_reader | role_report_reader |
| root       | localhost | role_app_readonly  | NULL               |
+------------+-----------+--------------------+--------------------+

이 쿼리는 role이 부여되어 있지만 기본 role로 설정되지 않은 후보를 찾는 데 도움이 된다. 모든 role이 기본 role이어야 하는 것은 아니지만, 애플리케이션 계정에 필요한 role이 기본 활성화되지 않았다면 장애 원인이 될 수 있다.

13. 흔한 장애와 오해

13.1 “권한을 줬는데 여전히 Access denied가 발생한다”

가능한 원인은 다음과 같다.

  • 실제 접속 계정이 다른 user@host 행이다.
  • role이 부여되었지만 현재 세션에서 활성화되지 않았다.
  • connection pool이 기존 세션을 재사용하고 있다.
  • 권한을 준 객체와 애플리케이션이 참조하는 객체가 다르다.
  • view나 procedure의 definer 문제다.
  • 계정은 맞지만 TLS 요구 조건이나 인증 플러그인 문제로 다른 오류가 발생한다.

진단은 반드시 CURRENT_USER(), CURRENT_ROLE(), SHOW GRANTS부터 시작해야 한다.

13.2 “GRANT ALL이면 안전하게 해결된다”

GRANT ALL은 장애를 빠르게 우회할 수 있지만, 권한 경계를 무너뜨린다. 특히 *.* 범위의 ALL PRIVILEGES는 운영 사고 시 피해 범위를 크게 만든다. 임시로 강한 권한을 부여해야 한다면 만료 시점, 승인자, 회수 명령, 감사 기록을 함께 남겨야 한다.

-- 예시: 임시 권한 부여 후 반드시 회수 계획을 기록한다.
CREATE USER IF NOT EXISTS 'migration_user'@'%' IDENTIFIED BY '비밀번호는_안전한_방식으로_관리';
CREATE DATABASE IF NOT EXISTS service_db;
GRANT ALTER, CREATE, DROP ON service_db.* TO 'migration_user'@'%';
-- 작업 완료 후
REVOKE ALTER, CREATE, DROP ON service_db.* FROM 'migration_user'@'%';

13.3 “% host는 운영에서 항상 금지해야 한다”

%가 항상 잘못된 것은 아니다. 네트워크 계층에서 접근이 엄격히 제한되고, TLS와 strong authentication이 적용되며, 계정 권한이 좁다면 운영상 허용될 수 있다. 그러나 %는 계정 매칭 범위가 넓으므로, 가능하면 애플리케이션 접속 경로나 프록시 계층에 맞춰 더 구체적인 host를 사용하는 것이 좋다. 클라우드와 컨테이너 환경에서는 원본 host가 유동적일 수 있으므로 네트워크 보안 경계와 함께 설계해야 한다.

13.4 “role을 쓰면 권한 감사가 쉬워진다”

role은 권한 관리를 구조화하지만, role 중첩과 기본 role 누락이 늘어나면 오히려 감사가 어려워질 수 있다. role 이름 규칙, 소유 팀, 부여 대상, 기본 활성화 여부, 폐기 절차를 함께 관리해야 한다.

14. 운영 체크리스트

  • 각 계정의 user@host
  • GRANT ALL ON *.* 또는 불필요한 SUPER
  • role을 사용하는 계정에 필요한 SET DEFAULT ROLE
  • CURRENT_USER()CURRENT_ROLE()
  • view, procedure, trigger, event의 DEFINER
  • 정기적으로 mysql.user, mysql.global_grants, mysql.role_edges, mysql.default_roles

15. 결론

MySQL의 계정과 권한 모델은 user@host 매칭, 인증 플러그인, grant table, role 활성화, dynamic privilege, 객체 실행 문맥이 결합된 구조다. 운영자가 이 구조를 단순한 사용자명과 권한 목록으로만 이해하면 장애 진단과 보안 감사에서 중요한 단서를 놓치기 쉽다.

권한 설계의 목표는 편의가 아니라 예측 가능성이다. 어떤 클라이언트가 어떤 계정으로 매칭되고, 어떤 role이 활성화되며, 어떤 객체에 어떤 범위의 작업을 수행할 수 있는지를 SQL로 검증할 수 있어야 한다. 다음 글들에서는 이 권한 모델 위에서 InnoDB, optimizer, 트랜잭션, 복제, 백업 계정 같은 운영 주제를 더 구체적인 실행 경로와 함께 다룰 수 있다.