デーモンは chroot の失敗を記録し、プロファイルはすでに寛大に見える。そこで魅力的な修正はパスのルールを広げて再起動することだ。しかしこれはたいてい何も変えない。chroot を制御しているのは、プロファイルが決して渡せなかった権限だからだ。どの層に触れるかを決めるには、三つの安価な確認が要る。プロセスが何を保持できるか、ポリシーが何を許すか、起動側がそのどちらも意味を持つ前に何を取り除いたか、である。

二つの別々の関門があり、AppArmor のものは一つだけ

chroot は、プロセスが動作しているユーザー名前空間の中で CAP_SYS_CHROOT を必要とする。これは通常の Linux の権限で、呼び出しがパスに対して何かを行う前にカーネルが確認する。これとは別に、強制モードの AppArmor プロファイルは、プロセスが権限を持っていてもそれを拒否できる。必須ポリシーは資格情報の代わりではなく、それに加えて参照されるからだ。この区別は実務で重要で、二つの層は別のファイルにあり、たいてい別の人が保守している。

その二つの関門は同じ errno で失敗する。どちらも EPERM を出し、どちらもデーモンのログでは一般的な権限の苦情として現れる。だからプロファイルの中身を推測すると多くの時間が無駄になる。追加しようとしていたパスのルールは、chroot が成功した後のファイルアクセスを制御するもので、その操作がそもそも許可されるかには関係しない。

第三の関門は両方の上流にある。サービスマネージャやラッパーが、そのバウンディングセットに権限がない状態でプロセスを起動したなら、AppArmor の変更では取り戻せない。プロファイルに権限を足すのは、カーネルが尊重しない意図を記録するだけだ。

ポリシーを編集する前に権限の状態を読む

信じている設定ではなく、実行中のプロセスから始める。状態ファイルは五つの権限マスクと no-new-privileges フラグを公開しており、それぞれ別の問いに答える。

grep -E '^(CapInh|CapPrm|CapEff|CapBnd|CapAmb|NoNewPrivs):' /proc/$(pidof openvpn)/status
sudo aa-status | grep -i openvpn

CapEff が呼び出しにとって重要な集合だ。CapBnd にあって CapEff にない権限は、上限が許しているが現在は保持していない。CapAmb は、バイナリが通常の setuid の意味で特権でないときに重要になる。環境権限は、サービスがファイル権限なしで exec をまたいで特権を保つ方法だからだ。CapInh は exec をまたいで継承されうるビットを持つが、実行されるファイルの継承可能集合が許す場合だけであり、そこにあるビットだけでは何も与えない。

次に、どのプロファイルが実際に付いているかを確認する。プロセスは予想しなかったプロファイル名で閉じ込められていることがあり、とくにディストリビューションが独自のユニットファイルとプロファイルを一緒に配布する場合にそうなる。/proc/PID/attr/current を読めば一行で決着する。編集したファイルをカーネルが報告するプロファイルと比べることは、原因を直すことと誰も読み込まないファイルを編集することの違いだ。

監査証跡がそう言うときだけ権限ルールを足す

カーネルと監査の記録は、拒否した層を名指しする。操作 capable と名前 sys_chroot を持ち、それを生んだプロファイルを伴う拒否を探す。その記録があれば、プロセスはすでに権限を持っていて AppArmor が拒否したのであり、狙うべき変更は実際のプロファイル内の権限ルールだ。

capability sys_chroot,

本当に付いているプロファイルの中に断片として置き、キーワードは無関係なチュートリアルからルールを写すのではなく apparmor.d のプロファイル参照 で確認する。共有 include に入れると、そのファイルを取り込むすべての閉じ込められたプログラムに付与してしまう。プロファイルを complain モードに切り替えるのは、この拒否を解決するどころか次の拒否を隠すだけだ。プロファイルのルールはポリシーの拒否を取り除くが、権限を製造しない。だから効果的な集合に CAP_SYS_CHROOT を一度も持たなかったプロセスは、ルールがあっても失敗する。

監査記録に AppArmor の拒否がまったく出ないなら、プロファイルの編集をやめる。資格情報の問題を見ているのであり、残る作業はユニットファイルか親プロセスにある。

サービスマネージャは独自の上限を持つ

systemd はサービスごとに権限のバウンディングセットを設定し、既定ではその集合は狭い。そこから除外されたものは、プロファイルが何と言おうとプロセスには使えない。だから強化されたユニットは、デーモンを手で起動したときには動いた操作で失敗することがある。関係するディレクティブはユニットの中で読みやすく、アプリケーションが失敗を報告するときには見落としやすい。ディストリビューションのパッケージが同梱する drop-in が、誰も設定した覚えのない上限の通例の出所だ。

上限を上げることと権限を与えることは、一つの変更ではなく二つだ。バウンディングセットの項目は、プロセスが権限を保持してよいと言うだけで、効果的な集合には加えない。NoNewPrivs は、ほかがすべて揃っていても後の昇格を失敗させうる。操作が必要とするものだけを足し、残りの強化はそのままにし、一つの呼び出しを通すためにすべての権限を与える近道に屈しないことだ。

ここでも順序が重要だ。まずユニットの上限を変え、CapEff にビットが入ったことを確認し、それから AppArmor がまだ拒否するかを決める。両方を一度に変えると、失敗が続いたときにまた推測することになる。

拒否が消えたら、監獄そのものを確認する

chroot が成功するとルートが移動するので、デーモンがその後触るパスはすべて新しいツリーの中で解決される。設定の欠落、存在しないデバイスノード、コピーされなかった証明書ディレクトリは、これからファイルのエラーとして現れる。そのエラーは権限の問題ではなく配置の問題だ。広い許可をさらに足すのではなく、次のイベントを読む。移動は相対パスの意味も変えるので、強化の手引きはデーモンの設定で絶対パスを求める。

chroot が何をしないかも覚えておくとよい。これはパスの境界であり、サンドボックスではない。権限を保持したプロセスは脱出できる。だから権限は通常、呼び出しの後に落とされ、閉じ込めはデーモンの周りに残すべきなのだ。AppArmor プロファイル、権限のバウンディングセット、ファイルシステムの制限を一緒に保つことがこの演習の目的であり、サービスが起動したら外す残り物ではない。

判断のルールは短い。監査記録が AppArmor を名指しするなら、付いているプロファイルに権限ルールを一つ足す。効果的な集合が空なら、ユニットの上限と権限を落とす順序を直す。プロファイルが編集したものと違うなら、編集をやめて本物を探す。

関連記事