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と履歴の外に置き、状態と一つのリソースを検証し、同じスクリプトで切断する。

関連記事