보호의 원리
보호 도메인과 접근 행렬을 바탕으로 ACL·역량 리스트를 비교하고 최소 권한과 권한 분리 원칙을 적용합니다.
멀티유저 시스템에서는 한 사용자의 프로세스가 다른 사용자의 데이터를 읽거나 수정할 수 없어야 합니다.
한 프로세스의 버그가 시스템 전체를 망가뜨려서도 안 됩니다.
보호(Protection)는 OS가 자원에 대한 접근을 제어하는 메커니즘입니다.
보안(Security)은 내·외부 위협으로부터 기밀성·무결성·가용성을 지키는 더 넓은 목표입니다. 보호는 프로그램이 허용된 범위 안에서 자원을 사용하도록 접근을 통제하는 메커니즘입니다.
왜 보호가 필요한가
보호가 없다면 어떤 일이 벌어질까요?
- 일반 사용자 프로세스가 커널 메모리를 덮어써서 시스템이 크래시합니다.
- 프로세스 A가 프로세스 B의 메모리를 읽어 비밀번호를 탈취합니다.
- 버그가 있는 프로그램이 /etc/passwd를 덮어써서 모든 사용자가 로그인 불가능해집니다.
- 무한 루프 프로그램이 모든 CPU를 독점하여 다른 프로세스가 실행되지 못합니다.
OS는 여러 계층의 보호 메커니즘으로 이런 상황을 방지합니다.
CPU의 모드 비트(커널/사용자 모드), 메모리 보호(기본/한계 레지스터, 페이지 테이블 권한), 파일 시스템 권한, 프로세스 격리가 모두 보호의 구성요소입니다.
보호 도메인
보호 도메인(Protection Domain)은 프로세스가 접근할 수 있는 자원과 그 권한의 집합입니다.
프로세스는 항상 하나의 도메인에서 실행되며, 도메인이 허용하는 범위 내에서만 자원에 접근할 수 있습니다.
Unix의 UID는 보호 도메인을 설명하는 중요한 요소입니다. 실제 접근에는 유효 ID·그룹·ACL·capability·보안 정책 등이 함께 관여합니다.
따라서 UID가 같아도 모든 프로세스의 접근 가능 범위가 항상 같지는 않습니다.
커널 모드와 사용자 모드도 도메인의 일종입니다.
도메인 전환의 예: 일반 사용자가 passwd 명령을 실행하면 SetUID 비트 때문에 root 도메인으로 전환됩니다.
시스템 콜을 호출하면 사용자 모드에서 커널 모드 도메인으로 전환됩니다.
접근 행렬
접근 행렬(Access Matrix)은 보호의 이론적 모델입니다.
행은 도메인(사용자), 열은 자원(파일, 장치)이며, 각 항목은 허용된 연산(읽기, 쓰기, 실행)입니다.
다음은 빈 항목을 생략한 행별 표현입니다. 표시하지 않은 자원에는 허용 연산이 없습니다.
| 도메인 | 자원별 허용 연산 |
|---|---|
| domain1 (사용자 A) | file1: read, write / file2: read / printer: print / domain2: switch |
| domain2 (사용자 B) | file1: read / file2: read, write |
| domain3 (사용자 C) | file2: read / printer: print |
자원으로 등장하는 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")) # False위 Python 딕셔너리는 principal 문자열을 비교하는 개념 코드입니다. 실제 Linux 사용자·보조 그룹·ACL mask를 조회하지 않으며 시스템 권한을 바꾸지도 않습니다.
Unix의 전통적 rwx 권한은 owner·group·others 세 범주의 간략한 접근 제어 모델입니다.
더 세밀한 제어가 필요하면 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
# mask::r-x ← named user와 그룹 계열 권한의 상한
# 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) { perror("cap_get_proc"); return 1; }
{
char *text = cap_to_text(caps, NULL);
if (!text) { perror("cap_to_text"); cap_free(caps); return 1; }
printf("현재 커패빌리티: %s\n", text);
cap_free(text);
cap_free(caps);
}
return 0;
}
/* 표시 값은 현재 프로세스의 실제 capability 집합에 따라 다름 */이 C 예제에는 libcap 개발 헤더와 링크 옵션 -lcap이 필요합니다. root도 namespace·bounding set·보안 정책의 영향을 받으므로 고정된 출력으로 가정하지 않습니다.
최소 권한 원칙
최소 권한 원칙(Principle of Least Privilege)은 프로세스에게 작업 수행에 필요한 최소한의 권한만 부여하는 원칙입니다.
보안 사고 발생 시 피해 범위를 최소화합니다.
실무 적용 예시:
- 웹 서버는
/var/www만 접근 가능, 읽기 전용. 로그는 append만 허용. - 데이터베이스 프로세스는 데이터 디렉토리만 읽기/쓰기. 네트워크는 특정 포트만.
- CI/CD 봇 계정은 배포에 필요한 작업만 수행 가능.
# 보안 옵션 조합 예시: nginx 설정과 쓰기 경로 준비는 별도 필요
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 사용자로 실행위 명령은 옵션의 의미를 설명하는 조합입니다. 기본 nginx 이미지를 UID 1000과 읽기 전용 루트로 즉시 구동할 수 있다는 완성된 실행 예제가 아닙니다. 이미지의 entrypoint·설정·PID·캐시·로그 경로에 맞는 읽기 권한과 제한된 쓰기 경로를 준비해야 합니다.
권한 분리 (Privilege Separation)
하나의 프로그램을 권한이 높은 부분과 낮은 부분으로 분리합니다.
OpenSSH가 대표적입니다.
네트워크 연결 처리는 비특권 프로세스가 담당하고, 인증 확인만 특권 프로세스가 수행합니다.
비특권 부분이 침해됐을 때 영향 범위를 줄입니다. 특권 프로세스와의 IPC 검증이나 커널에 결함이 있으면 분리만으로 모든 피해를 막지는 못합니다.
다음 절에서는 Linux의 구체적인 권한 체계를 다루겠습니다.