OpenVPN フックがスクリプトの隣のファイルを見つけられない理由
フック内の相対パスはスクリプトではなくプロセスに対して解決されます。__file__ で固定し、権限より先に名前空間と身元を確認してください。
エラーなく動いているのにファイルが見つからないと報告するフックは、多くの場合、仕事に失敗しているのではなく、見当違いの場所を見ている。スクリプト内の相対パスはプロセスの作業ディレクトリに対して解決され、サービスは自分で選んだわけではない作業ディレクトリで起動する。スクリプトの隣のファイルとプロセスが開くファイルが同じになるのは、何かが両者を固定しているときだけである。このずれが、権限の問題と誤読されるフック障害の大きな部分を説明する。
権限より先にパスを確認すべきである、なぜなら間違ったファイルに対する権限エラーは、やはり間違ったファイルのエラーにすぎないからだ。
相対パスはファイルではなくプロセスに対して解決される
スクリプトが open("policy.json") を呼ぶとき、カーネルはその名前をプロセスの現在の作業ディレクトリに対して解決する。スクリプト自身の位置はその探索に一切関与しない。 OpenVPN は自分が選んだディレクトリからフックを起動し、cd オプションでそれを変えられるため、自分のフォルダから手動で試したスクリプトは、デーモンが実行すると挙動が変わる。手動テストは通り、サービス実行は失敗し、ログ行は欠けている名前だけを告げる。
Python はスクリプトの位置を __file__ として公開しており、これが固定を可能にする。 Path(__file__).resolve().parent / "policy.json" はプロセスではなくファイルからパスを組み立てる。この一行が、権限には何も触れずに作業ディレクトリへの依存を取り除く。
パスを意図的に固定し、そのうえで resolve の動作を確認する
resolve() はシンボリックリンクをたどるため、フックを所定の位置にリンクする配置では、リンクを保持するディレクトリではなくリンク先に固定される。バージョン付きの配備では通常それが望ましく、シンボリックリンクの隣にデータを置く前提のレイアウトでは誤りである。配備がどちらを意味するかを決め、本番で違いを発見するのではなく、それに合わせてコードを書く。 Path(__file__).parent を付けなければ resolve() はリンク自身のディレクトリを保つ。
データがプログラムではなくインスタンスに属するなら、設定から絶対パスを取る。場所をハードコードすると、フックが一つのホスト構成に縛られる。
四つの境界を順にたどる
まず作業ディレクトリから始め、ユニットの WorkingDirectory、OpenVPN の cd オプション、exec の前にディレクトリを変えるラッパーを比較する。次に解決された場所を確認する、というのも別の配備にある同名のファイルが探索に応答して誤りを隠してしまうことがあるからだ。続いてファイルシステムの見え方であり、chroot やサービスのマウント名前空間はホストの絶対パスを単に存在しないものにできる。最後がアクセス主体であり、これは可視性とは別の問いである。
ProtectHome と PrivateTmp は異なる場所を隠し、どちらもホストから取得したディレクトリ一覧には現れない。シェルで root に存在するパスが、ユニットの名前空間の内側では見えないことがあり、だからこそサービス外で同じコマンドを試してもほとんど何も証明できない。
サービスを広げずに探索を直す
パスを固定しても解決するのは最初の境界だけである。サービスが隠しているディレクトリを露出させるわけでも、権限のないアカウントに root 専用ファイルへのアクセスを与えるわけでもない。script-security を上げると、OpenVPN がどのスクリプトを実行するかは変わるが、相対名がどこで解決されるかには何の影響もない。サービス全体の作業ディレクトリを変えるのは問題より広すぎる、他のすべての相対的な証明書、ログ、補助のパスが一緒に動いてしまうからだ。
狭い修正を選ぶ、すなわちスクリプト内の決定的なパス、インスタンスデータのための明示的な設定値、そしてフックが実際に動くアカウントに合ったファイル所有権である。
境界を名指す証拠を記録する
役に立つ障害報告には三つが含まれる、すなわちフックが試した正確なパス、それを生んだ環境、そしてどの境界が拒んだかである。ファイルの内容を吐き出さずに、解決されたパスと例外を出力する。この三つ組によって、誤った固定と隠れたマウントと権限拒否が切り分けられるが、いずれも別の修正を要する。
root が端末からファイルを読めるという主張は、それらの問いの一つに答えるだけで、他には何も答えていない。代わりにサービス自身の文脈から試すべきである。