어제 Docker에서 Hermes 돌리다가 uid 하나 때문에 하루를 갈아넣은 이야기에서 ACL을 걸었다가 실패한 이야기를 짧게 썼는데, 그때는 “안 됐다”까지만 넘어갔다. 이번엔 ACL이 정확히 뭔지, 왜 그걸로 접근했는지, 왜 안 됐는지를 제대로 짚어보려고 한다.
문제 상황 복기
컨테이너 안 프로세스는 uid 10000, 나는 uid 1000이었다. 둘이 파일을 주고받아야 하는데 권한이 안 맞았다. 그룹을 새로 만들어서 묶어봤는데, 이것도 결국 안 됐다(이유는 뒤에서 설명한다). 그다음에 시도한 게 ACL이었다.
왜 그냥 rwx로는 안 되나
리눅스의 기본 권한 모델은 파일 하나당 딱 세 부류로만 나뉜다.
-rw-r--r-- 1 decipher decipher 파일
소유자(owner) 그룹(group) 나머지(other)
소유자 하나, 그룹 하나, 그 외 전부(other) — 이 세 슬롯 안에서만 권한을 나눠줄 수 있다. “이 특정 사용자 한 명한테만 추가로 쓰기 권한을 주고 싶다”거나 “이 그룹 A는 읽기만, 그룹 B는 쓰기까지” 같은 세밀한 조정이 안 된다. 그룹을 새로 만들어서 두 uid를 묶어보려던 것도 결국 이 세 슬롯 모델 안에서 어떻게든 끼워 맞추려는 시도였다.
ACL이란
**ACL(Access Control List)**은 이 세 슬롯 모델을 확장해서, 파일 하나에 임의의 사용자나 그룹을 원하는 만큼 추가하고 각각 다른 권한을 줄 수 있게 해주는 메커니즘이다. “소유자/그룹/나머지” 세 칸이 아니라, 필요한 만큼 규칙을 리스트로 쌓아올리는 방식이라고 생각하면 된다.
POSIX ACL
리눅스에서 이 ACL을 실제로 구현한 표준이 POSIX ACL이다(ext4, xfs 등 대부분의 파일시스템이 지원). 명령어는 두 개만 알면 된다.
getfacl <경로> # 지금 걸려있는 ACL 확인
setfacl <옵션> <경로> # ACL 설정
setfacl -m g:hermes-share:rwx ~/hermes-workspace
이러면 hermes-share 그룹에게 그 디렉토리에 rwx 권한을 추가로 부여한다. 소유자/그룹
슬롯을 안 건드리고도 특정 그룹 하나를 콕 집어서 권한을 얹을 수 있다.
거기다 -d(default) 옵션을 쓰면, 지금 있는 파일뿐 아니라 앞으로 그 디렉토리 안에 새로
생기는 파일에도 자동으로 같은 ACL이 상속되게 할 수 있다.
setfacl -d -m g:hermes-share:rwx ~/hermes-workspace
여기까지만 보면 완벽한 해결책 같다. 실제로 나도 그렇게 생각했다.
함정: mask
getfacl로 확인해보면 낯선 줄이 하나 껴 있는데, 이게 핵심이다. 실제로 명령어를 순서대로
따라가면서 뭐가 바뀌는지 눈으로 보자.
1단계 — ACL을 건다
$ setfacl -m g:hermes-share:rwx ~/hermes-workspace
$ getfacl ~/hermes-workspace
# owner: decipher
# group: decipher
user::rwx
group::rwx
group:hermes-share:rwx
mask::rwx
other::r-x
hermes-share 그룹에 rwx를 얹었다. 이 시점의 mask::rwx를 기억해두자.
2단계 — 실제 유효 권한 계산법
group:으로 시작하는 모든 ACL 항목의 실제 적용 권한은, “그 항목에 써있는 권한” 그대로가
아니라 “그 항목과 mask를 AND(교집합)한 값”이다.
유효 권한(effective) = ACL 항목의 권한 AND mask
지금은 rwx AND rwx = rwx라서 문제없이 다 적용된다. 그런데 mask가 낮으면 어떻게 될까?
mask가 r--인 경우를 가정해보면:
rwx (ACL 항목) AND r-- (mask) = r-- ← 실제로는 읽기만 적용됨
ACL 항목에 rwx라고 적혀 있어도, mask에 없는 w/x는 그냥 무시된다. mask는 ACL 항목들의
상한선인 셈이다.
3단계 — 그 위에서 다른 프로그램이 파일을 만들고 chmod를 부른다
chmod 숫자 표기(600 같은 거)가 헷갈린다면 펼쳐보기 — 아는 사람은 건너뛰어도 됨
chmod의 숫자(octal) 표기는 세 자리 숫자로, 각 자리가 순서대로 소유자(owner) / 그룹(group) / 나머지(other)를 뜻한다. 각 자리 숫자는 읽기(r)=4, 쓰기(w)=2, 실행(x)=1을 더한 값이다.
| 권한 | 값 |
|---|---|
| r-- (읽기만) | 4 |
| -w- (쓰기만) | 2 |
| --x (실행만) | 1 |
| rw- (읽기+쓰기) | 4+2 = 6 |
| r-x (읽기+실행) | 4+1 = 5 |
| rwx (전부) | 4+2+1 = 7 |
| --- (권한 없음) | 0 |
600을 자리별로 풀면:
6 0 0
│ │ └─ other: 0 = --- (남 전부 권한 없음)
│ └────── group: 0 = --- (그룹 권한 없음)
└─────────── owner: 6 = rw- (소유자만 읽기+쓰기)
즉 chmod 600은 "소유자만 읽고 쓸 수 있고, 그룹이든 남이든 전혀 접근 못 하게" 만드는 명령어다. 아래에서 볼 ls -la의 -rw-------가 바로 이 600을 문자로 표현한 것과 같은 뜻이다 (rw- + --- + ---).
$ echo hello > ~/hermes-workspace/파일
$ ls -la ~/hermes-workspace/파일
-rw-r--r-- 1 decipher hermes-share 6 Jul 8 00:00 파일
$ chmod 600 ~/hermes-workspace/파일
$ ls -la ~/hermes-workspace/파일
-rw-------+ 1 decipher hermes-share 6 Jul 8 00:00 파일
ls -la만 보면 파일 소유자(decipher)와 그룹(hermes-share)은 전혀 안 바뀌었다. 바뀐 건
권한 비트뿐이고, 맨 끝에 + 표시가 추가된 게 눈에 띈다(이 파일에 ACL이 얹혀 있다는 표시).
진짜 무슨 일이 일어났는지는 getfacl로 봐야 보인다.
$ getfacl ~/hermes-workspace/파일
user::rw-
group::rwx #effective:---
group:hermes-share:rwx #effective:---
mask::---
chmod 600이 “그룹 권한 없음”을 지시하니까, ACL의 그룹 비트가 아니라 mask 자체를
rwx에서 ---로 덮어써버린 것이다. chmod가 만들어진 시절엔 ACL이라는 개념 자체가
없었고, ACL이 나중에 그 위에 얹히면서 “chmod로 그룹 권한을 바꾸면 mask가 따라간다”는
규칙으로 하위 호환을 맞춘 결과다.
4단계 — 그래서 결과가 이렇게 된다
[chmod 전] group:hermes-share:rwx AND mask::rwx = rwx (정상 작동)
[chmod 후] group:hermes-share:rwx AND mask::--- = --- (완전히 막힘)
group:hermes-share:rwx 항목 자체는 3단계 출력에서 보다시피 지워지지 않고 그대로 남아
있다. 다만 mask가 0으로 깎이면서 AND 연산 결과가 전부 0이 되어버렸다. ACL 설정을
아무리 잘 해놔도, 그 뒤에 누군가(또는 어떤 프로그램)가 chmod를 한 번 부르는 순간 mask가
갈아치워지면서 전부 무력화되는 셈이다.
정리
- 기본 rwx 권한은 소유자/그룹/나머지 세 슬롯뿐이라 세밀한 제어가 안 된다.
- ACL은 임의의 사용자/그룹에게 개별 권한을 추가로 얹을 수 있는 확장 메커니즘이고, 리눅스에선
POSIX ACL(
getfacl/setfacl)로 쓴다. -d옵션으로 새로 생기는 파일에 ACL을 자동 상속시킬 수 있다.- 하지만 ACL에는
mask라는 상한선이 있고, 하위 호환 때문에 전통적인chmod가 이 mask를 같이 건드린다. 어떤 프로그램이 파일 저장 후 명시적으로chmod를 부르는 구조라면, ACL만 걸어놓는 걸로는 못 이긴다.
이 mask 동작 방식을 미리 알았다면, 어제 글에서처럼 이것저것 시도하다가 한 걸음 늦게 알아차리는 일은 없었을 거다. 정확히 이해하고 있으면 막을 수 있는 삽질이라, 여기에 기록을 남긴다.