Devin.KR

경로와 파일 권한 읽기

80분 안팎

학습 목표

소유자·그룹·권한 비트를 해석합니다.

개념

파일 이름에서 접근 근거로

자료 보관 앱이 실행된다는 사실만으로 자료 파일의 접근 범위를 알 수는 없습니다. 같은 컨테이너의 다른 계정이 파일을 읽을 수 있으면 HTTP에서 접근을 막아도 다른 경로가 남습니다. 이 레슨에서는 자료·설정·로그의 경로, 종류, 소유 UID, 그룹 GID, 권한을 한 목록에 적습니다. 목록을 만드는 목적은 넓게 열린 파일을 찾고 다음 레슨의 접근 정책을 실제 값에 연결하는 것입니다.

이번 모듈의 Linux 실습은 본인 Docker 환경에서 수행합니다. 각 ZIP 루트에서 bash check.sh를 실행하며 Docker와 호스트 Python 3가 필요합니다. 첫 빌드는 지정 Python 이미지와 Debian의 Node.js·procps·iproute2 패키지를 내려받습니다. 이후 검사 컨테이너는 네트워크를 none으로 제한하고 호스트 포트·디렉터리를 연결하지 않습니다. 실제 계정이나 운영 자료를 넣지 않습니다. Docker를 실행할 수 없는 환경에서는 외부 검증 대기로 남깁니다.

앞 모듈 앱은 자료를 Map에 저장합니다. 이번 파일은 합성 자료 fixture와 접근 시험 파일이며 앱에 영속 저장이 생긴 것은 아닙니다. /srv/vault/data, /srv/vault/config, /srv/vault/logs를 관찰하되 개인 홈이나 저장소 config를 조사하지 않습니다. 미션에는 앞 모듈의 앱·테스트·보고서가 함께 들어 있습니다. 레슨 ZIP을 서로 다른 폴더에 풀면 앞의 관찰 파일과 뒤의 변경 결과가 섞이지 않습니다.

경로를 같은 대상으로 맞추기

절대 경로는 루트에서 시작하고 상대 경로는 현재 작업 위치에서 해석합니다. data/demo.txt라는 문자열이 같아도 실행 폴더가 다르면 다른 파일일 수 있습니다. 검사 코드에서는 /srv/vault/data/demo.txt처럼 컨테이너 내부 절대 경로를 사용합니다. /work는 다운로드한 코드를 놓는 위치입니다. 호스트의 같은 이름 폴더와 컨테이너의 경로를 같은 대상으로 설명하지 않습니다.

ls -ld는 디렉터리 자체를, ls -l 디렉터리는 보통 그 안의 항목을 보여 줍니다. 자료 디렉터리의 접근을 확인하려면 자체 정보가 필요합니다. 파일 이름 뒤에 붙인 확장자는 권한과 무관합니다. .conf라는 이유로 비밀 파일이라고 단정하거나 .txt라는 이유로 공개하지 않습니다. 어떤 자료가 들어 있고 어떤 계정에게 필요한지 확인한 뒤 판단합니다.

GNU stat의 -c는 출력 필드를 지정합니다. stat -c "%a %u:%g %n" /srv/vault/config/app.conf는 8진 권한, 숫자 UID:GID, 경로를 묶습니다. 이 명령은 Linux 컨테이너에서만 실행합니다. 맥의 stat 옵션은 다르므로 맥 터미널의 옵션 오류를 권한 문제로 읽지 않습니다. 따라하기의 Python os.stat 예시는 현재 환경에서 권한 비트만 측정하여 출력의 재현성을 확인합니다.

한 줄의 아홉 비트 읽기

일반 파일의 -rw-r-----는 처음 문자가 파일 종류이고 그 뒤 세 묶음이 소유자·그룹·기타입니다. r은 읽기, w는 쓰기, x는 실행입니다. 없는 권한은 대시로 표시됩니다. 읽기 4, 쓰기 2, 실행 1을 더하면 rw-는 6, r--는 4, ---는 0입니다. 따라서 640은 소유자 읽기·쓰기, 그룹 읽기, 기타 없음입니다. 앞의 파일 종류 문자를 숫자 권한에 포함하지 않습니다.

UID와 GID는 계정을 구분하는 숫자이고 app 같은 이름은 그 숫자를 읽기 쉽게 표시한 것입니다. 이미지 안에서는 app을 10001:10001, auditor를 10002:10002로 고정합니다. 이름이 같다고 호스트와 컨테이너의 신원을 같다고 가정하지 않습니다. inventory에는 숫자를 기록하고 보고서에서 역할 이름을 설명하면 계정 이름 해석이 달라도 비교할 수 있습니다.

설정 파일은 root:app 640으로 둡니다. app은 소유자가 아니지만 파일 그룹이 일치하여 읽기를 얻습니다. 설정을 바꾸는 권한은 app에 주지 않습니다. 자료 파일은 app:app 600, 자료와 로그 디렉터리는 app:app 700입니다. 이 값은 실습 정책의 선택이며 모든 서비스에 같은 숫자를 적용하라는 지침은 아닙니다. 실제 서비스는 공유 역할과 쓰기 요구를 먼저 확인합니다.

디렉터리 권한을 별도로 해석하기

디렉터리 r은 이름 목록 읽기, x는 경로의 다음 요소를 찾는 탐색, w는 항목 생성·삭제와 관련됩니다. 파일 내용을 읽으려면 상위 디렉터리들을 통과할 수 있어야 합니다. 그래서 파일이 644여도 부모가 700이면 다른 계정의 읽기가 막힐 수 있습니다. 반대로 파일 비트만 닫아 놓고 자료 디렉터리를 아무나 쓸 수 있게 하면 삭제·이름 변경 위험을 따로 검토해야 합니다.

이 레슨의 목록에는 디렉터리와 일반 파일을 함께 넣습니다. type을 생략하면 700의 의미를 잘못 설명하기 쉽습니다. ACL, 특수 비트, 심볼릭 링크, 보안 모듈 정책까지 여기서 전부 판정하지는 않습니다. 기본 비트 관찰을 전체 권한 검사로 확대하지 않고, 후속 검토가 필요한 파일 종류는 목록의 한계에 적습니다. 이름과 권한만으로 실제 접근 성공을 단정하지 않습니다.

관찰 목록 만들기와 실패 해석

collect.py는 네 경로를 os.stat으로 읽고 inventory.json을 생성합니다. starter는 mode를 777로 고정하는 잘못된 관찰 코드입니다. 실제 파일 권한을 바꾸는 문제가 아니라 측정값을 제대로 기록하는 문제입니다. stat.S_IMODE로 메타데이터에서 권한 비트를 꺼내고 format의 03o로 세 자리 8진 문자열을 만듭니다. UID와 GID는 측정값 그대로 정수로 남깁니다.

verify-inventory.py는 경로 네 개의 중복과 개수를 검사한 뒤 각 행을 새 os.stat 값과 비교합니다. inventory mismatch가 나오면 오류에 적힌 경로의 mode·uid·gid·type을 하나씩 대조합니다. 파일을 777로 바꿔 잘못된 목록에 맞추는 것은 관찰을 왜곡합니다. 테스트 기대를 지우거나 경로를 줄이는 대신 측정 코드를 고칩니다. 수정 후 다시 생성된 목록을 확인합니다.

No such file or directory는 경로가 없다는 단서이고 Permission denied는 탐색 또는 접근이 막혔다는 단서입니다. 먼저 ZIP 루트와 컨테이너 경로를 확인하고, 오류가 나온 파일뿐 아니라 부모도 봅니다. 관찰 실패를 비밀 보호 성공으로 기록하지 않습니다. Docker 이미지 빌드 실패는 패키지·연결 문제일 수 있으므로 권한 테스트에 도달했는지 구분합니다.

목록을 다음 판단에 넘기기

보고서에는 경로마다 자료 성격과 필요한 동작을 덧붙입니다. 설정은 읽기, 자료와 로그는 쓰기라는 설명이 있으면 단순 숫자 표가 접근 요구의 근거가 됩니다. 이번에는 서비스 계정의 작업과 다른 계정의 거절을 아직 실제로 시험하지 않았다고 구분합니다. 다음 최소 권한 레슨에서 이 목록을 허용·거절 시험에 연결합니다.

출력과 파일은 컨테이너가 정상 종료한 뒤 docker cp로 가져옵니다. 실패한 실행 전에 만들어 둔 inventory.json을 이번 결과로 제출하지 않습니다. 명령 종료 코드, 새 파일의 생성 여부, 검사 통과를 함께 봅니다. 완료 기준은 보기 좋은 표가 아니라 다른 담당자가 같은 경로를 다시 측정해 네 행의 값을 비교할 수 있는 상태입니다. stat의 전체 필드와 시각 해석은 더 읽기로 넘깁니다.

따라하기

권한 비트를 실측하기

Python 임시 파일을 만들고 직접 설정한 비트를 다시 읽습니다. 파일의 UID 값을 고정 출력으로 만들지는 않습니다.

import tempfile, os, stat
from pathlib import Path
with tempfile.TemporaryDirectory() as directory:
    p = Path(directory) / 'demo.txt'
    p.write_text('synthetic-only\n')
    os.chmod(p, 0o600)
    print('mode=' + format(stat.S_IMODE(p.stat().st_mode), '03o'))
    print('type=' + ('file' if p.is_file() else 'other'))

실행 결과

mode=600
type=file

세 자릿수 해석하기

640을 클래스별로 해석합니다. 이 단계는 실제 접근 실행이 아니라 숫자 표기 계산입니다.

mode = '640'
for role, digit in zip(['owner', 'group', 'other'], mode):
    value = int(digit)
    print(role, 'read=' + str(bool(value & 4)), 'write=' + str(bool(value & 2)))

실행 결과

owner read=True write=True
group read=True write=False
other read=False write=False

Linux 목록 검사하기

starter ZIP 루트에서 실행합니다. inventory mismatch의 경로를 읽고 collect.py의 고정 mode를 실측값으로 바꿉니다. Docker 미실행이므로 여기에는 결과를 넣지 않았습니다.

bash check.sh

새 관찰 파일 확인하기

검사를 다시 실행하여 마지막 PASS: files와 종료 코드 0을 확인합니다. inventory.json 네 경로의 mode·uid·gid·type을 보고서에 연결합니다. 오래된 파일을 이번 성공 근거로 쓰지 않습니다.

python3 -m json.tool inventory.json

확인 문제

실습

collect.py의 mode TODO를 실제 stat.S_IMODE 기반 8진 문자열로 고칩니다. 경로 네 개와 UID·GID·종류는 유지합니다. bash check.sh는 이미지의 네 행을 실제 os.stat과 비교하고 성공 시 inventory.json을 복사합니다. 실행 후 각 경로의 자료 성격과 필요한 동작을 보고서에 한 줄씩 적습니다.

시작 코드·테스트 내려받기

실행 명령

bash check.sh

기대 결과

inventory matches stat, 경계 검사와 목록 복사 성공, 마지막 PASS: files

모범 답안모범 답안 내려받기

더 읽기

면접 질문

  • Linux 파일 소유자와 권한 비트만으로 실제 읽기 성공을 확정할 수 없는 이유를 설명합니다.