みんな大好き SCC

Advent Calendar のおかげでなんとか年 1 ペースでの記事投稿を維持してます。
これは OpenShift Advent Calendar 2024 の 12/8 の記事です。

## 前置き

去年は Red Hat Advent Calendar に、 [ローカル環境の Podman で Kubernetes にデプロイするためのマニフェストも動作検証しちゃおうという記事](https://qiita.com/k-srkw/items/952ab09f6071a732e3bd)を書きました。

ローカルで大体動くことが確認できたら、次は OpenShift にデプロイしたいですね。
例えば Podman で検証済みの次のような Deployment でアプリケーションをデプロイしてみます。

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: sample-service-1
    environment: openshift
  name: sample-service-1
  namespace: scc-test
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample-service-1
  template:
    metadata:
      labels:
        app: sample-service-1
    spec:
      serviceAccountName: sample-service-1
      containers:
      - image: quay.io/kshiraka/sample-service-1:latest
        imagePullPolicy: IfNotPresent
        name: sample-service-1
        ports:
        - containerPort: 8080
          name: http
          protocol: TCP
        resources:
          limits:
            memory: 1Gi
          requests:
            cpu: 500m
            memory: 500Mi
        securityContext:
          privileged: true
          runAsUser: 0
```

```
$ oc apply -f deployment.yaml
```

さて、Deployment を作成したのでアプリケーションの Pod が実行されていることを確認してみます。...おや？ Pod が作成されていません...なぜ...

```
$ oc get pod
No resources found in scc-test namespace.
```

トラブルシューティングをしていきます。まずは Deployment の詳細を確認してみましょう。
なるほど、 `Conditions` の内容から ReplicaSet は作成したものの、何らかの理由で Pod が作成できていないようです。

```
$ oc describe deployment sample-service-1
~省略~
Conditions:
  Type             Status  Reason
  ----             ------  ------
  Progressing      True    NewReplicaSetCreated
  Available        False   MinimumReplicasUnavailable
  ReplicaFailure   True    FailedCreate
OldReplicaSets:    <none>
NewReplicaSet:     sample-service-1-6888568fdc (0/1 replicas created)
Events:
  Type    Reason             Age   From                   Message
  ----    ------             ----  ----                   -------
  Normal  ScalingReplicaSet  25s   deployment-controller  Scaled up replica set sample-service-1-6888568fdc to 1
```

原因特定のため、次に ReplicaSet の詳細を確認します。

```
$ oc describe replicaset sample-service-1-6888568fdc
~省略~
Conditions:
  Type             Status  Reason
  ----             ------  ------
  ReplicaFailure   True    FailedCreate
Events:
  Type     Reason        Age                From                   Message
  ----     ------        ----               ----                   -------
  Warning  FailedCreate  5s (x15 over 87s)  replicaset-controller  Error creating: pods "sample-service-1-6888568fdc-" is forbidden: unable to validate against any security context constraint: [provider "anyuid": Forbidden: not usable by user or serviceaccount, provider restricted-v2: .containers[0].runAsUser: Invalid value: 0: must be in the ranges: [1000810000, 1000819999], provider restricted-v2: .containers[0].privileged: Invalid value: true: Privileged containers are not allowed, provider "restricted": Forbidden: not usable by user or serviceaccount, provider "nonroot-v2": Forbidden: not usable by user or serviceaccount, provider "nonroot": Forbidden: not usable by user or serviceaccount, provider "hostmount-anyuid": Forbidden: not usable by user or serviceaccount, provider "machine-api-termination-handler": Forbidden: not usable by user or serviceaccount, provider "hostnetwork-v2": Forbidden: not usable by user or serviceaccount, provider "hostnetwork": Forbidden: not usable by user or serviceaccount, provider "hostaccess": Forbidden: not usable by user or serviceaccount, provider "node-exporter": Forbidden: not usable by user or serviceaccount, provider "privileged": Forbidden: not usable by user or serviceaccount]
```

`pods "sample-service-1-6888568fdc-" is forbidden` となっており、その理由として `unable to validate against any security context constraint: ...` と書いてあります。
そう！ Kubernetes に慣れ親しんだ開発者の多くが OpenShift を触り始めたときにぶつかるであろう壁、みんな大好き Security Context Constraint (SCC) が Pod を起動できない理由なのでした (タイトルでネタバレしてますね)。

## SCC ってなに？

Pod / コンテナに許可する権限を制限・管理する OpenShift 独自の機能です。
Kubernetes はデフォルトの設定では Pod に与える設定や権限のチェックを特に行いません。
一方、OpenShift は基本的にデフォルトでセキュアな設定になっており、セキュリティ的なリスクのある設定や権限を付与したアプリケーションを実行できないしくみになっています。
そのしくみを実現する機能が SCC です。

改めて今回作成した Deployment を見てみると、以下の部分でセキュリティ的にリスクのある特権モードでコンテナを実行しようとしている事が分かります。特権コンテナはホストOSのデバイスやファイルシステムにフルアクセスできる非常にリスクの高い設定であるため、OpenShift は SCC によりこのような設定を行ったアプリケーションをデフォルトで実行できないようにしています。

```yaml
spec:
  template:
    spec:
      containers:
        securityContext:        # 特権ユーザ設定を追加
          privileged: true      # 特権モードを有効にする
          runAsUser: 0          # root ユーザ（UID=0）として実行
```

上の例は明らかにセキュリティ上リスクのある例ですが、例えば特定の UID でプロセスを実行することが前提となっているアプリケーションでは `securityContext.runAsUser` で UID を指定したくなります。
しかし、OpenShift がデフォルトで適用する SCC の制約ではこれも制限されており、 OpenShift により割り当てられる[任意の UID を使用してアプリケーションを実行することが推奨](https://docs.openshift.com/container-platform/4.17/openshift_images/create-images.html#use-uid_create-images)されます。

## SCC の種類

SCC は OpenShift 独自の API リソースとして実装されており、制約の異なる複数の SCC を作成できます。
`oc get scc` と実行してみると、[デフォルトで用意されている SCC](https://docs.openshift.com/container-platform/4.17/authentication/managing-security-context-constraints.html#default-sccs_configuring-internal-oauth) とそれらがどのような設定を制限する定義となっているかの概要が確認できます (例えば `PRIV` が `false` の場合 `securityContext.privileged: true` は設定できない)。

```
$ oc get scc
NAME                              PRIV    CAPS                   SELINUX     RUNASUSER          FSGROUP     SUPGROUP    PRIORITY     READONLYROOTFS   VOLUMES
anyuid                            false   <no value>             MustRunAs   RunAsAny           RunAsAny    RunAsAny    10           false            ["configMap","csi","downwardAPI","emptyDir","ephemeral","persistentVolumeClaim","projected","secret"]
hostaccess                        false   <no value>             MustRunAs   MustRunAsRange     MustRunAs   RunAsAny    <no value>   false            ["configMap","csi","downwardAPI","emptyDir","ephemeral","hostPath","persistentVolumeClaim","projected","secret"]
hostmount-anyuid                  false   <no value>             MustRunAs   RunAsAny           RunAsAny    RunAsAny    <no value>   false            ["configMap","csi","downwardAPI","emptyDir","ephemeral","hostPath","nfs","persistentVolumeClaim","projected","secret"]
hostnetwork                       false   <no value>             MustRunAs   MustRunAsRange     MustRunAs   MustRunAs   <no value>   false            ["configMap","csi","downwardAPI","emptyDir","ephemeral","persistentVolumeClaim","projected","secret"]
hostnetwork-v2                    false   ["NET_BIND_SERVICE"]   MustRunAs   MustRunAsRange     MustRunAs   MustRunAs   <no value>   false            ["configMap","csi","downwardAPI","emptyDir","ephemeral","persistentVolumeClaim","projected","secret"]
machine-api-termination-handler   false   <no value>             MustRunAs   RunAsAny           MustRunAs   MustRunAs   <no value>   false            ["downwardAPI","hostPath"]
node-exporter                     true    <no value>             RunAsAny    RunAsAny           RunAsAny    RunAsAny    <no value>   false            ["*"]
nonroot                           false   <no value>             MustRunAs   MustRunAsNonRoot   RunAsAny    RunAsAny    <no value>   false            ["configMap","csi","downwardAPI","emptyDir","ephemeral","persistentVolumeClaim","projected","secret"]
nonroot-v2                        false   ["NET_BIND_SERVICE"]   MustRunAs   MustRunAsNonRoot   RunAsAny    RunAsAny    <no value>   false            ["configMap","csi","downwardAPI","emptyDir","ephemeral","persistentVolumeClaim","projected","secret"]
privileged                        true    ["*"]                  RunAsAny    RunAsAny           RunAsAny    RunAsAny    <no value>   false            ["*"]
restricted                        false   <no value>             MustRunAs   MustRunAsRange     MustRunAs   RunAsAny    <no value>   false            ["configMap","csi","downwardAPI","emptyDir","ephemeral","persistentVolumeClaim","projected","secret"]
restricted-v2                     false   ["NET_BIND_SERVICE"]   MustRunAs   MustRunAsRange     MustRunAs   RunAsAny    <no value>   false            ["configMap","csi","downwardAPI","emptyDir","ephemeral","persistentVolumeClaim","projected","secret"]
```

特に SCC に関する設定をなにもしていない場合、 Pod にはデフォルト SCC のうち以下のように最も限定された権限のみを許可する `restricted-v2` SCC を適用します。
- 特権を許可しない
- OpenShift により割り当てられる任意の UID でコンテナプロセスを実行する
- hostPath を許可しない
- ホストの Network やポートへのアクセスを許可しない

SCC が制約する設定についての詳細は以下のように `oc describe scc` 実行結果の `Settings` から確認できます。

```
$ oc describe scc restricted-v2 
Name:                                           restricted-v2
Priority:                                       <none>
Access:
  Users:                                        <none>
  Groups:                                       <none>
Settings:
  Allow Privileged:                             false
  Allow Privilege Escalation:                   false
  Default Add Capabilities:                     <none>
  Required Drop Capabilities:                   ALL
  Allowed Capabilities:                         NET_BIND_SERVICE
  Allowed Seccomp Profiles:                     runtime/default
  Allowed Volume Types:                         configMap,csi,downwardAPI,emptyDir,ephemeral,persistentVolumeClaim,projected,secret
  Allowed Flexvolumes:                          <all>
  Allowed Unsafe Sysctls:                       <none>
  Forbidden Sysctls:                            <none>
  Allow Host Network:                           false
  Allow Host Ports:                             false
  Allow Host PID:                               false
  Allow Host IPC:                               false
  Read Only Root Filesystem:                    false
  Run As User Strategy: MustRunAsRange
    UID:                                        <none>
    UID Range Min:                              <none>
    UID Range Max:                              <none>
  SELinux Context Strategy: MustRunAs
    User:                                       <none>
    Role:                                       <none>
    Type:                                       <none>
    Level:                                      <none>
  FSGroup Strategy: MustRunAs
    Ranges:                                     <none>
  Supplemental Groups Strategy: RunAsAny
    Ranges:                                     <none>
```

## 結局どうすれば Pod を実行できる？

SCC の制約に引っかかったときの対処方法は以下のどちらかです。
- 適用される SCC の制約の範囲内で Pod を実行できるようアプリケーションの設定や実装を変更する
- アプリケーションの実行に必要な設定を許容する SCC を適用する

前者の対応ができるならそれが望ましいですが、できない場合は後者の対応が必要です。
SCC は Pod を実行する Service Account に対して RBAC により割り当てます。前置きの例を使って説明します。

アプリケーション `sample-service-1` は実は特権は不要だったのですが特定のユーザ (UID 1000) での実行が必要だったため、以下のように設定を変更しました。

```yaml
spec:
  template:
    spec:
      containers:
        securityContext:
          runAsUser: 1000       # UID=1000として実行
```

`runAsUser` で特定の UID を指定してコンテナを実行することは `restricted-v2` SCC で許可されないため、root 以外のユーザでの実行を許容する `nonroot-v2` SCC を適用したいです。

SCC の割り当てには以下のコマンドが利用できます。

```
$ oc adm policy add-scc-to-user <scc_name> -z <serviceaccount_name>
```

`nonroot-v2` SCC を Service Account `sample-service-1` に割り当てる場合は以下を実行します。

```
$ oc adm policy add-scc-to-user nonroot-v2 -z sample-service-1
clusterrole.rbac.authorization.k8s.io/system:openshift:scc:nonroot-v2 added: "sample-service-1"
```

実際に割り当たっていることを確認してみましょう。各 SCC には以下のような ClusterRole が作成されています。

```
$ oc get clusterrole system:openshift:scc:nonroot-v2 -oyaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  ~省略~
  name: system:openshift:scc:nonroot-v2
rules:
- apiGroups:
  - security.openshift.io
  resourceNames:
  - nonroot-v2
  resources:
  - securitycontextconstraints
  verbs:
  - use
```

コマンド実行により ClusterRole を Service Account に割り当てる ClusterRoleBinding が作成されていることが確認できます。

```
$ oc get clusterrolebinding system:openshift:scc:nonroot-v2 -oyaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  creationTimestamp: "2024-12-08T16:39:07Z"
  name: system:openshift:scc:nonroot-v2
  resourceVersion: "4861406"
  uid: ecb30477-2db7-4a9d-af86-253f90f35af7
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: system:openshift:scc:nonroot-v2
subjects:
- kind: ServiceAccount
  name: sample-service-1
  namespace: scc-test
```

RBAC による割り当て設定が確認できたので、 `nonroot-v2` SCC が適用され Pod が実行できたか確認してみましょう。

```
$ oc get pod
NAME                                READY   STATUS    RESTARTS   AGE
sample-service-1-66b9fb868c-qtn6z   1/1     Running   0          16m
```

無事 Pod が起動しています！よかったですね。

Pod にたいしてどの SCC が適用されているかはアノテーション `openshift.io/scc` の値から確認できます。

```
$ oc get pod sample-service-1-66b9fb868c-qtn6z -o jsonpath='{.metadata.annotations.openshift\.io\/scc}'
nonroot-v2
```

## SCC の適用順序

Service Account に対して複数の SCC が割り当てられている場合、以下の[適用順序](https://docs.openshift.com/container-platform/4.17/authentication/managing-security-context-constraints.html#scc-prioritization_configuring-internal-oauth)により SCC が選択されます。

1. ワークロードに SCC が指定されている場合は指定した SCC を適用
2. Service Account に割り当てられた SCC をリスト
3. SCC の priority フィールドの値が大きい順にソート
4. 3 で順序が決定しない場合は権限が少ない順にソート
5. 4 で順序が決定しない場合は名前順でソート
6. ソートされた SCC 順に Pod のフィールドを検証し、最初にマッチした SCC を適用

すでに適用する SCC が決まってる場合や専用のカスタム SCC を作成する場合は 1 で SCC を設定することが確実です。[アノテーション `openshift.io/required-scc` により設定できます](https://docs.openshift.com/container-platform/4.17/authentication/managing-security-context-constraints.html#security-context-constraints-requiring_configuring-internal-oauth)。

## まとめ

SCC はちょっぴりややこしくて邪険にされがちだけど、OpenShift を守ってくれるいいやつなので嫌いにならないであげてください。