オペレータの導入と権限境界の検証
目標
Operatorのマニフェスト一式(ネームスペース、RBAC、コントローラーのDeployment、リーダー選出のLease)を正しい順序でインストールし、その権限が実際に意図した境界の中にあることを、自分で問い合わせて証明します。
なぜ重要なのか
Operatorは、クラスターで最も強い権限を持つワークロードになりうるものです。複数のネームスペースのリソースを作成・変更するため、そのサービスアカウントが窃取されると、攻撃者がその権限をそのまま手に入れます。そのため、2つのことが核心です。1つ目は、インストールの順序が依存関係であることです。コントローラーは起動した直後に自分の型をwatchするのでCRDが先に必要で、一覧の取得から始めるので権限が先に必要です。順序が間違っているとクラッシュループや403として現れますが、原因がコードではなくデプロイの順序だと気づくのに時間がかかります。2つ目は、権限はverbを絞る作業であることです。verbs: ["*"]は楽ですが、子の後片付けを所有者参照に任せれば、deleteさえ必要ないことが多いです。そして、RBACではwebservicesとwebservices/statusは完全に別のリソースです。この事実を知らないと、「権限を与えたのにstatusを書けない」という症状にはまり込みます。最後に、権限はドキュメントではなく検証の対象です。kubectl auth can-i --as=で、できることとできてはいけないことの両方を確認しておけば、その表がそのまま権限の境界の証拠になります。
ステップ
開始前の準備: ラボのPodはラボごとに新しく起動するため、前のラボのクラスターの状態は残っていません。ステップ2とステップ6がCRD webservices.apps.labhub.ioを要求し、ステップ7・ステップ8の権限の問い合わせがcrd-labネームスペースを対象にしているため、kubectl get crd webservices.apps.labhub.ioの結果が空なら、CRDを再び適用し、kubectl create ns crd-labも実行してください。CRDはv1alpha1とv1の2つのバージョンを持つ必要があり、ストレージバージョンはv1でなければなりません(ステップ6がその保存を検査します)。
- ネームスペース
labhub-operatorを作成し、ラベルapp.kubernetes.io/part-of=labhub-operatorとpod-security.kubernetes.io/enforce=restrictedを付けてください。 /root/op/install/out/install-order.txtに、インストールの順序を1段階につき1行で書いてください。1行目はCustomResourceDefinition、2行目はServiceAccountとClusterRole/ClusterRoleBinding、3行目はDeploymentに言及する必要があり、前の行でDeploymentや컨트롤러(韓国語で「コントローラー」を意味する語です)という語を先に書かないでください(順序の判定は、最初に登場した行を基準にします)。CRDwebservices.apps.labhub.ioもクラスターに存在している必要があります。labhub-operatorにServiceAccountwebservice-controllerを作成し、ClusterRolewebservice-controllerを次のルールで作成してください。apiGroups: ["apps.labhub.io"]、resources: ["webservices"]、verbs: ["get","list","watch","update","patch"]apiGroups: ["apps.labhub.io"]、resources: ["webservices/status","webservices/finalizers"]、verbs: ["get","update","patch"]apiGroups: [""]、resources: ["events"]、verbs: ["create","patch"]apiGroups: ["coordination.k8s.io"]、resources: ["leases"]、verbs: ["get","list","watch","create","update","patch"]verbにもリソースにも*を使ってはいけません。そして、ClusterRoleBindingwebservice-controllerを作成し、subjectをkind: ServiceAccount、name: webservice-controller、namespace: labhub-operatorに指定してください。
labhub-operatorにDeploymentwebservice-controllerを作成してください。spec.replicasは1または2、spec.template.spec.serviceAccountNameはwebservice-controller、コンテナイメージはghcr.io/labhub/webservice-controller:v0.1.0とし、argsに--leader-elect=trueを入れ、コンテナにlivenessProbeとreadinessProbeをそれぞれ置いて、resources.limits.memoryを設定してください。PodのsecurityContext.runAsNonRootはtrueである必要があります。labhub-operatorにLeasewebservice-controller.labhub.ioを作成してください。spec.holderIdentityはリーダーの身元を示す文字列、spec.leaseDurationSecondsは5以上、spec.renewTimeはマイクロ秒まであるRFC3339の時刻(date -u +%Y-%m-%dT%H:%M:%S.%6NZ)です。そして、/root/op/install/out/lease-note.txtに、リーダー選出がないとなぜ困るのか(同じオブジェクトを2つが同時に調整する問題)を書いてください。- Deploymentのコンテナイメージを
ghcr.io/labhub/webservice-controller:v0.2.0に上げて、ロールアウトが終わるまで待ってください。CRDはバージョンが2つ以上そのまま残っている必要があり、status.storedVersionsにv1がある必要があります。アップグレードの前後で何を確認したかを/root/op/install/out/upgrade-check.txtに書き、storedVersionsを確認した内容を必ず含めてください。 /opt/lab/fixtures/operator/broken-operator.yamlを/root/op/install/fixed-operator.yamlにコピーして、間違っている3か所を直して適用してください。そのうえで、/root/op/install/out/diagnosis.txtに、何がなぜ間違っていたかを1行に1つずつ、3行以上書いてください。バインディングのサブジェクトの名前の問題と、statusサブリソースの権限の抜けは、必ず含める必要があります。修正後、kubectl auth can-i update webservices/status -n crd-lab --as=system:serviceaccount:labhub-operator:webservice-controllerとkubectl auth can-i list webservices --all-namespaces --as=...が、どちらもyesである必要があります。/root/op/install/out/permissions.jsonを作成してください。最上位のキーはchecksで、各要素はverb、resource、expected(yes/no)を持ち、必要ならnamespaceを持ちます。namespaceを書かない場合は、全ネームスペースを基準に確認されます。最低6件以上である必要があり、expectedがnoの項目が2件以上である必要があり、そのうち1つは必ずresourceがsecretsでなければなりません。たとえば、list webservices(yes)、watch webservices(yes)、crd-labでのupdate webservices/status(yes)、crd-labでのcreate events(yes)、crd-labでのupdate webservices/finalizers(yes)、crd-labでのget secrets(no)、crd-labでのdelete webservices(no)、create clusterrolebindings(no)の8件で十分です。すべての項目のexpectedが、実際の応答と同じである必要があります。
参考
- ラボのPodはラボごとに新しく起動するため、前のラボのクラスターの状態は残っていません。それでも、宣言をファイルとして残しておけば、どのPodでも同じ状態を再現できます。これが宣言的であることの実質的な利点であり、Operatorのインストールをマニフェストで管理すべき理由です。
- 権限の確認の基準となるサブジェクトは
system:serviceaccount:labhub-operator:webservice-controllerです。kubectl auth can-i <동사> <리소스> -n <ns> --as=<주체>(プレースホルダーはverb、リソース、ネームスペース、サブジェクトです)の形で書きます。 - ステップ3のルールには、
deleteをあえて入れていません。子の後片付けを所有者参照とガベージコレクターに任せるのが標準で、そうすれば、侵害されたときの大量削除の経路がふさがれます。ステップ8のdelete webservices(no)が、この設計を検証します。 - リーダー選出のLeaseは、
resourceNamesでその名前1つだけに権限を限定するのがよりよいハードニングですが、このラボでは要求しません。 - restrictedポリシーを満たすには、Podに
seccompProfile.type: RuntimeDefaultを、コンテナにallowPrivilegeEscalation: falseとcapabilities.drop: ["ALL"]を、あわせて置くのがよいです。 - よくあるミス1: ステップ2で、1行目に「CRDを先に入れないとコントローラーが起動しない」のように、後の段階の語を先に書いてしまうことです。順序の判定は最初に登場した行番号で行われるため、判定が逆転します。
- よくあるミス2: ステップ3で、
webservicesにだけ権限を与えて、webservices/statusを抜かしてしまうことです。RBACでは、サブリソースは完全に別のリソースです。 - よくあるミス3: ステップ5で、
renewTimeを秒単位のRFC3339でしか書かないことです。Leaseの時刻フィールドは、マイクロ秒の6桁を要求します。
Operator専用のネームスペースを作る
ネームスペースlabhub-operatorを作成し、ラベルapp.kubernetes.io/part-of=labhub-operatorとpod-security.kubernetes.io/enforce=restrictedを付けてください。
構成要素を一度に探すための所属ラベルと、Podに特権を許可しないポリシーのラベルが、それぞれ必要です。コントローラーは、特権がまったく必要ないワークロードです。
インストールの順序を整理する
/root/op/install/out/install-order.txtに、インストールの順序を1段階につき1行で書いてください。1行目はCustomResourceDefinition、2行目はServiceAccountとClusterRole/ClusterRoleBinding、3行目はDeploymentに言及する必要があり、前の行でDeploymentや컨트롤러(韓国語で「コントローラー」を意味する語です)という語を先に書かないでください(順序の判定は、最初に登場した行を基準にします)。CRD webservices.apps.labhub.ioもクラスターに存在している必要があります。
コントローラーは、起動した直後に自分の型をwatchして、一覧を取得します。その2つが先に準備できている必要があります。順序のドキュメントは1段階に1行ずつ書き、前の行に後の段階の名前を先に言及しないでください。
ワイルドカードのない最小権限を作る
labhub-operatorにServiceAccount webservice-controllerを作成し、ClusterRole webservice-controllerを次のルールで作成してください。
apiGroups: ["apps.labhub.io"]、resources: ["webservices"]、verbs: ["get","list","watch","update","patch"]apiGroups: ["apps.labhub.io"]、resources: ["webservices/status","webservices/finalizers"]、verbs: ["get","update","patch"]apiGroups: [""]、resources: ["events"]、verbs: ["create","patch"]apiGroups: ["coordination.k8s.io"]、resources: ["leases"]、verbs: ["get","list","watch","create","update","patch"]verbにもリソースにも*を使ってはいけません。そして、ClusterRoleBindingwebservice-controllerを作成し、subjectをkind: ServiceAccount、name: webservice-controller、namespace: labhub-operatorに指定してください。
メインのリソースとサブリソースは、RBACでは別々のリソースです。statusとfinalizersを別々に書く必要があり、イベントはコアグループです。アスタリスクは、verbにもリソースにも使ってはいけません。
コントローラーのDeploymentを作成する
labhub-operatorにDeployment webservice-controllerを作成してください。spec.replicasは1または2、spec.template.spec.serviceAccountNameはwebservice-controller、コンテナイメージはghcr.io/labhub/webservice-controller:v0.1.0とし、argsに--leader-elect=trueを入れ、コンテナにlivenessProbeとreadinessProbeをそれぞれ置いて、resources.limits.memoryを設定してください。PodのsecurityContext.runAsNonRootはtrueである必要があります。
デフォルトのサービスアカウントで動かすと、せっかく作った権限が適用されません。レプリカを複数置けるようにする引数と、生きているか・受け付ける準備ができているかを確認する2つのプローブ、キャッシュが大きくなるときに備えたメモリの上限が必要です。
リーダー選出のLeaseを作成する
labhub-operatorにLease webservice-controller.labhub.ioを作成してください。spec.holderIdentityはリーダーの身元を示す文字列、spec.leaseDurationSecondsは5以上、spec.renewTimeはマイクロ秒まであるRFC3339の時刻(date -u +%Y-%m-%dT%H:%M:%S.%6NZ)です。そして、/root/op/install/out/lease-note.txtに、リーダー選出がないとなぜ困るのか(同じオブジェクトを2つが同時に調整する問題)を書いてください。
Leaseには、今誰がリーダーなのか、どれだけの間有効なのか、最後に更新した時刻が入ります。更新時刻はマイクロ秒まである形式なので、桁数が合わないと拒否されます。
イメージを上げて、CRDのバージョンが保存されているか確認する
Deploymentのコンテナイメージをghcr.io/labhub/webservice-controller:v0.2.0に上げて、ロールアウトが終わるまで待ってください。CRDはバージョンが2つ以上そのまま残っている必要があり、status.storedVersionsにv1がある必要があります。アップグレードの前後で何を確認したかを/root/op/install/out/upgrade-check.txtに書き、storedVersionsを確認した内容を必ず含めてください。
コントローラーのイメージはローリングアップデートで上げてもかまいませんが、CRDの古いバージョンは、むやみに削除してはいけません。保存されたことのあるバージョンの一覧を、先に確認してください。ロールアウトが終わったかどうかも、確認する必要があります。
壊れたOperatorのマニフェストを直す
/opt/lab/fixtures/operator/broken-operator.yamlを/root/op/install/fixed-operator.yamlにコピーして、間違っている3か所を直して適用してください。そのうえで、/root/op/install/out/diagnosis.txtに、何がなぜ間違っていたかを1行に1つずつ、3行以上書いてください。バインディングのサブジェクトの名前の問題と、statusサブリソースの権限の抜けは、必ず含める必要があります。修正後、kubectl auth can-i update webservices/status -n crd-lab --as=system:serviceaccount:labhub-operator:webservice-controllerとkubectl auth can-i list webservices --all-namespaces --as=...が、どちらもyesである必要があります。
存在しないサブジェクトを指すバインディングは、エラーなしに作られます。そして、メインのリソースに対する権限があっても、サブリソースは別です。確認はログではなく、権限の問い合わせで行ってください。
権限の境界の検証表を作る
/root/op/install/out/permissions.jsonを作成してください。最上位のキーはchecksで、各要素はverb、resource、expected(yes/no)を持ち、必要ならnamespaceを持ちます。namespaceを書かない場合は、全ネームスペースを基準に確認されます。最低6件以上である必要があり、expectedがnoの項目が2件以上である必要があり、そのうち1つは必ずresourceがsecretsでなければなりません。たとえば、list webservices(yes)、watch webservices(yes)、crd-labでのupdate webservices/status(yes)、crd-labでのcreate events(yes)、crd-labでのupdate webservices/finalizers(yes)、crd-labでのget secrets(no)、crd-labでのdelete webservices(no)、create clusterrolebindings(no)の8件で十分です。すべての項目のexpectedが、実際の応答と同じである必要があります。
できることだけを確認しても、境界を証明できません。できてはいけないことも一緒に入れて、実際の応答と照合してください。ネームスペースを書かなかった項目は、全ネームスペースを基準に確認されます。