보호의 원리
보호 도메인과 접근 행렬을 바탕으로 ACL·역량 리스트를 비교하고 최소 권한과 권한 분리 원칙을 적용합니다.
멀티유저 시스템에서는 한 사용자의 프로세스가 다른 사용자의 데이터를 읽거나 수정할 수 없어야 합니다.
한 프로세스의 버그가 시스템 전체를 망가뜨려서도 안 됩니다.
보호(Protection)는 OS가 자원에 대한 접근을 제어하는 메커니즘입니다.
보안(Security)이 외부의 공격을 막는 것이라면, 보호는 내부의 프로그램이 허용된 범위 안에서만 자원을 쓰게 하는 것입니다.
보안이 외부 공격 방어라면 보호는 프로세스, 사용자, 자원 사이의 접근 권한을 운영체제가 통제하는 메커니즘입니다.
- 모드 비트
사용자 모드와 커널 모드를 나눠 위험한 명령을 직접 실행하지 못하게 합니다. CPU
- 메모리 보호
페이지 권한과 주소 범위로 다른 프로세스나 커널 메모리 접근을 막습니다. memory
- 파일 권한
소유자, 그룹, ACL로 자원별 접근 가능 사용자를 제한합니다. ACL
- 프로세스 격리
한 프로세스의 버그가 시스템 전체나 다른 사용자 데이터로 번지지 않게 합니다. domain
왜 보호가 필요한가
보호가 없다면 어떤 일이 벌어질까요?
- 일반 사용자 프로세스가 커널 메모리를 덮어써서 시스템이 크래시합니다.
- 프로세스 A가 프로세스 B의 메모리를 읽어 비밀번호를 탈취합니다.
- 버그가 있는 프로그램이 /etc/passwd를 덮어써서 모든 사용자가 로그인 불가능해집니다.
- 무한 루프 프로그램이 모든 CPU를 독점하여 다른 프로세스가 실행되지 못합니다.
OS는 여러 계층의 보호 메커니즘으로 이런 상황을 방지합니다.
CPU의 모드 비트(커널/사용자 모드), 메모리 보호(기본/한계 레지스터, 페이지 테이블 권한), 파일 시스템 권한, 프로세스 격리가 모두 보호의 구성요소입니다.
보호는 내부 프로그램이 허용된 자원만 쓰게 만드는 OS의 통제 장치입니다. CPU 모드, 메모리 권한, 파일 권한, 도메인 전환이 함께 작동해 피해 범위를 제한합니다.
- 모드 비트
사용자 모드가 커널 명령을 직접 실행하지 못하게 막습니다.
- 메모리 보호
다른 프로세스나 커널 영역을 임의로 읽고 쓰지 못하게 합니다.
- 파일 권한
소유자, 그룹, ACL 기준으로 연산 가능 여부를 판단합니다.
- 도메인 전환
SetUID나 시스템 콜처럼 통제된 방식으로 권한을 바꿉니다.
- 권한 제한 기준
보호는 외부 공격보다 내부 실행 흐름의 권한 제한에 초점을 둡니다. 접근 행렬은 이론 모델이고 실제 구현은 ACL과 Capability로 나뉩니다. 권한은 필요한 만큼만 부여하고 기본값은 거부가 안전합니다.
- 보호 모델 비교
ACL 자원 기준 Capability 주체 기준 SetUID 도메인 전환 Deny 기본 거부
보호 도메인
보호 도메인(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) 등을 개별적으로 부여하거나 제거할 수 있습니다.
ACL은 파일이나 장치에 권한 목록을 붙이고, Capability List는 프로세스가 사용할 수 있는 권한 토큰을 들고 있게 한다.
- /etc/shadowACL: 자원에 붙는 목록
/etc/shadow root: read/write, shadow 그룹: read, others: 없음 /var/log syslog: write, adm: read, 일반 사용자: 없음 revoke 특정 파일의 ACL 항목을 지우면 그 자원 접근을 바로 끊기 쉽다.
- process ACapability: 도메인이 가진 토큰
process A {file1: rw, printer: print}처럼 사용할 권한이 토큰으로 전달된다. CAP_NET Linux는 root 권한을 CAP_NET_BIND_SERVICE 같은 단위로 쪼갠다. forge 역량 토큰은 위조되면 안 되므로 커널이나 신뢰 경계 안에서 관리한다.
#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가 대표적입니다.
네트워크 연결 처리는 비특권 프로세스가 담당하고, 인증 확인만 특권 프로세스가 수행합니다.
비특권 프로세스가 해킹되어도 시스템 전체가 위험해지지 않습니다.
보호 도메인, 접근 행렬, 역량 리스트를 실제 운영에 적용할 때의 결론은 최소 권한과 권한 분리다.
- 1
자원과 연산을 먼저 분리 웹 콘텐츠는 읽기, 로그 파일은 추가 쓰기, 설정 파일은 배포 계정만 수정처럼 연산 단위를 좁힌다.
- 2
기본값은 거부 접근 제어 목록이나 역량 토큰에 명시되지 않은 파일, 장치, 네트워크 권한은 실패하게 둔다.
- 3
권한 상승 경로를 제한 세트 사용자 식별자, 시스템 호출, 커널 모드 진입은 필요한 지점에서만 짧게 허용한다.
- 분리해야 할 부분
높은 권한 인증 확인과 비밀 파일 수정 비밀번호 변경처럼 반드시 필요한 순간에만 루트 도메인이나 커널 도메인을 사용한다. 낮은 권한 네트워크 처리와 사용자 입력 외부 입력을 많이 받는 코드는 비특권 프로세스로 실행해 침해 시 영향을 제한한다.
다음 절에서는 Linux의 구체적인 권한 체계를 다루겠습니다.
보호는 주체가 객체에 수행할 수 있는 연산을 커널이 중재하는 일입니다. access matrix, ACL, capability는 같은 관계를 다른 방향에서 저장합니다.
- 01
subject
- 02
object
- 03
right
- 04
커널 권한 검사
- subject 프로세스, 사용자, 보호 도메인
요청을 보내는 주체가 된다.
- object 파일, 메모리 페이지, 장치
IPC endpoint가 보호해야 할 대상이다.
- right
read/write/execute/send처럼 허용된 연산 단위가 정책의 최소 칸이다.
- 커널 권한 검사 커널
system call마다 현재 자격 증명과 객체 메타데이터를 대조한다.