필요한 접근만 허용하기
80분 안팎
학습 목표
서비스 계정과 자료 디렉터리에 최소 권한을 적용합니다.
개념
서비스 동작을 접근 요구로 바꾸기
최소 권한은 모든 접근을 막는 것이 아니라 필요한 일을 할 수 있는 가장 작은 범위를 정하는 방식입니다. 자료 보관 앱은 설정을 읽어야 하고 자료와 로그를 쓸 수 있어야 합니다. 그러나 서비스가 설정을 임의로 바꾸거나 다른 계정이 자료를 읽어야 할 이유는 이번 프로젝트에 없습니다. 정상 동작과 거절 동작을 함께 적으면 권한 변경이 기능을 망가뜨렸는지도 확인할 수 있습니다.
이번 실습은 파일 소유자와 서비스 실행 주체를 분리합니다. 서비스 app은 UID 10001이고 별도 관찰 역할 auditor는 UID 10002입니다. root는 이미지 구성 때 계정과 초기 권한을 설정합니다. 실행되는 앱과 테스트는 app 또는 auditor로만 수행합니다. 루트로 실행해 파일 접근 성공을 얻은 뒤 서비스 권한이 맞다고 말하지 않습니다.
역할과 자원별 정책
설정 디렉터리는 root:app 750, 설정 파일은 root:app 640입니다. app은 그룹을 통해 디렉터리를 탐색하고 설정을 읽을 수 있지만 설정 파일을 쓸 수 없습니다. 설정 디렉터리를 app이 쓰지 못하게 하는 것은 파일 이름 변경이나 대체 경로도 줄이기 위한 선택입니다. 파일 쓰기 비트만 제거하고 부모 쓰기를 열어 두면 파일 내용 변경 거절만으로 충분하다고 보기 어렵습니다.
자료 디렉터리와 로그 디렉터리는 app:app 700, 합성 자료 파일은 app:app 600입니다. app은 자기 자료와 로그를 만들 수 있고 auditor는 부모 탐색부터 거절됩니다. 로그에 민감 내용이 남으면 700만으로 서비스 내부 유출을 해결할 수는 없습니다. 이번 레슨은 운영체제 계정 간 접근을 다루며 로그 내용 최소화는 이후 비밀 관리에서 별도로 다룹니다.
앱 코드와 검사 코드는 root 소유 파일로 이미지에 복사합니다. /work 디렉터리 자체만 app이 증거를 생성할 수 있도록 소유권을 줍니다. 이 선택은 실습 증거 생성 편의를 위한 것이며 코드 디렉터리를 운영 서비스가 쓸 수 있는 구조를 권장하는 뜻은 아닙니다. 실제 배포에서는 코드와 쓰기 가능한 출력 위치를 분리하는 개선이 필요하다고 보고서에 남깁니다.
두 계정에서 실제로 시험하기
access.py는 app UID인지 먼저 확인하고 설정을 읽습니다. 이어서 설정 파일을 append로 열어 PermissionError가 나는지 검사합니다. 자료 디렉터리에는 probe.txt를 쓰고 로그에는 probe.log를 씁니다. 이들은 합성 시험 파일입니다. 원래 앱의 자료가 디스크에 자동 저장되도록 바뀐 것은 아니므로 파일 쓰기 성공과 API 자료 영속화를 같은 결과로 설명하지 않습니다.
denied.py는 별도 auditor 컨테이너에서 실행합니다. 같은 이미지의 초기 자료 읽기, 설정 읽기, 로그 디렉터리 목록 읽기가 모두 PermissionError로 거절되는지 봅니다. app 검사에서 만든 probe 파일은 다른 컨테이너에 공유되지 않으므로 demo.txt를 대상으로 합니다. 동일 초기 정책의 다른 계정 시험이라고 설명하면 두 실행의 파일 상태를 혼동하지 않습니다.
거절 시험은 예외가 생기는 위치를 정확히 제한합니다. 파일이 없거나 코드 문법이 틀려 생긴 실패는 권한 거절 성공이 아닙니다. PermissionError만 기대한 거절로 받고 다른 예외는 검사 실패로 드러나게 둡니다. 성공 시험에서도 실제 read_text와 write_text를 수행합니다. 비트 표만 계산하는 것보다 현재 커널이 적용한 결과를 확인하는 데 도움이 됩니다.
starter의 넓은 권한 줄이기
starter의 자료·로그 디렉터리는 755이고 자료 파일은 644입니다. auditor가 합성 자료를 읽을 여지가 생깁니다. 설정 파일은 660이라 app 그룹에 쓰기가 열려 있습니다. Dockerfile의 해당 세 chmod를 각각 700, 600, 640으로 바꿉니다. 빌드 뒤 실제 접근 검사를 다시 실행합니다. 소스의 숫자가 맞는지만 보는 것이 아니라 변경한 이미지가 실행되는지 확인합니다.
수정 대상은 이미지 안의 실습 경로입니다. 호스트 홈이나 저장소 전체에 chmod -R을 적용하지 않습니다. 자료 파일과 디렉터리에는 필요한 비트가 다르므로 모든 항목에 700을 한꺼번에 적용하는 방식도 피합니다. 명령 앞에서 대상 경로가 어디인지 확인하고 코드 파일과 설정, 자료의 역할을 구분합니다. 실습에 필요한 한정 변경이 검토하기 쉽습니다.
chown은 소유자·그룹을 바꾸고 chmod는 권한 비트를 바꿉니다. 소유자가 잘못된 파일을 777로 열면 정상 동작처럼 보일 수 있지만 다른 주체의 변경도 허용됩니다. 먼저 실행 UID와 파일 UID:GID를 대조하고 필요한 클래스에만 권한을 줍니다. 비루트 서비스가 root 소유 설정을 읽으려면 그룹 경로가 연결됐는지 확인하는 식으로 원인을 좁힙니다.
새 파일과 기존 파일을 구분하기
umask는 새 파일 생성 때 요청한 권한에서 일부 비트를 제거하는 마스크입니다. umask 077은 그룹·기타 비트를 제거하도록 하지만 기존 파일을 고치지 않습니다. 같은 폴더의 오래된 644 파일은 별도로 조정해야 합니다. 여기서는 Dockerfile이 초기 파일 권한을 명시하고 probe 내용은 합성이므로 umask 결과만으로 전체 보호를 선언하지 않습니다.
앞에서 파일을 만들 때 Python이나 셸이 요청한 권한, umask, 부모 정책 등이 함께 작용합니다. 디렉터리 생성과 일반 파일 생성의 시작 권한도 다를 수 있습니다. 따라서 umask 값만 보고 실제 파일이 600이라고 단정하지 않고 생성 뒤 stat으로 확인합니다. 이번 따라하기는 임시 파일에서 실측하고 이전 umask를 복원하여 다른 작업에 설정을 남기지 않습니다.
정상 동작을 남긴 변경
미션은 앞 모듈 API 6건과 범위 4건을 유지합니다. 최소 권한 변경 뒤 생성·조회가 계속 성공하는지 보며 observe.cjs의 실제 HTTP 201·200·404도 확인합니다. 거절 시험만 통과하고 정상 쓰기가 실패하면 과도하게 닫은 정책일 수 있습니다. 그런 실패는 서비스 요구를 삭제할 이유가 아니라 소유자와 부모 탐색, 쓰기 위치를 다시 볼 이유입니다.
서비스 계정에 Linux root 권한이 없다는 점과 다른 사용자의 웹 자료를 읽을 수 없다는 점은 별개의 주장입니다. 현재 앱은 owner가 고정되고 인증이 없습니다. 비루트 실행과 파일 접근 분리는 프로세스 피해 범위를 줄이는 조치이며 자료 소유자 인가는 다음 모듈에서 구현합니다. OS 계정과 웹 계정의 경계를 구분하여 남은 과제를 기록합니다.
검사 결과의 오류를 판단하기
app must not write config가 나오면 설정 append가 실제로 가능했다는 뜻입니다. 이미지의 설정 소유자·그룹·mode를 다시 봅니다. auditor unexpectedly allowed는 분리한 계정이 대상 자료를 읽거나 로그 목록을 봤다는 뜻입니다. Permission denied라는 메시지를 눈으로 보는 것만으로 끝내지 않고 어떤 사용자·어떤 경로·어떤 동작인지 테스트 이름과 묶습니다.
테스트를 모두 통과하면 app의 필요한 접근 허용과 auditor의 세 대상 거절을 관찰한 것입니다. root·추가 그룹·ACL·호스트 마운트가 다른 환경까지 같다고 확대하지 않습니다. 이 실습은 호스트 마운트를 금지하여 이미지 내 정책을 검사합니다. 운영 이전에는 실제 배포의 쓰기 경로와 실행 설정을 다시 검토해야 한다는 한계를 적습니다.
보고서에는 변경 전 숫자와 실패 사례, 변경 후 숫자와 통과 사례를 짝으로 남깁니다. “보안 강화 완료”보다 “설정 그룹 쓰기를 제거했고 서비스 읽기는 유지했습니다”라는 문장이 더 검토하기 쉽습니다. Docker 실행 문서를 근거로 계정과 실행 경계를 확인하며 chmod의 자세한 기호 표기는 더 읽기로 보냅니다.
따라하기
역할의 접근 요구 정리하기
표는 실습 정책의 요구를 출력한 것입니다. 실제 접근 관찰로 해석하지 않습니다.
requirements = {'config': 'read only', 'data': 'read/write', 'logs': 'write'}
for path, action in requirements.items():
print('app', path, action)
print('auditor: config/data/logs denied')
실행 결과
app config read only app data read/write app logs write auditor: config/data/logs denied
umask가 기존 파일을 바꾸지 않음 확인
자신의 임시 파일만 다룹니다. umask 이전 상태를 finally에서 복원하며 새 파일과 기존 파일의 실측 mode를 비교합니다.
import os, stat, tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as directory:
old_file = Path(directory) / 'old.txt'
old_file.write_text('demo')
old_file.chmod(0o644)
previous = os.umask(0o077)
try:
new_file = Path(directory) / 'new.txt'
new_file.write_text('demo')
for name, p in [('old', old_file), ('new', new_file)]:
print(name, format(stat.S_IMODE(p.stat().st_mode), '03o'))
finally:
os.umask(previous)
실행 결과
old 644 new 600
실제 허용·거절 검사하기
starter에서 설정 쓰기 거절 검사가 실패하는지 봅니다. Dockerfile의 자료·로그 700, 자료 파일 600, 설정 파일 640을 적용하고 같은 명령을 다시 실행합니다. 호스트 파일 권한은 바꾸지 않습니다.
bash check.sh다른 역할까지 재검증하기
app의 읽기·쓰기 성공, 설정 쓰기 거절과 auditor 세 경로 거절이 모두 나와야 합니다. 마지막 PASS: access와 종료 코드 0을 확인합니다. FileNotFoundError를 권한 거절이라고 쓰지 않습니다.
확인 문제
실습
Dockerfile의 자료·로그 디렉터리, 합성 자료 파일, 설정 파일 권한을 정책대로 고칩니다. app의 설정 읽기·자료/로그 쓰기는 유지하고 설정 쓰기를 거절합니다. 별도 auditor 실행은 초기 자료·설정 읽기와 로그 목록을 거절해야 합니다. check.sh와 access.py·denied.py를 수정해 검사를 없애지 않습니다.
실행 명령
bash check.sh
기대 결과
app 허용·설정 쓰기 거절, auditor 세 경로 PermissionError, 마지막 PASS: access
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- 최소 권한 변경 뒤 정상 동작과 거절 동작을 함께 검사해야 하는 이유를 설명합니다.