VPN 데몬의 연결 hook은 데몬으로 실행되며, 보통 그것은 root로 실행된다는 뜻이다. 그 hook 중 하나가 누군가의 홈 디렉터리에 쓰거나 데스크톱 도우미를 다시 시작하는 순간, 당신은 네트워크 이벤트에 임의의 로컬 사용자로 행동할 능력을 준 셈이다. 흔한 지름길인 환경 변수에서 사용자 이름을 읽어 전달하는 것으로는 그것이 고쳐지지 않는다. 그것은 작업의 이름표만 바꿀 뿐 어느 계정이 실행하는지는 바꾸지 않는다. 효과가 있는 것은 세 부분으로 된 인계로, 특권을 가진 단계가 무슨 일이 있었는지 말하고 별도의 유닛이 올바른 신원으로 작업을 수행한다.

환경의 사용자 이름은 신원 전환이 아니다

Unix는 프로세스가 무엇을 만질 수 있는지를 유효 사용자 ID로 결정하지, 환경의 문자열로 결정하지 않는다. USER=alice 를 설정하거나 LOGNAME 를 내보내는 것은 부주의한 소프트웨어가 출력에 찍는 내용을 바꿀 뿐이며, 파일 소유권 검사에서는 아무것도 바꾸지 않는다. 그 변수를 설정한 채 root로 실행되는 hook은 여전히 root로 Alice의 파일을 연다.

이름도 같은 이름이 아니다. 원격 접속 로그인, 로컬 Unix 계정, 인증서의 공통 이름은 자주 일치하고 때로 일치하지 않는 세 가지 식별자다. a.smith 로 인증하는 사용자는 로컬 계정이 전혀 없을 수도 있고, 디렉터리 서비스를 통해 asmith 로 매핑될 수도 있다. 하나의 문자열이 세 역할을 모두 한다고 가정하는 설계는 첫 외주 업체에서 깨진다.

그래서 첫 질문은 «어느 사용자가 연결되었는가»가 아니라 «어느 로컬 계정이 이 작업을 수행할 권한을 가지는가»다. 그것은 클라이언트가 보낸 것이 아니라 당신 자신의 계정 데이터베이스에서 답하라:

id -u alice
getent passwd alice | cut -d: -f1,3,6
stat -c '%U:%G %a %n' /home/alice/.local/state/vpn-status.json

세 번째 명령이 사람들이 건너뛰는 것이다. 대상 파일이 이전 hook이 만들었기 때문에 root 소유라면, 올바르게 구성된 사용자 작업은 systemd 문제처럼 보이지만 실제로는 아닌 권한 오류로 실패한다.

데몬은 제자리에 머문다

VPN 서비스 자체의 User= 를 Alice로 바꾸는 것은 잘못된 지렛대다. 데몬이 인터페이스를 바인딩하고, 키를 읽고, 상태 파일을 쓰고, 경로를 관리하는 방식을 바꾸며, 그 모든 위험을 작동하던 유닛으로 옮긴다. 데몬에는 그 특권이 필요하지만, 후속 작업에는 필요 없다.

만지기 전에 유닛이 실제로 무엇을 하는지 보라. 두 명령이 실행 신원과 유효한 정확한 명령행을 알려 주며, 오버라이드가 추가한 것도 포함된다:

systemctl show -p User -p Group -p ExecStart openvpn-server@server
systemctl cat openvpn-server@server

기본 패키지 설치에서 데몬은 root이고 --up 스크립트가 그것을 상속한다. 가정하지 말고 확인하라. 강화된 배포판이라면 전용 계정으로 옮겼을 수 있고, 손으로 쓴 drop-in이 다른 규칙의 래퍼를 통해 hook을 실행할 수도 있다.

세 개의 이름 붙은 부분, 하나의 좁은 트리거

유지보수를 견디는 구성에는 이벤트, 트리거, 작업이 있다. 데몬은 이름 있는 세션이 올라왔다고 보고한다. 트리거는 그 작은 사실을 받아들이고 하나의 고정된 작업을 요청한다. 작업은 자신이 만지는 파일을 소유한 로컬 계정 아래에서 실행된다. 사슬의 어느 것도 임의의 명령 경로를 받아들이지 않는다.

작업 자체는 평범한 서비스 유닛이고, 신원은 그 안의 지시문 하나다:

[Service]
Type=oneshot
User=alice
Group=alice
ExecStart=/usr/local/bin/refresh-vpn-status --slot %i

User=alice 가 유닛 전체에 적용되며, systemd는 알아 둘 만한 예외를 하나 문서화한다. + 접두사가 붙은 명령은 그와 무관하게 전체 특권으로 실행된다. 그 접두사는 단일 특권 단계에는 정당한 도구이고 방금 만든 분리를 무효화하는 편리한 방법이므로, 그 유닛의 어떤 + 도 검토에서 정당화해야 할 것으로 다루어라.

트리거의 입력을 최소로, 비밀이 아니게 유지하라. 세션 식별자, 매핑된 계정, 타임스탬프가 보통 필요한 전부다. 셸 조각, 실행 파일 경로, 클라이언트가 제공한 환경 블록을 경계 너머로 절대 넘기지 말라. 받는 쪽은 정당한 값과 주입된 값을 구별할 방법이 없기 때문이다.

실제로 실행된 신원을 확인하라

구성 검토는 증거가 아니다. 승인된 테스트 연결 후에 파일을 만진 UID와 데몬이 여전히 사용하는 UID를 확인하라:

stat -c '%U %G %y %n' /home/alice/.local/state/vpn-status.json
ps -o user=,pid=,cmd= -p "$(pgrep -o -f 'openvpn --daemon')"
systemd-run --uid=alice --pipe /usr/local/bin/refresh-vpn-status --dry-run

첫 줄은 Alice와 테스트 창 안의 타임스탬프를 보여야 한다. 둘째 줄은 여전히 root나 패키지의 서비스 계정을 보여야 하며, 이것이 데몬이 움직이지 않았음을 증명한다. 셋째는 실제 재연결을 기다리지 않고 작업의 권한을 시험하는 가장 싼 방법이다.

파일 소유자가 Alice인데 실행 사이에 그룹이나 모드가 바뀌었다면, 논리 오류가 아니라 UMask= 나 디렉터리 권한 차이를 찾아라. 타임스탬프가 전혀 움직이지 않았다면 트리거가 한 번도 발동하지 않은 것이고, 유용한 다음 단계는 클라이언트 재연결이 아니라 작업 유닛에 대한 journalctl -u 다.

연결 이벤트가 여전히 증명하지 않는 것

확립된 터널은 하나의 사실이다. 데스크톱 세션이 열려 있다거나, 세션 버스에 닿을 수 있다거나, 작업이 원하는 사설 자원이 이미 사용 가능하다는 뜻은 아니다. Alice의 UID 아래의 oneshot 유닛은 그녀의 신원을 가지지만 세션은 아무것도 갖지 않으므로, 그래픽 버스가 필요한 모든 것은 지원되는 사용자 세션 메커니즘을 거쳐야 하고 그녀가 로그아웃했을 때 어떻게 되는지 정의해야 한다.

재연결도 반복된다. 기차에서 잠든 노트북은 한 시간에 열두 번의 연결 이벤트를 만들 수 있고, 매번 한 줄을 덧붙이는 hook은 상태 파일을 아무도 읽을 수 없는 로그로 바꾼다. 작업을 멱등으로 만들거나, 세션을 키로 하는 레코드 하나를 대체하게 하라.

마지막으로 피해 반경을 유닛 자체에서 제한하라. 상태 디렉터리 밖은 읽기 전용 파일 시스템 뷰, 새로운 특권 없음, 명시적 타임아웃. 데몬과 사용자 작업의 분리는 그 자체로 가치가 있지만, 작업이 승인한 한 가지 외에는 아무것도 할 수 없을 때 훨씬 더 가치가 있다.

관련 글