ノート PC には緑のトンネルが表示され、クライアントログはルートが push されたと報告し、応答する唯一のアドレスは VPN サーバー自身のトンネル IP です。その先への ping は、ClusterIP であれ別の Pod のアドレスであれ公開ホスト名であれ、何を狙っても死にます。この組み合わせは曖昧ではなく診断的で、カプセル化解除は機能しており、二番目のホップが機能していません。

私はこれを chart でインストールした OpenVPN イメージを動かす Debian ノードで見ましたが、トンネルは簡単な部分でした。クライアントは 10.8.0.x のリースを取り、10.8.0.1 が応答し、クラスタ DNS サービスである 10.96.0.10 はタイムアウトしました。サーバーログに不健康なものは何もなく、だからこそ人々は触る必要のなかった iptables ルールを書き換え始めるのです。

VPN Pod はルーターであり、それをはっきり言う価値があります。暗号化されたパケットが届き、Pod 自身のネットワーク namespace で開かれ、その後は別のインターフェースから出ていかなければなりません。Linux は、まさにその namespace の中で転送が有効でない限り、この二番目のステップを拒否します。

クライアントが到達できるものとできないもの

応答するアドレスはトンネルのエンドポイントに属し、VPN プロセスがローカルに所有しているため、転送はまったく関与しません。それ以外のすべて、別の Pod、サービスの VIP、インターネットは、カーネルがパケットを tun0 から eth0 へ移動させる必要があります。その移動が拒否されると、症状は拒否ではなく一様な沈黙で、トンネルの下へ ICMP エラーを返すものが何もないからです。到達不能メッセージではなくタイムアウトが返り、それが誰もルーティングしていないという手がかりです。

二分の分類で層が絞れます。DNS 名が失敗する一方でサービスの IP への生の ping が成功するなら、問題は転送ではなく DNS の配管です。ピアアドレスより先のすべての宛先が、生の IP も含めてタイムアウトするなら、namespace から始めてください。

namespace の中で設定を読む

各 Pod は独自のネットワーク namespace を持ち、net.ipv4.ip_forward は namespace ごとの sysctl です。ノードで変更してもコンテナについては何も分からず、間違ったマシンで午後を過ごす確実な方法です。

kubectl exec -it deploy/openvpn -- sysctl net.ipv4.ip_forward
# net.ipv4.ip_forward = 0

ゼロが診断を裏付けます。値はノードのランタイム設定から継承されることもなく、コンテナが作られるたびにイメージの既定値から始まります。そのため kubectl exec ... sysctl -w で手作業で適用した修正は次の再起動で消え、断続的に見える障害として戻ってきます。

マニフェストが保持するように有効化する

持続する選択肢は、同じ Pod sandbox で動き、VPN コンテナが起動する前に値を書き込む init コンテナです。Pod 内のコンテナはネットワーク namespace を共有するので、設定はメインコンテナに引き継がれます。

initContainers:
- name: enable-forwarding
  image: busybox:1.36
  command: ["sysctl", "-w", "net.ipv4.ip_forward=1"]
  securityContext:
    privileged: true

まさに privileged: true がレビュアーが反対する行であり、彼らは正しいのです。sysctl を書くには特権か狭く限定されたケーパビリティが必要で、制限付き Pod Security Standard を運用するクラスタは特権 init コンテナを一律に拒否することがあります。admission が Pod を拒否すれば作成時に分かるので、少なくとも正直で読みやすいです。

よりクリーンな代替は Pod レベルの securityContext.sysctls リストで、どのコンテナにも特権を与えずに kubelet が namespace 付き sysctl を適用できます。クラスタの許可リストにある名前だけを対象にします。net.ipv4.ip_forward がそのリストにあるかは kubelet の設定次第で、リスト外の sysctl を求める Pod は admission で拒否されるか、sandbox が再試行される間 CreateContainerConfigError で止まります。フィールドが壊れていると決めつける前に許可リストを確認してください。

手作業で組む前に、chart がすでに ipForwardInitContainer: true のような形でスイッチを公開していないか確認してください。chart の値を使えば override ファイルは小さく保たれ、次のアップグレードも生き延びますが、手でパッチした Deployment の編集にはできないことです。

後半:戻り経路とポリシー

転送を有効にするのは必要条件であり、十分条件ではありません。クライアントがクラスタネットワークにルートのないプライベートプールを使うなら、応答はトンネルへ戻る道を見つけられず、Pod はそのトラフィックを宛先に向けてマスカレードする必要があります。

ポリシーはその上に載ります。NetworkPolicy オブジェクトとクラウドのファイアウォールルールは、変換後にパケットが持つ送信元アドレスに対して評価されるため、クライアントプール向けに書いたルールはマスカレードを有効にすると静かに一致しなくなることがあります。

クライアントがクラスタ内のサービスだけに届けばよい場合、VPN をサービスの VIP で終端させるほうが Pod ネットワークへルーティングするより小さい設計です。クライアントプールを CNI のルーティングテーブルから完全に外に保てます。

失敗するホップをクライアントから切り分ける

外側へ作業し、最初の沈黙の段階で止めます。VPN ピアから始め、次にいずれかの Pod のアドレス、次にサービスの VIP、その後にだけ公開の宛先を試します。成功のたびに境界が 一ホップ進み、失敗のたびに調べる価値のある層が名指しされます。どのロールアウトの後でも exec で入って sysctl を読み直してください、というのも置き換えられた Pod はイメージの既定値から始まり、マニフェストになかった修正はすでに消えているからです。

Kubernetes のドキュメント クラスタでの sysctl の使用 には、Pod のセキュリティコンテキストで設定できる名前が列挙されており、admission が変更を拒否したときに最初に見る場所です。

関連記事