OpenVPN은 셸에서는 되지만 cron 아래에서 실패한다: 원인은 PATH
셸에서 연결되는 명령도 터널이 시작되기 전에 cron이나 subprocess 아래에서 실패할 수 있습니다. 경로와 작업 디렉터리, 터미널을 고치십시오.
같은 명령이 셸에서는 연결되고 cron 아래에서는 죽는다. 그러니 터널이나 인증서가 원인이라고 결론 내리기 쉽지만, 믿기 전에 두 실행의 공통점을 보라. 바이너리와 구성, 자격 증명은 동일하고 프로세스 환경만 바뀌었다. 프로그램이 아예 발견되지 않는다면 TLS 디버깅은 소용없고, 해결책은 대개 경로와 작업 디렉터리, 몇 개의 환경 변수에 있으며 VPN에는 없다.
동작하는 셸에서 command -v openvpn 를 실행하면 답이 출력되고, 그것은 매우 자주 /usr/sbin/openvpn 이며, cron은 이 디렉터리를 검색 경로에 넣지 않는다.
터널은 시작된 적이 없으므로 터널이 문제가 아니다
cron이 시작한 작업은 최소한의 환경, 관례적으로 PATH=/usr/bin:/bin 를 물려받고 제어 터미널이 없다. /usr/sbin 나 /usr/local/sbin 에 설치된 것은 그 목록에 그냥 없고, 로그 줄은 TLS 오류 대신 openvpn: not found 를 읽는다. Python의 subprocess 는 인터프리터 프로세스의 환경을 물려받아 cron보다 풍부할 수 있지만 틸드를 확장할 셸은 여전히 없다는 식으로 다르게 실패한다. 두 실패 모두 패킷 하나가 호스트를 떠나기 전에 일어난다.
구분하는 빠른 방법은 마지막 오류가 아니라 첫 오류를 읽는 것이다. 바이너리가 없으면 셸이나 Python 예외가 나오고, 구성이 나쁘면 OpenVPN 사용법 메시지가 나오며, 진짜 핸드셰이크 실패라면 타임스탬프와 피어 주소가 나온다. 세 번째만 터널 문제다.
절대 경로와 인자 목록이 셸 문자열을 이긴다
해석된 경로를 명시적으로 넘기고 셸을 완전히 건너뛴다. subprocess.run(["/usr/sbin/openvpn", "--config", config, "--verb", "4"], capture_output=True, text=True, timeout=90) 는 셸에 파일 이름 해석을 맡기지 않고 반환 코드와 stdout, stderr를 준다. 구성 경로는 os.environ["HOME"] 나 pathlib.Path.home() 로 만들어 틸드를 확장할 일이 없게 한다. 정말 셸 문법이 필요하면 shlex.split 를 쓰되, .split() 로 문자열을 나누면 공백이 있는 경로에서 깨진다. stderr도 읽어야 하는데, OpenVPN 이 초기 실패 대부분을 거기에 쓰기 때문이다.
같은 규칙이 cron에도 적용되어, 작업 줄은 로그인 셸이 아니므로 프로필을 읽지 않고 별칭도 정의하지 않는다.
crontab이 실제로 물려받는 것
crontab 항목은 crontab이 선언한 환경에 상속된 기본값 몇 개가 더해진 것을 받으므로, 경로를 끝까지 쓰거나 그것을 해 주는 스크립트로 호출을 감싸라. 파일 맨 위의 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 같은 줄은 흔한 설치 위치를 덮고, >> cron.log 2>&1 로 리다이렉트하는 것이 다음 실패를 읽을 수 있게 한다. 암호 문구로 보호된 프로필은 물어볼 터미널이 없기 때문에 cron 아래에서 비밀을 물어볼 방법이 없다. 그런 연결은 자격 증명 파일을 읽을 수 있는 systemd 유닛에 속하고, 대화형인 척하는 작업에 속하지 않는다.
상대 경로가 두 번째 함정이다. 테스트 실행은 프로필이 있는 폴더에서 하는 경우가 많고 cron 작업은 전혀 다른 곳에서 시작하므로, --config office.ovpn 는 거기에 없는 파일로 해석되고 메시지는 터널이 없다기보다 파일이 없다고 읽힌다.
환경을 벗겨 증명하라
최소 환경을 직접 재현해 env -i PATH=/usr/bin:/bin /usr/sbin/openvpn --config office.ovpn --verb 4 를 실행한다. 그 실행에서 경로나 터미널 불평이 나오면 문제는 VPN 위에 있다. 연결된다면 남은 용의자는 실행 계정과 시작 디렉터리, 제시하는 자격 증명이다. 어느 결과든 인증서 점검을 한 번 더 하는 것보다 낫다.
그다음 래퍼가 스스로 보고하게 만들라. 도달한 단계마다 다른 종료 상태로 나가고 두 스트림을 타임스탬프와 함께 기록하면, 다음 실패가 경로 해석, 인증, 이미 터널이 올라간 뒤 중 어디서 죽었는지 알려 준다.
유닛에는 올바른 순서도 필요해서, Wants=network-online.target 와 After=network-online.target 는 주소나 기본 경로가 생기기 전에 클라이언트가 시작하지 않게 막고, 그렇지 않으면 수동 재시작에서 사라지는 인증서나 TLS 오류로 나타난다.