DockerのAppArmor適用失敗:プロファイルの読み込みとreadby権限で対処
UbuntuでCodex用のDockerコンテナを起動したところ、AppArmorプロファイルの適用に失敗した。原因は、起動時に指定したカスタムプロファイルがホストに読み込まれていなかったことだった。読み込むとコンテナは起動したが、続いてコンテナ内のbubblewrapで権限不足が見つかり、プロファイルの修正も必要になった。 この記事では、Dockerを使う技術者に向けて、拒否ログによる切り分けと対処を説明する。今回確認した問題は次の2段階だった。 発生段階 ログの手掛かり 対処 コンテナの起動時 label not found 指定したプロファイルをホストに読み込む 起動後のbubblewrap実行時 denied_mask="readby" 同じプロファイルのプロセス間にreadbyを許可する 調査はCodexに依頼し、ホストの設定変更とコマンド実行は自分で行った。修正後はコンテナへのプロファイル適用を確認し、提示した直近5分のログに拒否はなかった。その後、ホストの再起動後も自分で確認し、問題なかった。 この環境でのAppArmorとbubblewrapの役割 AppArmorは、プロセスが実行できる操作をプロファイルで制限するLinuxのセキュリティ機構だ。今回の環境では、Dockerコンテナにcodex-bwrapというカスタムプロファイルを適用していた。これは自分の環境で用意した名前で、Docker標準のプロファイル名ではない。 コンテナ内では、Codexのコマンド実行を制限するためにbubblewrap(実行ファイル名はbwrap)も使っている。コンテナが起動した後も、その中のプロセスはAppArmorの制約を受けるため、bubblewrapの動作に必要な操作をプロファイルで許可する必要がある。 また、プロファイルには次の3つの状態がある。設定ファイルがあるだけでは、コンテナに適用できない。 状態 今回の対象・確認方法 設定ファイルが存在する ~/work/codex-docker/codex-bwrap.apparmor ホストのLinux Kernelに読み込まれている apparmor_parserで読み込み、aa-statusで確認 コンテナに適用されている docker inspectのAppArmorProfileで確認 以下のコマンドは、Dockerデーモンが動いているUbuntuホストの通常のユーザー端末で実行する。パスとプロファイル名は自分の環境に合わせて読み替える必要がある。掲載するルールは既存プロファイルの変更箇所であり、プロファイル全体ではない。 発生したエラーと環境 実行したのは、自作のCodex用Docker起動スクリプトだ。このスクリプトは、コンテナの起動時にcodex-bwrapを指定する。 ~/work/codex-docker/run-codex.sh 出力は次のとおりだった。末尾のヘルプ案内は省略している。 Docker AppArmor setting: codex-bwrap docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: unable to apply apparmor profile: apparmor failed to apply profile: write fsmount:fscontext:proc/thread-self/attr/apparmor/exec: no such file or directory unable to apply apparmor profileから、コンテナを準備するOCIランタイムがAppArmorを適用する段階で失敗したと分かる。コンテナ内のCodexは、まだ動き始めていなかった。調査時に確認した環境は次のとおり。 項目 バージョン・状態 Ubuntu 26.04.1 LTS Linux Kernel 7.0.0-38-generic Docker Client / Server 29.9.0 / 29.9.0 Docker context default containerd v2.4.1 runc 1.5.2 AppArmor parser 5.0.2 AppArmor 有効、codex-bwrapを指定 起動時の拒否ログから、プロファイル未読み込みを確認する エラー末尾はno such file or directoryだが、Codexが確認すると、ホストの/proc/thread-self/attr/apparmor/execは存在していた。 原因を判断する手掛かりになったのは、Linux Kernelのログだった。 sudo aa-status | rg 'codex-bwrap' sudo journalctl -k -b --no-pager --grep='codex-bwrap|label not found' -n 30 aa-statusの抽出結果にcodex-bwrapの行はなく、ログには次の拒否が記録されていた。時刻やホスト名などを省略し、メッセージ部分を抜粋している。 apparmor="DENIED" operation="change_onexec" class="file" info="label not found" error=-2 profile="runc" name="codex-bwrap" pid=8853 comm="runc:[2:INIT]" ここでいうラベルは、AppArmorがプロファイルを識別するための名前だ。name="codex-bwrap"とinfo="label not found"から、起動時に指定したプロファイルを見つけられなかったと分かる。runc 1.5.2の実装では、exec属性への書き込みでプロファイルを指定する。今回の書き込みエラーは、パス自体が存在しないという意味には限定できなかった。 Codexが起動スクリプトを調べたところ、AppArmorを自動選択するCODEX_APPARMOR=auto設定は、DockerのAppArmor対応だけを確認していた。カスタムプロファイルの登録確認や読み込みは行っていなかった。Dockerの公式資料でも、カスタムプロファイルは先に読み込み、その後でコンテナに指定する手順になっている。 今回のaa-statusと拒否ログを合わせると、指定したプロファイルをホストに読み込む必要があった。ただし、以前の登録が再起動などで失われたのか、別の理由で未読み込みだったのかは特定できていない。 プロファイルを読み込んで起動エラーを解消した Codexがプロファイルの実ファイルを確認し、自分で次のコマンドを実行した。apparmor_parserは設定ファイルを解析してホストに読み込むコマンドで、-rは既存のプロファイルがあれば置き換える指定だ。 ここではファイルの場所が明確になるよう、絶対パスを使っている。管理者権限の実行環境では、$HOMEが通常ユーザーのホームを指すとは限らない。 sudo apparmor_parser -r -W /home/sonohen/work/codex-docker/codex-bwrap.apparmor sudo aa-status | rg 'codex-bwrap' 実行した結果、apparmor_parserの終了コードは0となり、aa-statusにもcodex-bwrapが表示された。コンテナのdocker inspectでも同じプロファイル名を確認でき、元のOCI起動エラーは解消した。 起動後の拒否ログから、readby権限を追加する コンテナへの適用を確認した後、Linux Kernelのログに別の拒否が出ていた。comm="bwrap"はbubblewrapのプロセスを示す。最初の起動エラーが解消しても、プロファイルの内容が実行時の操作に足りるとは限らなかった。 apparmor="DENIED" operation="ptrace" class="ptrace" profile="codex-bwrap" pid=11588 comm="bwrap" requested_mask="readby" denied_mask="readby" peer="codex-bwrap" 当時のプロファイルには、次のルールがあった。 ptrace (read, trace) peer=codex-bwrap, このログでは、profileとpeerがともにcodex-bwrapで、拒否された権限はreadbyだ。同じプロファイルが適用されたプロセス同士のアクセスで、読まれる側の許可が不足していた。 AppArmorのptraceルールは、デバッガによる操作に加え、一部の/proc経由のプロセス情報へのアクセスも制御する。readは相手の情報を読む側、readbyは相手に読まれる側の許可を表す。Ubuntu 26.04のAppArmorマニュアルでも、アクセスする側と受ける側の双方に許可が必要とされる。同じプロファイルのプロセス間でも、readだけでは足りなかった。 Codexの提案に従い、元ファイル/home/sonohen/work/codex-docker/codex-bwrap.apparmorの該当ルールを次のように変更した。 ptrace (read, readby, trace) peer=codex-bwrap, 許可対象はpeer=codex-bwrapのままにした。今回の拒否ログではtracedbyは要求されていないため、追加していない。 修正版をホストへ配置し、再起動後も読み込めるようにする apparmor_parserによる読み込みは、現在動いているLinux Kernelに対する操作だ。再起動後も読み込めるよう、修正版のファイルをホストのAppArmor設定ディレクトリ/etc/apparmor.d/codex-bwrapへ配置した。 作業時には、初回の配置で宛先ファイルが存在しないことを確認していた。その後のreadby追加では、配置済みのファイルをバックアップしてから更新した。以下の掲載例は、既存ファイルがある場合だけバックアップする形にまとめている。 バックアップは/var/backupsに置く。元の作業手順では/etc/apparmor.d内に置いていたが、AppArmor 5.0.2の実装では、.bak-日時は読み込みの除外対象にならない。古いファイルも読み込み対象になり得るため、掲載例では配置先を変更した。 sudo install -d -m 0700 /var/backups/codex-apparmor if [ -f /etc/apparmor.d/codex-bwrap ]; then sudo cp -a /etc/apparmor.d/codex-bwrap \ /var/backups/codex-apparmor/codex-bwrap.bak-$(date +%Y%m%d-%H%M%S) fi sudo install -m 0644 /home/sonohen/work/codex-docker/codex-bwrap.apparmor \ /etc/apparmor.d/codex-bwrap sudo apparmor_parser -r -W /etc/apparmor.d/codex-bwrap sudo aa-status | rg 'codex-bwrap' ホストのポリシー変更、配置、再読み込みは自分で実施した。Codexは元ファイルのreadby追加と、配置済みファイルとの一致を読み取りで確認した。 適用結果と再起動後の確認 配置と再読み込みが終わったら、コンテナを起動して適用結果を確認する。自作スクリプトのCODEX_APPARMOR=requiredは、AppArmorを必須にする設定だ。作業対象のディレクトリで次を実行する。 CODEX_APPARMOR=required ~/work/codex-docker/run-codex.sh 起動後、別の端末から対象コンテナと拒否ログを確認する。 docker ps docker inspect --type container --format '{{.AppArmorProfile}}' CONTAINER_ID sudo journalctl -k --since '5 minutes ago' --no-pager \ --grep='codex-bwrap|apparmor="DENIED"' CONTAINER_IDには、docker psで確認した実際のIDを指定する。期待する結果は、コンテナが起動し、AppArmorProfileがcodex-bwrapとなり、対象の操作時に新しい拒否ログが出ないことだ。確認に5分以上かかる場合は、--sinceの範囲を起動時刻まで広げる。 自分が提示した修正後の結果では、docker inspectの出力はcodex-bwrapだった。直近5分のログには次の更新通知だけがあり、DENIEDはなかった。メッセージ部分を抜粋する。 apparmor="STATUS" operation="profile_replace" profile="unconfined" name="codex-bwrap" pid=12838 comm="apparmor_parser" ここにあるprofile="unconfined"は、更新を行ったプロセス側の表示だ。コンテナに適用されたプロファイルはdocker inspectの結果で確認した。 ここまでの適用結果とログは調査記録に残っている。再起動後の自動読み込みは、その記録をまとめた時点では未確認だったが、その後、自分でホストを再起動して確認し、問題なかった。 ただし、当初行おうとしていたCodexの具体的な操作の成否は、調査記録には残っていない。コンテナ起動とAppArmor適用の確認だけで、Codexの全機能が動作したとは判断していない。また、修正後に拒否がなかったという判断は、提示したログの時間範囲に基づく。 同じ症状を調べるときの確認順序 今回の調査では、エラー末尾のno such file or directoryだけでは原因を判断できなかった。Linux Kernelのログでラベル解決失敗を確認し、読み込み後には別の権限不足を切り分けた。同じ症状を調べる場合は、次の順序で確認すると、今回の2つの問題を区別できる。 aa-statusと起動時の拒否ログで、指定したプロファイルが読み込まれているか確認する。 起動できたら、docker inspectでコンテナへの適用を確認する。 対象の操作を実行し、新しい拒否ログのprofile、peer、denied_maskから不足している許可を調べる。