권한과 보안
로그인·사용자·역할을 나누고 최소 권한만 줍니다.
세 층으로 나뉩니다
SQL Server 의 권한은 서버와 데이터베이스가 따로입니다. 둘을 잇는 것이 사용자입니다.
| 무엇 | 어디에 | 하는 일 |
|---|---|---|
| 로그인(LOGIN) | 서버 | 서버에 접속합니다 |
| 사용자(USER) | 데이터베이스 | 그 데이터베이스 안에서의 신분입니다 |
| 역할(ROLE) | 데이터베이스 | 권한을 묶어 두는 이름입니다 |
로그인만으로는 아무 데이터베이스에도 들어가지 못합니다. 들어갈 데이터베이스마다 사용자를 만들어 이어 주어야 합니다.
-- 서버에 로그인을 만듭니다. CREATE LOGIN lab_app WITH PASSWORD = '…'; GO -- 이 데이터베이스에 그 로그인의 사용자를 만듭니다. CREATE USER app_user FOR LOGIN lab_app; GO SELECT name AS 이름, type_desc AS 종류, authentication_type_desc AS 인증 FROM sys.database_principals WHERE name = 'app_user';
| 이름 | 종류 | 인증 |
|---|---|---|
| app_user | SQL_USER | INSTANCE |
Windows 인증을 쓸 수 있으면 그쪽이 낫습니다.
CREATE LOGIN [DOMAIN\사용자] FROM WINDOWS 로 만들면
암호를 연결 문자열에 적지 않아도 됩니다. 웹 서버라면
응용 프로그램 풀 계정으로 접속하게 두는 방법이 있습니다.
주지 않으면 못 합니다
사용자를 만들기만 하면 접속만 되고 아무것도 하지 못합니다.
EXECUTE AS USER 로 그 사용자인 척 실행해
확인해 봅니다.
EXECUTE AS USER = 'app_user'; -- 여기서부터 그 사용자입니다 SELECT COUNT(*) FROM Board.POSTS; REVERT; -- 원래 신분으로 돌아옵니다
EXECUTE AS USER 는 창을 두 개 열지
않고도 권한을 확인하는 방법입니다. 반드시
REVERT 로 돌아오십시오. 잊으면 그 세션이 계속
그 사용자로 남습니다.
GRANT SELECT ON Board.POSTS TO app_user; GO EXECUTE AS USER = 'app_user'; SELECT COUNT(*) FROM Board.POSTS; -- 이제 됩니다 INSERT INTO Board.POSTS (…) VALUES (…); -- 이것은요 REVERT;
| 문장 | 결과 |
|---|---|
| SELECT | 507 |
| INSERT | 229 — The INSERT permission was denied |
| 문장 | 하는 일 |
|---|---|
| GRANT | 허용합니다 |
| REVOKE | 주었던 것을 거둡니다. 허용도 거부도 아닌 상태로 돌립니다 |
| DENY | 막습니다. 다른 데서 허용해도 막힙니다 |
표를 열지 않고 프로시저만 엽니다
응용 프로그램 계정에 표 권한을 주지 마십시오. 프로시저 실행 권한만 주면 그 프로시저가 하는 일만 할 수 있습니다.
-- 표 권한은 거둡니다. REVOKE SELECT ON Board.POSTS FROM app_user; GO CREATE OR ALTER PROCEDURE Board.P_POST_COUNT @board_num int AS BEGIN SET NOCOUNT ON; SELECT COUNT(*) AS 글수 FROM Board.POSTS WHERE board_num = @board_num; END GO GRANT EXECUTE ON Board.P_POST_COUNT TO app_user; GO EXECUTE AS USER = 'app_user'; SELECT COUNT(*) FROM Board.POSTS; -- 직접 조회 EXEC Board.P_POST_COUNT @board_num = 1; -- 프로시저로 REVERT;
| 방법 | 결과 |
|---|---|
| 표를 직접 조회 | 229 — 거부 |
| 프로시저로 | 35 |
소유권 체인이라고 합니다. 프로시저와 표의
소유자가 같으면 안쪽 표의 권한을 다시 따지지 않습니다.
둘 다 Board 스키마에 있고 그 스키마의 소유자가
같기 때문입니다.
이것이 최소 권한을 실제로 하는 방법입니다. 응용 프로그램은 정해진 프로시저만 부를 수 있고, 표를 통째로 지우거나 다른 사람의 글을 고칠 수 없습니다. 연결 문자열이 새어 나가도 피해가 그 프로시저들이 하는 일로 한정됩니다.
동적 SQL 은 체인을 끊습니다
CREATE OR ALTER PROCEDURE Board.P_POST_COUNT_DYN @board_num int AS BEGIN SET NOCOUNT ON; DECLARE @sql nvarchar(200) = N'SELECT COUNT(*) AS 글수 FROM Board.POSTS WHERE board_num = @b'; EXEC sp_executesql @sql, N'@b int', @b = @board_num; END GO GRANT EXECUTE ON Board.P_POST_COUNT_DYN TO app_user;
동적 SQL 은 별개의 배치로 실행되어 소유권 체인을 잇지 못합니다. 그래서 그 안의 표 권한을 사용자가 직접 가지고 있어야 합니다. 그러면 최소 권한이 무너집니다 — 표 권한을 주는 순간 프로시저를 거치지 않고도 무엇이든 할 수 있게 됩니다.
동적 SQL 이 꼭 필요하다면 WITH EXECUTE AS OWNER
를 붙입니다.
CREATE OR ALTER PROCEDURE Board.P_POST_COUNT_DYN2 @board_num int WITH EXECUTE AS OWNER -- 소유자의 권한으로 실행합니다 AS BEGIN … END
| 프로시저 | 결과 |
|---|---|
| 동적 SQL 그대로 | 229 |
| WITH EXECUTE AS OWNER 를 붙임 | 35 |
EXECUTE AS OWNER 는 함부로 쓰지
마십시오. 그 프로시저 안에서는 소유자가 할 수 있는 모든
일이 열립니다. 동적 SQL 에 검색어를 이어 붙이는 곳이라면 주입
하나로 데이터베이스 전체가 넘어갑니다(4.9). 동적 SQL 을 쓰지 않는
것이 먼저입니다.
사람마다 주지 말고 묶어 둡니다
사람이 늘 때마다 권한을 하나씩 주면 누가 무엇을 할 수 있는지 아무도 모르게 됩니다. 역할을 만들어 거기에 주고, 사람은 역할에 넣습니다.
CREATE ROLE app_reader; GRANT SELECT ON SCHEMA::Board TO app_reader; -- 스키마 통째로 ALTER ROLE app_reader ADD MEMBER app_user; GO EXECUTE AS USER = 'app_user'; SELECT COUNT(*) FROM Board.POSTS; REVERT;
스키마 단위로 주면 나중에 만드는 표에도 자동으로 붙습니다.
-- 권한을 준 뒤에 표를 새로 만듭니다. CREATE TABLE Board.T_NEW (num int IDENTITY PRIMARY KEY, memo nvarchar(50)); INSERT INTO Board.T_NEW (memo) VALUES (N'나중에 만든 표'); GO EXECUTE AS USER = 'app_user'; SELECT COUNT(*) FROM Board.T_NEW; REVERT;
DENY 는 GRANT 를 이깁니다
-- 역할로는 스키마 전체를 읽을 수 있는 상태입니다. DENY SELECT ON Board.POSTS TO app_user; GO EXECUTE AS USER = 'app_user'; SELECT COUNT(*) FROM Board.POSTS; -- 막은 표 SELECT COUNT(*) FROM Board.COMMENTS; -- 같은 스키마의 다른 표 REVERT;
| 표 | 결과 |
|---|---|
| Board.POSTS | 229 — 거부 |
| Board.COMMENTS | 1,013 |
DENY 는 되도록 쓰지 마십시오.
"거의 다 되는데 이것만 안 되는" 상태를 만들면 왜 안 되는지 찾기가
어렵습니다. 필요한 것만 GRANT 하는 쪽이
읽기도 쉽고 사고도 적습니다.
고정 역할은 너무 넓습니다
| 고정 역할 | 주는 것 | 응용 프로그램 계정에 |
|---|---|---|
| db_owner | 그 데이터베이스의 모든 것 | 주지 마십시오 |
| db_datareader | 모든 표 읽기 | 되도록 피하십시오 |
| db_datawriter | 모든 표 쓰기 | 되도록 피하십시오 |
| db_ddladmin | 표·프로시저를 만들고 지우기 | 배포 계정에만 |
db_datareader 는 앞으로 생길 표까지
포함합니다. 나중에 누군가 개인 정보를 담은 표를 만들면 그것도 자동으로
읽힙니다. 스키마를 나누고 필요한 스키마에만 주는 편이
낫습니다(3.7).
누가 무엇을 할 수 있습니까
-- 준 것을 목록으로 봅니다. SELECT USER_NAME(p.grantee_principal_id) AS 받는이, p.state_desc AS 상태, p.permission_name AS 권한, CASE p.class WHEN 0 THEN N'데이터베이스' WHEN 1 THEN OBJECT_SCHEMA_NAME(p.major_id) + N'.' + OBJECT_NAME(p.major_id) WHEN 3 THEN N'스키마 ' + SCHEMA_NAME(p.major_id) END AS 대상 FROM sys.database_permissions p WHERE p.grantee_principal_id > 4 ORDER BY 받는이, 대상;
| 받는이 | 상태 | 권한 | 대상 |
|---|---|---|---|
| app_reader | GRANT | SELECT | 스키마 Board |
| app_user | GRANT | CONNECT | 데이터베이스 |
| app_user | GRANT | EXECUTE | Board.P_POST_COUNT |
| app_user | DENY | SELECT | Board.POSTS |
실제로 무엇이 되는지는 그 사용자로 바꿔 물어봐야 합니다.
EXECUTE AS USER = 'app_user'; SELECT 'POSTS' AS 표, permission_name AS 권한 FROM fn_my_permissions('Board.POSTS', 'OBJECT') WHERE subentity_name = '' -- 열 단위 권한은 뺍니다 AND permission_name IN ('SELECT','INSERT','UPDATE','DELETE') UNION ALL SELECT 'COMMENTS', permission_name FROM fn_my_permissions('Board.COMMENTS', 'OBJECT') WHERE subentity_name = '' AND permission_name IN ('SELECT','INSERT','UPDATE','DELETE'); REVERT;
| 표 | 권한 |
|---|---|
| COMMENTS | SELECT |
직접 해보기
게시판 웹 사이트가 사용할 계정을 만듭니다. 글을 읽고 쓰되 표에는 직접 접근하지 못하게 하십시오. 회원 표는 아예 보이지 않아야 합니다.
인수인계받은 시스템의 연결 문자열에
User ID=sa 가 적혀 있습니다.
무엇이 문제이고 어떤 순서로 고치겠습니까. 서비스를
멈추지 않고 바꿔야 합니다.
실습에서 만든 것을 지웁니다.
DROP PROCEDURE Board.P_POST_COUNT, Board.P_POST_COUNT_DYN, Board.P_POST_COUNT_DYN2;
DROP TABLE Board.T_NEW;
ALTER ROLE app_reader DROP MEMBER app_user;
DROP ROLE app_reader;
DROP USER app_user;
DROP LOGIN lab_app;
- 권한은 서버(로그인)와 데이터베이스(사용자)가 따로입니다. 로그인만으로는 어느 데이터베이스에도 들어가지 못합니다.
- 사용자를 만들기만 하면 접속만 되고 아무것도 못 합니다. 권한이 없으면 229 입니다.
- 권한은 동작마다 따로입니다.
SELECT를 주었다고INSERT가 되지 않습니다. EXECUTE AS USER로 그 사용자인 척 실행해 확인할 수 있습니다.REVERT를 잊지 마십시오.- 표 권한을 주지 말고 프로시저 실행 권한만 주십시오. 소유자가 같으면 프로시저 안의 표 권한을 다시 따지지 않습니다(소유권 체인).
- 동적 SQL 은 체인을 끊습니다(229).
WITH EXECUTE AS OWNER로 넘길 수는 있지만 그 안에서 소유자의 모든 권한이 열리므로 동적 SQL 을 쓰지 않는 편이 먼저입니다(4.9). - 사람마다 주지 말고 역할에 주고 사람을 역할에 넣으십시오.
GRANT … ON SCHEMA::로 주면 나중에 만드는 것에도 붙습니다. DENY는GRANT를 이깁니다. 다만 왜 안 되는지 찾기 어려워지므로 필요한 것만GRANT하는 편이 낫습니다.db_owner·db_datareader같은 고정 역할은 앞으로 생길 것까지 포함하므로 응용 프로그램 계정에 주지 마십시오.- 준 것은
sys.database_permissions, 실제로 되는 것은fn_my_permissions로 봅니다. 역할로 받은 것이 섞이므로 둘이 다릅니다.