훅은 root 로 직접 실행하면 동작하며Operation not permitted서비스가 시작하는 순간 다음을 반환한다. 두 실행 사이에 파일은 바뀌지 않았고 환경이 바뀌었다. systemd 는 프로세스에 더 작은 세계를 준다. 기능은 줄고,/dev는 달라지고, 파일 시스템 경로는 읽기 전용이 되며 작업 수에도 상한이 생긴다. 실패는 스크립트가 망가진 것이 아니라 특정 작업 하나를 거부한 경계로 읽어야 한다.

거부된 작업에서 시작하라

unit 이 실패했다는 로그 한 줄은 거의 아무것도 알려주지 않는다. 쓸모 있는 줄은 거부된 호출을 지목한다. 열지 못한 장치 노드, 쓰지 못한 디렉터리, 만들지 못한 네임스페이스다. 지시자 하나를 건드리기 전에 그 호출을 찾아라. 각각이 서로 다른 제약에 대응하기 때문이다. 그다음에는 패키지가 넣어 준 파일이 아니라 실제로 실행된 unit 을 drop-in 까지 포함해 읽어라.

아래 질의는 프래그먼트 경로, drop-in 디렉터리, 그리고 장치나 파일 시스템 거부에서 중요한 속성을 출력한다. 어떤 경계가 관련되는지 정하기 전에 모두 읽어라.

systemctl show "[email protected]" -p FragmentPath -p DropInPaths -p PrivateDevices -p DevicePolicy -p DeviceAllow -p ProtectHome -p ProtectSystem -p TasksMax
journalctl -u "[email protected]" -b --no-pager | grep -iE 'operation not permitted|permission denied|cannot open|failed'

그 블록은 아무것도 쓰지 않는다. 거부된 호출이 저널에 없다면 지시자를 짐작하는 대신 한 번의 실행에 대해 클라이언트의 상세 수준을 높여라. 프로세스가 돌고 있다는 것은 unit 이 시작되었다는 증거이지, 훅이 필요한 권한으로 실행되었다는 증거가 아니다.

네임스페이스에는 TUN 장치가 절대 쓰지 않는 마운트 권한이 필요하다

한 훅 안에서 비슷해 보이는 두 작업은 권한 프로필이 다르다./dev/net/tun를 열고 인터페이스를 만드는 데는 네트워크 관리와 그 노드에 대한 접근이 필요하다. 이름 있는 네트워크 네임스페이스를 만드는 데도 마운트 작업이 필요하다. 네임스페이스 도구가 새로운/sys뷰를 마운트하고 상태 디렉터리를 제자리에 묶기 때문이다. 업스트림 클라이언트 unit 은 마운트 관리를 의도적으로 자신의 바운딩 세트 밖에 둔다. 그래서 터널 자체는 동작했겠지만 네임스페이스 단계가 거부된다.

그래서 장치 노드를 주는 것으로는 스크립트가 구제되지 않는다. 장치 허용은 한 가지 질문에 답한다. 이 프로세스가 이 노드를 열 수 있는가. 네임스페이스의 질문은 다르다. 무엇이든 넓히기 전에 실패한 호출을 그 기능에 맞춰라.

셸에서의 테스트는 제한 없는 root 가 가진 모든 기능을 물려받고, unit 은 바운딩 세트가 남긴 것만 물려받는다. 두 실행은 서로 다른 실험이다 바이너리가 같더라도.

장치, 보호된 경로, 작업 상한

거부된 작업이 밝혀지면 세 가지 지시자 묶음이 대부분의 실패를 설명한다. 장치 접근을 제어하는 것은PrivateDevices이며, 이것은/dev를 최소한의 의사 장치 집합으로 바꾼다. 다른 하나는DevicePolicy과DeviceAllow이며, cgroup 이 무엇을 허용하는지 결정한다./dev/net/tun는 그 최소 집합에 속하지 않으므로,PrivateDevices=yes이 있는 unit 은 장치 정책이 뭐라 하든 열 수 없다.

파일 시스템 보호는 두 번째 질문에 답한다.ProtectHome=yes는/home, /root과/run/user을 접근 불가로 만들고,ProtectSystem=full는/usr, /boot과/etc을 읽기 전용으로 만든다. 그래서 자기 설정 파일 옆에 쓰는 훅은 셸에서는 동작하고 unit 아래에서는 실패한다. 기능이 빠졌을 때와 같은 모양의 권한 오류다. 쓰기 가능한 입력을 정책이 허용하는 경로로 옮기거나, 좁은ReadWritePaths=항목을 추가하라.

자원 한도는 세 번째 묶음이며 가장 눈에 띄지 않는다.TasksMax=는 unit 이 만들 수 있는 작업 수를 제한하고, 파일 디스크립터 상한도 소켓과 핸들에 대해 같은 방식으로 동작한다. 상한을 넘겨 자란 배포는 권한 오류가 아니라 자원 오류로 실패한다. 정확한 메시지가 여전히 중요한 이유다.

네임스페이스 단계를 훅 밖으로 빼라

클라이언트가 가져서는 안 되는 권한을 요구하는 준비 작업은 자체 unit 에 속한다. 클라이언트보다 먼저 실행되고, 결과를 유지하며, 연결보다 앞서 정렬된 oneshot 은 그 권한을 터널 프로세스에 넘기지 않고 네임스페이스 단계에 필요한 것을 준다. 그다음 클라이언트는 up 훅에서 스크립트를 부르는 대신 그 unit 을 참조한다.

아래 템플릿은 출발점 형태이며 읽지 않고 적용할 것이 아니다. 준비 작업을 권한이 있는 곳에 두고 터널 unit 은 건드리지 않는다.

# /etc/systemd/system/[email protected]
[Unit]
Description=Namespace preparation for %i
Before=openvpn-client@%i.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/netns-up.sh %i
CapabilityBoundingSet=CAP_NET_ADMIN CAP_SYS_ADMIN
[Install]
WantedBy=multi-user.target

순서는 패키지 unit 을 편집하는 대신 drop-in 으로 추가하라. 그래야 업그레이드가 변경을 조용히 떨어뜨릴 수 없다. 디렉터리는 인스턴스 이름에 묶이므로 두 번째 프로필에는 자체 파일이 필요하다.

sudo mkdir -p /etc/systemd/system/[email protected]
sudo tee /etc/systemd/system/[email protected]/10-namespace.conf >/dev/null <<'CONF'
[Unit]
[email protected]
[email protected]
CONF
sudo systemctl daemon-reload

변경 후 같은 호출을 반복하라

수리는 같은 호출이 다른 결과를 낼 때만 확인된다. 두 unit 을 재시작하고, 실패했던 작업을 실행하고, 그 결과를 변경 전에 저장한 저널 줄과 비교하라. 훅이 다시는 실행되지 않는데 unit 이 시작되는 것은 수리가 아니다.

아래 순서는 비교를 정직하게 유지한다. unit 의 색이 아니라 거부된 호출을 확인하기 때문이다. 그 출력을 변경 전에 잡아 둔 저널 줄 옆에 저장하라.

sudo systemctl restart "[email protected]" "[email protected]"
systemctl status "[email protected]" --no-pager
journalctl -u "[email protected]" -b --no-pager | grep -c 'Operation not permitted'
ip -brief addr show tun0

마지막 줄을 읽는 대신 거부 횟수를 세라. 훅은 실패하고 재시도하고 성공할 수 있으며, 경계가 움직였는지는 횟수만 보여준다. drop-in 에 완화한 지시자와 이유를 주석으로 남겨라. 다음 업그레이드가 당신의 설정을 패키지 기본값과 비교하기 때문이다. 모든 보호를 끈 뒤에야 시작되는 unit 은 고쳐지지 않았다.

관련 글