GUI 없이 Linux 셸에서 Cisco Secure Client에 로그인하기
here-document는 로그인 프로토콜이 아니다. 실제 프롬프트 순서를 기록하고, 순서가 달라지면 expect를 쓰며, 암호는 argv와 기록에 남기지 않는다.
네 줄짜리 here-document면 비대화형 로그인이 완성된다는 믿음이 있다. 하지만 게이트웨이가 두 그룹을 제시하거나 배너를 출력하거나 암호 뒤에 토큰을 요구하는 순간 그 믿음은 무너진다. 그러면 같은 스크립트가 숫자를 기다리던 메뉴에 암호를 입력해 버린다. Linux의 명령줄 클라이언트는 스크립트로 다룰 수 있지만, 튜토리얼이 가정한 것이 아니라 실제로 기다리는 것을 넘겨줄 때만 그렇다.
고정된 here-document는 신화다
Cisco의 Linux 클라이언트는 /opt/cisco/anyconnect/bin/vpn에 작은 프로그램을 설치하며, Secure Client 5도 여전히 그것을 제공한다. vpnagentd 서비스가 실행 중일 때 -s 플래그는 클라이언트가 요청하는 순서대로 표준 입력에서 답을 읽는다. 그 순서는 서버마다 고정되어 있지 않다.
그룹 메뉴는 한 줄을 더한다. 로그인 배너는 또 한 줄을 더한다. 암호 뒤의 토큰 요구는 네 줄짜리 스크립트를 완전히 깨뜨린다. 다섯 번째 줄이 토큰일 수는 없었기 때문이다. 실패는 조용하다. 클라이언트는 잘못된 문자열을 받고, 연결 시도는 끝나며, 스크립트는 쓸모 있는 것을 아무것도 보고하지 않는다.
같은 게이트웨이와 버전을 대상으로 클라이언트를 한 번 직접 실행하고, 프롬프트가 나타나는 순서대로 적어 둔다. 그 기록이 바로 스크립트가 만족해야 하는 명세다.
클라이언트가 기다리는 것
그룹이 하나인 흔한 경우 순서는 사용자 이름, 암호, 때로는 배너 확인이다. here-document의 첫 줄은 서버가 실제로 메뉴를 제시할 때만 메뉴 색인이 되며, 그래서 같은 스크립트가 한 사이트에서는 되고 다음 사이트에서는 실패한다.
/opt/cisco/anyconnect/bin/vpn -s connect vpn.example.net <<EOF
0
alice
$VPN_PASSWORD
y
EOF
이것은 고정된 프로토콜이 아니라 네 가지 가능한 질문에 대한 네 가지 답으로 읽어야 한다. 영은 메뉴 선택이지 암호가 아니며, 끝의 y는 많은 게이트웨이가 결코 보내지 않는 배너를 승인한다.
순서를 예측할 수 없을 때는 expect를 쓴다
프롬프트 순서가 달라지면 줄 세기를 그만두고 프롬프트를 기다린다. expect 스크립트는 connect 명령을 실행하고, 사용자 이름 프롬프트를 기다려 보내고, 암호 프롬프트를 기다려 보내고, 그다음 연결된 상태를 기다린다. 각 대기에는 저마다의 타임아웃이 있다.
타임아웃은 실패로 처리하고 종료한다. 예상치 못한 프롬프트 뒤에도 입력을 계속 보내는 스크립트는 셸이나 메뉴, 또는 마침 읽고 있는 무엇에라도 기꺼이 암호를 입력한다. 이렇게 로그인 스크립트가 사고가 된다.
expect는 비밀을 지운 채 남길 수 있는 기록도 준다. 게이트웨이가 업그레이드 뒤 프롬프트를 바꿀 때 그것은 종료 코드보다 값지다.
비밀은 어딘가에 있어야 한다
이 문제의 모든 판본이 공유하는 약점이 하나 있다. 암호는 클라이언트가 읽을 수 있는 형태로 존재해야 한다. 인수로 넘기면 프로세스 표에 노출된다. 파이프로 흘리면 셸 기록에 남는 일이 많다. 둘 다 공유 점프 호스트의 다른 로컬 사용자가 읽을 수 있다.
install -m 600 /dev/null ~/.vpn-secret
umask 077; VPN_PASSWORD=$(cat ~/.vpn-secret) /opt/cisco/anyconnect/bin/vpn -s connect vpn.example.net
계정이 소유한 모드 600 파일이 최소이며, 그것도 디스크 위의 평문 비밀이다. secret-tool lookup은 Linux 키링에서 읽고, macOS에서는 security find-generic-password -w가 같은 일을 한다. 조직이 인증서나 SAML 인증을 지원한다면 그것을 택하라. 저장된 암호와 MFA를 합쳐도 사람 없이 로그인을 끝낼 수는 없기 때문이다.
터널을 확인하고, 그다음 종료를 맡는다
오류 문구가 없다고 믿지 마라. 클라이언트에 상태를 물어보고 인터페이스를 따로 확인한다.
/opt/cisco/anyconnect/bin/vpn state
ip -brief address
상태는 Connected여야 하고, 터널 인터페이스에는 주소가 있어야 한다. 그다음 터널이 닿아야 할 자원 하나를 요청한다. 경로 없는 연결 상태는 실제로 일어나는 결과이며 놓치기 쉽다.
vpn disconnect로 끊고, 터널을 시작한 스크립트가 종료까지 맡게 한다. 세션을 열어 둔 실패한 작업은 라이선스 자리를 소비하고, 다음에 시도하는 사람에게는 인증 문제로 보인다.
작업 순서는 vpnagentd 확인, 실제 프롬프트 순서 기록, here-document나 expect 선택, 비밀을 argv와 기록 밖에 두기, 상태와 자원 하나 검증, 같은 스크립트에서 끊기다.