Certified Kubernetes Security Specialist (CKS) #7: seccomp プロファイル

#6 AppArmor プロファイル では、コンテナがどのファイルにアクセスしどの機能を使えるかをプロファイルにまとめました。同じ System Hardening ドメインの対になるツールが seccomp です。AppArmor がファイルと機能を見るなら、seccomp は コンテナがカーネルに投げるシステムコールそのもの をふるいにかけます。この記事では seccomp の概念と 3 つのプロファイルタイプ、Pod への適用方法、ノードにカスタムプロファイルを載せて参照する方法、そして遮断を検証する方法まで整理します。

seccomp とは #

seccomp (secure computing mode) は Linux カーネルの機能で、プロセスが呼び出せる システムコール (syscall) を制限します。システムコールは、ユーザー空間のプロセスがカーネルに作業を依頼する唯一の経路です。ファイルを開く、ネットワークソケットを作る、新しいプロセスを起動する、カーネルモジュールを読み込む、こうしたすべての動作がシステムコールで行われます。Linux には 300 を超えるシステムコールがあり、ほとんどのコンテナはそのうちのごく一部しか使いません。

問題は、攻撃者がコンテナを掌握したとき、残りのシステムコールを自由に使えるという点です。mountkeyctlunsharebpf のようなシステムコールは、権限昇格とコンテナエスケープの足がかりになります。seccomp はコンテナが使わないシステムコールをあらかじめ塞ぎ、攻撃面をシステムコールのレベルで狭めます

seccomp プロファイルは JSON ドキュメントで、デフォルト動作 (defaultAction) を決め、例外となるシステムコールのリストを並べる構造です。最もよくあるパターンは「デフォルトは遮断しつつ、既知の安全なシステムコールだけ許可」です。

AppArmor との違い #

seccomp と AppArmor はどちらも System Hardening ツールですが、塞ぐ層が違います。

項目seccompAppArmor
対象システムコール (syscall)ファイルパス、capability、ネットワーク
問い「このシステムコールを許可するか」「このファイルを読むか書くか」
定義の場所JSON プロファイルテキストプロファイル (/etc/apparmor.d/)
適用キーsecurityContext.seccompProfileannotation または securityContext.appArmorProfile
適用単位Pod または containercontainer

この 2 つは競合ではなく補完の関係です。seccomp で危険なシステムコールを塞ぎ、AppArmor でファイルと機能のアクセスをまとめれば、防御が幾重にも積み上がります。試験では 2 つのツールをそれぞれ扱いますが、実務では一緒に適用するのが定石です。

3 つのプロファイルタイプ #

Kubernetes の seccompProfile.type は 3 つの値を取ります。

type意味備考
RuntimeDefaultコンテナランタイムが提供するデフォルトプロファイルを適用containerd・CRI-O が検証した合理的なデフォルト値。推奨
Localhostノードに載せたカスタムプロファイルファイルを参照localhostProfile でファイルパスを指定
Unconfinedseccomp 未適用。すべてのシステムコールを許可事実上無防備。避けるべき

RuntimeDefault をデフォルトに #

まず覚えるべきは RuntimeDefault をデフォルト値として使う という原則です。containerd や CRI-O のようなランタイムは、コンテナワークロードがほとんど使わない危険なシステムコールを塞ぐ、検証済みのデフォルトプロファイルを内蔵しています。このプロファイルは、一般的なアプリケーションを壊さずに mountrebootkeyctl のような危険なシステムコールを遮断します。

注意すべき点は、Kubernetes の過去のデフォルト値が Unconfined だった という事実です。seccomp を明示しなかった Pod は、システムコールの制限なしで起動していました。これを正すために、kubelet の --seccomp-default フラグ (または SeccompDefault フィーチャーゲート) をオンにすると、プロファイルを指定しないすべての Pod に自動で RuntimeDefault が適用されます。試験で「ノードのすべての Pod にデフォルトの seccomp を強制せよ」という作業が出たら、このフラグを思い出すべきです。

securityContext.seccompProfile の設定 #

seccomp プロファイルは Pod レベルと container レベルの 2 か所で指定します。Pod レベルに置くとすべてのコンテナに適用され、container レベルに置くとそのコンテナだけに適用され、Pod レベルの設定を上書きします。

RuntimeDefault を Pod 全体に適用 #

apiVersion: v1
kind: Pod
metadata:
  name: secure-app
spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: app
    image: nginx:1.27

spec.securityContext に置いた seccompProfile は、この Pod のすべてのコンテナに RuntimeDefault を被せます。ほとんどの試験作業はこの形で終わります。

container レベルの適用 #

apiVersion: v1
kind: Pod
metadata:
  name: mixed-app
spec:
  containers:
  - name: app
    image: nginx:1.27
    securityContext:
      seccompProfile:
        type: RuntimeDefault
  - name: sidecar
    image: busybox:1.36
    command: ["sleep", "3600"]
    securityContext:
      seccompProfile:
        type: Localhost
        localhostProfile: profiles/audit.json

同じ Pod の中でコンテナごとに違うプロファイルを使う例です。app はランタイムのデフォルト値を、sidecar はノードに載せたカスタムプロファイルを参照します。container レベルの設定が Pod レベルの設定より優先されます。

カスタムプロファイルの作成 #

ランタイムのデフォルト値で足りないときは、自分で JSON プロファイルを作成します。カスタムプロファイルは ノードの決まったディレクトリ に載せておくと、Localhost タイプで参照できます。

プロファイルディレクトリ #

kubelet はカスタム seccomp プロファイルをノードの次のパスで探します。

/var/lib/kubelet/seccomp/

localhostProfile に書くパスは、このディレクトリを基準とした 相対パス です。慣例として、プロファイルは profiles/ の下にまとめておきます。例えばファイルを次の場所に置くと、

/var/lib/kubelet/seccomp/profiles/audit.json

マニフェストでは localhostProfile: profiles/audit.json で参照します。絶対パスやディレクトリの外のパスは許可されません。

プロファイル JSON の構造 #

カスタムプロファイルの核心は defaultActionsyscalls の 2 つのフィールドです。

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_X86",
    "SCMP_ARCH_X32"
  ],
  "syscalls": [
    {
      "names": [
        "accept4",
        "bind",
        "listen",
        "read",
        "write",
        "close",
        "exit_group"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

defaultActionSCMP_ACT_ERRNO なので 列挙されていないすべてのシステムコールは遮断 され、呼び出すとエラー (EPERM) を返されます。syscalls ブロックの names に載せたシステムコールだけが SCMP_ACT_ALLOW で許可されます。この「デフォルト遮断 + 明示許可」方式が最も安全なホワイトリストパターンです。

主なアクション値は次のとおりです。

アクション動作
SCMP_ACT_ERRNO呼び出しを遮断。エラーコードを返す
SCMP_ACT_ALLOW呼び出しを許可
SCMP_ACT_LOG許可しつつログ記録 (監査用)
SCMP_ACT_KILL呼び出し時にプロセスを終了

監査目的のプロファイルは、defaultActionSCMP_ACT_LOG にして、どのシステムコールが使われるかをまず観察したうえで、その結果からホワイトリストを狭める方式で作成します。

カスタムプロファイルを参照する Pod #

apiVersion: v1
kind: Pod
metadata:
  name: custom-seccomp
spec:
  securityContext:
    seccompProfile:
      type: Localhost
      localhostProfile: profiles/audit.json
  containers:
  - name: app
    image: nginx:1.27

typeLocalhost にして、localhostProfile にディレクトリ基準の相対パスを書きます。当該ファイルがノードになければ、Pod は作成段階でエラーになります。試験でカスタムプロファイルの作業が出たら、ファイルを正しいパスに載せたかどうかから確認すべきです。

検証 #

適用した seccomp が実際にシステムコールを塞ぐかを確認する手順が、検証の核心です。

プロファイルが被さったか確認 #

kubectl get pod secure-app -o jsonpath='{.spec.securityContext.seccompProfile}'

Pod スペックにプロファイルタイプが入ったか確認します。container レベルなら .spec.containers[0].securityContext.seccompProfile を見ます。

遮断されたシステムコールのテスト #

defaultAction が遮断のプロファイルで、許可リストにないシステムコールをわざと呼び出して塞がれるかを見ます。例えば mkdir を許可していないプロファイルなら、ディレクトリ作成が失敗するはずです。

kubectl exec custom-seccomp -- mkdir /tmp/test
mkdir: can't create directory '/tmp/test': Operation not permitted

Operation not permitted は、システムコールが SCMP_ACT_ERRNO で遮断されたという合図です。逆に RuntimeDefault のように一般的な動作を許可するプロファイルでは、普通のコマンドが正常に動くはずです。こうして「塞がれるべきものが塞がれ、動くべきものが動くか」を両方向で確認すれば作業が終わります。

Unconfined に落ちたか点検 #

プロファイルを指定したつもりが、実際は Unconfined で動いている、というのがよくあるミスです。Pod スペックに seccompProfile が空で、kubelet の --seccomp-default もオフなら、コンテナはシステムコールの制限なしで起動します。上の jsonpath 照会の結果が空なら、プロファイルが適用されていないので、マニフェストを見直すべきです。

試験ポイント #

  • RuntimeDefault がデフォルトの推奨値 です。「Pod に seccomp を適用せよ」という作業のほとんどは、securityContext.seccompProfile.type: RuntimeDefault の 1 行で終わります。
  • プロファイルは Pod レベル (spec.securityContext) と container レベル (spec.containers[].securityContext) の 2 か所で指定し、container レベルが優先されます。
  • カスタムプロファイルはノードの /var/lib/kubelet/seccomp/ ディレクトリに載せ、localhostProfile にはこのディレクトリ基準の 相対パス を書きます。
  • JSON プロファイルのホワイトリストパターンは、defaultAction: SCMP_ACT_ERRNO (デフォルト遮断) + 許可するシステムコールの SCMP_ACT_ALLOW の組み合わせです。
  • Unconfined は seccomp 未適用なので 避けるべき値 です。プロファイルを指定しないと過去のデフォルト値に落ちることがある点を覚えておきます。
  • ノード全体の強制は kubelet の --seccomp-default フラグ (または SeccompDefault フィーチャーゲート) で処理します。
  • 検証は kubectl get pod -o jsonpath で適用の有無を確認し、遮断対象のシステムコールを呼び出して Operation not permitted が出るかで締めくくります。

まとめ #

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

  • seccomp はコンテナがカーネルに投げる システムコールをフィルタリング して攻撃面を狭める Linux カーネルの機能です。
  • プロファイルタイプは RuntimeDefault (ランタイムのデフォルト値・推奨)、Localhost (ノードのカスタムファイルを参照)、Unconfined (未適用) の 3 つです。
  • 適用は securityContext.seccompProfile で行い、Pod レベルと container レベルの両方が可能です。
  • カスタムプロファイルは /var/lib/kubelet/seccomp/ に載せて Localhost タイプで参照し、defaultActionsyscalls で許可リストを定義します。
  • seccomp がシステムコールを見るなら、AppArmor はファイルと機能を見ます。この 2 つは一緒に積み上げると防御が厚くなります。

次へ — kernel hardening #

seccomp でシステムコールを、AppArmor でファイルと機能をまとめました。System Hardening ドメインの最後のピースは、コンテナに渡すカーネル権限そのものを減らす ことです。

#8 kernel hardening、capabilities、/proc 保護 では、Linux capability を最小に落とす securityContext.capabilities、権限昇格を防ぐ allowPrivilegeEscalationprivileged/proc マスキングと readOnlyRootFilesystem のようなカーネルレベルのハードニング設定を直接作りながら整理します。

X