토큰 코드를 기다리는 VPN 로그인을 자동화하는 방법
터미널에서 입력을 요구하는 CLI VPN 클라이언트는 비밀번호만 파이프로 넘기는 스크립트를 멈춰 세운다. expect에 기대기 전에 이 다섯 가지 점검을 순서대로 하라.
공급업체 VPN 클라이언트를 실행하는 스크립트가 시간 초과까지 멈춰 있고, 로그에는 아무도 입력하지 않은 확인 코드를 클라이언트가 요구한다고 나온다. 비밀번호는 넘겼으므로 비밀번호가 틀렸다는 결론이 당연해 보인다. 아니다. 코드 요구는 별도 채널의 별도 대화이며, 스크립트는 그것을 한 번도 열지 않았다.
아래 다섯 가지 점검을 순서대로 진행하며, 처음 실패하는 점검에서 멈춰라. 이후 단계는 앞선 단계가 성립한다고 가정하기 때문이다.
점검 1: 클라이언트가 표준 입력을 읽기는 하는가.
자동화를 쓰기 전에, 더미 비밀을 파이프로 클라이언트에 넣고 무슨 일이 벌어지는지 보라. 프롬프트를 출력한 뒤 기다린다면 파이프를 읽지 않는 것이다.
많은 클라이언트가 터미널 장치를 직접 열거나 터미널이 아닌 스트림에서 비밀을 받기를 거부하는데, 길 잃은 파이프가 비밀번호를 흘리지 않게 하려는 것이다. OpenVPN은 문서화된 예외로, --auth-user-pass가 파일 경로를 받는다. 일회용 코드를 요구하는 공급업체 클라이언트에는 대등한 플래그가 없는 경우가 많고, 그 프롬프트는 스크립트가 마련하지 않은 터미널을 기다린다.
점검 2: 자식 프로세스에 터미널이 있는가.
pty는 자식에게 터미널을 할당하며, 이는 프롬프트를 읽을 수 있는 출력으로 바꾸어 순서대로 응답할 수 있게 한다.
import pexpect
child = pexpect.spawn("openconnect vpn.example.net", encoding="utf-8", timeout=30)
child.expect("GROUP:")
child.sendline("staff")
child.expect("USERNAME:")
child.sendline("alice")
child.expect("PASSWORD:")
child.sendline(PASSWORD)
child.expect("Connected as", timeout=60)위의 프롬프트 문자열은 예시이므로, 클라이언트가 실제로 출력하는 정확한 문자열을 대소문자와 구두점까지 복사하라. 모든 기대에는 시간 초과를 붙이고, 닫힌 자식은 상태를 반환하지 않고 예외를 던지므로 전체를 감싸고 파일 끝을 실패로 다뤄라. 다만 이 블록은 자식을 분리하지 않으며, pty가 분리 수단이 아니고 disown이 셸 내장 명령이라 스크립트가 띄운 프로세스에 닿지 못하므로 자식은 스크립트와 함께 죽는다.
점검 3: 지원되는 비대화형 입력이 있는가.
화면 긁기는 클라이언트 업그레이드에서 깨지므로 먼저 문서화된 플래그를 찾아라.
도움말 출력에서 토큰이나 일회용 코드 옵션을 확인하고, 비밀번호 필드에 덧붙인 코드를 서비스가 받아들이는지도 확인하며, 클라이언트가 자격 증명 파일을 읽는 경우에는 프롬프트 대신 파일을 넘겨라.
openvpn --config office.ovpn --auth-user-pass <(printf "%s\n%s\n" "$USER_NAME" "$PASSWORD")프로세스 치환은 /dev/fd를 제공하는 시스템에서 클라이언트에 읽을 수 있는 경로를 준다. 비밀은 클라이언트 입장에서 여전히 비밀이므로 로그와 셸 기록, 프로세스 목록에서 멀리 두라. 클라이언트가 /dev/fd를 거부하거나 자격 증명 경로를 두 번 이상 열면 엄격한 권한의 실제 파일로 쓰고 같은 스크립트에서 지워라.
점검 4: 터널이 스크립트보다 오래 살아남는가.
기본값으로 터널은 부모와 함께 죽으며, 이는 부트스트랩 스크립트에 필요한 것과 정반대다.
disown -h는 클라이언트를 셸 작업으로 띄웠을 때만 도움이 되는데, 그 셸의 작업 표를 고치는 명령이라 스크립트가 pty로 만든 프로세스에는 닿지 않는다. 감독 없이 다시 연결해야 하는 모든 것에는 서비스 관리자가 더 나은 자리로, 클라이언트를 재시작하고 왜 죽었는지도 기록한다. 어쨌든 일회용 토큰은 첫 인증 성공에서 소비되므로 재연결마다 새 코드를 예상하라.
sudo가 이 순서에 무엇을 하는지 보라. sudo의 비밀번호 프롬프트는 VPN의 프롬프트가 아니며, 거기에 VPN 비밀번호로 답하는 것이 그 밖에는 맞는 expect 순서가 실패하는 가장 흔한 이유다.
점검 5: 사람의 코드가 스크립트에 있어야 하는가.
사람들이 건너뛰는 점검이다. 토큰은 사람에게 속하고, 일정에 따라 올라와야 하는 터널은 기계에 속한다.
작업이 감독 없이 다시 연결되어야 한다면 기계 인증서나 서비스 계정이 대화형 로그인을 대체할 수 있는지 물어라. 누군가의 일회용 코드를 자동화하는 것은 두 번째 요소를 저장된 비밀로 바꾸는 일이며, 스크립트 편의가 아니라 정책 변경이다. 서비스가 둘 다 지원한다면 작업이 실제로 도는 방식에 맞는 자격 증명을 골라라. 답을 적어 두라, 다음 사람이 스크립트가 왜 토큰을 쥐고 있는지 반드시 물을 것이기 때문이다.
작동하는 스크립트의 모습
스크립트는 도달한 단계를 출력하고 비밀은 결코 출력하지 않으며, 기대가 실패하면 영이 아닌 코드로 종료한다.
틀린 비밀번호, 틀린 그룹, 만료된 토큰으로 일부러 망가뜨려 시험하라. 모든 실패는 멈춰 버리지 않고 이름 붙은 단계에서 멈춰야 한다. 그다음 실제 서비스를 상대로 한 번 실행하고 터널을 스크립트 밖에서, 안이 아니라 확인하라.