NET_ADMIN はプロセスにネットワーク管理を行わせるLinuxのケーパビリティであり、コンテナではプロセスが動作するネットワーク名前空間の内側でまさにそれを行います。ソケットを開くためのライセンスではなく、デバイスやルートの代わりでもありません。興味深い問いは、ケーパビリティが与えられているかどうかではなく、どの名前空間が変更を受け取るかです。

ケーパビリティが覆う範囲

自分の名前空間の内側では、NET_ADMIN を持つプロセスはインターフェースを上げ下げし、アドレスを割り当て、ルートを追加・削除し、ファイアウォールのルールを書くことができます。これは capabilities(7) がこのケーパビリティの下に集めている操作群です。カーネルがトラフィックをどうキューに入れてフィルタするかを制御するインターフェース単位の設定も調整できます。VPNクライアントは通常、起動時にそのうちのいくつかを必要とし、その後は何も必要としません。そのためほとんどのイメージは、使う以上の権限を要求します。

その名前空間の外の操作は対象外であり、隣接する特権は別個です。生パケットの読み書きは NET_RAW に属し、tunデバイスの作成にはケーパビリティと デバイスノードへのアクセス の両方が必要です。デバイスを持たないプロセスは、ネットワークの問題に見えてそうではない権限エラーを受け取ります。

名前空間が影響の範囲を決める

独自のブリッジネットワーク上のコンテナは、自分のloopback、自分のインターフェース、自分のルーティングテーブルに到達します。そこに NET_ADMIN を与えると、VPNクライアントはホストのルーティング判断に触れずにトンネルを構築でき、これはほとんどのコンテナイメージが前提とする構成です。

そのコンテナをホストネットワークに切り替えると、同じ付与がホストのルートとホストのファイアウォールを編集するようになります。別の名前空間が残っていないからです。ひとつのトンネルを管理するはずだったクライアントは、マシン全体のトラフィックを転送できるようになり、そのteardownスクリプトの誤りは誰も書いていないルールをホストに残します。ケーパビリティ自体は何も変わっていません。変わったのは文脈です。同じcomposeファイルが二台のホストで違う振る舞いをするのはそのためです。

ひとつにまとめられる三つの前提条件

VPNコンテナの起動失敗は通常、三つの別個の事柄のうちひとつに関係します。ケーパビリティ、トンネルデバイス、プロトコルへのカーネル対応です。三つを一度に与えれば動きますが、次の失敗を帰属できなくもします。最小限のcompose断片はケーパビリティを与え、デバイスノードを明示的に渡します。

services:
  vpn:
    image: example/vpn-client:latest
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun
    sysctls:
      - net.ipv4.conf.all.src_valid_mark=1

どのsysctlが必要かはイメージ自身のドキュメントを読んでください。コンテナ内のsysctlは追加の特権やホスト側の設定を必要とする場合があるからです。イメージがユーザースペースのみのモードをサポートする場合、デバイスはまったく不要で、ケーパビリティの一覧もそれに応じて小さくなります。

モードではなく狭い操作を与える

特権モードはすべてのケーパビリティを渡し、同時にデバイスへのアクセスも広げるため、三つの前提条件のどれが欠けていたかを隠します。コンテナの失敗の仕方もホストの変更と区別できなくなります。書くのが長くなっても明示的な一覧を選んでください。

rootlessやユーザー名前空間への配備では、ケーパビリティの実際の意味が変わります。プロセスはマップされた名前空間の内側でしかそれを保持できないからです。rootfulの例からケーパビリティ一覧をrootlessの実行環境にコピーして同じ権限を期待するのは、午後を失うよくある方法です。

秘密は別の判断に属します。NET_ADMIN を保持していても、クライアントがどのピア識別子を提示し、どの資格情報を読み込み、それらをどうローテーションするかについては何も言えません。ネットワーク管理は信頼を供給しません。

変更を検証し、付与がなぜ存在するかを記録する

トンネルをデバッグする前に、コンテナが実際に何を受け取ったかを確認してください。以下の検査は追加されたケーパビリティとネットワークモードを報告し、次に内側から見たインターフェース一覧を示します。

docker inspect --format '{{.HostConfig.CapAdd}} {{.HostConfig.NetworkMode}} {{.HostConfig.Privileged}}' vpn
docker exec vpn sh -c 'grep CapEff /proc/self/status'
docker exec vpn ip -br addr
docker exec vpn ip route

ケーパビリティがあり、インターフェース一覧がまだ空なら、クライアントは何かを作る前に失敗しており、次に見る場所はコンテナのログです。トンネルはあるのにトラフィックが流れないなら、コンテナ内のルーティングテーブルが次の問いに答えます。

teardownの経路も試してください。ファイアウォールのルールを書くクライアントはそれを残すことがあります。コンテナを停止し、ホストネットワークを使っている場合はホストのルールセットを再確認してください。起動時に二秒だけ必要な付与を、それを説明する注記なしにcomposeファイルで恒久的な要件として書くべきではありません。

各ケーパビリティのそばに理由を書いてください。どの操作に必要で、それがなければ何が失敗するのか。その注記はイメージの更新を越えて残り、レビュー済みの特権一覧とコピー&ペーストで増えた一覧の違いになります。

関連記事