서론
현재 Proxmox OS 기반의 홈서버를 구축하여 아래처럼 서비스들을 운용하고 있다.
기본적으로 헤놀로지(XPEnology) NAS를 중심으로 하여 각 서비스들을 사용하는 구조이다.
현재 서비스들은 헤놀로지의 공유 폴더를 Proxmox OS로 마운트 한 뒤,
해당 마운트 디렉토리를 다시 각 VM, LXC 컨테이너 내부로 연결하여 사용하는 구조이다.
일례로, Dockge LXC 컨테이너 내부에 Immich(사진 관리 서비스)는 다음과 같은 흐름으로 구성된다.
대충 아래같은 그림이라고 생각하면 되겠다.

위와 같은 구조를 설계하는 와중 지속적으로 겪은 문제가 존재하는데,
마운트 된 디렉토리에 접근할 때마다 “Permission Denied” 오류가 반복적으로 발생했다.
이에 이번 글을 통해 리눅스 파일 권한 구조에 대해 보다 알아보고자 한다.
본론
리눅스 파일 권한 구조
리눅스에서 모든 파일은 소유자(User), 그룹(Group), 기타 사용자(Others) 세 가지 범주로 접근 권한을 제어한다.
당연한 얘기겠지만, 디렉토리도 파일이다. 디렉토리 실행 권한이 있어야 하위 디렉토리로 진입할 수 있다.

우리가 흔히 말하는 755 권한을 해석해보면 아래와 같다.
- 소유자(User) : 읽기, 쓰기, 실행 가능 (rwx) = 7
- 그룹(Group) : 읽기, 실행 가능 (r-x) = 5
- 기타(Others) : 읽기, 실행 가능 (r-x) = 5
이러한 권한 구조는 모두 익숙할 것이다.
아쉽게도 이번 문제는 단순한 chmod 문제가 아니라, UID와 GID 매핑 불일치에서 비롯된 것이었다.
UID(User ID)와 GID(Group ID)
리눅스의 파일 시스템의 권한은 이름이 아니라 숫자값, 즉 UID(User ID)와 GID(Group ID)로 구분된다.
'ls -al'을 통해 확인할 수 있는 소유자 이름은 내부적으로 /etc/passwd 에 등록된 사용자 UID를 이름으로 매핑해 보여주는 것뿐이다. 따라서, 두 시스템 간에 UID가 다르다면 동일한 이름의 사용자라도 전혀 다른 사용자로 인식된다.
이 UID, GID의 불일치가 이번 문제의 원인이었다.

NFS(Network File System)
일반적으로 우리가 다른 시스템의 파일을 사용하기 위해 NFS(Network File System)를 사용한다. 디렉토리 마운트(Mount)는 일전에 다뤄보았으니, 이번엔 NFS에 대해서만 이야기해보고자 한다.
전지전능한 주에서 이제는 범부로 영락해 버리신, 그러나 언젠간 복귀하실 ChatGPT님께 자문을 구해보았다.
참고로 필자는 GPT 결제 끊었다. 언젠가 왕좌에 복귀하신다면 그간의 무례를 사죄드리며 다시 결제할 예정이다.
가장 간단히 말하면,
“서버의 특정 디렉터리를 네트워크를 통해 다른 클라이언트가 로컬 폴더처럼 접근하도록 하는 시스템”이다.
출처 | OpenAI. (2025). ChatGPT (GPT-5 Version) [Large language model]. https://chatgpt.com/

그냥 쉽게 네트워크 기반의 파일 마운트 방식이라고 이해하면 될 것 같다.
이러한 NFS의 가장 큰 특징으로는, NFS 서버에서는 '/etc/exports'를 통해 디렉토리의 마운트를 허용하는 데, 해당 설정 파일에 명시된 UID, GID를 숫자 형태 그대로 전달한다는 점에 있다.
이 말이 무슨 뜻이냐면, NFS 서버에 root(UID 1000)라는 사용자가 존재하고, 클라이언트에 guest(UID 1000) 사용자가 존재할 경우, guest(UID 1000) 사용자 권한으로 해당 디렉토리의 원본 소유자(root)처럼 동작할 수 있다는 것이다.
이러한 특징으로 인해 NFS를 사용할 땐 굉장한 주의가 필요하다.
자칫 잘못하면 엄한 사람에게 권한을 허용해 줄 수 있게 된다.
그렇다면 원인은?
여기까지 미루어 보면 문제점이 어느 정도 예상이 된다.
- NFS 서버(XPEnology)의 UID/GID
- Proxmox OS의 UID/GID
- Dockge LXC의 UID/GID
- Immich 컨테이너 내부의 UID/GID
위 4가지 단계 중 일부, 혹은 전체에서 UID/GID가 불일치하여 발생한 문제일 것이란 가설을 세워볼 수 있다.
그렇다면 가설을 검증하기 위해 NFS 서버에 생성된 볼륨의 권한부터 살펴보자.
NFS를 통해 외부로 공유되는 모든 볼륨 정보는 '/etc/exports'를 통해 확인할 수 있다.

상기 설정 중 유심히 살펴봐야 할 부분은 'no_root_squash'와 'anonuid=1025' 부분이다.
Root Squash와 UID
앞서 언급하기로, NFS는 디렉토리의 UID, GID를 클라이언트에 그대로 전달한다고 하였다. 이로 인해 마운트 시스템에 존재하는 UID가 우연히 일치한다면, 아찔한 보안사고가 발생할 수 있게 된다.
최근 이슈가 된 삼* 바이오로직스의 공유폴더가 공개된 것도 이와 관련이 있지 않을까 하는 생각을 잠깐 해봤다.
'root_squash'는 이를 방지하기 위한 NFS의 보안체계 중 하나로, 원격 클라이언트의 root(UID 0) 사용자의 접근을 익명 사용자(anouid)로 강제 변환시키는 보안 체계이다. 이를 통해 NFS 클라이언트의 root를 익명 사용자로 매핑시켜 NFS 서버의 root 계정과 분리하는 것이다.
현재 생성된 볼륨들의 경우 'no_root_squash' 옵션을 사용하여 Proxmox의 root가 NFS의 root와 동일시되게 된다.
그렇다면 또 왜?
그렇다면 이 시점에서 또 의문이 생겨야 한다.
Proxmox와 NFS 서버의 root가 동일하다면, 왜 권한 문제가 발생한 것일까?
여기서부터는 Proxmox LXC의 Privileged에 대해 알아야 한다.
앞서 언급한 NFS의 root squash 옵션과 동일하게, Proxmox LXC에는 Privileged 옵션이 존재한다.
Proxmox의 모든 LXC들은 기본적으로 unprivileged(특권이 없는) 상태로 실행하는데, 이 경우 컨테이너 내부의 사용자들은 호스트(Proxmox) 입장에서는 UID 100000 처럼 매핑되어 버린다.

이 설정으로 인해 Dockge LXC의 사용자가 익명화(일종의 squash)되게 되고, 다시 NFS 서버의 'anonuid=1025' 설정에 의해 익명 사용자의 모든 접근이 guest(1025)로 처리된다.
당연하게도 공유폴더를 생성할 때 guest 사용자는 접근이 불가하도록 생성하였으니, LXC 내부에서 마운트 된 디렉토리에 접근할 때 Permission Denied가 발생하게 된 것이다.
필자는 혼자 쓰는 서버이기에 문제가 된 LXC를 privileged로 변경하여 해결했지만 근본적인 해결책은 아니다.
결국 root 사용자가 익명화되는걸 방지한 미봉책으로,
Dockge 내부에 있는 컨테이너들 중 일부가 root가 아닌 다른 사용자로 띄워지면 동일 현상이 발생할 것이다.
제일 확실한 수단은 그냥 공유폴더의 접근 권한을 guest에 대해서도 허용해 주면 해결되지 않을까 싶다.
이를 요약해 보면 아래와 같이 거슬러 올라가는 구조이다.
번외. 그렇다면 도커(Docker)는?
앞서 다룬 내용은 UID/GID 불일치로 인해 발생하는 문제였다. 그렇다면 여기서 또 하나의 의문이 떠오른다.
우리는 호스트 디렉토리를 도커 컨테이너 내부로 마운트 하여 자유롭게 사용하곤 한다. 근데 어떻게 도커가 마운트 된 호스트 디렉토리 내부에 자유롭게 읽기, 쓰기, 실행이 가능한 걸까? 단순히 '마운트 하였으니까 가능하지'라고 넘어가기엔 앞서 살펴본 내용을 보니 그럴 리가 없다는 사실을 알 수 있다.
그래서 컨테이너 내부에서 ID를 직접 확인해 보았더니 놀라운 사실을 알 수 있었다.
root@aafc1c025e8c:/app# id
uid=0(root) gid=0(root) groups=0(root)그렇다. Docker 컨테이너는 root 권한(UID 0)으로 실행되는 것이다. 그렇다면 간혹 호스트에서 Docker Volume을 열어보려 할 때 Permission Denied가 발생하는 것도 납득이 된다.
- 컨테이너 내부 프로세스 = root (UID 0)
- 호스트에서 로그인한 일반 사용자 = root 아님
- 따라서 호스트 사용자는 root가 생성한 파일에 접근할 권한이 없다
즉, 호스트에 로그인한 사용자 계정이 root가 아니므로, root가 생성한 Docker Volume을 제어할 수 없다는 것이 자연스레 납득된다.
이쯤 되니 흥미로운 가설 하나가 또 떠오른다.
도커 컨테이너는 자체적으로 root 권한으로 동작한다.
그렇다면 root 권한은 없지만 Docker 그룹에 속해있다면,
호스트의 '/' 디렉토리를 컨테이너로 마운트 하면 사실상의 root처럼 동작할 수 있는 것 아닌가?
이게 가능하다면 사실상 Docker 그룹 = root 그룹이 되는 셈이다.
실험 결과
일전에 pfSense 내부망 설정을 위해 만들어두었던 Ubuntu VM에서 실험을 진행해 보았다.
실험을 위해 root 계정으로 Docker를 설치 및 실행한 뒤, test(UID 1001)을 만든 다음 Docker 그룹에 추가하였다.
root@siroh-Standard-PC-i440FX-PIIX-1996:~# sudo usermod -aG docker tester
root@siroh-Standard-PC-i440FX-PIIX-1996:~# su - tester
tester@siroh-Standard-PC-i440FX-PIIX-1996:~$ id
uid=1001(tester) gid=1001(tester) groups=1001(tester),999(docker)
tester@siroh-Standard-PC-i440FX-PIIX-1996:~$ docker --version
Docker version 29.0.2, build 8108357
tester@siroh-Standard-PC-i440FX-PIIX-1996:~$ ls -ld /root
drwx------ 4 root root 4096 11월 19 14:08 /root
tester@siroh-Standard-PC-i440FX-PIIX-1996:~$ ls -al /root/
ls: cannot open directory '/root/': Permission denied위처럼 tester 사용자로는 '/root' 디렉토리로 접근이 불가능하다는 사실을 알 수 있다.
그렇다면 '/' 디렉토리를 도커 컨테이너 내부로 마운트 한 다음 접근을 시도하면 어떨까? 이를 위해 간단히 Ubuntu 컨테이너를 만든 다음 컨테이너에 '/' 디렉토리 전체를 마운트 하여 시도해 보았다.
tester@siroh-Standard-PC-i440FX-PIIX-1996:~$ docker run -it --rm \
-v /:/host \
ubuntu bash
Unable to find image 'ubuntu:latest' locally
latest: Pulling from library/ubuntu
20043066d3d5: Pull complete
Digest: sha256:c35e29c9450151419d9448b0fd75374fec4fff364a27f176fb458d472dfc9e54
Status: Downloaded newer image for ubuntu:latest
root@9433c46a3e3e:/# id
uid=0(root) gid=0(root) groups=0(root)
root@9433c46a3e3e:~# cd /root
root@9433c46a3e3e:~# ls -al
total 16
drwx------ 2 root root 4096 Oct 13 14:09 .
drwxr-xr-x 1 root root 4096 Nov 19 05:21 ..
-rw-r--r-- 1 root root 3106 Apr 22 2024 .bashrc
-rw-r--r-- 1 root root 161 Apr 22 2024 .profile
root@9433c46a3e3e:~# echo test > test.txt
root@9433c46a3e3e:~# ls -aln
total 20
drwx------ 1 0 0 4096 Nov 19 05:31 .
drwxr-xr-x 1 0 0 4096 Nov 19 05:21 ..
-rw-r--r-- 1 0 0 3106 Apr 22 2024 .bashrc
-rw-r--r-- 1 0 0 161 Apr 22 2024 .profile
-rw-r--r-- 1 0 0 5 Nov 19 05:31 test.txt
root@9433c46a3e3e:~# cat /etc/shadow
root:!:20358:0:99999:7:::
daemon:*:19977:0:99999:7:::
bin:*:19977:0:99999:7:::놀랍게도 컨테이너 내부에서 호스트, 즉 Ubuntu VM의 root 권한을 탈취하는데 성공하였다. 즉, 도커 그룹(docker group)에 사용자를 추가하는 것은 사실상 root 권한을 주는 것과 같다는 사실을 알 수 있다.
심지어 /etc/shadow, /etc/passwd 도 접근이 가능하였다.
한계점
같은 실험을 기존 작업환경인 macOS + OrbStack에서 수행하니 결과가 상이하였다.
마찬가지로 '/' 디렉토리를 마운트 하여 도커 내부에서 접근은 가능하였으나, 아래와 같은 현상이 발생하였다.
- 컨테이너 내부 ID는 root(UID 0)으로 보인다.
- 하지만 실제 macOS에서의 root가 아닌, 컨테이너 내부에서만 root로써 동작한다.
이유는 간단하다. 기본적으로 Linux Docker는 Kernel이 제공하는 namespaces, cgroups, chroot를 통해 커널의 일부를 격리한다. 즉, 완전히 별개의 커널이 아닌 일부만 격리된, 같은 커널을 공유하는 셈이므로 root 권한 탈취가 가능하였다.
반면 macOS는 Linux Kernel이 제공하는 namespaces, cgroups, chroot와 같은 기능을 사용하지 못한다. 때문에 OrbStack(=Docker Desktop)은 자체적으로 Linux VM을 만든 다음 도커 컨테이너를 생성한다. 즉, Linux Docker처럼 호스트 커널을 공유하는 것이 아닌 별개의 커널(Linux VM의 Kernel)을 사용하는 셈이다.
이로 인해 Linux와는 달리 호스트(macOS)의 root 권한을 탈취당하는 것을 방지할 수 있는 것이다.

결론
처음 문제의 발단은 Immich LXC에서 Permission Denied가 발생하여 찾아보기 시작한 것이었다. 이러한 공부를 통해 얻는 것도 홈서버를 구매함으로써 생긴 이점 중 하나가 아닌가 싶다.
시행착오를 통해 얻는 무형자산들로 홈서버 구매비용의 어느 정도는 멘징 했다고 합리화하였다.
특히 놀랐던 사실은 그동안 무분별히 사용해 왔던 Docker 권한이 이런 식으로까지 확장될 수 있었단 점이다.
물론 macOS 같은 환경에서는 이를 방지할 수 있다곤 하지만, 어느 서버가 macOS에서 돌겠나. Linux에서 돌지.
찾아보니 Rootless Docker를 사용하면 이러한 것을 방지할 수 있다고 한다. 관심 있다면 아래 글을 보면 좋을 듯하다
참고자료
