Certified Kubernetes Security Specialist (CKS) #8: kernel hardening、capabilities、/proc 保護

#6 AppArmor プロファイル#7 seccomp プロファイル では、コンテナが呼べるシステムコールとアクセスできるファイルをカーネルレベルで狭めました。この記事は同じ System Hardening ドメインをもう一段上、つまり Pod とコンテナに最初から過剰な権限を与えない方法 を整理します。AppArmor と seccomp が「できる行動」を防ぐ道具なら、この記事の securityContext 設定は「そもそも持っている権限」を削る道具です。

CKS 試験で最もよく出る種類の一つが、過剰な権限で動く Pod を見つけて最小権限に直す 作業です。privileged: true を外し、capabilities を drop ALL で空にしたあと必要なものだけ add し、root で動くコンテナを非特権ユーザーに下げる作業が常連です。この記事でその手の動きを一つずつ身につけます。

なぜ権限を削るのか #

コンテナはホストカーネルを共有します。仮想マシンのようにカーネルまで分離されていないので、コンテナの中で権限を十分に集めるとホストへ脱出する道が開きます。攻撃者が狙うのはまさにこの一点です。脆弱なアプリケーションでコンテナの中に足を踏み入れたあと、コンテナが持つ過剰な権限を足場にしてホストや他のワークロードまで掌握する 流れです。

そのため System Hardening の核心は単純です。コンテナに必要な権限だけを残し、残りをすべて取り除く ことです。権限がなければ、その権限を悪用した攻撃も成立しません。この記事で扱うすべての設定は、この一文の具体的な実践です。

CKAD #15 SecurityContext と CapabilitiessecurityContext の基本的な使い方を扱ったことがあります。CKS は同じフィールドを 攻撃面を減らすセキュリティの観点 で見直します。各設定がどの攻撃を防ぐのかを一緒に押さえます。

Linux capabilities #

伝統的な Unix は権限を root と非 root の二つにしか分けませんでした。root はすべてのことができ、非 root はほとんど何もできませんでした。この二分法は粗すぎて、ポート 1024 未満のバインドのような小さな権限が一つ必要でも、プロセス全体を root で動かす必要がありました。Linux capabilities は root の全能の権限を 約 40 個の小さな権限の単位に分割した ものです。プロセスに必要な capability だけを正確に与えれば、root なしでもその仕事ができます。

危険な capabilities #

いくつかの capability は事実上 root に匹敵する威力を持ちます。試験と実務で特に警戒すべきものです。

capability何を許可するか危険
SYS_ADMINマウント、ネームスペース操作など広範囲な管理作業事実上 root。コンテナ脱出の常連
NET_ADMINネットワークインターフェース・ルーティング・ファイアウォール規則の変更トラフィックの傍受、ネットワークの回避
NET_RAWraw ソケットの生成パケットスプーフィング、ARP スプーフィング
SYS_PTRACE他のプロセスのメモリの追跡・操作同じノードのプロセスの侵害
SYS_MODULEカーネルモジュールのロード・アンロードカーネルレベルの掌握
DAC_OVERRIDEファイル権限チェックの回避任意のファイルの読み書き

ほとんどのアプリケーションはこのうちどれも必要としません。それでもコンテナランタイムはデフォルトで複数の capability を付与します。そのためセキュリティの基本は、すべて落としたあと本当に必要なものだけを再び足す 方式です。

drop ALL したあと必要なものだけ add #

securityContext.capabilitiesdropadd で capability を調整します。最も安全なパターンは drop: ["ALL"] ですべての capability を空にしたあと、アプリケーションが実際に要求するものだけを add に書くことです。

apiVersion: v1
kind: Pod
metadata:
  name: cap-minimal
spec:
  containers:
    - name: app
      image: nginx:1.27
      securityContext:
        capabilities:
          drop:
            - ALL
          add:
            - NET_BIND_SERVICE

上の例はすべての capability を落としたあと、1024 未満のポートバインドに必要な NET_BIND_SERVICE 一つだけを再び足しました。ほとんどの Web サーバーはこの程度で十分です。capability 名は CAP_ 接頭辞なしで書く点を覚えておきます。マニフェストでは NET_BIND_SERVICE と書きますが、getcap やカーネルのドキュメントでは CAP_NET_BIND_SERVICE として出てきます。

コンテナがどの capability を持って動いているかは、ノードで次のように確認します。

# コンテナ PID を確認したあと
grep CapEff /proc/<pid>/status
# capsh で人が読める形に解読
capsh --decode=00000000a80425fb

試験では「このコンテナから NET_ADMIN を削除しろ」のように特定の capability を外す作業も出ます。このときは add リストからそれを消すか、drop に該当の capability を明示すればよいです。

privileged コンテナの危険 #

securityContext.privileged: true はコンテナに ホストのほぼすべての権限 を一度に与えます。すべての capability が付与され、ホストのすべてのデバイスにアクセスでき、AppArmor と seccomp のような保護装置もデフォルトで外れます。事実上コンテナという境界がないのと同じです。

# 絶対に避けるべき設定
securityContext:
  privileged: true

privileged コンテナの中ではホストのディスクをマウントしたり、カーネルモジュールをロードしたり、他のコンテナのプロセスを覗き込むことが可能です。つまりコンテナ脱出の最も広い道です。一部のシステムレベルのワークロード (ストレージドライバー、ネットワークプラグイン) は privileged を要求することもありますが、一般のアプリケーションには絶対に必要ありません。CKS 試験で privileged Pod を見つけたら、それ自体が直すべき欠陥 です。

privileged: false を明示するか、いっそフィールドを置かないのが基本です。privileged が必要に見えるワークロードでも、たいていは特定の capability をいくつか add すれば十分な場合が多いです。

allowPrivilegeEscalation: false #

allowPrivilegeEscalation は、コンテナの中のプロセスが 自分を起動した親より多くの権限を得ること を許可するかを決めます。この値がデフォルトの true だと、setuid バイナリやファイル capability を通じてプロセスが自ら権限を引き上げられます。

securityContext:
  allowPrivilegeEscalation: false

この一行を false にすると、コンテナの中で権限昇格の経路が一つ閉じます。Linux カーネルの no_new_privs フラグを設定するのと同じです。非 root で動くコンテナでも、この値を明示的に切るのが安全です。root で動くコンテナに setuid バイナリがあれば権限昇格の通路になるので、この設定は特に重要です。

runAsNonRoot と runAsUser #

コンテナを root (UID 0) で動かすと、コンテナ脱出時にホストでも強力な権限を持つ可能性が高まります。そのため コンテナは非 root ユーザーで動くのが基本 であるべきです。

securityContext:
  runAsNonRoot: true
  runAsUser: 1000
  runAsGroup: 3000
  • runAsNonRoot: true は、イメージが root で起動しようとすればコンテナの起動を拒否します。イメージ自体が非 root を保証しなくても、ランタイムがもう一度防いでくれる安全装置です。
  • runAsUser: 1000 は、プロセスが動く UID を直接指定します。イメージのデフォルトユーザーを上書きします。
  • runAsGroup は、デフォルトのグループ ID を指定します。

runAsNonRoot: true だけ置いて runAsUser を空けても、イメージが非 root ユーザーでビルドされていれば正常に起動します。イメージが root でしか動かない場合は、runAsUser で非 root の UID を明示する必要があります。ただし任意の UID で動かすときは、アプリケーションがその UID のホームディレクトリやファイルにアクセスできるかの確認が必要です。

readOnlyRootFilesystem #

readOnlyRootFilesystem: true は、コンテナのルートファイルシステムを読み取り専用にします。攻撃者がコンテナの中に入っても、悪性バイナリを落としたり既存のファイルを改ざんしたりできません。

securityContext:
  readOnlyRootFilesystem: true

ほとんどのアプリケーションは、ログや一時ファイルのために書き込み可能なパスが一部必要です。このときはルートを読み取り専用にしておき、書き込みが必要なパスにだけ emptyDir ボリュームを付けます。

apiVersion: v1
kind: Pod
metadata:
  name: readonly-root
spec:
  containers:
    - name: app
      image: nginx:1.27
      securityContext:
        readOnlyRootFilesystem: true
      volumeMounts:
        - name: tmp
          mountPath: /tmp
        - name: cache
          mountPath: /var/cache/nginx
        - name: run
          mountPath: /var/run
  volumes:
    - name: tmp
      emptyDir: {}
    - name: cache
      emptyDir: {}
    - name: run
      emptyDir: {}

こうするとアプリケーションは正常に動きながら、ルートファイルシステムは改ざん不可能な状態を保ちます。このパターンは #18 Container immutability で扱う不変コンテナの基礎でもあります。

procMount で /proc を保護 #

コンテナランタイムはデフォルトで /proc 以下の機微なパスを マスキング します。/proc/kcore (カーネルメモリ)、/proc/sys の一部、/proc/keys のようなパスは、読んだり書いたりするとホスト情報が漏れたりカーネル設定が変わったりするので、デフォルトで隠されるか読み取り専用に縛られます。この動作を制御するのが procMount です。

securityContext:
  procMount: Default

procMount には二つの値があります。

  • Default: ランタイムが /proc の機微なパスをマスキング・読み取り専用で処理します。デフォルト値であり安全な値です。
  • Unmasked: マスキングをすべて解除し、コンテナが /proc 全体をそのまま見ます。

Unmasked は、コンテナの中から ホストのカーネルメモリと設定にアクセスする道を開く 危険な設定です。一部のデバッグツールやネストしたコンテナ環境でのみ正当な使い道があり、一般のワークロードには絶対に必要ありません。CKS 試験で procMount: Unmasked を見つけたら、Default に戻すかフィールドを取り除くのが正解です。ちなみに Unmasked を使うには Pod Security ポリシー上最も緩いレベルが許可されている必要があるので、#9 Pod Security Admission のポリシーでもこれを遮断できます。

host ネームスペースの遮断 #

Pod レベルでホストのネームスペースを共有するように開くフィールドがあります。これらのフィールドはコンテナとホストの間の隔離を直接崩すので、セキュリティの観点で最初に点検すべきです。

フィールドオンにすると何が起きるか危険
hostPID: trueコンテナがホストのすべてのプロセスを見る他のワークロードのプロセスの追跡・終了
hostNetwork: trueコンテナがホストのネットワークスタックをそのまま使うすべてのノードポートの露出、トラフィックの傍受
hostIPC: trueコンテナがホストの IPC ネームスペースを共有ホスト・他のコンテナの共有メモリへのアクセス
# すべて false がデフォルトであり安全
spec:
  hostPID: false
  hostNetwork: false
  hostIPC: false

この三つのフィールドはすべてデフォルト値が false なので、安全なマニフェストならそもそも登場しません。true に設定されたものが見えたら、それ自体が欠陥のシグナルです。試験では「この Pod がホストのプロセスを見られないようにしろ」のような指示が出ますが、これは hostPID を外せという意味です。

host パスマウントの危険 #

hostPath ボリュームは ホストのファイルシステムのパスをコンテナの中へ直接マウント します。便利ですが危険が大きいです。//etc/var/run/docker.sock のようなパスをマウントすると、コンテナの中からホストの設定と資格情報、コンテナソケットまで手に入れることになります。

# 非常に危険なマウント
volumes:
  - name: host-root
    hostPath:
      path: /

特に docker.sock や containerd ソケットをマウントすると、コンテナがホストのコンテナランタイムを直接操り、新しい privileged コンテナを立ち上げられます。これはそのままホストの掌握につながります。CKS 試験で危険な hostPath を見つけたら、取り除くか、本当に必要な場合はマウント範囲を最小限の下位パスに狭め、可能なら読み取り専用 (readOnly: true) にするのが正解です。

総合: hardened Pod #

ここまで見た設定を一つのマニフェストに集めると次のようになります。一般的な Web アプリケーションを安全に動かす出発点とするに値します。

apiVersion: v1
kind: Pod
metadata:
  name: hardened-app
spec:
  hostPID: false
  hostNetwork: false
  hostIPC: false
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    fsGroup: 2000
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: app
      image: nginx:1.27
      securityContext:
        allowPrivilegeEscalation: false
        privileged: false
        readOnlyRootFilesystem: true
        runAsNonRoot: true
        runAsUser: 1000
        procMount: Default
        capabilities:
          drop:
            - ALL
          add:
            - NET_BIND_SERVICE
      volumeMounts:
        - name: tmp
          mountPath: /tmp
  volumes:
    - name: tmp
      emptyDir: {}

このマニフェストは非 root で動き、権限昇格を防ぎ、capability を空にし、ルートファイルシステムを読み取り専用にし、/proc を保護し、ホストネームスペースを共有しません。Pod レベルの securityContext とコンテナレベルの securityContext が重なるときは、コンテナレベルが優先する 点を覚えておきます。

試験ポイント #

  • capabilities は drop ALL したあと add。すべての capability を空にしたあと必要なものだけ足すのが定石です。名前は CAP_ 接頭辞なしで書きます。
  • privileged: true は欠陥のシグナル。見つけたら削除が基本です。たいてい特定の capability をいくつかで代替されます。
  • allowPrivilegeEscalation: false。権限昇格の経路を閉じる一行であり、非 root コンテナにも明示するのが安全です。
  • runAsNonRoot・runAsUser。コンテナを非 root に下げます。イメージが root でしか動かなければ runAsUser で UID を指定します。
  • readOnlyRootFilesystem: true + emptyDir。ルートは読み取り専用にし、書き込みパスには emptyDir を付けます。
  • procMount: Unmasked は危険。見つけたら Default に戻します。
  • hostPID・hostNetwork・hostIPC・hostPath。すべてホストとの隔離を崩す設定です。true や危険なパスが見えたら取り除きます。
  • 試験の常連は 過剰な権限の Pod を見つけて最小権限に直す 作業です。マニフェストを読んで危険なフィールドを即座に指摘する目を養う必要があります。

まとめ #

この記事で押さえたこと:

  • System Hardening の核心は コンテナに必要な権限だけを残すこと です。権限がなければ悪用もありません。
  • Linux capabilities は root の権限を小さな単位に分割したものであり、drop: ["ALL"] したあと必要なものだけ add するのが基本です。
  • privilegedallowPrivilegeEscalationrunAsNonRootreadOnlyRootFilesystemprocMount はコンテナの攻撃面を左右する核心のフィールドです。
  • hostPIDhostNetworkhostIPC、危険な hostPath マウントはホストとの隔離を直接崩すので、最初に点検します。
  • 試験では過剰な権限の Pod を最小権限に直す作業が常連なので、危険なフィールドを一目で指摘する練習が点数につながります。

次へ — Pod Security Admission #

ここまではマニフェスト一つ一つを直接 hardened な状態に直してきました。ところがクラスターに入ってくるすべての Pod を人が一つずつ検査することはできません。そのため 危険な Pod を admission 段階で自動的に拒否する 装置が必要です。

#9 Pod Security Admission (PSA, Pod Security Standards) では、ネームスペースのラベル一つで privileged・baseline・restricted レベルのポリシーを強制する方法、この記事で見た危険なフィールドがどのレベルで遮断されるのか、そして enforce・audit・warn モードをどう組み合わせるのかを直接適用しながら整理します。

X