Claude CodeでsecurityContextを足しkube-linterで確かめる
既存のDeploymentにrunAsNonRoot・readOnlyRootFilesystem・capabilities dropを足し、起動失敗を直しながらkube-linterの指摘を0件にする手順です。
既存のDeploymentにsecurityContextを足す作業は、書くこと自体は数行です。難しいのは足した後で、readOnlyRootFilesystem: trueにした途端にアプリが書き込み先を失って落ちます。Claude Codeにマニフェストを直させ、kube-linterで指摘を数え、起動失敗はPodのログを見て直す。この往復を回すと、lintを通すだけでなく実際に起動するところまで閉じられます。
この記事は、runAsNonRoot・readOnlyRootFilesystem・capabilitiesのdropを既存のDeploymentに足し、指摘0件にするまでを順に追います。
足す設定とkube-linterのチェックの対応
kube-linterは、Kubernetesのマニフェスト、Helmチャート、Kustomizeを読んで、本番運用とセキュリティの観点で指摘するlintツールです。指摘が出ると、直し方の案を添えて、終了コードが0以外になります。
今回足す設定は4つで、それぞれ対応するチェックがあります。表の最後の行は、非rootで動かすと関係してくる既定無効のチェックです。名前とdefaultの有無は、kube-linterのチェック一覧に載っているとおりです。
| 足す設定 | 対応するチェック | 既定で有効 |
|---|---|---|
runAsNonRootと非0のrunAsUser / runAsGroup | 対応するチェックrun-as-non-root | 既定で有効はい |
readOnlyRootFilesystem: true | 対応するチェックno-read-only-root-fs | 既定で有効はい |
allowPrivilegeEscalation: false | 対応するチェックprivilege-escalation-container | 既定で有効はい |
capabilities.drop | 対応するチェックdrop-net-raw-capability | 既定で有効はい |
| 1024以下のポートを使わない | 対応するチェックprivileged-ports | 既定で有効いいえ |
drop-net-raw-capabilityは名前のとおりNET_RAWだけを見ます。dropの対象を広げたいときは、チェックの元になっているテンプレートverify-container-capabilitiesのforbiddenCapabilitiesに値を足して、自前のチェックを作ります。
ステップ1: kube-linterを入れて現状を測る
インストール方法は複数あります。READMEには、Go、Homebrew(macOSとLinux)、nix-shell、Dockerが載っています。
# macOS / Linux
brew install kube-linter
# Go を使う場合
go install golang.stackrox.io/kube-linter/cmd/kube-linter@latest
# Docker を使う場合
docker pull stackrox/kube-linter:latest入れたら、足す前のマニフェストにかけます。k8s/にあるYAMLを丸ごと対象にできます。
kube-linter lint k8s/
echo "exit=$?"出力の形はREADMEの例がそのまま参考になります。次はREADMEにあるpod.yamlの例で、指摘ごとに「オブジェクト名、内容、check名、remediation」が1行で並びます。
pod.yaml: (object: <no namespace>/security-context-demo /v1, Kind=Pod) container "sec-ctx-demo" does not have a read-only root file system (check: no-read-only-root-fs, remediation: Set readOnlyRootFilesystem to true in the container securityContext.)
Error: found 3 lint errorsこれは公式READMEの例の抜粋です。自分のマニフェストでは、check名ごとに件数が出ます。この時点の件数を控えておくと、Claude Codeの修正が効いたかを数字で比べられます。
ステップ2: Claude Codeに足させる
指示は、設定名とlintのcheck名を両方入れて書きます。check名を渡しておくと、直した結果をClaude Code自身がlintで確かめ直せます。
k8s/deployment.yaml の api コンテナに securityContext を足して。
- runAsNonRoot: true、runAsUser / runAsGroup は 10001
- readOnlyRootFilesystem: true
- allowPrivilegeEscalation: false
- capabilities.drop: ["ALL"]
足したら `kube-linter lint k8s/` を実行して、run-as-non-root・
no-read-only-root-fs・privilege-escalation-container・
drop-net-raw-capability が消えたか確認して。出てくるYAMLは、例えば次のような形になります(公式の例に沿った一例です)。Podレベルに共通の実行ユーザーを置き、コンテナレベルに個別の制限を置く分け方です。
spec:
template:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
containers:
- name: api
image: registry.example.com/api:1.4.2
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]Kubernetesの公式チュートリアルによると、コンテナ側のsecurityContextはそのコンテナだけに効き、Pod側と重なる項目はコンテナ側が優先されます。Podに共通のユーザーを置き、コンテナで上書きする使い方ができます。なおcapabilitiesを書くときは、CAP_の接頭辞を外した名前で書きます。
allowPrivilegeEscalation: falseは、プロセスが親より多くの権限を得ることを防ぐ設定です。kube-linterのprivilege-escalation-containerは、これに加えてprivilegedをfalseにすること、CAP_SYS_ADMINを外すことを直し方として挙げています。privileged-containerも既定で有効なので、特権コンテナが残っていれば別のcheck名で指摘されます。
imageにタグを付けているのは、kube-linterのlatest-tagチェックがlatestや、タグなしのイメージを指摘するためです。READMEの例でもbusyboxがこれで指摘されています。
Podレベルとコンテナレベルの分け方
どの設定をどちらに置くかは、迷いやすい点です。実行ユーザー(runAsUser、runAsGroup、runAsNonRoot)とfsGroupはPod全体に効くので、Podレベルに1回書けば足ります。readOnlyRootFilesystem、allowPrivilegeEscalation、capabilitiesはコンテナごとの設定なので、コンテナレベルに書きます。サイドカーがあるDeploymentでは、コンテナごとに書き忘れが出ます。
kube-linterのrun-as-non-rootの説明は、PodまたはコンテナのsecurityContextに設定するとしています。どちらに書いても検査は通るので、書き方の統一はClaude Codeへの指示とCLAUDE.mdで決めておきます。
適用後にrootで動いていないことを確かめる
lintはYAMLの記述を見るだけで、実行中のプロセスのユーザーまでは見ません。Kubernetesの公式チュートリアルは、コンテナに入ってps auxでプロセスのユーザーを確かめる方法を示しています。同じ確認を、起動したPodに対して行います。
kubectl exec -it <pod名> -- ps aux先頭のUSER列が、runAsUserに指定したID(ここでは10001)になっていれば反映されています。psがないイメージでは、この確認は使えません。その場合はkubectl exec <pod名> -- idのように、イメージにあるコマンドで代えます。
ステップ3: 起動失敗を直す
lintが通っても、Podが起動するとは限りません。lintはマニフェストを読むだけで、アプリが実際にどこへ書き込むかは知らないからです。適用してから、次の順に見ます。
kubectl apply -f k8s/deployment.yaml
kubectl get pods
kubectl describe pod <pod名>
kubectl logs <pod名>kubectl describeでPodが起動できない理由とイベントを見て、起動した後に落ちるならkubectl logsでアプリ側のエラーを見ます。この2つの出力をClaude Codeに貼ると、原因の切り分けを任せられます。起動失敗の典型は2つです。
書き込み先がなくて落ちる
readOnlyRootFilesystem: trueはルートファイルシステムを読み取り専用にするので、/tmpやキャッシュ用のディレクトリへ書くアプリは書き込みエラーで落ちます。直し方は、書き込みが要る場所だけをemptyDirのボリュームとして載せることです。
containers:
- name: api
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /var/cache/app
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}書き込み先の調べ方は、ログに出たパスを拾うのが確実です。Claude Codeには「ログのエラーに出た書き込み先を列挙して、必要最小限のemptyDirだけを足して」と頼みます。ルート全体に書き込み権を戻す修正は、設定を足した意味が消えます。readOnlyRootFilesystemをfalseに戻す案が返ってきたら、指示から外して書き込み先の列挙に戻します。
イメージのユーザーがrootのまま
runAsNonRoot: trueは、rootで動くイメージを拒否します。イメージ側がrootのままUSERを決めていない場合は、runAsUserで非0のIDを明示すれば動くことがあります。ただしそのIDで読み書きできないファイルがイメージに残っていると、別の権限エラーに変わります。
ボリュームの所有者は、PodのfsGroupが効きます。公式の説明では、fsGroupに対応するボリュームは、指定したGIDが所有者になり書き込み可能になります。ステップ2の例にfsGroupを入れたのはこのためです。
1024以下のポートで待ち受けるアプリは、非rootでは待ち受けに失敗することがあります。kube-linterのprivileged-portsチェックは既定で無効ですが、有効にすると0から1024のポートのマッピングを指摘します。ポートをアプリ側で8080などに変えるのが確実です。
ステップ4: 設定ファイルで残りを閉じる
指摘が少数になったら、設定ファイルで扱いを決めます。kube-linterは、カレントディレクトリの.kube-linter.yamlか.kube-linter.ymlを自動で読みます。
checks:
addAllBuiltIn: true
exclude:
- "unset-cpu-requirements"
- "unset-memory-requirements"addAllBuiltIn: trueで組み込みのチェックを全部有効にし、excludeで外すものを書く構成です。excludeはincludeより優先されます。逆に、今回の4設定に関わるチェックだけを見たいなら、doNotAutoAddDefaults: trueとincludeを組み合わせます。
checks:
doNotAutoAddDefaults: true
include:
- "run-as-non-root"
- "no-read-only-root-fs"
- "privilege-escalation-container"
- "drop-net-raw-capability"
- "privileged-container"どうしても例外が要るオブジェクトには、アノテーションで指摘を外せます。キーはignore-check.kube-linter.io/<check名>で、値に理由を書くことが推奨されています。
metadata:
annotations:
ignore-check.kube-linter.io/no-read-only-root-fs: "起動時に /opt/app へ設定を生成するため。emptyDir 化は次回対応"特定のオブジェクトのすべてのチェックを外すkube-linter.io/ignore-allというキーもありますが、範囲が広すぎるので、チェック単位で外す形を先に検討します。理由の文字列は、後からマニフェストを読む人にもClaude Codeにも効きます。理由がなければ、次にClaude Codeがlintを回したとき、例外の経緯が分からないまま残ります。
この往復をCLAUDE.mdに覚えさせる
同じ手順を毎回指示するのは手間なので、プロジェクトのCLAUDE.mdに書いておきます。
## Kubernetes マニフェスト
- 変更後は必ず `kube-linter lint k8s/` を実行し、指摘が 0 件になるまで直す
- securityContext は readOnlyRootFilesystem を true にし、書き込み先は emptyDir で足す
- lint を通すためだけに readOnlyRootFilesystem や runAsNonRoot を外さない
- 例外にするときは ignore-check アノテーションに理由を書くCIにも載せるなら、kube-linterは指摘があると終了コードが0以外になるので、そのままジョブの失敗として扱えます。Claude Codeの非対話モードは、標準入力を受け取って標準出力に返せます。公式の共通ワークフローではgit log --oneline -20 | claude -p "..."の形が例に出ています。lintの出力を同じ形でClaude Codeに渡し、修正案を出させることもできます。
kube-linter lint k8s/ 2>&1 | claude -p "この指摘を直す修正方針を check 名ごとに書いて"Dockerfileの指摘をhookで返して直させる流れは、hadolintをhookで使う記事に同じ型の手順があります。YAMLを保存した直後にlintを走らせたいなら、そのhookの考え方をkube-linterに置き換えられます。
症状からどこを見るか
| 症状 | 見る場所 | 直し方 |
|---|---|---|
no-read-only-root-fsが消えない | 見る場所コンテナのsecurityContextにreadOnlyRootFilesystemがあるか | 直し方対象コンテナのsecurityContextに足りない項目を足す |
run-as-non-rootが消えない | 見る場所runAsUser / runAsGroupが非0か、runAsNonRootがtrueか | 直し方Pod側かコンテナ側のどちらかに揃える |
| lintは通るがPodが起動しない | 見る場所kubectl describe pod | 直し方イメージのユーザー、fsGroup、ポートを見る |
| 起動はするがすぐ落ちる | 見る場所kubectl logs | 直し方出力に出た書き込み先をemptyDirにする |
| lintの件数が増えた | 見る場所変更の差分 | 直し方latest-tagやunset-memory-requirementsなど別チェックを確認する |
設定を足したマニフェストの書き方そのものはClaude CodeでKubernetesマニフェストを書く手順に、クラスタに接続するときの権限の絞り方はKubernetes MCPサーバーの権限設計にあります。マニフェストを守る設定と、操作する側の権限は、別々に閉じる必要があります。
まとめ
securityContextを足す作業は、lint → 適用 → 起動確認の3段で見ると取りこぼしません。kube-linterのcheck名をClaude Codeへの指示に入れておけば、lintの指摘ゼロまでは任せられます。残るのは、アプリが実際に書き込む場所をemptyDirで渡す判断で、これはPodのログを見ないと決まりません。まず手元のDeploymentにkube-linter lintを1回かけて、no-read-only-root-fsとrun-as-non-rootの件数を確かめるところから始められます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。