보호의 원리
멀티유저 시스템에서는 한 사용자의 프로세스가 다른 사용자의 데이터를 읽거나 수정할 수 없어야 합니다. 한 프로세스의 버그가 시스템 전체를 망가뜨려서도 안 됩니다. 보호(Protection)는 OS가 자원에 대한 접근을 제어하는 메커니즘입니다. 보안(Security)이 외부의 공격을 막는 것이라면, 보호는 내부의 프로그램이 허용된 범위 안에서만 자원을 쓰게 하는 것입니다.
왜 보호가 필요한가
보호가 없다면 어떤 일이 벌어질까요?
- 일반 사용자 프로세스가 커널 메모리를 덮어써서 시스템이 크래시합니다.
- 프로세스 A가 프로세스 B의 메모리를 읽어 비밀번호를 탈취합니다.
- 버그가 있는 프로그램이 /etc/passwd를 덮어써서 모든 사용자가 로그인 불가능해집니다.
- 무한 루프 프로그램이 모든 CPU를 독점하여 다른 프로세스가 실행되지 못합니다.
OS는 여러 계층의 보호 메커니즘으로 이런 상황을 방지합니다. CPU의 모드 비트(커널/사용자 모드), 메모리 보호(기본/한계 레지스터, 페이지 테이블 권한), 파일 시스템 권한, 프로세스 격리가 모두 보호의 구성요소입니다.
보호 도메인
보호 도메인(Protection Domain)은 프로세스가 접근할 수 있는 자원과 그 권한의 집합입니다. 프로세스는 항상 하나의 도메인에서 실행되며, 도메인이 허용하는 범위 내에서만 자원에 접근할 수 있습니다.
Unix에서 도메인은 사용자 ID(UID)에 의해 결정됩니다. UID가 같은 프로세스들은 같은 도메인에 속합니다. 커널 모드와 사용자 모드도 도메인의 일종입니다.
도메인 전환의 예: 일반 사용자가 passwd 명령을 실행하면 SetUID 비트 때문에 root 도메인으로 전환됩니다. 시스템 콜을 호출하면 사용자 모드에서 커널 모드 도메인으로 전환됩니다.
접근 행렬
접근 행렬(Access Matrix)은 보호의 이론적 모델입니다. 행은 도메인(사용자), 열은 자원(파일, 장치)이며, 각 항목은 허용된 연산(읽기, 쓰기, 실행)입니다.
| file1 | file2 | printer | domain2 | |
|---|---|---|---|---|
| domain1 (사용자 A) | read, write | read | switch | |
| domain2 (사용자 B) | read | read, write | - | - |
| domain3 (사용자 C) | - | read | - |
마지막 열 domain2에 대한 switch 권한은 도메인 전환 가능성을 표현합니다. 접근 행렬은 매우 강력한 모델이지만, 그대로 구현하면 대부분이 빈 칸이므로 공간이 낭비됩니다. 실제 구현은 행 방향 또는 열 방향으로 분해합니다.
ACL과 역량 리스트
ACL (Access Control List)
접근 행렬을 열(자원) 방향으로 분해한 것입니다. 각 자원에 누가 어떤 권한을 가지는지 목록을 붙입니다.
# ACL 개념 구현
file_acl = {
"/etc/shadow": [
("root", {"read", "write"}),
("shadow", {"read"}),
],
"/var/log/syslog": [
("root", {"read", "write"}),
("syslog", {"read", "write"}),
("adm", {"read"}),
],
}
def check_access(user, resource, operation):
"""ACL 기반 접근 검사"""
if resource not in file_acl:
return False # 기본 거부 (Deny by Default)
for (principal, perms) in file_acl[resource]:
if principal == user and operation in perms:
return True
return False
print(check_access("root", "/etc/shadow", "read")) # True
print(check_access("nobody", "/etc/shadow", "read")) # FalseUnix의 전통적 권한(rwx)은 3개 그룹(owner, group, others)만 있는 축약된 ACL입니다. 더 세밀한 제어가 필요하면 POSIX ACL(setfacl, getfacl)이나 NFSv4 ACL을 사용합니다.
# 특정 사용자에게 읽기 권한 부여 (소유자/그룹 외)
setfacl -m u:deploy:rx /var/www/app
# ACL 확인
getfacl /var/www/app
# user::rwx
# user:deploy:r-x ← 추가된 ACL
# group::r-x
# other::---역량 리스트 (Capability List)
접근 행렬을 행(도메인) 방향으로 분해한 것입니다. 각 프로세스가 어떤 자원에 어떤 권한이 있는지 토큰을 보유합니다.
사용자A의 Capability: {file1: rw, file2: r, printer: print}
역량은 위조가 불가능해야 하므로 커널이 관리합니다. 분산 시스템에서 유용합니다(토큰을 네트워크로 전달 가능). 단점: 특정 자원에 대한 모든 접근 권한을 취소하려면, 해당 역량을 보유한 모든 프로세스를 찾아야 합니다.
Linux의 커패빌리티(Capabilities)는 root 권한을 세분화한 것입니다. 전통적으로 root는 모든 것을 할 수 있었지만, 커패빌리티는 네트워크 포트 바인딩(CAP_NET_BIND_SERVICE), 프로세스 종료(CAP_KILL), 시스템 시간 변경(CAP_SYS_TIME) 등을 개별적으로 부여하거나 제거할 수 있습니다.
#include <stdio.h>
#include <sys/capability.h>
int main() {
/* 현재 프로세스의 커패빌리티 확인 */
cap_t caps = cap_get_proc();
if (caps) {
char *text = cap_to_text(caps, NULL);
printf("현재 커패빌리티: %s\n", text);
cap_free(text);
cap_free(caps);
}
return 0;
}
/* root: =ep (모든 커패빌리티) */
/* 일반 사용자: = (없음) */최소 권한 원칙
최소 권한 원칙(Principle of Least Privilege)은 프로세스에게 작업 수행에 필요한 최소한의 권한만 부여하는 원칙입니다. 보안 사고 발생 시 피해 범위를 최소화합니다.
실무 적용 예시:
- 웹 서버는
/var/www만 접근 가능, 읽기 전용. 로그는 append만 허용. - 데이터베이스 프로세스는 데이터 디렉토리만 읽기/쓰기. 네트워크는 특정 포트만.
- CI/CD 봇 계정은 배포에 필요한 작업만 수행 가능.
# Docker에서 최소 권한 적용
docker run \
--read-only \
--cap-drop ALL \
--cap-add NET_BIND_SERVICE \
--security-opt no-new-privileges \
-u 1000:1000 \
nginx:alpine
# --read-only: 파일 시스템 쓰기 불가
# --cap-drop ALL: 모든 리눅스 커패빌리티 제거
# --cap-add NET_BIND_SERVICE: 80/443 포트 바인딩만 허용
# --security-opt no-new-privileges: SetUID 통한 권한 상승 차단
# -u 1000:1000: non-root 사용자로 실행권한 분리 (Privilege Separation)
하나의 프로그램을 권한이 높은 부분과 낮은 부분으로 분리합니다. OpenSSH가 대표적입니다. 네트워크 연결 처리는 비특권 프로세스가 담당하고, 인증 확인만 특권 프로세스가 수행합니다. 비특권 프로세스가 해킹되어도 시스템 전체가 위험해지지 않습니다.
다음 절에서는 Linux의 구체적인 권한 체계를 다루겠습니다.