각 계정·역할에 업무에 꼭 필요한 권한만 부여하는 원칙. 사고를 막는 게 아니라 사고의 피해 범위를 제한한다.
왜 결정적인가
SQL 인젝션이 뚫렸다
앱 계정이 슈퍼유저 → DROP TABLE · 전체 데이터 유출 · 다른 DB 까지 앱 계정이 SELECT/INSERT/UPDATE 만 → 피해가 그 테이블의 데이터로 한정된다
뚫린 뒤에 차이를 만드는 유일한 장치다.
역할로 묶는다
-- PostgreSQL: 역할에 권한을 모으고 사용자에게 역할을 준다
CREATE ROLE app_read;
GRANT CONNECT ON DATABASE mydb TO app_read;
GRANT USAGE ON SCHEMA public TO app_read;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_read;
-- 앞으로 만들어질 테이블에도 자동 적용 (자주 빠뜨리는 부분)
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO app_read;
CREATE USER analytics PASSWORD '...';
GRANT app_read TO analytics;
ALTER DEFAULT PRIVILEGES가 핵심이다 — 없으면 새 테이블이 생길 때마다 권한이 빠져 조회가 안 된다.
-- MySQL
CREATE ROLE 'app_read';
GRANT SELECT ON mydb.* TO 'app_read';
CREATE USER 'analytics'@'%' IDENTIFIED BY '...';
GRANT 'app_read' TO 'analytics'@'%';
SET DEFAULT ROLE ALL TO 'analytics'@'%'; -- 없으면 로그인해도 역할이 비활성이다
실무에서 나누는 계정
- app_rw — 애플리케이션. SELECT/INSERT/UPDATE/DELETE 만. DDL 없음
- app_ro — 읽기 전용 복제본 조회 · 리포트
- migration — DDL 전용. 배포 시에만 쓴다
- admin — 사람이 직접. 감사 로그와 함께
애플리케이션 계정에 DDL 권한을 주지 않는 것만으로도 인젝션 피해가 크게 줄고, 실수로 마이그레이션이 도는 사고도 막는다.
확인하는 쿼리
-- PostgreSQL: 누가 무엇을 할 수 있나
SELECT grantee, table_name, privilege_type
FROM information_schema.table_privileges WHERE table_schema = 'public';
-- 슈퍼유저 계정 점검
SELECT rolname, rolsuper, rolcreatedb FROM pg_roles WHERE rolsuper;
-- MySQL
SHOW GRANTS FOR 'app'@'%';
SELECT user, host FROM mysql.user WHERE Super_priv = 'Y';
면접 함정
- ❌ "권한 관리는 보안팀 일" → 애플리케이션 계정 권한은 개발자가 정한다.
- ❌ "편의상 앱 계정에 전권을" → 뚫렸을 때 피해 범위가 곧 그 권한 범위다.