Claude Media
Claude CodeでsecurityContextを足しkube-linterで確かめる

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の件数を確かめるところから始められます。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. github.com/stackrox/kube-linter
  2. docs.kubelinter.io/
  3. kubernetes.io/docs/tasks/configure-pod-container/security-context
  4. code.claude.com/docs/en/common-workflows
この記事を共有:XはてブLinkedIn