본문 바로가기

Linux 권한 실습과 파일 속성

전용 임시 디렉터리에서 읽기 전용 파일은 지워지고 쓰기 가능한 파일은 지워지지 않는 상황을 만들어, 내용 쓰기와 이름 삭제가 서로 다른 객체의 권한을 검사한다는 것을 확인합니다. umask의 결과와 chattr의 불변 속성도 직접 관찰합니다.

12장 2편에서 권한 비트의 규칙을 보았지만, 숫자를 외우는 것만으로는 "읽기 전용 파일을 왜 지울 수 있나?" 같은 질문에 답하기 어렵습니다. 파일을 지우는 것은 파일의 내용을 고치는 일이 아니라 부모 디렉터리에서 이름을 빼는 일이므로, 검사하는 권한도 파일이 아니라 부모 디렉터리의 것입니다. 이 글에서는 먼저 규칙을 작은 모델로 예측하고, 전용 임시 디렉터리에서 실제로 확인합니다.

실습 환경 준비

실습은 Linux 가상 머신, WSL, 또는 전용 컨테이너의 bash에서 합니다. root는 파일 권한 검사를 대부분 건너뛰므로, 기본 실습은 반드시 UID가 0이 아닌 일반 사용자로 실행해야 합니다. 먼저 환경을 확인합니다.

id -u                            # 0이면 root이므로 일반 사용자로 다시 접속한다
uname -r                         # 커널 버전
command -v bash stat mktemp      # 필요한 도구가 있는지
stat -f -c '%T' /tmp             # /tmp의 파일 시스템 종류

실습 스크립트는 mktemp -d로 /tmp 아래에 자기만 쓰는 새 디렉터리를 만들고, 그 안에서만 파일을 만들고 지웁니다. 호스트와 공유하는 폴더나 기존 자료가 있는 경로에서는 실행하지 않습니다. 기본 ACL, SELinux 같은 MAC 정책, 읽기 전용 마운트가 있으면 결과가 달라질 수 있으므로, 예상과 다른 결과가 나오면 그런 설정부터 확인합니다.

실행하기 전에 예측하기

내용을 쓸 때는 파일 자체의 w를, 이름을 지울 때는 부모 디렉터리의 w와 x를 검사합니다. 다음 모델은 자기 소유의 파일과 디렉터리라고 보고, 소유자 범주의 비트만으로 두 동작을 판정합니다.

def owner_can(mode, want):                 # 소유자 범주의 비트만 본다(자기 파일, 자기 디렉터리)
    bits = (mode >> 6) & 7
    return all(bits & {"r": 4, "w": 2, "x": 1}[c] for c in want)

cases = [
    ("파일 400, 부모 700", 0o400, 0o700),
    ("파일 600, 부모 500", 0o600, 0o500),
    ("파일 000, 부모 700", 0o000, 0o700),
]
for name, file_mode, dir_mode in cases:
    write = owner_can(file_mode, "w")       # 내용을 고치려면 파일의 w
    delete = owner_can(dir_mode, "wx")      # 이름을 지우려면 부모 디렉터리의 w와 x
    print(f"{name}: 내용 쓰기 {'허용' if write else '거부'}, 이름 삭제 {'허용' if delete else '거부'}")
파일 400, 부모 700: 내용 쓰기 거부, 이름 삭제 허용
파일 600, 부모 500: 내용 쓰기 허용, 이름 삭제 거부
파일 000, 부모 700: 내용 쓰기 거부, 이름 삭제 허용

내용 변경과 이름 삭제는 검사하는 객체가 다르다

추가 ACL·MAC·sticky/immutable·특권 예외가 없는 기본 DAC 모델을 전제로 비교합니다.

내용 변경과 이름 삭제는 검사하는 객체가 다르다추가 ACL·MAC·sticky/immutable·특권 예외가 없는 기본 DAC 모델을 전제로 비교합니다. 파일 내용 쓰기:파일 owner w 검사;디렉토리 항목 삭제:부모 owner wx 검사;같은 파일 400:쓰기 거절·삭제 가능。마지막 결과는 부모가 자신 소유의 700인 전제입니다. 파일 400을 바꿔도 부모 500의 쓰기 부재는 해결되지 않습니다.파일 내용 쓰기파일 owner w 검사디렉토리 항목 삭제부모 owner wx 검사같은 파일 400쓰기 거절·삭제 가능
내용 변경과 이름 삭제는 검사하는 객체가 다르다추가 ACL·MAC·sticky/immutable·특권 예외가 없는 기본 DAC 모델을 전제로 비교합니다. 파일 내용 쓰기:파일 owner w 검사;디렉토리 항목 삭제:부모 owner wx 검사;같은 파일 400:쓰기 거절·삭제 가능。마지막 결과는 부모가 자신 소유의 700인 전제입니다. 파일 400을 바꿔도 부모 500의 쓰기 부재는 해결되지 않습니다.파일 내용 쓰기파일 owner w 검사디렉토리 항목 삭제부모 owner wx 검사같은 파일 400쓰기 거절·삭제 가능
마지막 결과는 부모가 자신 소유의 700인 전제입니다. 파일 400을 바꿔도 부모 500의 쓰기 부재는 해결되지 않습니다.

모델은 읽기 전용 파일이라도 부모 디렉터리에 쓸 수 있으면 지울 수 있고, 마음대로 고칠 수 있는 파일이라도 부모 디렉터리에 쓸 수 없으면 지울 수 없다고 예측합니다. unlink(2)에 따르면 이름을 지우려면 그 이름이 든 디렉터리에 쓰기 권한이 있어야 하고, 경로의 디렉터리마다 검색(x) 권한도 있어야 합니다. 이제 실제 시스템에서 확인합니다.

파일 쓰기와 이름 삭제를 나누어 보기

다음 스크립트를 permissions-lab.sh라는 새 파일로 저장한 뒤 bash permissions-lab.sh로 실행합니다. 스크립트는 예상한 거부가 일어나면 PASS로 시작하는 줄을 출력하고, 예상과 다른 결과가 나오면 unexpected를 출력하고 1로 끝납니다. root로 실행하면 처음에 멈추고 2로 끝납니다.

permissions-lab.sh
#!/usr/bin/env bash
set -eu
[ "$(id -u)" -ne 0 ] || { echo 'Run as an unprivileged user'; exit 2; }
lab_dir=$(mktemp -d /tmp/cs-os-permissions.XXXXXX)
cd "$lab_dir"
umask 027
printf 'original\n' > report.txt
mkdir work
printf 'created modes: '
stat -c '%a %n' report.txt work      # 기본 ACL이 없다면 파일 640, 디렉터리 750
chmod 400 report.txt
if (printf 'changed\n' > report.txt) 2>/dev/null; then
  echo 'unexpected: file write succeeded'; exit 1
else
  echo 'PASS file write denied'
fi
rm -f -- report.txt
[ ! -e report.txt ] && echo 'PASS name deletion allowed by parent directory'
printf 'kept\n' > work/keep.txt
chmod 500 work
if rm -- work/keep.txt 2>/dev/null; then
  echo 'unexpected: deletion succeeded'; exit 1
else
  echo 'PASS parent directory write denied'
fi
chmod 700 work
rm -- work/keep.txt
rmdir -- work
cd /tmp
rmdir -- "$lab_dir"
echo 'PASS cleaned only the lab directory'

스크립트는 세 가지를 확인합니다. 첫째, umask 027 아래에서 만든 파일과 디렉터리의 권한을 출력합니다. 기본 ACL이 없다면 12장 2편의 계산대로 파일은 0666 & ~027인 640, 디렉터리는 0777 & ~027인 750이 됩니다. 둘째, report.txt를 400으로 바꾸면 내용을 쓸 수 없지만, 부모인 실습 디렉터리에 쓰기와 실행 권한이 있으므로 이름은 지울 수 있습니다. 셋째, work를 500으로 바꾸면 그 안의 keep.txt는 내용 권한이 그대로인데도 지울 수 없습니다.

rm -f는 쓰기 금지 파일을 지울지 묻는 확인 질문을 건너뛸 뿐, 권한 검사를 건너뛰지는 않습니다. 대화형 rm에서 사용자가 질문에 아니라고 답한 것과, 커널이 삭제를 거부한 것은 다른 일이므로 종료 상태로 결과를 확인합니다. 중간에 오류로 멈추면 ls -d /tmp/cs-os-permissions.*로 남은 실습 디렉터리를 찾아, 그 안의 work에 소유자 쓰기 권한을 돌려준 뒤 그 디렉터리 안의 파일만 정리합니다.

chattr로 바꾸는 파일 속성

chmod가 바꾸는 권한 비트와 별개로, 파일 시스템은 파일마다 추가 속성을 둘 수 있습니다. chattr로 바꾸고 lsattr로 확인하며, ls -l에는 나타나지 않습니다. chattr(1)에 따르면 불변(immutable) 속성 i가 켜진 파일은 내용을 고칠 수도, 지우거나 이름을 바꿀 수도 없고, 덧붙이기 전용(append-only) 속성 a가 켜진 파일은 쓰기용으로 열 때 덧붙이기 모드로만 열 수 있습니다. 두 속성은 root나 CAP_LINUX_IMMUTABLE을 가진 프로세스만 켜고 끌 수 있습니다.

다음은 sudo를 쓸 수 있는 일회용 환경에서만 하는 선택 실습입니다. 파일 시스템이 이 속성을 지원하지 않거나, 컨테이너처럼 커패빌리티가 제한된 환경이라면 chattr +i가 실패하고 SKIP으로 끝납니다.

immutable-lab.sh
(
  set -eu
  dir=$(mktemp -d /tmp/cs-os-attr.XXXXXX)
  file="$dir/demo.txt"
  printf 'lab file\n' > "$file"
  if sudo chattr +i "$file"; then
    lsattr "$file"                                    # 속성 목록에 i가 보인다
    if rm -f -- "$file" 2>/dev/null; then echo 'unexpected: user deletion succeeded'; fi
    if sudo rm -f -- "$file" 2>/dev/null; then echo 'unexpected: root deletion succeeded'; fi
    [ -e "$file" ] && echo 'PASS immutable file survived both deletions'
    sudo chattr -i "$file"                            # 속성을 풀어야 지울 수 있다
  else
    echo 'SKIP: chattr +i is not allowed here (filesystem, capability, or mount)'
  fi
  rm -- "$file"
  rmdir -- "$dir"
)

불변 속성이 켜진 파일은 디렉터리의 주인인 일반 사용자도, root도 지우지 못합니다. 앞 실습의 규칙대로라면 부모 디렉터리에 쓰기 권한이 있으니 지울 수 있어야 하지만, 권한 검사를 통과하더라도 커널이 불변 속성을 따로 확인해 거부하기 때문입니다. root라도 먼저 속성을 풀어야 하므로 중요한 설정 파일을 실수로 지우거나 덮어쓰는 일을 막는 데 쓸 수 있습니다. 다만 root는 속성을 풀 수 있으므로, root를 얻은 침입자까지 막아 주지는 못합니다. 덧붙이기 전용 속성은 로그 파일처럼 기록을 더할 수는 있어도 지우거나 고칠 수는 없어야 하는 파일에 씁니다. 실습이 중간에 멈췄다면 sudo chattr -i로 속성을 먼저 푼 뒤에 그 파일을 지웁니다.

정리

  • 파일의 내용을 고칠 때는 파일의 w를, 이름을 만들거나 지울 때는 부모 디렉터리의 w와 x를 검사합니다. 그래서 읽기 전용 파일도 지울 수 있고, 쓸 수 있는 파일도 못 지울 수 있습니다.
  • rm -f는 확인 질문을 건너뛸 뿐 권한 검사를 건너뛰지 않습니다.
  • umask 027 아래에서 만든 파일은 640, 디렉터리는 750이 되며, 기본 ACL이 있으면 결과가 달라집니다.
  • chattr의 불변 속성은 권한 비트와 별개로 검사되어 root도 그 파일을 지우거나 고치지 못하게 하며, 켜고 끄는 데는 CAP_LINUX_IMMUTABLE이 필요합니다.
  • 실습은 일반 사용자로, 전용 임시 디렉터리 안에서만 하고, 예상과 다른 결과는 ACL, MAC, 마운트 옵션 같은 환경을 먼저 확인합니다.

연습 문제

1. 권한이 000인 내 파일

자기 소유의 파일 권한이 000이고, 그 파일이 든 자기 소유 디렉터리의 권한이 700입니다. ACL이나 특권 같은 예외가 없다면, 이 파일을 읽을 수 있나요? 지울 수 있나요?

해설 보기

읽을 수 없습니다. 소유자 범주의 r이 꺼져 있기 때문이며, 소유자라고 해서 다른 범주의 권한을 빌려 오지 않습니다. 하지만 지울 수는 있습니다. 이름을 지우는 데 필요한 것은 부모 디렉터리의 w와 x인데, 디렉터리가 700이므로 둘 다 있습니다. 소유자는 언제든 chmod로 파일의 권한을 되돌려 놓을 수도 있습니다.

2. 파일 권한을 넓혀도 지워지지 않는 이유

부모 디렉터리의 권한이 500일 때 그 안의 파일을 지우지 못해서, 파일의 권한을 600으로 바꾸었습니다. 이제 지울 수 있나요? 지우려면 무엇을 바꿔야 하나요?

해설 보기

여전히 지울 수 없습니다. 삭제는 부모 디렉터리에서 이름을 빼는 일이라 디렉터리의 쓰기 권한을 검사하는데, 500에는 소유자의 w가 없습니다. 파일의 권한은 내용을 읽고 쓰는 데만 쓰이므로 600으로 바꾸어도 삭제와는 상관이 없습니다. 디렉터리를 700처럼 소유자의 w와 x가 있는 권한으로 바꾸면 지울 수 있습니다.

3. 컨테이너에서 실패한 chattr

컨테이너 안에서 root로 chattr +i demo.txt를 실행했는데 "Operation not permitted"가 났습니다. 명령을 잘못 쓴 것이라고 단정할 수 있나요? 무엇을 확인해야 하나요?

해설 보기

단정할 수 없습니다. 속성을 켜려면 CAP_LINUX_IMMUTABLE이 필요한데, 컨테이너 런타임은 보통 이 커패빌리티를 빼고 컨테이너를 실행하므로, 컨테이너 안의 UID 0이 이 일을 할 수 있다는 보장이 없습니다. 또 컨테이너의 파일이 overlay 파일 시스템 위에 있다면 그 파일 시스템이 속성을 지원하는지도 확인해야 합니다. grep CapEff /proc/self/status와 capsh --decode로 커패빌리티를, stat -f -c '%T' .과 findmnt -T .으로 파일 시스템과 마운트를 확인합니다. root라는 사실만으로 모든 커널 검사를 통과하는 것은 아닙니다.

다음 장의 가상화의 개념에서는 하나의 컴퓨터 위에서 여러 운영체제를 동시에 실행하는 가상화가 어떻게 동작하는지 살펴봅니다.