본문 바로가기

보호의 원리

보호 도메인과 접근 행렬을 바탕으로 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_concept.py
# 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을 사용합니다.

posix_acl.sh
# 특정 사용자에게 읽기 권한 부여 (소유자/그룹 외)
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)

접근 행렬을 행(도메인) 방향으로 분해한 것입니다.

보호 모델 · 같은 권한의 두 방향

ACL은 자원별 열, 역량 리스트는 도메인별 행으로 접근 행렬을 읽습니다

본문 접근 행렬에서 file1의 열과 domain1의 행을 선택해 펼쳤습니다. 같은 domain1 → file1의 read·write 권한이 두 표현에서 어디에 놓이는지 비교합니다. 전체 행렬의 모든 권한을 나열한 그림은 아닙니다.

열 선택 · file1을 중심으로

질문: 이 자원에 누가 어떤 연산을 할 수 있는가?

file1 열에서 허용된 도메인을 찾는 ACL 투영domain1에서 file1로 read와 write, domain2에서 file1로 read 화살표가 들어간다. domain3는 file1에 허용 권한이 없으므로 허용 화살표가 없다. 화살표 방향은 도메인에서 자원이며 두 가지 표현이 권한을 새로 만드는 것은 아니다.domain1read·writedomain2readdomain3허용 권한 없음file1선택한 열domain3는 허용 간선 없음
같은 file1 ACL을 세로로 펼친 두 허용 관계domain1의 read·write와 domain2의 read가 같은 file1로 향한다. domain3에는 허용 권한이 없어 선이 없다. 배치만 세로로 바꾸며 원 행렬의 domain1과 file1 관계를 바꾸지 않는다.file1선택한 열domain1read·writedomain2readdomain3허용 권한 없음선 방향: 도메인→자원domain3는 허용 선 없음

file1 열의 개념 목록

  • domain1 → file1: read, write
  • domain2 → file1: read
  • domain3 → file1: 없음

빈 항목은 허용 권한 없음입니다. 실제 ACL에서는 무권한 항목을 생략할 수 있습니다.

행 선택 · domain1을 중심으로

질문: 이 도메인은 어느 자원에 어떤 연산을 할 수 있는가?

domain1 행에서 허용된 네 자원을 찾는 역량 투영domain1에서 file1 read·write, file2 read, printer print, domain2 switch 네 자원으로 향하는 네 화살표다. domain2는 switch 연산의 전환 대상 자원이다. 자원 이름 아래 연산은 그 도메인과 자원 사이 권한이며 새로운 허용을 추가하지 않는다.domain1선택한 행file1read·writefile2readprinterprintdomain2switch
같은 domain1 역량의 네 방향을 세로로 펼치기하나의 domain1 노드에서 file1 read·write, file2 read, printer print, domain2 switch로 향하는 네 화살표다. 네 경로의 포트를 나누며 원 행렬의 허용 연산을 그대로 유지한다.domain1선택한 행file1read·writefile2readprinterprintdomain2switch선의 출발은 domain1domain2는 전환 대상

domain1 행의 개념 목록

  • domain1 → file1: read, write
  • domain1 → file2: read
  • domain1 → printer: print
  • domain1 → domain2: switch

domain2는 전환 대상 자원이고 switch는 그 도메인으로 전환할 수 있는 권한입니다.

역량은 위조·변조할 수 없도록 보호해야 합니다. 이 객체별 역량 리스트는 Linux의 CAP_NET_BIND_SERVICE처럼 root 특권을 세분화한 capability 집합과 같은 자료구조가 아닙니다.

각 프로세스는 어떤 자원에 어떤 권한이 있는지 나타내는 토큰을 보유합니다.

사용자 A의 Capability: {file1: rw, file2: r, printer: print}

역량은 위조할 수 없도록 보호해야 합니다. 커널이 보유·전달을 중재하거나 인증된 토큰을 검증하는 등 구현에 따라 신뢰 경계를 둡니다.

분산 시스템에서 유용합니다(토큰을 네트워크로 전달 가능).

취소는 설계 과제입니다. 분산된 토큰을 일일이 회수하는 방식뿐 아니라 간접 참조·세대 번호·만료 같은 방식으로 접근을 무효화할 수도 있습니다.

Linux의 커패빌리티(Capabilities)는 root의 특권 검사를 세분화한 것으로, 앞서 설명한 객체별 역량 리스트와 같은 자료구조는 아닙니다.

전통적으로 root는 모든 것을 할 수 있었지만, 커패빌리티는 네트워크 포트 바인딩(CAP_NET_BIND_SERVICE), 프로세스 종료(CAP_KILL), 시스템 시간 변경(CAP_SYS_TIME) 등을 개별적으로 부여하거나 제거할 수 있습니다.

capability_check.c
#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·보안 정책의 영향을 받으므로 고정된 출력으로 가정하지 않습니다.

한 권한을 두 저장 방향으로 다시 읽기

원 접근 행렬의 domain2→file2에는 read와 write가 있습니다. file2의 ACL 방향으로 옮기면 domain1 read, domain2 read·write, domain3 read가 모입니다. 반대로 domain2의 역량 리스트에는 file1 read와 file2 read·write가 모입니다. 저장 방향을 바꿨다고 새로운 권한이 생기거나 사라지는 것은 아닙니다. 원 Python 함수는 principal 문자열이 정확히 일치하는 항목만 찾으므로 adm이라는 호출이 실제 Linux의 보조 그룹을 조회한 결과라고 읽지 않습니다.

독립 확인: 원 행렬에서 domain3의 역량 리스트와 printer의 ACL을 각각 적으세요. domain3의 file1 write 요청과 domain1의 domain2 switch 요청을 판정하세요. 이어 원 Python의 check_access("adm", "/var/log/syslog", "write") 결과와 이 함수가 실제 프로세스의 Linux 접근을 증명하는지 설명하세요.

직접 계산한 뒤 풀이 확인

domain3은 file2 read와 printer print입니다. printer에는 domain1 print와 domain3 print가 있고 domain2의 허용 항목은 없습니다. domain3의 file1 write는 거부, domain1의 domain2 switch는 행렬의 해당 권한으로 허용합니다. Python에서 adm의 syslog 항목에는 read만 있어 write는 False입니다. 이 함수는 실제 ID·보조 그룹·ACL mask·MAC 등을 조회하지 않으므로 실제 OS 판정의 증명은 아닙니다.

최소 권한 원칙

최소 권한 원칙(Principle of Least Privilege)은 프로세스에게 작업 수행에 필요한 최소한의 권한만 부여하는 원칙입니다.

보안 사고 발생 시 피해 범위를 최소화합니다.

실무 적용 예시
  • 웹 서버는 /var/www에만 접근 가능, 읽기 전용. 로그는 append만 허용.
  • 데이터베이스 프로세스는 데이터 디렉터리만 읽기/쓰기. 네트워크는 특정 포트만.
  • CI/CD 봇 계정은 배포에 필요한 작업만 수행 가능.
least_privilege.sh
# 보안 옵션 조합 예시: 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의 구체적인 권한 체계를 다루겠습니다.