タグアーカイブ AppArmor

DockerのCopy Fail脆弱性対応とseccomp破壊の教訓

DockerのCopy Fail脆弱性対応とseccomp破壊の教訓

2026年4月末に公開されたLinuxカーネルの脆弱性CVE-2026-31431、通称「Copy Fail」は、2017年以降のほぼ全てのカーネルに影響する深刻な問題だ。Docker社はこの脆弱性に対し、コンテナランタイムレベルでの緩和策を急ピッチで提供した。

その過程で、Docker Engine v29.4.2の修正が32ビットバイナリのネットワーク機能を完全に破壊するという予期せぬ副次的被害を引き起こした。本記事では、この一連の対応と教訓を技術的に深掘りする。

コンテナ運用者は、カーネルパッチの適用が最優先だが、それが叶わない場合でもDocker Engineのアップデートによりリスクを大幅に低減できる。ここで得られた知見は、今後のコンテナセキュリティ対策における多層防御の重要性を浮き彫りにしている。

Copy Fail脆弱性の仕組みとリスク

Copy Fail脆弱性の仕組みとリスク

AF_ALGサブシステムの欠陥

Copy Failは、Linuxカーネルの暗号処理をユーザー空間から利用するためのAF_ALG(Algorithm Sockets)サブシステムに存在する。具体的にはalgif_aeadモジュールの不具合により、特権のないプロセスがページキャッシュに対して不正な書き込みを行える状態になっていた。

ページキャッシュとは、ファイルの読み取りデータをメモリ上に一時保存する仕組みだ。全プロセスが参照するため、ここを汚染されると、システム全体でファイルの内容が改ざんされて見える可能性がある。最も直接的な攻撃経路は、setuidバイナリ(実行時に高い権限で動作するプログラム)の改ざんによる権限昇格である。

この脆弱性の深刻さは、その単純さにある。PoC(概念実証コード)はきわめて簡潔で、カーネルがパッチされていない限り、2017年以降のあらゆるバージョンで動作する。攻撃が成功すると、コンテナ内からホスト全体、そして同じノード上の他コンテナにまで影響が及ぶのだ。

脆弱性の悪用フロー(Before)
攻撃者 AF_ALGソケット作成 ページキャッシュ汚染 setuid改ざん root権限取得
※ デフォルトのコンテナ権限で実行可能。約7年にわたり影響。
緩和策適用後(After)
攻撃者 AF_ALGソケット作成 システムコールブロック
※ 多層防御によりAF_ALGへの経路が遮断される

このデモが示す通り、脆弱なカーネルでは攻撃者に一直線のルートを提供してしまう。Dockerの役割は、コンテナランタイムのレイヤーでこの経路を物理的に塞ぐことだった。

コンテナ環境への影響範囲

Dockerのデフォルトセキュリティプロファイルでは、コンテナからのAF_ALGソケット作成が許可されていた。つまり、攻撃者が何らかの方法でコンテナ内でコードを実行できた場合、この脆弱性を利用してホストのroot権限を奪取できる状態にあった。

さらに悪いことに、ページキャッシュはホスト全体で共有される。攻撃が成功した場合、被害はそのコンテナ内に留まらず、同じDockerイメージのレイヤーを共有する他のすべてのコンテナにも波及する。これは、マルチテナント環境やマイクロサービスを密に配置しているノードでは壊滅的な被害につながりかねない。

Docker Engineの対応と失敗の分析

Docker Engineの対応と失敗の分析

v29.4.2 seccomp修正の試み

Dockerチームは当初、seccomp(Secure Computing Mode / セキュアコンピューティングモード)プロファイルの更新で対応しようとした。seccompは、コンテナが発行できるシステムコールをフィルタリングする仕組みだ。具体的には、socket()システムコールの第一引数を検査し、AF_ALGアドレスファミリが指定された場合に拒否するルールを追加した。

しかし、x86_64 Linuxにはsocketcall()という古い多重化システムコールが存在する。これはsocket()bind()などの複数のソケット操作を一つのシステムコール番号の背後にまとめたものだ。問題は、socketcall()では実際の引数(アドレスファミリを含む)がユーザー空間の配列にパックされ、そのポインタが渡されることだ。seccompのフィルタエンジンであるBPFは、このポインタ先を参照して検査できない。

つまり、seccompだけではsocketcall()経由のAF_ALGを選択的にブロックできない。やむを得ずDockerチームは、socketcall()全体を拒否するという決断を下し、v29.4.2をリリースした。

v29.4.2でのブロック範囲(Bad)
socket(2) AF_ALG拒否 socketcall(2) 全体拒否
※ 32bitバイナリのネットワーク機能が破壊。SteamCMDやWineが動作不能に。
v29.4.3でのブロック範囲(Good)
AppArmor AF_ALG選択拒否 SELinux AF_ALG選択拒否 socketcall 許可(32bit互換性維持)
※ LSMを活用し、通常のネットワーク機能は維持したままAF_ALGだけを遮断。

この比較が示すのは、セキュリティ対策における粒度の重要性だ。全体をブロックすれば安全だが、システムの機能を破壊する。真に効果的な対策は、悪意のある操作だけをピンポイントで無効化することにある。

32bitバイナリ破壊の実態

socketcall()の一律拒否は、思わぬ大規模な副次的被害を引き起こした。32bit版のglibcは、すべてのソケット操作をsocketcall()経由で行う古いバージョンが残っている。Go言語のランタイムも、GOARCH=386でビルドされたバイナリでは無条件にsocketcall()を利用する。さらに、SteamCMDやWineといったレガシー・ゲーミング系のワークロードも、この仕組みに依存している。

これは単なるi386(32bit)の問題ではない。amd64環境であっても、プロセスはint $0x80命令を使うことでia32互換モードに切り替わり、直接socketcall()を呼び出せる。つまり、64bitのコンテナやバイナリを使っていても、この経路を利用される可能性があるのだ。

結果として、v29.4.2へのアップグレード後に多数の32bitアプリケーションがネットワークに接続できなくなるというインシデントが発生した(GitHub Issue: moby/moby#52506)。セキュリティパッチが新たな機能不全を引き起こすという、運用者にとって最も避けたいシナリオが現実となった。

根本原因:seccompの限界

この問題の本質は、seccompがシステムコール境界でのみ動作する点にある。socketcall()は一つのシステムコール番号の背後に多種の操作を隠蔽する。seccompのフィルタは、その中身である配列のポインタ先を解析できない。これが、seccomp単独では対応できない構造的な限界だ。

Dockerのブログ記事の著者は、「seccompはsocket(AF_ALG)をすべてのシステムでブロックするが、socketcall()に対しては盲目だ」と端的に表現している。この「見えない経路」の存在が、多層防御の必要性を強く示す教訓となった。

v29.4.3 LSMベースの恒久対策

v29.4.3 LSMベースの恒久対策

AppArmorとSELinuxによる多層防御

v29.4.3では、より根本的な解決策としてLinuxセキュリティモジュール(LSM)を活用する方針に切り替えた。AppArmorとSELinuxは、カーネル内部のsecurity_socket_create()コールバックに直接フックする。このコールバックは、socket()経由であれsocketcall()経由であれ、カーネルが実際にソケットオブジェクトを生成する瞬間に必ず呼ばれる。システムコールの入り口ではなく、より深いレベルで制御を行うのだ。

具体的な実装として、AppArmorプロファイルには deny network alg, というルールが追加された。これはAF_ALGアドレスファミリだけを対象に拒否する。SELinux環境向けには、すべてのcontainer_domainタイプに対してalg_socketの作成を拒否するCIL(Common Intermediate Language)ポリシーモジュールが提供され、semoduleコマンドでロード可能だ。

対策の全体像と適用優先度

v29.4.3の防御スタックは、以下の3層で構成されている。seccompによる直接のsocket(AF_ALG)ブロックは防御の一層目として維持しつつ、AppArmorまたはSELinuxによってsocketcall()経由の抜け道を塞ぐ。これにより、どちらか一方の防御層が無効化されても、もう一方がカバーする体制を実現した。

ただし、AppArmorやSELinuxはホストの設定に依存するため、LSMが有効化されていない環境ではsocketcall()経路が無防備なままとなる。この点については、依然としてカーネルパッチが唯一の完全な解決策であることに変わりはない。

STEP 1 Linuxディストリビューションのカーネルパッチを適用する
STEP 2 Docker Engineをv29.4.3以上にアップグレードする(再起動不要)
STEP 3 アップグレード不可ならカーネルモジュールをブラックリスト化する
STEP 4 それも不可ならカスタムseccompプロファイルを適用する

このステップを踏むことで、カーネルパッチの提供を待つ間のリスクを段階的に低減できる。最優先はカーネル修正だが、それが叶わない状況でもDocker Engineの更新だけで強固な緩和策となる。

コンテナセキュリティのための実践的教訓

コンテナセキュリティのための実践的教訓

ランタイム更新のスピードが生む防御力

Copy Failのケースで特筆すべきは、脆弱性の詳細が公表された時点で、主要ディストリビューションの多くはカーネルパッチを提供できていなかった点だ。Ubuntuは記事執筆時点で未対応であり、DebianやRHEL 9が対応を発表した段階だった。この数日間のギャップにおいて、コンテナランタイムの更新は唯一の実用的な緩和策だった。

コンテナ運用においてDocker Engineを最新に保つことは、単なる機能向上のためではない。カーネル脆弱性の公開からパッチ適用までの「空白期間」を埋める、最も迅速な防御手段の一つなのだ。

多層防御の絶対的必要性

このインシデントは「単一の防御層に頼ることの危険性」を端的に示した。seccompは強力だが、システムコールの粒度でしか制御できない。AppArmorやSELinuxはカーネル内部のオブジェクト生成にフックするため、より精密な制御が可能だが、ホストOSの設定に依存する。両者を組み合わせることで初めて、互いの死角を補完できる。

また、v29.4.2のsocketcall()拒否が引き起こした互換性問題は、セキュリティと互換性のトレードオフの難しさを教えている。広範なブロックは新たな問題を生む。可能な限りピンポイントな制御を追求し、やむを得ず広範な制限をかける場合は、その影響範囲を事前に十分評価する必要がある。

単層防御のリスク(Bad)
seccompのみ socket(2)ブロック socketcall()経由で突破
※ seccompはポインタ先を検査できず、抜け道を許す。
多層防御の有効性(Good)
seccomp socket(2)ブロック + AppArmor 両経路でAF_ALG拒否
※ カーネル内部フックにより、システムコールの種類を問わず遮断。

この比較から得られる教訓は明確だ。セキュリティ対策は、異なるレイヤーで相互に補完し合う設計が必須である。一つの仕組みで完璧を目指すのではなく、それぞれの得意領域を理解し、弱点を他の層でカバーする。これこそがコンテナセキュリティの基本原則である。

この記事のポイント

  • CVE-2026-31431はAF_ALGソケットを悪用し、2017年以降のLinuxカーネルに影響する
  • Docker v29.4.2のseccomp修正は32bitバイナリのネットワークを破壊する副次的被害を起こした
  • v29.4.3ではAppArmorとSELinuxを組み合わせた多層防御で選択的なAF_ALG遮断を実現
  • カーネルパッチが最も確実な修正だが、エンジン更新が迅速な緩和策として有効
  • 単一防御層の限界を認識し、複数の技術で死角を補完する設計が今後の鉄則となる