1 - クラスターレベルでのPodセキュリティの標準の適用

Podセキュリティアドミッション(PSA)は、ベータへ進み、v1.23以降でデフォルトで有効になっています。 Podセキュリティアドミッションは、Podが作成される際に、Podセキュリティの標準の適用の認可を制御するものです。 このチュートリアルでは、クラスター内の全ての名前空間に標準設定を適用することで、クラスターレベルでbaseline Podセキュリティの標準を強制する方法を示します。

Podセキュリティの標準を特定の名前空間に適用するには、名前空間レベルでのPodセキュリティの標準の適用を参照してください。

v1.33以外のKubernetesバージョンを実行している場合は、そのバージョンのドキュメントを確認してください。

始める前に

ワークステーションに以下をインストールしてください:

このチュートリアルでは、完全な制御下にあるKubernetesクラスターの何を設定できるかをデモンストレーションします。 コントロールプレーンを設定できない管理されたクラスターのPodセキュリティアドミッションに対しての設定方法を知りたいのであれば、名前空間レベルでのPodセキュリティの標準の適用を参照してください。

適用する正しいPodセキュリティの標準の選択

Podのセキュリティアドミッションは、以下のモードでビルトインのPodセキュリティの標準の適用を促します: enforceauditwarn。 設定に最適なPodセキュリティの標準を選択するにあたって助けになる情報を収集するために、以下を行ってください:

  1. Podセキュリティの標準を適用していないクラスターを作成します:

    kind create cluster --name psa-wo-cluster-pss
    

    出力は次のようになります:

    Creating cluster "psa-wo-cluster-pss" ...
    ✓ Ensuring node image (kindest/node:v1.33.0) 🖼
    ✓ Preparing nodes 📦
    ✓ Writing configuration 📜
    ✓ Starting control-plane 🕹️
    ✓ Installing CNI 🔌
    ✓ Installing StorageClass 💾
    Set kubectl context to "kind-psa-wo-cluster-pss"
    You can now use your cluster with:
    
    kubectl cluster-info --context kind-psa-wo-cluster-pss
    
    Thanks for using kind! 😊
    
  2. kubectl contextを新しいクラスターにセットします:

    kubectl cluster-info --context kind-psa-wo-cluster-pss
    

    出力は次のようになります:

    Kubernetes control plane is running at https://127.0.0.1:61350
    
    CoreDNS is running at https://127.0.0.1:61350/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
    
    To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
    
  3. クラスター内の名前空間の一覧を取得します:

    kubectl get ns
    

    出力は次のようになります:

    NAME                 STATUS   AGE
    default              Active   9m30s
    kube-node-lease      Active   9m32s
    kube-public          Active   9m32s
    kube-system          Active   9m32s
    local-path-storage   Active   9m26s
    
  4. 異なるPodセキュリティの標準が適用されたときに何が起きるかを理解するために、-dry-run=serverを使います:

    1. privileged

      kubectl label --dry-run=server --overwrite ns --all \
      pod-security.kubernetes.io/enforce=privileged
      

      出力は次のようになります:

      namespace/default labeled
      namespace/kube-node-lease labeled
      namespace/kube-public labeled
      namespace/kube-system labeled
      namespace/local-path-storage labeled
      
    2. baseline

      kubectl label --dry-run=server --overwrite ns --all \
      pod-security.kubernetes.io/enforce=baseline
      

      出力は次のようになります:

      namespace/default labeled
      namespace/kube-node-lease labeled
      namespace/kube-public labeled
      Warning: existing pods in namespace "kube-system" violate the new PodSecurity enforce level "baseline:latest"
      Warning: etcd-psa-wo-cluster-pss-control-plane (and 3 other pods): host namespaces, hostPath volumes
      Warning: kindnet-vzj42: non-default capabilities, host namespaces, hostPath volumes
      Warning: kube-proxy-m6hwf: host namespaces, hostPath volumes, privileged
      namespace/kube-system labeled
      namespace/local-path-storage labeled
      
    3. restricted

      kubectl label --dry-run=server --overwrite ns --all \
      pod-security.kubernetes.io/enforce=restricted
      

      出力は次のようになります:

      namespace/default labeled
      namespace/kube-node-lease labeled
      namespace/kube-public labeled
      Warning: existing pods in namespace "kube-system" violate the new PodSecurity enforce level "restricted:latest"
      Warning: coredns-7bb9c7b568-hsptc (and 1 other pod): unrestricted capabilities, runAsNonRoot != true, seccompProfile
      Warning: etcd-psa-wo-cluster-pss-control-plane (and 3 other pods): host namespaces, hostPath volumes, allowPrivilegeEscalation != false, unrestricted capabilities, restricted volume types, runAsNonRoot != true
      Warning: kindnet-vzj42: non-default capabilities, host namespaces, hostPath volumes, allowPrivilegeEscalation != false, unrestricted capabilities, restricted volume types, runAsNonRoot != true, seccompProfile
      Warning: kube-proxy-m6hwf: host namespaces, hostPath volumes, privileged, allowPrivilegeEscalation != false, unrestricted capabilities, restricted volume types, runAsNonRoot != true, seccompProfile
      namespace/kube-system labeled
      Warning: existing pods in namespace "local-path-storage" violate the new PodSecurity enforce level "restricted:latest"
      Warning: local-path-provisioner-d6d9f7ffc-lw9lh: allowPrivilegeEscalation != false, unrestricted capabilities, runAsNonRoot != true, seccompProfile
      namespace/local-path-storage labeled
      

この出力から、privileged Podセキュリティの標準を適用すると、名前空間のどれにも警告が示されないことに気付くでしょう。 これに対し、baselinerestrictの標準ではどちらも、とりわけkube-system名前空間に対して警告が示されています。

モード、バージョン、標準のセット

このセクションでは、latestバージョンに以下のPodセキュリティの標準を適用します:

  • enforceモードでbaseline標準。
  • warnおよびauditモードでrestricted標準。

baseline Podセキュリティの標準は、免除リストを短く保てて、かつ既知の特権昇格を防ぐような、利便性のある中庸を提供します。

加えて、kube-system内の失敗からPodを守るために、適用されるPodセキュリティの標準の対象から名前空間を免除します。

環境にPodセキュリティアドミッションを実装する際には、以下の点を考慮してください:

  1. クラスターに適用されるリスク状況に基づくと、restrictedのようにより厳格なPodセキュリティの標準のほうが、より良い選択肢かもしれません。

  2. kube-system名前空間の免除は、Podがその名前空間でprivilegedとして実行するのを許容することになります。 実世界で使うにあたっては、以下の最小権限の原則に従ってkube-systemへのアクセスを制限する厳格なRBACポリシーを適用することを、Kubernetesプロジェクトは強く推奨します。 上記の標準を実装するには、次のようにします:

  3. 目的のPodセキュリティの標準を実装するために、Podセキュリティアドミッションコントローラーで利用可能な設定ファイルを作成します:

    mkdir -p /tmp/pss
    cat <<EOF > /tmp/pss/cluster-level-pss.yaml
    apiVersion: apiserver.config.k8s.io/v1
    kind: AdmissionConfiguration
    plugins:
    - name: PodSecurity
      configuration:
        apiVersion: pod-security.admission.config.k8s.io/v1
        kind: PodSecurityConfiguration
        defaults:
          enforce: "baseline"
          enforce-version: "latest"
          audit: "restricted"
          audit-version: "latest"
          warn: "restricted"
          warn-version: "latest"
        exemptions:
          usernames: []
          runtimeClasses: []
          namespaces: [kube-system]
    EOF
    
  4. クラスターの作成中にこのファイルを取り込むAPIサーバーを設定します:

    cat <<EOF > /tmp/pss/cluster-config.yaml
    kind: Cluster
    apiVersion: kind.x-k8s.io/v1alpha4
    nodes:
    - role: control-plane
      kubeadmConfigPatches:
      - |
        kind: ClusterConfiguration
        apiServer:
            extraArgs:
              admission-control-config-file: /etc/config/cluster-level-pss.yaml
            extraVolumes:
              - name: accf
                hostPath: /etc/config
                mountPath: /etc/config
                readOnly: false
                pathType: "DirectoryOrCreate"
      extraMounts:
      - hostPath: /tmp/pss
        containerPath: /etc/config
        # optional: if set, the mount is read-only.
        # default false
        readOnly: false
        # optional: if set, the mount needs SELinux relabeling.
        # default false
        selinuxRelabel: false
        # optional: set propagation mode (None, HostToContainer or Bidirectional)
        # see https://kubernetes.io/docs/concepts/storage/volumes/#mount-propagation
        # default None
        propagation: None
    EOF
    
  5. 目的のPodセキュリティの標準を適用するために、Podセキュリティアドミッションを使うクラスターを作成します:

    kind create cluster --name psa-with-cluster-pss --config /tmp/pss/cluster-config.yaml
    

    出力は次のようになります:

    Creating cluster "psa-with-cluster-pss" ...
     ✓ Ensuring node image (kindest/node:v1.33.0) 🖼
     ✓ Preparing nodes 📦
     ✓ Writing configuration 📜
     ✓ Starting control-plane 🕹️
     ✓ Installing CNI 🔌
     ✓ Installing StorageClass 💾
    Set kubectl context to "kind-psa-with-cluster-pss"
    You can now use your cluster with:
    
    kubectl cluster-info --context kind-psa-with-cluster-pss
    
    Have a question, bug, or feature request? Let us know! https://kind.sigs.k8s.io/#community 🙂
    
  6. kubectlをこのクラスターに向けます:

    kubectl cluster-info --context kind-psa-with-cluster-pss
    

    出力は次のようになります:

    Kubernetes control plane is running at https://127.0.0.1:63855
    CoreDNS is running at https://127.0.0.1:63855/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
    
    To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
    
  7. デフォルトの名前空間にPodを作成します:

    kubectl apply -f https://k8s.io/examples/security/example-baseline-pod.yaml
    

    Podは正常に開始されますが、出力には警告が含まれます:

    Warning: would violate PodSecurity "restricted:latest": allowPrivilegeEscalation != false (container "nginx" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "nginx" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "nginx" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "nginx" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
    pod/nginx created
    

後片付け

では、上記で作成したクラスターを、以下のコマンドを実行して削除します:

kind delete cluster --name psa-with-cluster-pss
kind delete cluster --name psa-wo-cluster-pss

次の項目

2 - 名前空間レベルでのPodセキュリティの標準の適用

Podセキュリティアドミッション(PSA)は、ベータへ進み、v1.23以降でデフォルトで有効になっています。 Podセキュリティアドミッションは、Podが作成される際に、Podセキュリティの標準の適用の認可を制御するものです。 このチュートリアルでは、一度に1つの名前空間でbaseline Podセキュリティ標準を強制します。

Podセキュリティの標準を複数の名前空間に一度にクラスターレベルで適用することもできます。やり方についてはクラスターレベルでのPodセキュリティの標準の適用を参照してください。

始める前に

ワークステーションに以下をインストールしてください:

クラスターの作成

  1. 以下のようにKinDクラスターを作成します。

    kind create cluster --name psa-ns-level
    

    出力は次のようになります:

    Creating cluster "psa-ns-level" ...
     ✓ Ensuring node image (kindest/node:v1.33.0) 🖼 
     ✓ Preparing nodes 📦  
     ✓ Writing configuration 📜 
     ✓ Starting control-plane 🕹️ 
     ✓ Installing CNI 🔌 
     ✓ Installing StorageClass 💾 
    Set kubectl context to "kind-psa-ns-level"
    You can now use your cluster with:
    
    kubectl cluster-info --context kind-psa-ns-level
    
    Not sure what to do next? 😅  Check out https://kind.sigs.k8s.io/docs/user/quick-start/
    
  2. kubectl のコンテキストを新しいクラスターにセットします:

    kubectl cluster-info --context kind-psa-ns-level
    

    出力は次のようになります:

    Kubernetes control plane is running at https://127.0.0.1:50996
    CoreDNS is running at https://127.0.0.1:50996/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
    
    To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
    

名前空間の作成

exampleと呼ぶ新しい名前空間を作成します:

kubectl create ns example

出力は次のようになります:

namespace/example created

名前空間へのPodセキュリティの標準チェックの有効化

  1. ビルトインのPod Security Admissionでサポートされているラベルを使って、この名前空間のPodセキュリティの標準を有効にします。 このステップでは、baseline Podセキュリティの標準の最新バージョンに合わないPodについて警告するチェックを設定します。

    kubectl label --overwrite ns example \
       pod-security.kubernetes.io/warn=baseline \
       pod-security.kubernetes.io/warn-version=latest
    
  2. ラベルを使って、任意の名前空間に対して複数のPodセキュリティの標準チェックを設定できます。 以下のコマンドは、baseline Podセキュリティの標準をenforce(強制)としますが、restricted Podセキュリティの標準には最新バージョンに準じてwarn(警告)およびaudit(監査)とします(デフォルト値)。

    kubectl label --overwrite ns example \
      pod-security.kubernetes.io/enforce=baseline \
      pod-security.kubernetes.io/enforce-version=latest \
      pod-security.kubernetes.io/warn=restricted \
      pod-security.kubernetes.io/warn-version=latest \
      pod-security.kubernetes.io/audit=restricted \
      pod-security.kubernetes.io/audit-version=latest
    

Podセキュリティの標準の強制の実証

  1. example名前空間内にbaseline Podを作成します:

    kubectl apply -n example -f https://k8s.io/examples/security/example-baseline-pod.yaml
    

    Podは正常に起動しますが、出力には警告が含まれています。例えば:

    Warning: would violate PodSecurity "restricted:latest": allowPrivilegeEscalation != false (container "nginx" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "nginx" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "nginx" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "nginx" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
    pod/nginx created
    
  2. default名前空間内にbaseline Podを作成します:

    kubectl apply -n default -f https://k8s.io/examples/security/example-baseline-pod.yaml
    

    出力は次のようになります:

    pod/nginx created
    

example名前空間にだけ、Podセキュリティの標準のenforceと警告の設定が適用されました。 default名前空間内では、警告なしに同じPodを作成できました。

後片付け

では、上記で作成したクラスターを、以下のコマンドを実行して削除します:

kind delete cluster --name psa-ns-level

次の項目

3 - AppArmorを使用してコンテナのリソースへのアクセスを制限する

FEATURE STATE: Kubernetes v1.31 [stable] (enabled by default: true)

このページでは、ノードでAppArmorプロファイルを読み込み、それらのプロファイルをPodに適用する方法を説明します。 AppArmorを使用してPodを制限するKubernetesの仕組みについて詳しく知りたい場合は、PodとコンテナのためのLinuxカーネルのセキュリティ制約をご覧ください。

目標

  • プロファイルをノードに読み込む方法の例を見る
  • Pod上でプロファイルを強制する方法を学ぶ
  • プロファイルが読み込まれたかを確認する方法を学ぶ
  • プロファイルに違反した場合に何が起こるのかを見る
  • プロファイルが読み込めなかった場合に何が起こるのかを見る

始める前に

AppArmorはカーネルモジュールおよびKubernetesのオプション機能です。 そのため、先に進む前にノード上でAppArmorがサポートされていることを確認してください:

  1. AppArmorカーネルモジュールが有効であること。 LinuxカーネルがAppArmorプロファイルを強制するためには、AppArmorカーネルモジュールのインストールと有効化が必須です。 UbuntuやSUSEなどのディストリビューションではデフォルトで有効化されますが、他の多くのディストリビューションでのサポートはオプションです。 モジュールが有効になっているかどうかを確認するためには、/sys/module/apparmor/parameters/enabledファイルを確認します:

    cat /sys/module/apparmor/parameters/enabled
    Y
    

    Kubeletは、AppArmorが明示的に設定されたPodを受け入れる前にホスト上でAppArmorが有効になっているかどうかを検証します。

  2. コンテナランタイムがAppArmorをサポートしていること。 KubernetesがサポートするcontainerdCRI-Oなどのすべての一般的なコンテナランタイムは、AppArmorをサポートしています。 関連するランタイムのドキュメントを参照して、クラスターがAppArmorを利用するための要求を満たしているかどうかを検証してください。

  3. プロファイルが読み込まれていること。 AppArmorがPodに適用されるのは、各コンテナが実行するAppArmorプロファイルを指定したときです。 もし指定されたプロファイルがまだカーネルに読み込まれていなければ、KubeletはPodを拒否します。 どのプロファイルがノードに読み込まれているのかを確かめるには、/sys/kernel/security/apparmor/profilesを確認します。 例えば:

    ssh gke-test-default-pool-239f5d02-gyn2 "sudo cat /sys/kernel/security/apparmor/profiles | sort"
    
    apparmor-test-deny-write (enforce)
    apparmor-test-audit-write (enforce)
    docker-default (enforce)
    k8s-nginx (enforce)
    

    ノード上でのプロファイルの読み込みの詳細については、プロファイルを使用したノードのセットアップを参照してください。

Podをセキュアにする

AppArmorプロファイルは、Podレベルまたはコンテナレベルで指定することができます。 コンテナのAppArmorプロファイルは、Podのプロファイルよりも優先されます。

securityContext:
  appArmorProfile:
    type: <profile_type>

ここで、<profile_type>には次の値のうち1つを指定します:

  • RuntimeDefault: ランタイムのデフォルトのプロファイルを適用します。
  • Localhost: ホストに読み込まれたプロファイルを適用します(下記を参照してください)。
  • unconfined: AppArmorなしで実行します。

AppArmorプロファイルAPIの詳細については、AppArmorによる制限の指定を参照してください。

プロファイルが適用されたかどうかを確認するには、proc attrを調べることでコンテナのルートプロセスが正しいプロファイルで実行されているかどうかを確認します:

kubectl exec <pod_name> -- cat /proc/1/attr/current

出力は以下のようになるはずです:

cri-containerd.apparmor.d (enforce)

この例は、クラスターがすでにAppArmorのサポート付きでセットアップ済みであることを前提としています。

まず、使用したいプロファイルをノード上に読み込む必要があります。 このプロファイルは、すべてのファイル書き込みを拒否します:

#include <tunables/global>

profile k8s-apparmor-example-deny-write flags=(attach_disconnected) {
  #include <abstractions/base>

  file,

  # Deny all file writes.
  deny /** w,
}

Podがどのノードにスケジュールされるかは予測できないため、プロファイルはすべてのノードに読み込ませる必要があります。 この例では、単純にSSHを使ってプロファイルをインストールしますが、プロファイルを使用したノードのセットアップでは、他のアプローチについて議論しています。

# この例では、ノード名がホスト名と一致し、SSHで到達可能であることを前提としています。
NODES=($( kubectl get node -o jsonpath='{.items[*].status.addresses[?(.type == "Hostname")].address}' ))

for NODE in ${NODES[*]}; do ssh $NODE 'sudo apparmor_parser -q <<EOF
#include <tunables/global>

profile k8s-apparmor-example-deny-write flags=(attach_disconnected) {
  #include <abstractions/base>

  file,

  # Deny all file writes.
  deny /** w,
}
EOF'
done

次に、deny-writeプロファイルを使用した単純な"Hello AppArmor" Podを実行します。

apiVersion: v1
kind: Pod
metadata:
  name: hello-apparmor
spec:
  securityContext:
    appArmorProfile:
      type: Localhost
      localhostProfile: k8s-apparmor-example-deny-write
  containers:
  - name: hello
    image: busybox:1.28
    command: [ "sh", "-c", "echo 'Hello AppArmor!' && sleep 1h" ]
kubectl create -f hello-apparmor.yaml

/proc/1/attr/currentを確認することで、コンテナがこのプロファイルで実際に実行されていることを検証できます:

kubectl exec hello-apparmor -- cat /proc/1/attr/current

出力は以下のようになるはずです:

k8s-apparmor-example-deny-write (enforce)

最後に、ファイルへの書き込みを行おうとすることで、プロファイルに違反すると何が起こるか見てみましょう:

kubectl exec hello-apparmor -- touch /tmp/test
touch: /tmp/test: Permission denied
error: error executing remote command: command terminated with non-zero exit code: Error executing in Docker Container: 1

まとめとして、読み込まれていないプロファイルを指定しようとするとどうなるのか見てみましょう:

kubectl create -f /dev/stdin <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: hello-apparmor-2
spec:
  securityContext:
    appArmorProfile:
      type: Localhost
      localhostProfile: k8s-apparmor-example-allow-write
  containers:
  - name: hello
    image: busybox:1.28
    command: [ "sh", "-c", "echo 'Hello AppArmor!' && sleep 1h" ]
EOF
pod/hello-apparmor-2 created

Podは正常に作成されましたが、さらに調査すると、PodがPendingで止まっていることがわかります:

kubectl describe pod hello-apparmor-2
Name:          hello-apparmor-2
Namespace:     default
Node:          gke-test-default-pool-239f5d02-x1kf/10.128.0.27
Start Time:    Tue, 30 Aug 2016 17:58:56 -0700
Labels:        <none>
Annotations:   container.apparmor.security.beta.kubernetes.io/hello=localhost/k8s-apparmor-example-allow-write
Status:        Pending
... 
Events:
  Type     Reason     Age              From               Message
  ----     ------     ----             ----               -------
  Normal   Scheduled  10s              default-scheduler  Successfully assigned default/hello-apparmor to gke-test-default-pool-239f5d02-x1kf
  Normal   Pulled     8s               kubelet            Successfully pulled image "busybox:1.28" in 370.157088ms (370.172701ms including waiting)
  Normal   Pulling    7s (x2 over 9s)  kubelet            Pulling image "busybox:1.28"
  Warning  Failed     7s (x2 over 8s)  kubelet            Error: failed to get container spec opts: failed to generate apparmor spec opts: apparmor profile not found k8s-apparmor-example-allow-write
  Normal   Pulled     7s               kubelet            Successfully pulled image "busybox:1.28" in 90.980331ms (91.005869ms including waiting)

イベントにはエラーメッセージとその理由が表示されます。 具体的な文言はランタイムによって異なります:

  Warning  Failed     7s (x2 over 8s)  kubelet            Error: failed to get container spec opts: failed to generate apparmor spec opts: apparmor profile not found 

管理

プロファイルを使用したノードのセットアップ

Kubernetes 1.33は、AppArmorプロファイルをノードに読み込むネイティブの仕組みは提供していません。 プロファイルは、カスタムインフラストラクチャーやKubernetes Security Profiles Operatorなどのツールを通じて読み込むことができます。

スケジューラーはどのプロファイルがどのノードに読み込まれているのかがわからないため、すべてのプロファイルがすべてのノードに読み込まれていなければなりません。 もう1つのアプローチとしては、各プロファイル(あるいはプロファイルのクラス)ごとにノードラベルを追加し、ノードセレクターを用いてPodが必要なプロファイルを読み込んだノードで実行されるようにする方法もあります。

Profilesの作成

AppArmorプロファイルを正しく指定するのはやっかいな作業です。 幸い、その作業を補助するツールがいくつかあります:

  • aa-genprofおよびaa-logprofは、アプリケーションの動作とログを監視し、実行されるアクションを許可することで、プロファイルのルールを生成します。 詳しい説明については、AppArmor documentationを参照してください。
  • baneは、Docker向けのAppArmorプロファイル・ジェネレーターです。簡略化されたプロファイル言語を使用しています。

AppArmorに関する問題をデバッグするには、システムログを確認して、特に何が拒否されたのかを確認できます。 AppArmorのログはdmesgにverboseメッセージを送り、エラーは通常システムログまたはjournalctlで確認できます。 詳しい情報は、AppArmor failuresで提供されています。

AppArmorによる制限の指定

セキュリティコンテキスト内のAppArmorプロファイル

appArmorProfileは、コンテナのsecurityContextまたはPodのsecurityContextのいずれかで指定できます。 プロファイルがPodレベルで設定されている場合、Pod内のすべてのコンテナ(initコンテナ、サイドカーコンテナ、およびエフェメラルコンテナを含む)のデフォルトのプロファイルとして使用されます。 Podとコンテナの両方でAppArmorプロファイルが設定されている場合は、コンテナのプロファイルが使用されます。

AppArmorプロファイルには2つのフィールドがあります:

type (必須) - 適用されるAppArmorプロファイルの種類を示します。 有効なオプションは以下の通りです:

Localhost
ノードに事前に読み込まれているプロファイル(localhostProfileで指定します)。
RuntimeDefault
コンテナランタイムのデフォルトのプロファイル。
Unconfined
AppArmorによる制限を適用しません。

localhostProfile - ノード上に読み込まれている、使用すべきプロファイルの名前。 このプロファイルは、ノード上であらかじめ設定されている必要があります。 このオプションは、typeLocalhostの場合にのみ指定する必要があります。

次の項目

追加のリソースとしては以下のものがあります:

4 - seccompを使用してコンテナのシステムコールを制限する

FEATURE STATE: Kubernetes v1.19 [stable]

seccomp(SECure COMPuting mode)はLinuxカーネル2.6.12以降の機能です。 この機能を用いると、ユーザー空間からカーネルに対して発行できるシステムコールを制限することで、プロセス権限のサンドボックスを構築することができます。 Kubernetesでは、ノード上で読み込んだseccompプロファイルをPodやコンテナに対して自動で適用することができます。

あなたのワークロードが必要とする権限を特定するのは、実際には難しい作業になるかもしれません。 このチュートリアルでは、ローカルのKubernetesクラスターでseccompプロファイルを読み込むための方法を説明し、seccompプロファイルのPodへの適用方法について学んだ上で、コンテナプロセスに対して必要な権限のみを付与するためのseccompプロファイルを作成する方法を概観していきます。

目標

  • ノードでseccompプロファイルを読み込む方法を学ぶ。
  • seccompプロファイルをコンテナに適用する方法を学ぶ。
  • コンテナプロセスが生成するシステムコールの監査出力を確認する。
  • seccompプロファイルが指定されない場合の挙動を確認する。
  • seccompプロファイルの違反を確認する。
  • きめ細やかなseccompプロファイルの作成方法を学ぶ。
  • コンテナランタイムの標準seccompプロファイルを適用する方法を学ぶ。

始める前に

このチュートリアルのステップを完了するためには、kindkubectlをインストールしておく必要があります。

このチュートリアルで利用するコマンドは、Dockerをコンテナランタイムとして利用していることを前提としています。 (kindが作成するクラスターは内部的に異なるコンテナランタイムを利用する可能性があります)。 Podmanを使うこともできますが、チュートリアルを完了するためには、所定の手順に従う必要があります。

このチュートリアルでは、現時点(v1.25以降)でのベータ機能を利用する設定例をいくつか示しますが、その他の例についてはGAなseccomp関連機能を用いています。 利用するKubernetesバージョンを対象としたクラスターの正しい設定がなされていることを確認してください。

チュートリアル内では、サンプルをダウンロードするためにcurlを利用します。 この手順については、ほかの好きなツールを用いて実施してもかまいません。

サンプルのseccompプロファイルをダウンロードする

プロファイルの内容については後で解説しますので、まずはクラスターで読み込むためのseccompプロファイルをprofiles/ディレクトリ内にダウンロードしましょう。

{
    "defaultAction": "SCMP_ACT_LOG"
}

{
    "defaultAction": "SCMP_ACT_ERRNO"
}

{
    "defaultAction": "SCMP_ACT_ERRNO",
    "architectures": [
        "SCMP_ARCH_X86_64",
        "SCMP_ARCH_X86",
        "SCMP_ARCH_X32"
    ],
    "syscalls": [
        {
            "names": [
                "accept4",
                "epoll_wait",
                "pselect6",
                "futex",
                "madvise",
                "epoll_ctl",
                "getsockname",
                "setsockopt",
                "vfork",
                "mmap",
                "read",
                "write",
                "close",
                "arch_prctl",
                "sched_getaffinity",
                "munmap",
                "brk",
                "rt_sigaction",
                "rt_sigprocmask",
                "sigaltstack",
                "gettid",
                "clone",
                "bind",
                "socket",
                "openat",
                "readlinkat",
                "exit_group",
                "epoll_create1",
                "listen",
                "rt_sigreturn",
                "sched_yield",
                "clock_gettime",
                "connect",
                "dup2",
                "epoll_pwait",
                "execve",
                "exit",
                "fcntl",
                "getpid",
                "getuid",
                "ioctl",
                "mprotect",
                "nanosleep",
                "open",
                "poll",
                "recvfrom",
                "sendto",
                "set_tid_address",
                "setitimer",
                "writev",
                "fstatfs",
                "getdents64",
                "pipe2",
                "getrlimit"
            ],
            "action": "SCMP_ACT_ALLOW"
        }
    ]
}

次のコマンドを実行してください:

mkdir ./profiles
curl -L -o profiles/audit.json https://k8s.io/examples/pods/security/seccomp/profiles/audit.json
curl -L -o profiles/violation.json https://k8s.io/examples/pods/security/seccomp/profiles/violation.json
curl -L -o profiles/fine-grained.json https://k8s.io/examples/pods/security/seccomp/profiles/fine-grained.json
ls profiles

最終的に3つのプロファイルが確認できるはずです:

audit.json  fine-grained.json  violation.json

kindでローカルKubernetesクラスターを構築する

手軽な方法として、kindを利用することで、seccompプロファイルを読み込んだ単一ノードクラスターを構築できます。 kindはKubernetesをDocker内で稼働させるため、クラスターの各ノードはコンテナとなります。 これにより、ノード上にファイルを展開するのと同じように、各コンテナのファイルシステムに対してファイルをマウントすることが可能です。

apiVersion: kind.x-k8s.io/v1alpha4
kind: Cluster
nodes:
- role: control-plane
  extraMounts:
  - hostPath: "./profiles"
    containerPath: "/var/lib/kubelet/seccomp/profiles"

kindの設定サンプルをダウンロードして、kind.yamlの名前で保存してください:

curl -L -O https://k8s.io/examples/pods/security/seccomp/kind.yaml

ノードのコンテナイメージを設定する際には、特定のKubernetesバージョンを指定することもできます。 この設定方法の詳細については、kindのドキュメント内の、ノードの項目を参照してください。 このチュートリアルではKubernetes v1.33を使用することを前提とします。

ベータ機能として、Unconfinedにフォールバックするのではなく、コンテナランタイムがデフォルトで推奨するプロファイルを利用するようにKubernetesを設定することもできます。 この機能を試したい場合、これ以降の手順に進む前に、全ワークロードに対する標準seccompプロファイルとしてRuntimeDefaultを使用するを参照してください。

kindの設定ファイルを設置したら、kindクラスターを作成します:

kind create cluster --config=kind.yaml

Kubernetesクラスターが準備できたら、単一ノードクラスターが稼働しているDockerコンテナを特定してください:

docker ps

kind-control-planeという名前のコンテナが稼働していることが確認できるはずです。 出力は次のようになるでしょう:

CONTAINER ID        IMAGE                  COMMAND                  CREATED             STATUS              PORTS                       NAMES
6a96207fed4b        kindest/node:v1.18.2   "/usr/local/bin/entr…"   27 seconds ago      Up 24 seconds       127.0.0.1:42223->6443/tcp   kind-control-plane

このコンテナのファイルシステムを観察すると、profiles/ディレクトリがkubeletのデフォルトのseccompパスとして正しく読み込まれていることを確認できるはずです。 Pod内でコマンドを実行するためにdocker execを使います:

# 6a96207fed4bを"docker ps"で確認したコンテナIDに変更してください。
docker exec -it 6a96207fed4b ls /var/lib/kubelet/seccomp/profiles
audit.json  fine-grained.json  violation.json

kind内で稼働しているkubeletがseccompプロファイルを利用可能であることを確認しました。

コンテナランタイムの標準seccompプロファイルを利用してPodを作成する

ほとんどのコンテナランタイムは、許可・拒否の対象とする標準的なシステムコールの集合を提供しています。

PodやContainerのセキュリティコンテキストでseccompタイプをRuntimeDefaultに設定すると、コンテナランタイムが提供するデフォルトのプロファイルをワークロードに適用することができます。

Pod内の全てのContainerに対してRuntimeDefault seccompプロファイルを要求するマニフェストは次のようなものです:

apiVersion: v1
kind: Pod
metadata:
  name: default-pod
  labels:
    app: default-pod
spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: test-container
    image: hashicorp/http-echo:1.0
    args:
    - "-text=just made some more syscalls!"
    securityContext:
      allowPrivilegeEscalation: false

このPodを作成してみます:

kubectl apply -f https://k8s.io/examples/pods/security/seccomp/ga/default-pod.yaml
kubectl get pod default-pod

Podが正常に起動できていることを確認できるはずです:

NAME        READY   STATUS    RESTARTS   AGE
default-pod 1/1     Running   0          20s

次のセクションに進む前に、Podを削除します:

kubectl delete pod default-pod --wait --now

システムコール監査のためのseccompプロファイルを利用してPodを作成する

最初に、新しいPodにプロセスの全システムコールを記録するためのaudit.jsonプロファイルを適用します。

このPodのためのマニフェストは次の通りです:

apiVersion: v1
kind: Pod
metadata:
  name: audit-pod
  labels:
    app: audit-pod
spec:
  securityContext:
    seccompProfile:
      type: Localhost
      localhostProfile: profiles/audit.json
  containers:
  - name: test-container
    image: hashicorp/http-echo:1.0
    args:
    - "-text=just made some syscalls!"
    securityContext:
      allowPrivilegeEscalation: false

クラスター内にPodを作成します:

kubectl apply -f https://k8s.io/examples/pods/security/seccomp/ga/audit-pod.yaml

このプロファイルは、いかなるシステムコールも制限しないため、Podは正常に起動するはずです。

kubectl get pod audit-pod
NAME        READY   STATUS    RESTARTS   AGE
audit-pod   1/1     Running   0          30s

このコンテナが公開するエンドポイントとやりとりするために、kindのコントロールプレーンコンテナの内部からこのエンドポイントにアクセスできるように、NodePort Serviceを作成します。

kubectl expose pod audit-pod --type NodePort --port 5678

どのポートがノード上のServiceに割り当てられたのかを確認しましょう。

kubectl get service audit-pod

次のような出力が得られます:

NAME        TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
audit-pod   NodePort   10.111.36.142   <none>        5678:32373/TCP   72s

ここまで来れば、kindのコントロールプレーンコンテナの内部からエンドポイントに対して、Serviceが公開するポートを通じてcurlで接続することができます。 docker execを使って、コントロールプレーンコンテナに属するコンテナの中からcurlを実行しましょう:

# 6a96207fed4bと32373を、それぞれ"docker ps"で確認したコンテナIDとポート番号に変更してください。
docker exec -it 6a96207fed4b curl localhost:32373
just made some syscalls!

プロセスが実行されていることは確認できましたが、実際にどんなシステムコールが発行されているのでしょうか? このPodはローカルクラスターで稼働しているため、発行されたシステムコールを/var/log/syslogで確認することができるはずです。 新しいターミナルを開いて、http-echoが発行するシステムコールをtailしてみましょう:

# あなたのマシンのログは"/var/log/syslog"以外の場所にあるかもしれません。
tail -f /var/log/syslog | grep 'http-echo'

すでにhttp-echoが発行したいくつかのシステムコールのログが見えているはずです。 コントロールプレーンコンテナ内から再度curlを実行すると、新たにログに追記された内容が出力されます。

例えば、次のような出力が得られるでしょう:

Jul  6 15:37:40 my-machine kernel: [369128.669452] audit: type=1326 audit(1594067860.484:14536): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=51 compat=0 ip=0x46fe1f code=0x7ffc0000
Jul  6 15:37:40 my-machine kernel: [369128.669453] audit: type=1326 audit(1594067860.484:14537): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=54 compat=0 ip=0x46fdba code=0x7ffc0000
Jul  6 15:37:40 my-machine kernel: [369128.669455] audit: type=1326 audit(1594067860.484:14538): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=202 compat=0 ip=0x455e53 code=0x7ffc0000
Jul  6 15:37:40 my-machine kernel: [369128.669456] audit: type=1326 audit(1594067860.484:14539): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=288 compat=0 ip=0x46fdba code=0x7ffc0000
Jul  6 15:37:40 my-machine kernel: [369128.669517] audit: type=1326 audit(1594067860.484:14540): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=0 compat=0 ip=0x46fd44 code=0x7ffc0000
Jul  6 15:37:40 my-machine kernel: [369128.669519] audit: type=1326 audit(1594067860.484:14541): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=270 compat=0 ip=0x4559b1 code=0x7ffc0000
Jul  6 15:38:40 my-machine kernel: [369188.671648] audit: type=1326 audit(1594067920.488:14559): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=270 compat=0 ip=0x4559b1 code=0x7ffc0000
Jul  6 15:38:40 my-machine kernel: [369188.671726] audit: type=1326 audit(1594067920.488:14560): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=202 compat=0 ip=0x455e53 code=0x7ffc0000

各行のsyscall=エントリに着目することで、http-echoプロセスが必要とするシステムコールを理解していくことができるでしょう。 このプロセスが利用する全てのシステムコールを網羅するものではないかもしれませんが、このコンテナのseccompプロファイルを作成する上での基礎とすることは可能です。

次のセクションに進む前にServiceとPodを削除します:

kubectl delete service audit-pod --wait
kubectl delete pod audit-pod --wait --now

違反が発生するseccompプロファイルでPodを作成する

デモとして、いかなるシステムコールも許可しないseccompプロファイルをPodに適用してみましょう。

マニフェストは次の通りです:

apiVersion: v1
kind: Pod
metadata:
  name: violation-pod
  labels:
    app: violation-pod
spec:
  securityContext:
    seccompProfile:
      type: Localhost
      localhostProfile: profiles/violation.json
  containers:
  - name: test-container
    image: hashicorp/http-echo:1.0
    args:
    - "-text=just made some syscalls!"
    securityContext:
      allowPrivilegeEscalation: false

クラスターにPodを作成してみます:

kubectl apply -f https://k8s.io/examples/pods/security/seccomp/ga/violation-pod.yaml

Podを作成しても問題が発生します。 Podの状態を確認すると、起動に失敗していることが確認できるはずです。

kubectl get pod violation-pod
NAME            READY   STATUS             RESTARTS   AGE
violation-pod   0/1     CrashLoopBackOff   1          6s

直前の事例で見てきたように、http-echoプロセスは多くのシステムコールを必要とします。 ここでは"defaultAction": "SCMP_ACT_ERRNO"が設定されているため、あらゆるシステムコールに対してseccompがエラーを発生させました。

この構成はとてもセキュアですが、有意義な処理は何もできないことを意味します。 私たちが実際にやりたいのは、ワークロードが必要とする権限のみを付与することです。

次のセクションに進む前にPodを削除します。

kubectl delete pod violation-pod --wait --now

必要なシステムコールのみを許可するseccompプロファイルを用いてPodを作成する

fine-grained.jsonプロファイルの内容を見れば、最初の例で"defaultAction":"SCMP_ACT_LOG"を設定していた際に、ログに表示されたシステムコールが含まれていることに気づくでしょう。 今回のプロファイルでも"defaultAction": "SCMP_ACT_ERRNO"を設定しているものの、"action": "SCMP_ACT_ALLOW"ブロックで明示的に一連のシステムコールを許可しています。 理想的な状況下であれば、コンテナが正常に稼働することに加え、syslogへのメッセージ送信を見ることはなくなるでしょう。

この事例のマニフェストは次の通りです:

apiVersion: v1
kind: Pod
metadata:
  name: fine-pod
  labels:
    app: fine-pod
spec:
  securityContext:
    seccompProfile:
      type: Localhost
      localhostProfile: profiles/fine-grained.json
  containers:
  - name: test-container
    image: hashicorp/http-echo:1.0
    args:
    - "-text=just made some syscalls!"
    securityContext:
      allowPrivilegeEscalation: false

クラスター内にPodを作成します:

kubectl apply -f https://k8s.io/examples/pods/security/seccomp/ga/fine-pod.yaml
kubectl get pod fine-pod

Podは正常に起動しているはずです:

NAME        READY   STATUS    RESTARTS   AGE
fine-pod   1/1     Running   0          30s

新しいターミナルを開いて、http-echoからのシステムコールに関するログエントリをtailで監視しましょう:

# あなたのマシンのログは"/var/log/syslog"以外の場所にあるかもしれません。
tail -f /var/log/syslog | grep 'http-echo'

次のPodをNodePort Serviceで公開します:

kubectl expose pod fine-pod --type NodePort --port 5678

ノード上のServiceに割り当てられたポートを確認します:

kubectl get service fine-pod

出力は次のようになるでしょう:

NAME        TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
fine-pod    NodePort   10.111.36.142   <none>        5678:32373/TCP   72s

kindのコントロールプレーンコンテナの内部から、curlを用いてエンドポイントにアクセスします:

# 6a96207fed4bと32373を、それぞれ"docker ps"で確認したコンテナIDとポート番号に変更してください。
docker exec -it 6a96207fed4b curl localhost:32373
just made some syscalls!

syslogには何も出力されないはずです。 なぜなら、このプロファイルは必要なシステムコールを全て許可しており、一覧にないシステムコールが呼び出された時にのみエラーを発生させるように構成しているためです。 これはセキュリティの観点からすると理想的なシチュエーションといえますが、seccompプロファイルを作成するためのプログラムの解析には多少の労力が必要です。 多くの労力を割かなくてもこれに近いセキュリティが得られるシンプルな手法があったなら、きっと素晴らしいものになるでしょう。

次のセクションに進む前にServiceとPodを削除します:

kubectl delete service fine-pod --wait
kubectl delete pod fine-pod --wait --now

全ワークロードに対する標準seccompプロファイルとしてRuntimeDefaultを使用する

FEATURE STATE: Kubernetes v1.27 [stable]

標準seccompプロファイルを指定するためには、この機能を利用したい全てのノードで--seccomp-defaultコマンドラインフラグを用いてkubeletを起動する必要があります。

この機能を有効化すると、kubeletはコンテナランタイムが定義するRuntimeDefaultのseccompプロファイルをデフォルトで使用するようになり、Unconfinedモード(seccomp無効化)になることはありません。 コンテナランタイムが用意する標準プロファイルは、ワークロードの機能性を維持しつつ、強力な標準セキュリティルールの一式を用意することを目指しています。 標準のプロファイルはコンテナランタイムやリリースバージョンによって異なる可能性があります。 例えば、CRI-Oとcontainerdの標準プロファイルを比較してみるとよいでしょう。

いくつかのワークロードでは、他のワークロードよりもシステムコール制限を少なくすることが必要な場合があります。 つまり、RuntimeDefaultを適用する場合、こうしたワークロードの実行が失敗する可能性があります。 このような障害を緩和するために、次のような対策を講じることができます:

  • ワークロードを明示的にUnconfinedとして稼働させる。
  • SeccompDefault機能をノードで無効化する。 また、機能を無効化したノードに対してワークロードが配置されていることを確認しておく。
  • ワークロードを対象とするカスタムseccompプロファイルを作成する。

実運用環境に近いクラスターに対してこの機能を展開する場合、クラスター全体に対して変更をロールアウトする前に、一部ノードのみを対象にしてこのフィーチャーゲートを有効化し、ワークロードが実行できることを検証しておくことをお勧めします。

クラスターに対してとりうるアップグレード・ダウングレード戦略について更に詳細な情報を知りたい場合は、関連するKubernetes Enhancement Proposal (KEP)であるEnable seccomp by defaultを参照してください。

Kubernetes 1.33では、specで特定のseccompプロファイルを指定していないPodに対して、デフォルトで適用するseccompプロファイルを設定することができます。 ただし、この標準seccompプロファイルを利用する場合、対象とする全ノードでこの機能を有効化しておく必要があります。

稼働中のKubernetes 1.33クラスターでこの機能を有効化する場合、kubeletに--seccomp-defaultコマンドラインフラグを付与して起動するか、Kubernetesの設定ファイルでこの機能を有効化する必要があります。 このフィーチャーゲートをkindで有効化する場合、kindが最低限必要なKubernetesバージョンを満たしていて、かつkindの設定ファイルSeccompDefaultを有効化してください:

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
    image: kindest/node:v1.28.0@sha256:9f3ff58f19dcf1a0611d11e8ac989fdb30a28f40f236f59f0bea31fb956ccf5c
    kubeadmConfigPatches:
      - |
        kind: JoinConfiguration
        nodeRegistration:
          kubeletExtraArgs:
            seccomp-default: "true"        
  - role: worker
    image: kindest/node:v1.28.0@sha256:9f3ff58f19dcf1a0611d11e8ac989fdb30a28f40f236f59f0bea31fb956ccf5c
    kubeadmConfigPatches:
      - |
        kind: JoinConfiguration
        nodeRegistration:
          kubeletExtraArgs:
            seccomp-default: "true"        

クラスターの準備ができたら、Podを実行します:

kubectl run --rm -it --restart=Never --image=alpine alpine -- sh

このコマンドで標準のseccompプロファイルを紐付けられるはずです。 この結果を確認するには、docker exec経由でcrictl inspectを実行することで、kindワーカー上のコンテナを確認します:

docker exec -it kind-worker bash -c \
    'crictl inspect $(crictl ps --name=alpine -q) | jq .info.runtimeSpec.linux.seccomp'
{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_X32"],
  "syscalls": [
    {
      "names": ["..."]
    }
  ]
}

次の項目

Linuxのseccompについて更に学びたい場合は、次の記事を参考にすると良いでしょう: