塞がれる時点が違えば、直す場所も違う
一言でいうと
設定を書く練習と、その設定が守られていることを確認する練習は別物です。事故が起きるのは、いつも後者です。
なぜ強制される場所でやる必要があるのか
前のモジュールで、securityContextとPod Securityを何度も使いました。ところが、それらのラボが動く場所にはkubeletもコンテナランタイムもなく、違反したPodもそのままRunningになりました。
設定を書く練習はできましたが、その設定が守られていることを確認する練習はできていません。そして、実務で事故が起きるのは後者です。設定をすべて書き終えたのに適用されていない状態が、正常に見えるからです。
同じ「できない」が3つの層に散らばっている
| 層 | ここにあるもの | 症状 | いつ見えるか |
|---|---|---|---|
| アドミッション | Pod Security Admission、ポリシーエンジン | kubectl applyが拒否される |
デプロイ前(CI) |
| kubelet | runAsNonRoot、イメージのUSERの検査 |
Podは作成されるが、CreateContainerConfigErrorになる |
デプロイ直後 |
| ランタイム | 読み取り専用のルート、capability、seccomp | PodはRunningで、アプリだけが失敗する |
そのコードパスを通るとき |
最も良いのはアドミッションです。フィードバックがすぐに返ってきて、誤ったものがそもそもクラスターに入ってきません。
最も悪いのはランタイムです。Podは何事もなく動いているため、症状がアプリケーションのエラーに見えます。普段は問題なく動くのに、特定のコードパスでだけ失敗することもよくあります。
そのため、順序があります。前の層で止められるものを、後ろの層に任せません。
Pod Security Admissionはwarnから
enforceをいきなり設定すると、すでに動いているワークロードが次の再デプロイですべてブロックされます。それ自体が障害です。
pod-security.kubernetes.io/warn: restricted # 먼저 이것만
pod-security.kubernetes.io/enforce: restricted # 경고가 0 이 된 뒤에
遮断してから数えるのではなく、数えてから遮断する順序です。
コンテナを絞ると何が壊れるのか
セキュリティ設定を有効にしたときに実際に何が失敗するかを知っておけば、障害を経験する前にイメージ側を直せます。よくぶつかるものだけをまとめます。
読み取り専用のルートファイルシステム。ほとんどのアプリは、どこかに一時ファイルを書き込みます。ログ、キャッシュ、ソケット、そして言語ランタイムが使う一時ディレクトリです。ルートを読み取り専用にすると、これらがすべて塞がれますが、症状はたいていアプリケーションの権限エラーとして現れるため、セキュリティ設定と結びつけるまでに時間がかかります。対処法は、書き込みが必要なパスごとにemptyDirを付けることで、どこに書き込むかは推測せず、絞った状態で一度起動して失敗したパスを集めるほうが速いです。
非rootユーザーでの実行。1024未満のポートを開けないという点が、最初に引っかかります。コンテナの中でポート80をリッスンしていたイメージは8080に変更する必要があり、ServiceのtargetPortも一緒に直す必要があります。また、イメージ内のファイルの所有者がrootになっていると、新しいユーザーが読めないことがあるため、イメージを作るときに所有権も一緒に引き渡す必要があります。
capabilityをすべて落とす。ほとんどのアプリは何も必要としませんが、低いポートを開く必要があればNET_BIND_SERVICE、pingを使うならNET_RAWが必要です。必要なものだけを載せ直す順序で進めるべきで、何が必要かわからないまますべてを残していては、絞る意味がありません。
seccompのデフォルトプロファイル。許可されていないシステムコールはEPERMで返ってきて、ライブラリがそれを握りつぶすと、症状が見当違いの形で現れます。このラボ環境でコンテナ関連のラボが制限されている理由も、同じ系統です。
まとめると、順序はこうです。絞った設定でまず起動して何が失敗するかを集め、失敗した分だけ例外を開け、その例外の一覧をイメージ側で減らしていきます。逆に、「とりあえずすべて緩めておき、あとで絞る」で始めると、その「あとで」は来ません。
実務で本当に大切なこと
前の層で止められるものを、後ろの層に任せません。アドミッションで引っかかればCIですぐにわかりますが、ランタイムで引っかかると、Podは問題なく動いたまま、特定のコードパスでだけ失敗して、アプリケーションのエラーのように見えます。同じルールでも、どこで強制するかによって、対応コストが10倍変わります。
enforceは、警告が0になってから設定します。いきなり設定すると、すでに動いているワークロードが次の再デプロイですべてブロックされ、それ自体が障害です。warnで先に数えてから遮断する順序を守る必要があります。
runAsNonRootの失敗は、Podの作成失敗には見えません。Podは作成され、CreateContainerConfigErrorだけが残るため、イメージのUSERを疑うまでに時間がかかります。イメージを作るときにUSERを明示しておくのが、最も安上がりな予防です。
次のラボで、この3つを本物のクラスター上で1つずつ確認します。