OpenVPN を起動しても決して接続しないスクリプトは、たいていネットワークの問題ではなく、クライアントが描画先の端末もないプロンプトの前で止まっているだけです。OpenVPN には、人を介さずにそのプロンプトへ答える方法がちょうど二つあり、auth-user-pass の背後にある資格情報ファイルと管理ソケットで、両者は交換できません。ファイルはディレクティブ一つで一つのプロンプトに答えますが、ソケットはすべてのプロンプトに答え、loopback ポートを開けるものに未認証の制御チャネルを渡します。どちらが導入に合うかは、実際にどちらのプロンプトが詰まっているか、そしてどの秘密を静止させておけるかで決まります。

仕組みを選ぶ前にプロンプトを名指しする

四つのプロンプトは端末上では同じに見えます。アカウントの組はサーバーの PAM または auth-user-pass-verify の設定から来て、auth-user-pass が答えます。秘密鍵のパスフレーズは鍵ファイルに属し、--askpass 経由で読むか手で入力します。PKCS#12 バンドルは独自のパスワードを求めます。プロキシ認証は、アカウントの資格情報ではなく --http-proxy が取る auth ファイルを使います。

OpenVPN が systemd、PHP、Node の子として動くときは端末がないので、これらのどれも黙って止まります。プロセスはそこに座ったままログは暗号の行の後で止まり、ユニットはやがてタイムアウトしますが、人々がどのプロンプトが待っているかを見る代わりに再試行ループや sleep を足すのはこの沈黙のせいです。四つすべてに答えられるのは管理ソケットだけです。資格情報ファイルは最初の一つに答え、それ以外には何も答えません。

auth-user-pass:一ファイル、一プロンプト

ファイルは最初の行にユーザー名、次の行にパスワードだけを置き、引用符も鍵の名前もコメントもありません。モード 600 を設定し、所有者を openvpn を実行するアカウントにしますが、サービスではログイン中のユーザーではなく専用ユーザーであることがよくあります。/root の下にある root 所有のファイルは、シェルで動くコマンドと起動時に失敗するユニットの違いになります。

二つの細部が正しいファイルをプロンプトに変えます。CRLF で保存するエディターはパスワードに復帰文字を貼り付けたままにするので、ファイル上は正しく見えるパスワードをサーバーが拒否しますが、目ではなく cat -A や od -c で確認してください。そしてプロファイル内の引数なしの auth-user-pass は設計上「ユーザーに尋ねる」という意味で、プロファイルは置かれた場所で解析されるため、コマンドライン上の二つ目のディレクティブでは上書きできないことがあります。

オプションの順序が三つ目の罠で、バージョン依存です。一部の OpenVPN 2.4 ビルドは --config より前に --auth-user-pass があると無視するため、同じ二つのオプションを逆順にすると接続できます。ディレクティブをプロファイル内か、コマンドラインで --config の後に置き、他を変える前にプロファイルのすべての出現を grep してください。

auth-nocache はこの仕組みに属しますが、何も供給しません。クライアントが資格情報をメモリに保持する時間を制限するもので、クラッシュ後には役立ち、ファイルがないときにはまったく役立ちません。サーバー側のセッションタイムアウト後の再接続はファイルを読み直すので、起動時に機能した資格情報でも日中に失効し得ます。

管理ソケット:すべてのプロンプト、より広い攻撃面

リスナー付きでクライアントを起動し、プロンプトがそこに報告されることを明示します。OpenVPN 2.6 のマニュアルに記載されたディレクティブを使います:

management 127.0.0.1 3030
management-query-passwords

bash からは、接続を余ったファイル記述子に保持し、位置ではなくプロンプト名で答えます。再接続は別の順序で尋ねることがあるからです:

exec 3<>/dev/tcp/127.0.0.1/3030
while read -r -u 3 line; do
  case "$line" in
    *"Need 'Auth' username/password"*)
      echo 'username "Auth" alice' >&3
      echo 'password "Auth" secret' >&3 ;;
    *"Need 'Private Key' password"*)
      echo 'password "Private Key" secret' >&3 ;;
  esac
done

境界はトランスポートです。管理プロトコルは loopback 上の TCP による平文であり、遠隔のコントローラーが届くようにするためにリスナーを LAN やトンネルのアドレスで公開してはならず、遠隔のコントローラーとは SSH 転送のことで、より広いバインドではありません。コミュニティ版のデーモンは同時に一つの管理クライアントしか受け付けないので、GUI やモニターがスクリプトとソケットを共有することはできません。二つ目の接続は待ち行列に入らず拒否され、その失敗は耳が聞こえなくなったスクリプトのように見えます。

そのソケットは制御チャネルでもあります。管理パスワードがなければ、127.0.0.1:3030 に接続できるものは何でも状態を読み、トンネルを殺し、あるいは自分でプロンプトに答えられます。管理パスワードと資格情報ファイルは同じ分類の秘密であり、どちらも同じ注意に値します。

どちらが導入に合うか

唯一のプロンプトがアカウントの組で、クライアントが安定した所有者のサービスとして動き、管理していないバックアップの外に 600 のファイルを置けるなら、資格情報ファイルを選びます。鍵のパスフレーズや PKCS#12 のパスワードが関わる場合、ラッパーがすでにプロセスを監督している場合、再接続のデバッグ中に状態とプロンプトを見る必要がある場合は、管理ソケットを選びます。

二つの条件が判断を動かします。ラッパーが /bin/sh、バイナリーを直接呼ぶ systemd ユニット、最小限のコンテナーなら、bash 専用のソケット糊は使えず、ファイルのほうが安い答えです。秘密が頻繁に切り替わるならどちらのファイルも良い住処ではなく、小さなラッパーからキーリングに問い合わせ、ユニットにラッパーを呼ばせれば、切り替えは一か所に保たれ、パスワードはシェル履歴の外に置かれます。

プロセス置換は両者の中間にあり、どちらにも属しません。下の一行が動くのは、openvpn が始まる前に bash が展開するからで、クライアントが構文を理解するからではありません:

openvpn --config client.ovpn --auth-user-pass <(printf "%s
%s
" "$USER" "$PASS")

/bin/sh の下、シェルを迂回するユニット、bash も /dev/fd もないイメージでは壊れます。対話的なテスト用に取っておき、サービスが依存する契約には決してしないでください。

秘密を守り、それからログで検証する

どちらの仕組みも、OpenVPN プロセスが読める秘密を残します。パスワードはコマンドラインやユニットファイルに置かず——ps や systemctl がすべてのローカルユーザーに見せてしまいます——ファイルへのパスを渡し、中の値は渡さないでください。証明書がアカウントの組を置き換えられる場所では、プロンプトの分類を隠すのではなく取り除けます。

正しい実行では、プロンプトの並びが完了し、セッションが「Initialization Sequence Completed」に達します。クライアントが同じプロンプトで止まるなら照合に失敗しているので、ソケットが送った正確な行を出力し、引用符とプロンプト名を確認し、sleep を足すのではなくパターンを直してください。成功を報告しても何も運ばないトンネルはルーティングか push の問題で、資格情報の問題ではありません。

次に、最初の起動だけでなく再接続を試します。サーバー側のセッションを落とすかそのタイムアウトを待つとクライアントは再び尋ね、起動時に正しく見えたファイルが、再接続するアカウントからまだ読めるかどうかがそこで分かります。

関連記事