[Real MySQL 8.0 (1권)] 3장 - 사용자 및 권한
Real MySQL 8.0 1권 3장에서 다루는 내용을 요약합니다.
3장에서 던지는 질문
- MySQL은 왜 사용자 이름과 접속 지점을 함께 식별해야 하는가?
- 시스템 관리 계정과 일반 계정의 경계는 어떻게 설정되는가?
- 계정 생성·비밀번호·권한·역할 관리를 어떻게 분리해서 운영 안정성을 확보하는가?
이 장은 위 질문에 답하기 위해 사용자 식별 규칙, 계정 생성 절차, 비밀번호 정책, 권한 및 역할 모델을 단계적으로 정리합니다.
사용자 식별: 계정 + 호스트
MySQL 계정은 user@host 형태로 아이디와 접속 호스트를 묶어야 완전하게 식별됩니다. 호스트는 IP, 호스트명, 도메인, 와일드카드(%)를 모두 허용합니다.
'svv_id'@'127.0.0.1'계정은 로컬 루프백 접속만 허용합니다.'svc_id'@'%'계정은 모든 외부 호스트에서 접속할 수 있습니다.
동일한 사용자 이름이라도 호스트 범위에 따라 서로 다른 계정으로 취급되며, MySQL은 가장 구체적인 호스트 매칭을 우선 선택합니다. 예를 들어 다음 두 계정이 있을 때 192.168.0.10에서 접속하면 첫 계정이 먼저 고려됩니다.
1
2
'svc_id'@'192.168.0.10' -- 비밀번호 123
'svc_id'@'%' -- 비밀번호 456
따라서 % 계정의 비밀번호를 입력하면 인증이 실패합니다. 접속 오류를 줄이려면 서비스 영역별로 호스트 패턴을 명시하고, 운영 자동화 시에도 항상 user@host 전체를 다룹니다.
호스트를 생략하면?
CREATE USER 'svc' IDENTIFIED BY 'pw';처럼 호스트를 지정하지 않으면 MySQL이 자동으로%를 붙여'svc'@'%'계정을 만듭니다. 의도치 않게 모든 호스트에서 접속 가능한 계정이 생길 수 있으므로, 운영 환경에서는 가능한 한 명시적으로 호스트 범위를 제한합니다.
시스템 계정과 일반 계정
MySQL 8.0은 SYSTEM_USER 권한 부여 여부로 계정을 두 종류로 나눕니다.
| 구분 | 설명 |
|---|---|
| 시스템 계정 | DBA용 계정입니다. 계정/권한 관리, 세션/쿼리 강제 종료, 스토어드 프로그램 DEFINER 변경 등을 수행합니다. |
| 일반 계정 | 애플리케이션·개발자용 계정입니다. 위 작업을 수행할 수 없으며, 주어진 권한 범위 내에서만 SQL을 실행합니다. |
내장된 특수 계정도 존재하며, 기본적으로 account_locked 상태이므로 삭제하거나 잠금 해제하지 않습니다.
| 계정 | 용도 |
|---|---|
mysql.sys@localhost | sys 스키마 뷰·프로시저의 DEFINER 로 사용됩니다. |
mysql.session@localhost | 플러그인이 서버 내부 API를 사용할 때 인증 토큰으로 사용됩니다. |
mysql.infoschema@localhost | information_schema 뷰의 DEFINER 로 사용됩니다. |
계정 생성과 옵션 분리
MySQL 8.0부터는 계정 생성은 CREATE USER, 권한 부여는 GRANT 로 명확히 분리합니다. 예시는 다음과 같습니다.
1
2
3
4
5
6
7
8
CREATE USER 'user'@'%'
IDENTIFIED WITH 'mysql_native_password' BY 'password'
REQUIRE NONE
PASSWORD EXPIRE INTERVAL 30 DAY
ACCOUNT UNLOCK
PASSWORD HISTORY DEFAULT
PASSWORD REUSE INTERVAL DEFAULT
PASSWORD REQUIRE CURRENT DEFAULT;
주요 옵션은 아래 표와 같습니다.
| 옵션 | 설명 |
|---|---|
IDENTIFIED WITH ... BY ... | 인증 플러그인과 초기 비밀번호를 지정합니다. 8.0 기본값은 caching_sha2_password 입니다. |
REQUIRE {NONE | SSL | X509 | ...} | SSL/TLS 사용 여부와 인증서 요구 조건을 정의합니다. 별도 지정이 없으면 평문 채널도 허용합니다. 단, caching_sha2_password 를 쓰면 암호화 채널이 요구됩니다. |
PASSWORD EXPIRE ... | 유효 기간 정책을 지정합니다. INTERVAL n DAY, NEVER, DEFAULT, EXPIRE 등을 지원합니다. |
PASSWORD HISTORY ... | 과거 비밀번호 재사용 금지 개수를 지정하거나 DEFAULT 를 사용합니다. 기록은 password_history 테이블에 저장됩니다. |
PASSWORD REUSE INTERVAL ... | 특정 기간 내 동일 비밀번호 사용을 막습니다. |
PASSWORD REQUIRE {CURRENT | OPTIONAL | DEFAULT} | 만료 시 기존 비밀번호 입력을 강제할지 결정합니다. |
ACCOUNT {LOCK | UNLOCK} | 계정을 생성 즉시 잠그거나 해제합니다. ALTER USER 로도 변경 가능합니다. |
인증 플러그인 선택지
mysql_native_password: 5.7까지 기본이던 방식입니다. 단순 해시 비교이므로 호환성 목적에 적합합니다.caching_sha2_password: 8.0 기본 플러그인입니다. SSL/TLS 또는 RSA 키쌍이 있어야 하며, 암호화된 채널을 강제합니다.authentication_pam: 외부 PAM/LDAP 연동용이며 상용 버전에서 지원합니다.authentication_ldap: LDAP 전용 플러그인입니다.
서버 차원의 기본 인증 방식을 바꾸려면 다음과 같이 시스템 변수를 조정합니다.
1
SET GLOBAL default_authentication_plugin = 'mysql_native_password';
읽기 전용 변수 오류가 발생하면 해당 인스턴스의 read_only 또는 권한 구성을 점검해야 합니다.
비밀번호 정책과 컴포넌트
강화된 비밀번호 정책을 사용하려면 validate_password 컴포넌트를 설치합니다.
1
INSTALL COMPONENT 'file://component_validate_password';
설치 후 세 가지 정책 레벨을 사용할 수 있습니다.
| 정책 | 검증 내용 |
|---|---|
LOW | 길이만 검증합니다. |
MEDIUM | 길이 + 숫자 + 대소문자 + 특수문자 조합을 요구합니다. |
STRONG | MEDIUM 조건 + 금칙어 사전 검증을 수행합니다. |
금칙어는 사전 파일을 만들어 다음과 같이 지정합니다.
1
2
SET GLOBAL validate_password.dictionary_file = 'prohibitive_word.data';
SET GLOBAL validate_password.policy = 'STRONG';
이 외에도 default_password_lifetime, password_reuse_interval, password_require_current 등 시스템 변수를 사용해 만료 주기, 재사용 금지 기간, 만료 시 확인 절차를 전역 기본값으로 설정할 수 있습니다.
이중 비밀번호(Dual Password)
서비스 중단 없이 비밀번호를 교체하려면 Dual Password 기능을 사용합니다. 계정은 기본(Primary)과 보조(Secondary) 비밀번호 두 개를 동시에 가질 수 있으며, 둘 중 하나만 일치해도 인증이 통과합니다.
1
2
3
ALTER USER 'app'@'%'
IDENTIFIED BY 'new_pass'
RETAIN CURRENT PASSWORD;
- 최신에 설정한 비밀번호가 Primary입니다.
- 기존 비밀번호는 Secondary로 유지됩니다.
- 전환이 완료되면
ALTER USER ... DISCARD OLD PASSWORD로 Secondary를 제거합니다.
이 방식으로 애플리케이션 배포 중 비밀번호 회전을 순차적으로 적용할 수 있습니다.
권한(Privilege) 모델
MySQL 8.0은 기존 글로벌/객체 권한에 더해 동적 권한 개념을 도입했습니다.
- 정적 권한: 서버 소스 코드에 고정돼 있으며 전통적인
SELECT,INSERT,SUPER등이 여기에 속합니다. - 동적 권한: 서버 시작 시점에 필요에 따라 생성됩니다. 복제, 퍼포먼스 스키마, 플러그인 등이 요구하는 권한이 여기에 포함됩니다.
권한 범위도 명확히 구분합니다.
| 범위 | 설명 |
|---|---|
| 글로벌 권한 | 데이터베이스/테이블 이외 객체에 적용합니다. GRANT 시 항상 ON *.* 형태를 사용합니다. |
| DB 권한 | 특정 데이터베이스 단위로 부여합니다. 테이블 이름을 명시하지 않습니다. |
| 객체 권한 | 테이블·뷰·스토어드 프로그램 등 개별 객체에 부여합니다. |
GRANT 구문은 GRANT ... ON ... TO ... 형태이며, 예시는 다음과 같습니다.
1
2
3
GRANT SUPER ON *.* TO 'admin'@'localhost'; -- 글로벌 권한
GRANT EVENT ON employees.* TO 'scheduler'@'%'; -- DB 권한
GRANT SELECT, INSERT ON employees.department TO 'app'@'%'; -- 객체 권한
권한 데이터는 mysql 시스템 DB 테이블에 저장됩니다.
| 테이블 | 내용 |
|---|---|
user | 계정 기본 정보, 글로벌 권한, 기본 역할 |
db | DB 단위 권한 |
tables_priv | 테이블 단위 권한 |
columns_priv | 칼럼 단위 권한 |
procs_priv | 스토어드 프로그램 권한 |
global_grants | 동적 글로벌 권한 |
역할(Role) 관리
역할은 여러 권한을 묶어 재사용하는 추상화입니다. MySQL 내부 표현은 계정과 동일하지만 CREATE ROLE/GRANT ROLE 을 별도로 제공해 직무 분리와 보안 감사를 쉽게 합니다.
1
2
3
4
5
CREATE ROLE role_emp_read, role_emp_write;
GRANT SELECT ON employees.* TO role_emp_read;
GRANT INSERT, UPDATE, DELETE ON employees.* TO role_emp_write;
CREATE USER 'reader'@'127.0.0.1' IDENTIFIED BY 'qwerty';
GRANT role_emp_read TO 'reader'@'127.0.0.1';
역할은 기본적으로 비활성 상태이므로 세션에서 명시적으로 활성화해야 합니다.
1
SET ROLE 'role_emp_read';
운영 편의를 위해 activate_all_roles_on_login 시스템 변수를 ON 으로 설정하면 로그인 시 부여된 모든 역할이 자동으로 활성화됩니다.
1
SET GLOBAL activate_all_roles_on_login = ON;
역할을 계정과 분리해 선언하면 보안 관리자와 개발자 역할을 나눠 책임을 분담할 수 있으며, 잘못된 권한 부여를 롤 단위에서 빠르게 조정할 수 있습니다.
역할 정의 시 호스트 기본값
CREATE ROLE role_emp_read;처럼 호스트를 생략하면 역할 역시 자동으로%호스트가 붙어'role_emp_read'@'%'로 저장됩니다. 특정 네트워크 구간의 계정에만 역할을 부여하려면'role_emp_read'@'192.168.%'와 같이 호스트를 명시해 관리합니다.
역할과 계정은 왜 구분해서 지원할까?
실제로 mysql에서는 역할과 계정을 내외부적으로 동일한 객체입니다.
내부 구현을 보면 역할과 계정은 모두 mysql.user 테이블에 저장되며 구조도 동일합니다. 그럼에도 불구하고 MySQL이 CREATE USER 와 CREATE ROLE 을 별도로 제공하는 이유는 직무 분리와 감사 용이성에 있습니다.
예를 들어 보안팀은 다음과 같이 개발팀 계정에는 직접 권한을 주지 않고, 역할만 관리하도록 정책을 세울 수 있습니다.
1
2
3
4
5
6
7
8
-- 보안팀이 역할 정의 및 권한 부여
CREATE ROLE role_api_read, role_api_write;
GRANT SELECT ON api_db.* TO role_api_read;
GRANT INSERT, UPDATE ON api_db.orders TO role_api_write;
-- 개발팀은 계정만 생성
CREATE USER 'api_user'@'10.%' IDENTIFIED BY 'safe_pass';
GRANT role_api_read TO 'api_user'@'10.%';
이 구조에서는 보안팀이 역할에 부여된 권한을 한 번만 검증하면 되고, 개발팀은 계정 추가나 교체를 자유롭게 처리합니다. 만약 역할이 아닌 계정을 그대로 복제해 사용한다면 누가 언제 어떤 권한을 부여했는지 기록이 분산되고, 필요 시 권한 회수도 계정마다 반복해야 합니다.
또한 역할은 여러 계정에 동시에 적용되므로 장애 대응이나 권한 조정 시 역할만 수정하면 즉시 일괄 반영됩니다. 결국 “역할=계정”이라는 내부 구조를 유지하면서도, 사용자 경험 차원에서 두 객체를 구분해 제공함으로써 운영 정책을 세분화할 수 있게 한 것입니다.
