본문 바로가기

리눅스 권한 체계

Linux 파일 권한의 소유자·그룹·나머지 범주와 rwx 비트가 파일과 디렉터리에서 갖는 의미, 권한을 판정하는 순서를 모델로 확인합니다. chmod·chown·umask, SetUID·SetGID·스티키 비트, 프로세스의 여러 사용자 ID와 sudo를 안전하게 쓰는 방법을 정리합니다.

Linux는 일반 파일뿐 아니라 디렉터리, 장치, 파이프, 소켓까지 많은 자원을 파일의 형태로 다룹니다. 그래서 파일 권한 체계가 곧 시스템 보호의 바탕입니다. 이 체계는 1970년대 Unix에서 시작된 단순한 모델이지만, 지금도 거의 그대로 쓰입니다. 이 글에서는 권한 비트를 읽고 판정하는 방법, 권한을 바꾸는 명령, 특수 권한, 그리고 root의 권한을 나누어 주는 sudo를 살펴봅니다.

소유자, 그룹, 나머지

앞 글에서 본 것처럼 Unix의 권한은 ACL을 아주 간단하게 줄인 것입니다. 파일마다 소유자(owner, u)인 사용자 한 명과 그룹(group, g) 하나가 기록되어 있고, 권한은 소유자, 그룹의 구성원, 나머지 모든 사용자(others, o)의 세 범주로 나뉩니다. 범주마다 읽기(r), 쓰기(w), 실행(x)의 세 비트가 있어, 권한은 모두 아홉 비트입니다.

같은 비트라도 파일과 디렉터리에서 뜻이 다릅니다. 디렉터리는 이름과 inode 번호의 목록이므로, 디렉터리의 권한은 그 목록을 다루는 권한입니다.

권한값파일에서디렉터리에서
읽기(r)4내용을 읽는다안에 있는 이름의 목록을 읽는다
쓰기(w)2내용을 고친다x와 함께 있을 때 항목을 만들고, 지우고, 이름을 바꾼다
실행(x)1프로그램으로 실행한다안으로 들어가고, 이름으로 항목을 찾아간다

디렉터리에 r만 있고 x가 없으면 이름의 목록은 볼 수 있지만, 각 항목의 inode로 찾아갈 수 없어 파일의 크기나 내용을 볼 수 없습니다. 반대로 x만 있고 r이 없으면 목록은 볼 수 없지만, 이름을 이미 알고 있는 파일에는 접근할 수 있습니다. 10장 2편에서 본 것처럼, 경로의 중간 디렉터리마다 x가 있어야 그 아래의 파일에 이를 수 있습니다.

ls -l의 첫 열은 파일 종류 한 글자와 아홉 개의 권한 글자로 이루어집니다. 아홉 글자를 세 개씩 끊어 각각을 r=4, w=2, x=1의 합으로 적으면 8진수 세 자리가 됩니다. 파이썬의 stat.filemode()로 숫자와 글자 표기를 맞춰 볼 수 있습니다.

import stat

for mode in (0o644, 0o755, 0o600, 0o640):
    print(f"{mode:03o} -> {stat.filemode(stat.S_IFREG | mode)}")
644 -> -rw-r--r--
755 -> -rwxr-xr-x
600 -> -rw-------
640 -> -rw-r-----

첫 글자 -는 일반 파일이라는 뜻이고, 디렉터리라면 d, 심볼릭 링크라면 l입니다. 644는 소유자가 읽고 쓰며 나머지는 읽기만 하는 보통의 문서, 755는 누구나 실행할 수 있는 프로그램, 600은 소유자만 읽고 쓰는 비밀 키 같은 파일에 씁니다.

권한은 어떤 순서로 판정하나

프로세스가 파일에 접근하면 커널은 다음 순서로 범주를 하나만 고르고, 그 범주의 비트로만 판정합니다.

  1. 프로세스의 사용자 ID가 파일의 소유자와 같으면, 소유자 비트로 판정합니다.
  2. 그렇지 않고 프로세스의 그룹 ID나 보조 그룹 가운데 하나가 파일의 그룹과 같으면, 그룹 비트로 판정합니다.
  3. 둘 다 아니면 나머지 비트로 판정합니다.

여기서 사용자 ID는 정확히는 파일 시스템 접근에 쓰는 ID로, 보통은 뒤에서 볼 유효 사용자 ID와 같습니다. 중요한 것은 범주를 한 번 고르면 다른 범주로 넘어가지 않는다는 점입니다. 소유자 비트가 rw-이고 그룹 비트가 rwx라면, 소유자가 그 그룹에 속해 있어도 실행할 수 없습니다.

일반 Unix 모드 비트의 범주 선택

파일 접근 UID가 소유 UID와 같다. group의 x를 대체 적용하지 않는다. 각 단계의 값과 전환 이유를 비교합니다.

보호 · 일반 모드 비트

owner가 선택되면 group·others 권한을 더하지 않습니다

확장 ACL·특권 예외를 제외한 판정입니다. 파일 접근 UID가 소유 UID와 일치하면 owner를 선택합니다. owner가 rw-이고 group이 rwx라도 소유자의 실행 요청은 owner 비트로 거부됩니다.

일반 모드 비트 범주 선택 후 한 번 판정소유 UID가 일치하면 owner를 선택한다. 불일치일 때만 파일 그룹 소속을 검사하고 group 또는 others를 선택한다. 세 선택 중 하나의 비트로 요청 연산을 허용 또는 거부한다. 거부 결과에서 다른 범주로 돌아가는 선은 없다. 그룹에는 접근 그룹과 보조 그룹이 포함되며 확장 ACL과 특권 예외는 이 일반 모드 비트 그림에서 제외한다.파일 소유 UID가같은가?owner 선택owner 비트파일 그룹소속인가?group 선택group 비트others 선택others 비트선택한 비트로요청 연산 판정허용거부예아니오예아니오예아니오거부 뒤 다른 범주로넘어가는 선은 없음
owner·group·others 중 하나 선택소유 UID가 같으면 owner, 아니면 파일 그룹 소속이면 group, 나머지는 others를 적용합니다. 어느 범주든 권한이 부족하면 거부합니다.파일 소유 UID 일치?owner파일 그룹 소속?접근 + 보조 그룹group둘 다 불일치others예아니오예아니오선택된 비트로 요청 연산 판정
owner·group·others 중 하나 선택소유 UID가 같으면 owner, 아니면 파일 그룹 소속이면 group, 나머지는 others를 적용합니다. 어느 범주든 권한이 부족하면 거부합니다.파일 소유 UID 일치?owner파일 그룹 소속?접근 + 보조 그룹group둘 다 불일치others예아니오예아니오선택된 비트로 요청 연산 판정

확장 ACL·capability·MAC 등은 생략한 일반 모드 비트 판정입니다. Linux fsuid/fsgid는 보통 유효 ID와 같고 그룹 판정에는 보조 그룹도 포함합니다. 선택한 범주가 거부하면 다른 범주로 넘어가 권한을 더하지 않습니다.

이 예의 조건

  • 본문과 전체 도표의 예를 따라 범주 확인부터 실행 거부까지 진행합니다.
관계도의 보충 설명
전체 도표의 표기·관계·조건
구분전체 도표의 내용
제목일반 Unix 모드 비트의 범주 선택
노드·선 표기아니요
예의 값과 변화
관찰·상황값·보충 정보변화·이유
범주 확인

파일 접근 UID가 소유 UID와 같다.

  • UID·모드 비트 1: owner rw-
  • UID·모드 비트 2: group rwx
  • UID·모드 비트 3: 실행 요청
    선택 비트 검사

    owner의 x 비트만 읽는다.

    • UID·모드 비트 1: owner x=0
    • 실행 허용 조건 실패
    실행 거부

    group의 x를 대체 적용하지 않는다.

    • UID·모드 비트 1: 거부
    • UID·모드 비트 2: 다른 범주로 이동 없음
    • 판정 종료; ACL/특권/MAC은 별도

    일반 Unix 모드 비트의 범주 선택

    파일 접근 UID가 소유 UID와 같다. group의 x를 대체 적용하지 않는다. 각 단계의 값과 전환 이유를 비교합니다.

    보호 · 일반 모드 비트

    owner가 선택되면 group·others 권한을 더하지 않습니다

    확장 ACL·특권 예외를 제외한 판정입니다. 파일 접근 UID가 소유 UID와 일치하면 owner를 선택합니다. owner가 rw-이고 group이 rwx라도 소유자의 실행 요청은 owner 비트로 거부됩니다.

    일반 모드 비트 범주 선택 후 한 번 판정소유 UID가 일치하면 owner를 선택한다. 불일치일 때만 파일 그룹 소속을 검사하고 group 또는 others를 선택한다. 세 선택 중 하나의 비트로 요청 연산을 허용 또는 거부한다. 거부 결과에서 다른 범주로 돌아가는 선은 없다. 그룹에는 접근 그룹과 보조 그룹이 포함되며 확장 ACL과 특권 예외는 이 일반 모드 비트 그림에서 제외한다.파일 소유 UID가같은가?owner 선택owner 비트파일 그룹소속인가?group 선택group 비트others 선택others 비트선택한 비트로요청 연산 판정허용거부예아니오예아니오예아니오거부 뒤 다른 범주로넘어가는 선은 없음
    owner·group·others 중 하나 선택소유 UID가 같으면 owner, 아니면 파일 그룹 소속이면 group, 나머지는 others를 적용합니다. 어느 범주든 권한이 부족하면 거부합니다.파일 소유 UID 일치?owner파일 그룹 소속?접근 + 보조 그룹group둘 다 불일치others예아니오예아니오선택된 비트로 요청 연산 판정
    owner·group·others 중 하나 선택소유 UID가 같으면 owner, 아니면 파일 그룹 소속이면 group, 나머지는 others를 적용합니다. 어느 범주든 권한이 부족하면 거부합니다.파일 소유 UID 일치?owner파일 그룹 소속?접근 + 보조 그룹group둘 다 불일치others예아니오예아니오선택된 비트로 요청 연산 판정

    확장 ACL·capability·MAC 등은 생략한 일반 모드 비트 판정입니다. Linux fsuid/fsgid는 보통 유효 ID와 같고 그룹 판정에는 보조 그룹도 포함합니다. 선택한 범주가 거부하면 다른 범주로 넘어가 권한을 더하지 않습니다.

    이 예의 조건

    • 본문과 전체 도표의 예를 따라 범주 확인부터 실행 거부까지 진행합니다.

    관계도의 보충 설명

    전체 도표의 표기·관계·조건
    구분전체 도표의 내용
    제목일반 Unix 모드 비트의 범주 선택
    노드·선 표기아니요
    예의 값과 변화
    관찰·상황값·보충 정보변화·이유
    범주 확인

    파일 접근 UID가 소유 UID와 같다.

    • UID·모드 비트 1: owner rw-
    • UID·모드 비트 2: group rwx
    • UID·모드 비트 3: 실행 요청
      선택 비트 검사

      owner의 x 비트만 읽는다.

      • UID·모드 비트 1: owner x=0
      • 실행 허용 조건 실패
      실행 거부

      group의 x를 대체 적용하지 않는다.

      • UID·모드 비트 1: 거부
      • UID·모드 비트 2: 다른 범주로 이동 없음
      • 판정 종료; ACL/특권/MAC은 별도

      다음 코드는 이 판정 규칙을 그대로 옮긴 모델입니다.

      def check(mode, file_uid, file_gid, uid, gids, want):
          """want는 'r', 'w', 'x' 가운데 하나. root, ACL, 커패빌리티는 따지지 않는다."""
          if uid == file_uid:
              bits = (mode >> 6) & 7            # 소유자라면 소유자 칸만 본다
          elif file_gid in gids:
              bits = (mode >> 3) & 7            # 그룹 구성원이라면 그룹 칸만 본다
          else:
              bits = mode & 7                   # 둘 다 아니면 나머지 칸
          return bool(bits & {"r": 4, "w": 2, "x": 1}[want])
      
      # 파일의 소유자는 UID 1000, 그룹은 GID 100(dev)
      alice = (1000, {100})        # 소유자이면서 dev 그룹에도 속한다
      bob = (1001, {100})          # dev 그룹 구성원
      carol = (1002, {200})        # 그 밖의 사용자
      
      print("0674, alice 실행:", check(0o674, 1000, 100, *alice, "x"))
      print("0464, alice 쓰기:", check(0o464, 1000, 100, *alice, "w"))
      print("0464, bob 쓰기:", check(0o464, 1000, 100, *bob, "w"))
      print("0464, carol 쓰기:", check(0o464, 1000, 100, *carol, "w"))
      0674, alice 실행: False
      0464, alice 쓰기: False
      0464, bob 쓰기: True
      0464, carol 쓰기: False

      권한이 0464인 파일에서는 소유자인 alice가 쓸 수 없는데, 그룹 구성원인 bob은 쓸 수 있습니다. 소유자에게 그룹보다 적은 권한을 주는 일은 드물지만, 판정이 범주 하나만 본다는 것을 잘 보여 줍니다.

      실제 커널은 이 모델에 몇 가지를 더 따집니다. POSIX ACL이 있으면 이름이 붙은 사용자와 그룹 항목, 그리고 mask가 판정에 들어갑니다. root처럼 CAP_DAC_OVERRIDE를 가진 프로세스는 권한 검사를 건너뛰지만, path_resolution(7)에 따르면 실행만은 세 실행 비트 가운데 하나라도 켜져 있어야 허락됩니다. 그 밖에 SELinux 같은 강제 접근 제어와 읽기 전용 마운트도 결과를 바꿀 수 있습니다.

      chmod, chown, umask

      권한과 소유자 바꾸기

      chmod는 권한을 바꿉니다. 8진수로 전체를 지정하거나, u, g, o, a(모두)와 +, -, =로 일부만 바꿀 수 있습니다.

      chmod 755 deploy.sh                      # rwxr-xr-x
      chmod 600 id_ed25519                     # rw-------
      chmod u+x deploy.sh                      # 소유자에게 실행 권한을 더한다
      chmod go-w notes.txt                     # 그룹과 나머지에게서 쓰기를 뺀다
      chmod o= private.txt                     # 나머지의 권한을 모두 없앤다
      chmod -R u+rwX,go+rX,go-w /var/www/html  # 대문자 X: 디렉터리와 이미 실행 가능한 파일에만 x

      마지막 줄의 대문자 X는 디렉터리와, 이미 누군가에게 실행 권한이 있는 파일에만 x를 줍니다. chmod -R 755처럼 전체에 x를 주면 일반 문서까지 실행 파일이 되므로, 디렉터리 트리 전체를 바꿀 때 유용합니다.

      chown은 소유자와 그룹을, chgrp은 그룹만 바꿉니다. Linux에서 파일의 소유자를 다른 사용자로 바꾸는 것은 CAP_CHOWN을 가진 프로세스, 보통 root만 할 수 있습니다. 일반 사용자가 자기 파일을 남에게 넘길 수 있다면, 디스크 할당량을 피하거나 남의 이름으로 파일을 만들어 둘 수 있기 때문입니다. 자기 파일의 그룹은 자기가 속한 그룹으로 바꿀 수 있습니다.

      sudo chown alice:dev report.txt          # 소유자 alice, 그룹 dev
      sudo chown -R www-data:www-data /var/www # 디렉터리 아래 전체
      chgrp dev project/                       # 자기가 속한 그룹으로 바꾸기

      umask: 새 파일에서 지울 권한

      새 파일과 디렉터리의 권한은 프로그램이 요청한 권한에서 umask에 켜진 비트를 지운 값입니다. 보통 프로그램은 파일을 만들 때 0666을, 디렉터리를 만들 때 0777을 요청하므로, umask가 022라면 파일은 644, 디렉터리는 755가 됩니다. 이 계산은 뺄셈이 아니라 비트 연산 요청 & ~umask입니다.

      for umask in (0o022, 0o027, 0o077):
          file_mode = 0o666 & ~umask           # 파일은 보통 0666을 요청한다
          dir_mode = 0o777 & ~umask            # 디렉터리는 0777을 요청한다
          print(f"umask {umask:03o}: 파일 {file_mode:03o}, 디렉터리 {dir_mode:03o}")
      print(f"0666에서 027을 빼면: {0o666 - 0o027:03o}")
      umask 022: 파일 644, 디렉터리 755
      umask 027: 파일 640, 디렉터리 750
      umask 077: 파일 600, 디렉터리 700
      0666에서 027을 빼면: 637

      뺄셈으로 계산하면 637, 곧 나머지에게 rwx를 주는 엉뚱한 값이 나옵니다. umask는 권한을 지울 뿐 더하지는 않으므로, 프로그램이 0600을 요청했다면 umask가 022여도 결과는 0600입니다. umask가 077이면 새 파일을 소유자만 쓸 수 있으므로, 비밀 정보를 다루는 서비스는 umask를 077로 두기도 합니다. 디렉터리에 기본 ACL이 있다면 umask 대신 그 ACL이 새 파일의 권한을 정합니다(umask(2)).

      umask          # 현재 umask(보통 0022)
      umask 027      # 이 셸에서 만드는 파일은 나머지에게 아무 권한도 주지 않는다

      비밀 키처럼 처음부터 남이 읽으면 안 되는 파일은, 만든 뒤에 chmod로 바꾸지 말고 만들 때부터 권한을 지정해야 합니다. 만들고 나서 바꾸기까지의 짧은 사이에 다른 프로세스가 파일을 열어 둘 수 있고, 앞 글에서 본 것처럼 이미 열린 fd는 권한을 바꿔도 막을 수 없기 때문입니다.

      create_secret.c
      #include <fcntl.h>
      #include <stdio.h>
      #include <unistd.h>
      
      int main(void)
      {
          /* O_EXCL: 이미 있으면 실패한다. 0600: 처음부터 소유자만 읽고 쓴다. */
          int fd = open("secret.key", O_WRONLY | O_CREAT | O_EXCL, 0600);
          if (fd < 0) {
              perror("open");
              return 1;
          }
          if (write(fd, "key material\n", 13) != 13 || close(fd) != 0) {
              perror("write/close");
              return 1;
          }
          return 0;
      }

      O_EXCL은 같은 이름의 파일이 이미 있으면 실패하게 해서, 공격자가 미리 만들어 둔 파일이나 링크에 비밀을 쓰는 일을 막습니다. 요청한 0600에서 umask를 지우므로 결과는 0600이거나 그보다 좁습니다.

      특수 권한: SetUID, SetGID, 스티키 비트

      아홉 개의 권한 비트 앞에는 특수 권한 비트 세 개가 더 있으며, 8진수로는 네 번째 자리에 4, 2, 1로 적습니다. 4가 SetUID, 2가 SetGID, 1이 스티키 비트입니다. 아래 코드는 stat.filemode로 특수 비트가 켜진 모드를 ls -l과 같은 모양의 문자열로 바꿔, 각 비트가 어느 자리에 어떻게 보이는지 확인합니다. 둘째 예는 일부러 실행 비트를 빼고 SetUID만 켠 경우입니다.

      import stat
      
      examples = [
          (stat.S_IFREG, 0o4755, "SetUID 프로그램(/usr/bin/passwd)"),
          (stat.S_IFREG, 0o4644, "실행 비트 없이 SetUID만"),
          (stat.S_IFDIR, 0o2775, "SetGID 공유 디렉터리"),
          (stat.S_IFDIR, 0o1777, "스티키 비트(/tmp)"),
      ]
      for kind, mode, note in examples:
          print(f"{mode:04o}  {stat.filemode(kind | mode)}  {note}")
      4755  -rwsr-xr-x  SetUID 프로그램(/usr/bin/passwd)
      4644  -rwSr--r--  실행 비트 없이 SetUID만
      2775  drwxrwsr-x  SetGID 공유 디렉터리
      1777  drwxrwxrwt  스티키 비트(/tmp)

      특수 비트는 해당 범주의 x 자리에 s나 t로 표시됩니다. 실행 비트가 함께 켜져 있으면 소문자, 실행 비트 없이 특수 비트만 켜져 있으면 대문자 S나 T로 나타나므로, 대문자는 대개 설정이 잘못되었다는 신호입니다.

      SetUID

      SetUID(4000)가 켜진 실행 파일을 실행하면, 프로세스의 유효 사용자 ID가 실행한 사용자가 아니라 파일의 소유자로 바뀝니다. /usr/bin/passwd는 root가 소유한 SetUID 프로그램이어서, 일반 사용자가 실행해도 root의 권한으로 /etc/shadow의 자기 비밀번호를 고칠 수 있습니다. 파일 시스템이 nosuid 옵션으로 마운트되었거나 프로세스에 no_new_privs가 설정되어 있으면 이 전환은 일어나지 않습니다.

      SetUID root 프로그램은 편리하지만 위험합니다. 그런 프로그램의 버그 하나가 곧바로 root 권한 탈취로 이어지기 때문입니다. 그래서 시스템에 어떤 SetUID 파일이 있는지 정기적으로 점검하고, 필요 없는 것은 비트를 지웁니다.

      ls -l /usr/bin/passwd                       # 소유자 실행 자리에 s가 보인다
      find / -xdev -perm -4000 -type f 2>/dev/null  # 루트 파일 시스템의 SetUID 파일 목록

      -perm -4000의 앞에 붙은 -는 4000 비트가 켜져 있기만 하면 나머지 비트와 상관없이 고르라는 뜻입니다. -를 빼면 권한이 정확히 4000인 파일만 찾으므로 거의 아무것도 나오지 않습니다. -xdev는 다른 파일 시스템으로 내려가지 않게 해 /proc 같은 곳을 건너뛰고, 2>/dev/null은 읽을 권한이 없는 디렉터리에서 나오는 오류 메시지를 버립니다.

      더 깊이: SetUID 비트가 무시되는 경우

      SetUID 비트가 켜져 있어도 언제나 유효 사용자 ID가 바뀌는 것은 아닙니다. execve(2)에 따르면 Linux는 대부분의 현대 Unix 시스템처럼 스크립트에 붙은 SetUID와 SetGID 비트를 무시합니다. 그래서 #!로 시작하는 셸 스크립트에 SetUID를 켜도, 스크립트는 실행한 사용자의 권한으로 실행됩니다.

      실행 파일이어도 비트가 무시되는 조건이 셋 있습니다. 호출한 스레드에 no_new_privs 속성이 켜져 있을 때, 파일이 있는 파일 시스템이 nosuid 옵션으로 마운트되어 있을 때, 그리고 프로세스가 ptrace로 추적되고 있을 때입니다. 이 경우에는 실행 파일에 붙은 커패빌리티도 함께 무시됩니다. no_new_privs는 prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)으로 켜며, 커널 문서에 따르면 한 번 켜면 끌 수 없고 fork(), clone(), execve()를 지나 자식에게도 이어집니다. 또 seccomp 필터는 exec를 지나서도 남아 새로 실행한 프로그램의 동작을 바꿀 수 있으므로, 권한이 없는 사용자는 이 속성을 켠 뒤에만 필터를 설치할 수 있습니다.

      SetGID

      SetGID(2000)가 켜진 실행 파일은 실행할 때 유효 그룹 ID가 파일의 그룹으로 바뀝니다. 더 자주 쓰이는 것은 디렉터리에 설정하는 경우로, 그 안에 새로 만드는 파일과 디렉터리가 만든 사람의 기본 그룹 대신 디렉터리의 그룹을 물려받습니다. 팀이 함께 쓰는 디렉터리에 알맞습니다.

      sudo mkdir /srv/project
      sudo chgrp dev /srv/project
      sudo chmod 2775 /srv/project     # SetGID + rwxrwxr-x

      이렇게 해 두면 dev 그룹의 누가 파일을 만들든 그룹이 dev가 되므로, umask가 002인 구성원들은 서로의 파일을 고칠 수 있습니다.

      스티키 비트

      스티키 비트(sticky bit, 1000)는 디렉터리에 설정합니다. 보통은 디렉터리에 쓰기 권한이 있으면 그 안의 어떤 파일이든 지우거나 이름을 바꿀 수 있지만, 스티키 비트가 켜진 디렉터리에서는 파일의 소유자, 디렉터리의 소유자, 또는 특권을 가진 프로세스만 그렇게 할 수 있습니다. 누구나 쓸 수 있는 /tmp에 이 비트가 켜져 있어, 사용자들이 서로의 임시 파일을 지우지 못합니다.

      프로세스의 여러 사용자 ID

      SetUID가 동작하려면 프로세스가 사용자 ID를 여러 개 가져야 합니다.

      ID이름쓰임
      RUID실제 사용자 ID(real UID)프로세스를 시작한 사용자
      EUID유효 사용자 ID(effective UID)권한을 판정할 때 쓰는 ID
      saved set-user-ID저장된 사용자 IDexec 직후의 유효 사용자 ID를 보관해, 내려놓은 권한을 되찾을 수 있게 한다

      보통의 프로세스에서는 세 값이 모두 같습니다. UID가 1000인 사용자가 SetUID root 프로그램을 실행하면, 실제 사용자 ID는 1000으로 남고 유효 사용자 ID와 저장된 사용자 ID는 0이 됩니다. 프로그램은 실제 사용자 ID로 누가 실행했는지 알 수 있고, 유효 사용자 ID로 root의 권한을 씁니다.

      show_ids.c
      #define _GNU_SOURCE
      #include <stdio.h>
      #include <unistd.h>
      
      int main(void)
      {
          uid_t real, effective, saved;
          if (getresuid(&real, &effective, &saved) != 0) {
              perror("getresuid");
              return 1;
          }
          printf("real=%d effective=%d saved=%d\n", (int) real, (int) effective, (int) saved);
          return 0;
      }

      getresuid(2)는 Linux에서 세 ID를 한꺼번에 돌려줍니다. 그냥 실행하면 세 값이 모두 자기 UID로 같고, 이 프로그램의 소유자를 root로 바꾸고 SetUID를 켠 뒤 일반 사용자로 실행하면 실제 ID만 자기 UID이고 나머지 둘은 0으로 나옵니다.

      특권이 필요한 일을 마친 프로그램은 권한을 내려놓아야 합니다. seteuid(getuid())는 유효 사용자 ID만 내리는 것이어서, 저장된 사용자 ID가 0으로 남아 나중에 seteuid(0)으로 되찾을 수 있습니다. 권한을 영구히 버리려면 root인 상태에서 setuid(getuid())를 불러 세 값을 모두 바꾸거나, setresuid()로 세 값을 명시해야 하며, 반환값으로 성공을 꼭 확인해야 합니다.

      sudo와 root 권한 관리

      root(UID 0)는 거의 모든 권한 검사를 건너뛰므로, root로 일하면 오타 하나가 시스템 전체를 망가뜨릴 수 있습니다. sudo는 일반 사용자가 허락된 명령만 다른 사용자, 보통 root의 권한으로 실행하게 해 줍니다. 누가 어떤 명령을 실행할 수 있는지는 /etc/sudoers에 적으며, 문법 오류로 sudo 자체를 못 쓰게 되는 일을 막으려면 반드시 문법을 검사해 주는 visudo로 편집합니다.

      # /etc/sudoers의 예 (visudo로 편집)
      alice   ALL=(ALL:ALL) ALL
      deploy  ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx

      첫 줄은 alice가 모든 호스트에서 어떤 사용자와 그룹으로든 모든 명령을 실행할 수 있다는 뜻이고, 둘째 줄은 deploy가 비밀번호 없이 nginx를 다시 시작하는 명령 하나만 root로 실행할 수 있다는 뜻입니다. 명령에 인자까지 적었으므로 deploy가 systemctl stop nginx처럼 인자가 다른 명령을 실행하려 하면 거절됩니다. 아래 명령은 이렇게 받은 권한을 확인하고 쓰는 방법입니다. sudo -l로 자기에게 허락된 명령을 보고, -u로 root가 아닌 다른 사용자의 권한으로 실행할 수도 있으며, sudo를 쓴 기록은 시스템 로그에 남습니다.

      sudo -l                        # 내가 sudo로 실행할 수 있는 명령 목록
      sudo -u postgres psql          # postgres 사용자로 실행
      journalctl _COMM=sudo          # sudo 사용 기록(배포판에 따라 /var/log/auth.log)

      명령 하나만 허락했다고 권한이 작아지는 것은 아닙니다. 쉘을 띄울 수 있는 편집기나, 임의의 파일을 다룰 수 있는 프로그램을 허락하면 사실상 root를 준 것과 같습니다. Docker 명령도 마찬가지로, Docker 문서가 경고하듯 Docker 데몬에 접근할 수 있으면 호스트의 root와 같은 권한을 얻을 수 있습니다. 서비스를 다시 시작하는 권한을 줄 때도, 그 서비스의 설정 파일이나 실행 파일을 그 사용자가 고칠 수 없는지 함께 확인해야 합니다.

      서버를 운영할 때는 SSH로 root가 바로 로그인하지 못하게 하고(sshd_config의 PermitRootLogin no), 비밀번호 대신 키로 인증하며, sudo로는 필요한 명령만 허락하고, SetUID 파일을 정기적으로 점검하는 것이 기본입니다.

      삭제는 디렉터리의 권한이다

      파일을 지우는 것은 파일의 내용을 바꾸는 일이 아니라, 부모 디렉터리에서 이름을 빼는 일입니다. 그래서 파일에 쓰기 권한이 없어도 부모 디렉터리에 쓰기와 실행 권한이 있으면 그 파일을 지울 수 있고, 반대로 파일을 마음대로 고칠 수 있어도 부모 디렉터리에 쓰기 권한이 없으면 지울 수 없습니다. 앞에서 본 스티키 비트는 이 규칙에 조건을 하나 더한 것입니다.

      rwx와 별개로, 파일 시스템이 지원하는 추가 속성도 있습니다. chattr +i로 불변(immutable) 속성을 켜면 root라도 그 파일을 고치거나 지우거나 이름을 바꿀 수 없고, 속성을 켜고 끄는 데는 CAP_LINUX_IMMUTABLE이 필요합니다. 속성은 lsattr로 확인합니다. 이 규칙들은 다음 실습에서 전용 임시 디렉터리를 만들어 직접 확인합니다.

      정리

      • Linux의 권한은 소유자, 그룹, 나머지의 세 범주와 rwx 세 비트로 이루어지며, 디렉터리에서 r은 목록 읽기, w는 항목 만들기와 지우기, x는 찾아 들어가기를 뜻합니다.
      • 권한은 소유자, 그룹, 나머지 가운데 범주 하나를 고른 뒤 그 범주의 비트로만 판정합니다. root는 검사를 건너뛰지만 실행에는 실행 비트가 하나라도 있어야 합니다.
      • 새 파일의 권한은 요청한 권한에서 umask의 비트를 지운 값이며, 비밀 파일은 만들 때부터 좁은 권한으로 만들어야 합니다.
      • SetUID와 SetGID는 실행할 때 유효 ID를 파일의 소유자나 그룹으로 바꾸고, 디렉터리의 SetGID는 그룹을 물려주며, 스티키 비트는 남의 파일을 지우지 못하게 합니다.
      • 프로세스에는 실제, 유효, 저장된 사용자 ID가 있으며, 권한을 영구히 버리려면 세 값을 모두 바꿔야 합니다. sudo로 명령을 허락할 때는 그 명령으로 무엇까지 할 수 있는지 따져야 합니다.

      연습 문제

      1. 범주와 mask로 판정하기

      ACL과 특권은 없다고 할 때, 권한이 0464인 파일에 소유자, 소유자가 아닌 그룹 구성원, 그 밖의 사용자가 각각 쓰려고 하면 결과는 어떻게 되나요? umask가 022일 때 0600을 요청해 만든 파일에 그룹 읽기 권한이 생기나요? 또 POSIX ACL에서 user:deploy:rwx 항목이 있고 mask가 r-x라면, deploy의 쓰기는 허락되나요?

      해설 보기

      0464에서 소유자 칸은 r--이므로 소유자의 쓰기는 거부되고, 그룹 칸은 rw-이므로 그룹 구성원의 쓰기는 허락되며, 나머지 칸은 r--이므로 그 밖의 사용자의 쓰기는 거부됩니다. 0600 & ~022는 0600이므로 그룹 읽기는 생기지 않습니다. umask는 비트를 지울 뿐 더하지 않습니다. ACL에서 이름이 붙은 사용자 항목은 mask와 겹치는 권한만 유효하므로, rwx와 r-x가 겹치는 r-x만 남아 deploy의 쓰기는 거부됩니다. mask는 이름이 붙은 사용자와 그룹 항목, 그리고 그룹 소유자 항목에 적용되며, 소유자와 나머지 항목에는 적용되지 않습니다.

      2. 디렉터리의 r과 x

      alice가 소유한 디렉터리 box 안에 누구나 읽을 수 있는 파일 note.txt가 있습니다. bob이 다음 두 경우에 ls box, ls -l box, cat box/note.txt를 실행하면 각각 어떻게 될지 설명하세요. 첫째, box의 권한이 drwxr--r--인 경우. 둘째, box의 권한이 drwx--x--x인 경우.

      해설 보기

      첫째 경우 bob에게는 box의 r만 있고 x가 없습니다. ls box는 디렉터리의 이름 목록을 읽을 수 있으므로 note.txt라는 이름은 보여 주지만, 항목의 inode로 찾아갈 수 없어 ls -l box는 크기나 권한 같은 정보를 얻지 못하고 오류를 함께 출력하며, cat box/note.txt는 거부됩니다. 둘째 경우 bob에게는 x만 있습니다. 목록을 읽을 수 없으므로 ls box와 ls -l box는 거부되지만, 이름을 알고 있는 box/note.txt로는 찾아갈 수 있어 cat은 성공합니다. 디렉터리의 r은 이름을 나열하는 권한이고, x는 이름으로 찾아 들어가는 권한이라는 점에서 차이가 납니다.

      3. 편집기 하나만 허락한 sudo

      관리자가 nginx 설정만 고치게 하려고 deploy 사용자에게 deploy ALL=(root) /usr/bin/vim /etc/nginx/nginx.conf를 허락했습니다. 이 설정은 안전한가요? 더 나은 방법은 무엇인가요?

      해설 보기

      안전하지 않습니다. vim은 root로 실행되므로, 편집기 안에서 :!sh 같은 명령으로 셸을 띄우면 root 셸을 얻고, 다른 파일을 열어 저장할 수도 있습니다. 허락한 것은 파일 하나를 고치는 일이 아니라 root로 vim을 실행하는 일이기 때문입니다. 더 나은 방법은 sudoers에 sudoedit /etc/nginx/nginx.conf를 허락하는 것입니다. sudoedit은 파일의 임시 사본을 만들어 편집기를 deploy 사용자의 권한으로 실행하고, 편집이 끝나면 sudo가 root 권한으로 원래 파일에 복사하므로 편집기 안에서 띄운 셸은 root가 아닙니다. 아니면 설정 파일을 dev 그룹이 쓸 수 있게 하고, 설정 검사와 nginx 재시작 명령만 sudo로 허락하는 방법도 있습니다.

      다음 글 보안 위협과 방어에서는 버퍼 오버플로우 같은 공격이 보호 장치를 어떻게 우회하는지, 그리고 운영체제와 컴파일러가 이를 막으려고 쓰는 방어 기법을 살펴봅니다.