新卒インフラエンジニア向け「Kubernetes 実践教科書 ③ CKS セキュリティ・ハードニング編」(全19回)の第5回です。第4回では「誰が、何をできるか」を RBAC と ServiceAccount トークンの側から絞りました。本回はその手前——リクエストが API Server に入る瞬間を扱います。
本回も白紙からは始めません。kubeadm が作ったクラスタには --authorization-mode=Node,RBAC も --enable-admission-plugins=NodeRestriction も最初から入っています。手つかずなのは匿名アクセスの扱いだけです。したがって本回の大半は「設定する」ではなく「既にあるものが何をどこまで止めているのかを測る」ことに時間を使います。
そのうえで、本回には手を動かす主題が 3 つあります。匿名アクセスを正しく締めること、第2回で保留した CIS 1.2.5 を恒久的に直すこと、そして v1.36.2 から v1.36.3 へのパッチアップグレードです。1 つ目には「CIS の Remediation どおりに直すと API Server が起動しなくなる」という山場があります。第2回の 1.2.5 と同じ構図の第 2 弾です。
- ラボ Kubernetes v1.36.2 →(本回で)v1.36.3
- kubeadm v1.36.2 → v1.36.3
- kubectl(k8s-ops) v1.35.6
- containerd v2.2.6
- AlmaLinux 10.2
- kube-bench v0.15.6
- CKS 試験環境 v1.35
- 確認日 2026-07-26
目次
- 第5回のスコープ・今ここマップ
- この回のゴール
- リクエストが API Server を通る 3 つの関門
- 匿名アクセスの実力を測る
- やってみよう①:匿名で API Server を叩いて境界を確かめる
- –anonymous-auth=false を素直に入れるとどうなるか
- 正しい締め方 ——–authentication-config で匿名を probe に限定する
- やってみよう②:匿名エンドポイント限定を 3 台へ展開する
- 認可の関門 ——Node 認可と RBAC の二本立て
- Admission の関門 ——既定の 27 個と kubeadm が足す NodeRestriction
- やってみよう③:越権を試して「どの関門が止めたか」を読む
- 第2回の宿題を回収する ——CIS 1.2.5 を恒久的に直す
- やってみよう④:kubelet サービング証明書をクラスタ CA 署名へ切り替える
- ステップ1:kubelet-config ConfigMap を直す(先にこちら)
- ステップ2:5 ノードのローカル設定にも同じ行を足す
- ステップ3:CSR が Pending で出ていることを確認する
- ステップ4:中身を確認してから承認する
- ステップ5:証明書が差し替わったことを確認する
- ステップ6:k8s-cp-01 の API Server にフラグを足す
- ステップ7:第2回で壊れた 3 つを全部試す
- ステップ8:k8s-cp-02 / k8s-cp-03 にも同じフラグを足す
- ステップ8-b:名指しを外して 6 回連続で成功することを確認する
- ステップ9:kube-bench で 1.2.5 が PASS になったことを確認する
- 試験相当の粒度
- やってみよう⑤:脆弱性を塞ぐ ——v1.36.2 から v1.36.3 へ
- 暗記必須コマンド(第5回)
- まとめ
- 理解度チェック(○×形式・全 9 問)
- 次回予告
第5回のスコープ・今ここマップ
本回は CKS ドメイン D2: Cluster Hardening(配点 15%) の後半にあたります。D2 は 4 つのコンピテンシーからなり、第4回が前 2 つ(RBAC と ServiceAccount)を担当しました。本回が担当するのは残る 2 つです。
| 本回が担当する Competency(英文) | 本回での扱い |
|---|---|
Restrict access to the Kubernetes API | 匿名アクセスの実測・--authentication-config による限定・認可 / Admission の読み解き・kubelet サービング証明書 |
Upgrade Kubernetes to avoid vulnerabilities | v1.36.2 → v1.36.3 のパッチアップグレード(CVE を塞ぐ手段として) |
RBAC そのものの実装は第4回で完了しています。本回で RBAC が出てくるのは、--as や auth can-i --list を測るための道具として使う場面だけです。
第3巻 19 回のうち、現在位置は次のとおりです。
第1部 第3巻オリエンテーション
第1回: 第3巻スコープ + CKA 境界 + 4C/脅威モデル + Kubestronaut
第2部 クラスタ堅牢化(D1/D2)
第2回: kube-bench + CIS ベンチマーク修復 + バイナリ検証(D1)
第3回: NetworkPolicy 完全設計 + ノードメタデータ保護 + TLS Ingress(D1)
第4回: RBAC 監査 + ServiceAccount トークン管理(D2)
★ 第5回: API Server 堅牢化 + kubeadm アップグレード(D2) ← 今ここ
第3部 OS / ノード堅牢化(D3)
第6回: カーネル層の強制アクセス制御
本回の起点
起点は第4回の完了状態です。第2巻から引き継いだ 9 VM 構成(Control Plane Node 3 台 + Workload Node 2 台 + 作業端末・レジストリ・LB・プロキシ)が稼働している前提で進めます。
| 項目 | 値 |
|---|---|
| ノード | 5/5 Ready・Kubernetes v1.36.2 |
kube-bench(k8s-cp-01・--targets master) | 44 PASS / 5 FAIL / 11 WARN |
| 残っている FAIL | 1.2.5(本回で回収)+ 1.2.16〜1.2.19(第16回) |
| fanclub | https://fanclub.local が 200 を返す |
アップグレードは第2巻で 1 度やっている
本回の後半で扱うクラスタアップグレードは、本シリーズで初めてのテーマではありません。文脈を先に整理します。
| 巻 | 既習内容 | 文脈 |
|---|---|---|
| 第2巻 第6回 | v1.35.6 → v1.36.2 のマイナーアップグレード(リポジトリ URL の変更・API 非推奨の事前調査) | 手順を身につける |
| 本回(第3巻 第5回) | v1.36.2 → v1.36.3 のパッチアップグレード | CVE を塞ぐという動機で打ち直す |
本回は第2巻の手順をなぞり直す回ではありません。第2巻は「クラスタを維持するための運用作業」としてアップグレードを扱いました。本回は「公表済みの脆弱性を塞ぐための防御手段」として同じ操作を打ちます。手順の一部は同じでも、判断の基準が変わります。パッチとマイナーで何が違うのか、バージョンスキューがなぜ順序を決めるのかは、後半で改めて整理します。
この回のゴール
本回を終えると、次のことができるようになります。到達できたかは記事末の「やってみよう」と「理解度チェック」で確認します。
- リクエストが API Server を通る 3 つの関門(認証 → 認可 → Admission)を、拒否メッセージから見分けられる
- 匿名アクセスで実際に何が取れるかを測り、リスクを自分の言葉で評価できる
--anonymous-auth=falseをそのまま入れると API Server が起動しなくなる理由を説明できる--authentication-config(AuthenticationConfiguration)で匿名を probe 用エンドポイントだけに限定できる- ファイル参照フラグに 3 点セット(フラグ + volumeMounts + volumes)が必要な理由を、必要なケースと不要なケースの両方で説明できる
- kubelet のサービング証明書をクラスタ CA 署名へ切り替え、CIS 1.2.5 を恒久的に PASS にできる
- パッチアップグレードで CVE を塞ぎ、バージョンスキューの制約を踏まえて順序を組める
- 手で足した設定が
kubeadm upgradeで消える理由を説明でき、kubeadm-configとkubelet-configの 2 つの ConfigMap へ移して恒久化できる
本回で扱わない範囲
| 本回で扱わない領域 | 理由 | 回収する回 |
|---|---|---|
監査ログ(--audit-policy-file / --audit-log-*・CIS 1.2.16〜1.2.19) | ポリシー設計とセットで扱う必要がある | 第16回 |
etcd の保存時暗号化(--encryption-provider-config・CIS 1.2.27 / 1.2.28) | Secret 管理の文脈 | 第9回 |
| Admission によるポリシー適用(PSS / Gatekeeper / ValidatingAdmissionPolicy) | 本回は Admission 層の位置づけまでを扱う | 第8回 |
AlwaysPullImages / 許可レジストリの制限(CIS 1.2.11) | サプライチェーンの文脈 | 第14回 |
| kubelet プロセス自体の堅牢化・ノード OS の攻撃面削減 | D3 の文脈 | 第6回・第7回 |
第2回から送られてきた宿題
第2回では kube-bench の FAIL を 10 件から 5 件まで減らしました。そのとき 1.2.5(--kubelet-certificate-authority)だけは意図的に保留しています。Remediation のとおりにフラグを足すと kubectl logs と kubectl exec が x509 エラーで壊れたため、バックアップから戻したうえで「恒久対応は第5回」と送りました。
その宿題を本回で回収します。壊れた原因はフラグではなく kubelet 側の証明書運用にあります。kubelet のサービング証明書をクラスタ CA の署名に切り替えれば、同じフラグがそのまま機能します。
リクエストが API Server を通る 3 つの関門
本回の背骨になる整理です。以降の各節が、この 3 層のどこかに対応します。kubectl であれ curl であれ、API Server に届いたリクエストは必ず次の順に判定されます。
| 関門 | 何を判断するか | 通らないと | その関門を決めている設定 |
|---|---|---|---|
| ① 認証(Authentication) | 「あなたは誰か」 | 401 Unauthorized | --anonymous-auth / --authentication-config / --client-ca-file(--client-ca-file は kubeadm 既定・本回では変更しません) |
| ② 認可(Authorization) | 「その人にこの操作を許すか」 | 403 Forbidden | --authorization-mode=Node,RBAC |
| ③ Admission 制御 | 「この内容を受け入れるか」 | 403 Forbidden(拒否理由の文面が違う) | --enable-admission-plugins |
401 と 403 の違いが、そのままどの関門で落ちたかを教えてくれます。
401 が返ったなら認証層、403 が返ったなら認可層か Admission 層です。認可層と Admission 層のどちらで落ちたかは、拒否メッセージの文面で見分けます。この見分け方は本回の やってみよう③ で実際に確認します。トラブルシュートでも試験でも、まず 401 か 403 かを見るのが最初の分岐になります。
ここで押さえておきたいのは、3 つのうち kubeadm が既定で締めているのは ② と ③ だけだという点です。--authorization-mode=Node,RBAC も --enable-admission-plugins=NodeRestriction も、kubeadm init が生成した /etc/kubernetes/manifests/kube-apiserver.yaml に最初から書かれています。手つかずなのは ① の認証層です。だから本回は ① から始めます。
なお、3 つを通過したリクエストが最終的に書き込まれる先は etcd です。etcd に保存されたあとの防御(保存時暗号化)は第9回、誰が何を通したかの記録(監査ログ)は第16回で扱います。本回の守備範囲は「etcd に届く前」です。

匿名アクセスの実力を測る
CIS ベンチマークにも、多くのハードニング記事にも「匿名アクセスを無効にせよ」と書かれています。しかし本書はその前に、切らないと何が起きるのかを自分で測ります。CKS が問うのは設定値の暗記ではなく、その設定が何を守っているのかを踏まえた判断だからです。
匿名リクエストは「拒否される」のではなく「通過する」
kube-apiserver の --anonymous-auth は既定で true です。kubeadm はこのフラグを明示しないので、マニフェストを検索しても見つかりません。書かれていない=有効です。
この状態でクライアント証明書もトークンも持たないリクエストが届くと、API Server はそれを拒否せず、system:anonymous というユーザー・system:unauthenticated というグループとして認証を通します。つまり匿名リクエストは、認証層を通過した状態で認可層に進みます。
その先で何ができるかを決めているのは RBAC です。そして匿名に権限を与えているのは、クラスタに最初から入っている次の 1 本だけです。
実行コマンド(k8s-ops 上・developer):
$ kubectl get clusterrole system:public-info-viewer -o yaml
実行結果(metadata.managedFields などの自動付与フィールドを落として整形しています):
kind: ClusterRole
metadata:
name: system:public-info-viewer
rules:
- nonResourceURLs:
- /healthz
- /livez
- /readyz
- /version
- /version/
verbs:
- get
実行コマンド(k8s-ops 上・developer):
$ kubectl get clusterrolebinding system:public-info-viewer -o yaml
実行結果(同じく整形しています):
kind: ClusterRoleBinding
metadata:
name: system:public-info-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:public-info-viewer
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: Group
name: system:authenticated
- apiGroup: rbac.authorization.k8s.io
kind: Group
name: system:unauthenticated
subjects に system:unauthenticated が入っています。ここが匿名アクセスの入口です。
nonResourceURLs という指定
nonResourceURLs は、Pod や Secret のようなリソースではない URL パスそのものに対する権限指定です。/healthz のように API リソースの形をしていないエンドポイントを許可するときに使います。第4回で扱った apiGroups / resources / verbs の組とは別枠で、1 つの Role の中に両方を書くこともできます。
/version と /version/ は末尾スラッシュの違いだけで同じ情報を返しますが、nonResourceURLs は文字列一致なので 2 行必要になります。したがって許可されているのは 4 パス(エントリ数としては 5 行)です。以降の本文もこの数え方で統一します。nonResourceURLs ではワイルドカード * を末尾に付けた前方一致も書けますが(例: /healthz/*)、この ClusterRole では使われていません。
匿名で実際に取れるもの
実機で測った結果が次の表です。k8s-cp-01 上から、その API Server を直接叩いています(HAProxy を経由すると、どの API Server に当たったのか分からなくなるためです)。
| パス | HTTP | 意味 |
|---|---|---|
/version | 200 | gitVersion / goVersion / buildDate まで読める |
/healthz /livez /readyz | 200 | ok が返る。probe が使う |
/api | 403 | User "system:anonymous" cannot get path "/api" |
/api/v1/namespaces | 403 | RBAC で落ちている |
この表の読み方には 2 段階あります。
- 匿名で取れるのはバージョン情報です。Secret も Pod 一覧も取れません。ここだけを見れば被害は限定的に見えます
- ただし 403 は「安全」を意味しません。403 が返っているということは、認証は通っているということです。落としているのは RBAC であり、RBAC は後から変わります
403 が返っている=安全ではありません
匿名リクエストは認証層を通過しています。誰かが system:unauthenticated グループに RoleBinding を 1 本足した瞬間、昨日まで 403 だったパスが今日から 200 になります。第4回で「権限がゼロのトークンでも API Server に到達できること自体が情報になる」と書いたのと同じ構図で、到達できてしまう状態そのものが土台のリスクです。
バージョン情報が漏れると何が起きるか
/version から「このクラスタは v1.36.2 で動いている」と分かれば、攻撃者はその版に効く CVE を狙い撃ちできます。Kubernetes の脆弱性情報は公開されており、影響を受けるバージョン範囲も公表されています。裏を返せば、バージョンが読めるクラスタは「まだパッチを当てていない穴」を探されている前提で考える必要があります。
この一点が、本回の後半でアップグレードを扱う理由でもあります。匿名アクセスを締める(読ませない)ことと、パッチを当てる(読まれても効かない状態にする)ことは、同じリスクに対する2 方向からの対処です。どちらか一方では足りません。
やってみよう①:匿名で API Server を叩いて境界を確かめる
本回の 5 つの演習には「試験ではここが 1 問(何分)」を併記しています。この見積もりについて先にお断りしておきます。
LF は出題数・各問の配点・試験クラスタのノード構成を公表していません。公表されているのは試験時間 120 分と合格点 67% だけです。本書が書いている「1 問 ◯ 分」「試験は 1 台」といった数字は、120 分から逆算した本書独自の目安であり、公式情報ではありません。
それでも併記しているのは、「ラボで 30 分かける作業が、試験では数分で片づける単位に切り出される」という感覚を持っておくと時間配分を誤らないからです。数字そのものではなく、ラボと試験の分量の差を読み取ってください。
この演習を始める前に
- 本演習は読み取りのみです。クラスタの状態は変えません
- ステップ1〜3 の作業場所は k8s-cp-01 上(root) です。HAProxy を経由せず、特定の API Server を直接見るためです(ステップ4・5 は k8s-ops から
kubectlで行います) - ラボ所要時間の目安は 5〜8 分です
- 試験ではここが 1 問(4〜6 分)になります。「指定されたクラスタで匿名ユーザーが到達できるエンドポイントを調べ、報告せよ」という形です
- 起点は第4回完了状態です。5 ノードが
Readyになってから始めてください
本番ガードレール:curl には --noproxy '*' を付けてください。
本書ラボは全ノードに http_proxy / https_proxy が設定されています。付け忘れると、リクエストが alma-proxy(Squid)へ回り、whitelist に無い宛先としてプロキシから 403 が返ります。その 403 は API Server の 403 ではありません。本文の 403 と紛らわしいので、企業ネットワーク配下で検証するときは最初にプロキシを外してから測る、と覚えてください。
ステップ1:匿名で /version を取る
実行コマンド(k8s-cp-01 上・root):
# curl -sk --noproxy '*' https://192.168.1.125:6443/version
実行結果:
{
"major": "1",
"minor": "36",
"emulationMajor": "1",
"emulationMinor": "36",
"minCompatibilityMajor": "1",
"minCompatibilityMinor": "35",
"gitVersion": "v1.36.2",
"gitCommit": "24e2b02af5543d7910c2bb074c7264df5a8f0467",
"gitTreeState": "clean",
"buildDate": "2026-06-11T18:09:09Z",
"goVersion": "go1.26.4",
"compiler": "gc",
"platform": "linux/amd64"
}
IP アドレスは暗記しないでください。
本文の 192.168.1.125 は本書ラボの <cp-01 の IP> です。読者の環境では別の値になりますし、試験環境でも当然別です。覚えるのは「Control Plane Node の IP を 6443 番で直接叩く」という形のほうで、値ではありません。ノードの IP は kubectl get nodes -o wide の INTERNAL-IP 列で確認できます。
-k は TLS 証明書の検証を省くオプションです。ここで確かめたいのは「証明書が正しいか」ではなく「資格情報を何も出さないリクエストが何を返すか」なので、検証は外しています。
ステップ2:probe が使う 3 パスのステータスを取る
本文の判断材料は HTTP ステータスコードなので、本文を捨ててコードだけを出します。
実行コマンド(k8s-cp-01 上・root):
# curl -s -o /dev/null -w '%{http_code}\n' -k --noproxy '*' https://192.168.1.125:6443/healthz
# curl -s -o /dev/null -w '%{http_code}\n' -k --noproxy '*' https://192.168.1.125:6443/livez
# curl -s -o /dev/null -w '%{http_code}\n' -k --noproxy '*' https://192.168.1.125:6443/readyz
実行結果:
200
200
200
本文を見たいときは -o /dev/null -w を外してください。3 パスとも ok が返ります。この 3 つが 200 であることが、あとの節で決定的な意味を持ちます。
ステップ3:403 の本文を読む
実行コマンド(k8s-cp-01 上・root):
# curl -sk --noproxy '*' https://192.168.1.125:6443/api
# curl -sk --noproxy '*' https://192.168.1.125:6443/api/v1/namespaces
実行結果(message フィールドの抜粋):
forbidden: User "system:anonymous" cannot get path "/api"
namespaces is forbidden: User "system:anonymous" cannot list resource "namespaces"
ここで確認してほしいのは system:anonymous という文字列です。「認証されていないから弾かれた」のであれば、拒否メッセージにユーザー名は出てきません。ユーザー名が出ているということは、そのユーザーとして認証されたうえで認可に落とされたということです。
2 つのメッセージの違いにも注目してください。/api のほうは cannot get path——非リソース URL としての拒否です。/api/v1/namespaces のほうは cannot list resource "namespaces"——リソースとしての拒否です。nonResourceURLs とリソース権限が別枠であることが、拒否メッセージにもそのまま現れます。
ステップ4:匿名の権限一覧を出す
第4回では impersonation(--as)で「その主体に何ができるか」を一覧しました。同じ手を匿名にも使えそうに見えます。まず素直に打ってみます。
実行コマンド(k8s-ops 上・developer):
$ kubectl auth can-i --list --as=system:anonymous
実行結果:
Error from server (Forbidden): selfsubjectrulesreviews.authorization.k8s.io is forbidden: User "system:anonymous" cannot create resource "selfsubjectrulesreviews" in API group "authorization.k8s.io" at the cluster scope
使えません。 しかもこの失敗そのものが、本節の結論を裏づけています。
kubectl auth can-i は、裏で SelfSubjectRulesReview(一覧のとき)や SelfSubjectAccessReview(単発のとき)というリソースを API Server に作成して問い合わせています。--as を付けると、その作成リクエスト自体がなりすました相手の権限で実行されるので、相手にその作成権限が無ければ問い合わせ自体が 403 になります。
第4回で ServiceAccount に対して --as が使えたのは、その主体が system:authenticated グループに属していたからです。このグループには system:basic-user という ClusterRole が既定で紐づいており、「自分に何ができるかを問い合わせる権限」が含まれています。system:unauthenticated にはそれが無い——匿名は「自分に何ができるか」すら聞けないほど狭い、ということです。
そこで、管理者が代理で問い合わせる形に変えます。Self の付かない SubjectAccessReview は「この主体はこの操作を許されるか」を第三者として尋ねるリソースで、作成するのは管理者自身なので匿名の権限に左右されません。
実行コマンド(k8s-ops 上・developer):
$ cat <<'EOF' | kubectl create -o jsonpath='{.status}{"\n"}' -f -
apiVersion: authorization.k8s.io/v1
kind: SubjectAccessReview
spec:
user: system:anonymous
groups:
- system:unauthenticated
nonResourceAttributes:
path: /healthz
verb: get
EOF
実行結果:
{"allowed":true,"reason":"RBAC: allowed by ClusterRoleBinding \"system:public-info-viewer\" of ClusterRole \"system:public-info-viewer\" to Group \"system:unauthenticated\""}
reason フィールドがどの ClusterRoleBinding のどの ClusterRole がどのグループに対して許可したのかを全部教えてくれます。前節で読んだ ClusterRole が、実際にこの許可を出している当人だと確定しました。
拒否されるほうも確かめます。path を /api に変えて同じコマンドを打つと、次のようになります。
実行結果(path: /api の場合):
{"allowed":false}
リソースに対して聞きたいときは nonResourceAttributes を resourceAttributes に置き換えます。たとえば resource: secrets / verb: list で尋ねると、やはり {"allowed":false} が返ります。
覚えておくと差がつく使い分け
Self が付くレビュー(SelfSubjectAccessReview / SelfSubjectRulesReview)は「いま自分は何ができるか」、付かない SubjectAccessReview は「この主体は何ができるか」を尋ねるものです。kubectl auth can-i は前者のラッパーなので、--as でなりすました相手が権限の狭い主体だと問い合わせ自体が通りません。相手の権限が狭いときほど SubjectAccessReview が要る——直感と逆なので覚えておいてください。
ステップ5:200 が返ったパスと RBAC を対応づける
前節で読んだ system:public-info-viewer の nonResourceURLs と、ステップ1〜3 の結果を突き合わせます。
| 叩いたパス | 結果 | 根拠 |
|---|---|---|
/version | 200 | nonResourceURLs に /version がある |
/healthz | 200 | 同上 |
/livez | 200 | 同上 |
/readyz | 200 | 同上 |
/api | 403 | 一覧に無い |
/api/v1/namespaces | 403 | リソース権限が無い |
200 が返ったパスと、ClusterRole に書かれたパスが完全に一致します。 匿名の権限は謎の内部仕様ではなく、kubectl get clusterrole で読める 1 本の RBAC で決まっています。
到達点
ここまでで、次のように説明できる状態になりました。「匿名リクエストは認証を通過して system:anonymous になり、RBAC の system:public-info-viewer によって 4 パス(5 エントリ)の nonResourceURLs だけを許されている。取れるのはバージョン情報である」。
試験相当の粒度
「匿名ユーザーが到達できるエンドポイントを調べよ」という 1 問で 4〜6 分が目安です。手数がいちばん少ないのは curl で直接叩くことで、次が SubjectAccessReview です。ステップ4 で見たとおり kubectl auth can-i --as=system:anonymous は使えないので、本番でも試験でも、匿名相手にはこの手が通らないことを先に知っておくのが時間の節約になります。system:public-info-viewer という ClusterRole 名を覚えておくと、kubectl get clusterrole system:public-info-viewer -o yaml の 1 発で答えが出ます。
--anonymous-auth=false を素直に入れるとどうなるか
ここが本回最大の山場です。第2回の 1.2.5 と同じ「CIS のとおりにやると壊れる」構図の第 2 弾を、読者自身の手で再現します。
この節は意図的にクラスタを壊します
- 本節は学習用ラボ専用です。本番クラスタでは実施しないでください
- 壊すのは k8s-cp-01 の 1 台だけです。k8s-cp-02 / k8s-cp-03 には触りません(3 台に同じ変更を流すとクラスタが完全に沈黙します)
- ステップ0 のバックアップは必須です。取らずに進めないでください
- 復旧手順(ステップ5)まで実施してから次の節へ進んでください
- ラボ所要時間の目安は 10〜15 分です(うち約 4 分は待ち時間です)
ステップ0:先にバックアップを取る(鉄則1)
第2回の鉄則1 のとおり、/etc/kubernetes/manifests/ の外へ退避します。本回は複数の節でマニフェストを編集するので、ファイル名に回番号を入れておきます。
実行コマンド(k8s-cp-01 上・root):
# cp /etc/kubernetes/manifests/kube-apiserver.yaml /root/kube-apiserver.yaml.bak-05
このバックアップは やってみよう②(次節)でもそのまま流用します。 第4回までに入れた --profiling=false / --service-account-extend-token-expiration=false / --service-account-max-token-expiration=24h を含んだ状態なので、ここから戻せば第4回完了状態に復帰できます。
ステップ1:kube-bench の Remediation を読む
第2回で入れた kube-bench で、該当項目だけを確認します。root は既定で kubeconfig を持たないので、先に KUBECONFIG を渡します。
実行コマンド(k8s-cp-01 上・root):
# export KUBECONFIG=/etc/kubernetes/admin.conf
# kube-bench run --targets master --check 1.2.1
実行結果(抜粋):
[WARN] 1.2.1 Ensure that the --anonymous-auth argument is set to false (Manual)
== Remediations master ==
1.2.1 Edit the API server pod specification file /etc/kubernetes/manifests/kube-apiserver.yaml
on the control plane node and set the below parameter.
--anonymous-auth=false
指示は明快です。フラグを 1 行足せ、と書いてあります。そのとおりにするとどうなるかを確かめます。
ステップ2:先に probe の構成を読む
フラグを足す前に、kubeadm が生成した static Pod の probe を確認します。
実行コマンド(k8s-cp-01 上・root):
# grep -A 12 -E 'livenessProbe|readinessProbe|startupProbe' /etc/kubernetes/manifests/kube-apiserver.yaml
実行結果:
livenessProbe:
failureThreshold: 8
httpGet:
host: 192.168.1.125
path: /livez
port: probe-port
scheme: HTTPS
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 15
name: kube-apiserver
ports:
- containerPort: 6443
--
readinessProbe:
failureThreshold: 3
httpGet:
host: 192.168.1.125
path: /readyz
port: probe-port
scheme: HTTPS
periodSeconds: 1
timeoutSeconds: 15
resources:
requests:
cpu: 250m
startupProbe:
failureThreshold: 24
httpGet:
host: 192.168.1.125
path: /livez
port: probe-port
scheme: HTTPS
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 15
volumeMounts:
- mountPath: /etc/ssl/certs
name: ca-certs
-A 12 は 12 行先まで出すので、name: や volumeMounts: のような隣接フィールドも一緒に出てきます。読み取りたいのは httpGet と failureThreshold / periodSeconds の 3 つです。
ここが本節の核心です。3 つの probe はいずれも、クライアント証明書もトークンも持たない HTTPS リクエストです。つまり kubelet は自分が起動した API Server を、匿名ユーザーとして叩いて生死を判定しています。
叩いているパスは /livez と /readyz。やってみよう① のステップ2 で 200 を確認した 3 パスのうちの 2 つです。匿名を切れば、この 2 つも 401 になります。
ステップ3:Remediation のとおりに適用する
実行コマンド(k8s-cp-01 上・root):
# vi /etc/kubernetes/manifests/kube-apiserver.yaml
第2回で立てたアルファベット順の原則に従い、--allow-privileged の次に入れます。編集後の該当箇所:
- --allow-privileged=true
- --anonymous-auth=false
- --authorization-mode=Node,RBAC
保存した瞬間に kubelet が static Pod を作り直します(鉄則2)。ここから約 4 分が観察時間です。
ステップ4:待ち時間にクラスタ全体を見る
この待ち時間の観察が本節の教材価値です。 k8s-ops へ移り、いつもどおりの確認コマンドを打ってください。
実行コマンド(k8s-ops 上・developer):
$ kubectl get nodes
$ kubectl get pods -A | grep -v Running
$ curl -sk --noproxy '*' -o /dev/null -w '%{http_code}\n' https://fanclub.local/
実行結果:
NAME STATUS ROLES AGE VERSION
k8s-cp-01 Ready control-plane 25h v1.36.2
k8s-cp-02 Ready control-plane 23h v1.36.2
k8s-cp-03 Ready control-plane 23h v1.36.2
k8s-wl-01 Ready <none> 23h v1.36.2
k8s-wl-02 Ready <none> 23h v1.36.2
NAMESPACE NAME READY STATUS RESTARTS AGE
velero fanclub-default-kopia-maintain-job-1785058568470-llqpr 0/1 Completed 0 3h
velero fanclub-default-kopia-maintain-job-1785062468479-bccg6 0/1 Completed 0 115m
velero fanclub-default-kopia-maintain-job-1785068865091-22ssg 0/1 Completed 0 8m40s
200
3 つとも平常どおりです。grep -v Running に引っかかっているのは Velero の定期ジョブ(Completed)だけで、異常ではありません。壊した本人の Pod は Running なので、この grep には出てきません——ここが次の観察につながります。
次に、壊した本人である kube-apiserver-k8s-cp-01 を名指しで見ます。
実行コマンド(k8s-ops 上・developer):
$ kubectl get pod kube-apiserver-k8s-cp-01 -n kube-system
実行結果:
NAME READY STATUS RESTARTS AGE
kube-apiserver-k8s-cp-01 0/1 Running 1 (33s ago) 5m3s
STATUS は Running のままです。 異常が現れているのは READY の 0/1 と、数分おきに 1 ずつ増える RESTARTS だけ。STATUS の列しか見ない癖がついていると、この状態は素通りしてしまいます。
続いて Events を読みます。
実行コマンド(k8s-ops 上・developer):
$ kubectl describe pod kube-apiserver-k8s-cp-01 -n kube-system | tail -20
実行結果(Events の抜粋・時刻と発生元の列は省いています):
Warning Unhealthy ... Startup probe failed: HTTP probe failed with statuscode: 401
Normal Killing ... Container kube-apiserver failed startup probe, will be restarted
匿名で /livez を叩いて、返ってきたコードも確かめます。
実行コマンド(k8s-cp-01 上・root):
# curl -sk --noproxy '*' https://192.168.1.125:6443/livez
実行結果:
{"status":"Failure","message":"Unauthorized","code":401}
なぜ約 4 分かかるのか
kill されるまでの時間は、ステップ2 で読んだ startupProbe の設定から計算できます。
| 項目 | 値 |
|---|---|
startupProbe.failureThreshold | 24 |
startupProbe.periodSeconds | 10 |
| kill までの時間 | 24 × 10 = 約 240 秒 |
10 秒おきに 24 回失敗して初めて「起動に失敗した」と判定され、コンテナが作り直されます。そして作り直されたコンテナも同じ理由で 401 を返すので、約 4 分周期の再起動ループになります。RESTARTS が数分おきに 1 ずつ増えていくのはこのためです。
ノード上では crictl で同じことが読めます。
実行コマンド(k8s-cp-01 上・root):
# crictl ps -a | grep kube-apiserver
実行結果:
4b92dc09856b1 9ec369c0ddb96 34 seconds ago Running kube-apiserver 1 29e742dc39f6a kube-apiserver-k8s-cp-01 kube-system
d6dc6f42e2ca5 9ec369c0ddb96 5 minutes ago Exited kube-apiserver 0 29e742dc39f6a kube-apiserver-k8s-cp-01 kube-system
右から 4 列目が ATTEMPT です。0 が Exited、1 が Running——1 度作り直されたことがそのまま読み取れます。放置すれば 2、3 と増えていきます。
ATTEMPT の数字が増えているコンテナが並びます。API が落ちている間、真実を教えてくれるのは crictl だけです(鉄則2)。
この状態がいちばん気づきにくい
本番ガードレール:HA だからこそ気づきません
ステップ4 で見たとおり、通常の確認コマンドはすべて正常を返します。
| 見ているもの | 表示 |
|---|---|
kubectl get nodes | 5/5 Ready(kubelet は生きている) |
kubectl get pods -A | 正常(k8s-cp-02 / k8s-cp-03 の API Server が応答している) |
https://fanclub.local | 200 |
| 唯一の異常 | kube-apiserver-k8s-cp-01 が 0/1・RESTARTS が増加 |
HA 構成では 3 台のうち 1 台が死んでも残る 2 台が応答するため、異常はダッシュボードにも kubectl get nodes にも現れません。逆に、もし 3 台すべてに同じ変更を一気に流していたら、クラスタは完全に沈黙します。第2回の鉄則5「1 台ずつ変更し、確認してから次へ進む」は、直す変更だけでなく壊す変更に対する保険でもあります。
気づくための現実的な手立ては 2 つです。ひとつは kubectl get pod -n kube-system -o wide | grep apiserver を変更のたびに打ち、READY と RESTARTS の 2 列を必ず見ること。もうひとつは監視側で kube_pod_container_status_restarts_total のような指標にアラートを張っておくことです。第2巻で入れた kube-prometheus-stack はこの手の異常を拾える位置にいます。
そして CIS はこれを教えてくれない
壊れた状態のまま、もう一度 kube-bench を打ってみます。
実行コマンド(k8s-cp-01 上・root):
# kube-bench run --targets master --check 1.2.1
1.2.1 は WARN から PASS に変わります。 master のスコアも 44 PASS / 5 FAIL / 11 WARN → 45 PASS / 5 FAIL / 10 WARN へ動きます——API Server が再起動ループに入っているにもかかわらず、です。
(Manual) は「手動確認が推奨される項目」という表示であって、「スコアに影響しない」という意味ではありません。 cis-1.12 の 1.2.1 は audit スクリプトを持っており、--anonymous-auth=false という文字列が見つかれば PASS を返します。 動いているかどうかは見ていません。
第2回では「chown etcd:etcd /var/lib/etcd は PASS になるがリスクはほとんど減らない」という例でベンチマーク通過 ≠ 安全を確認しました。本節はその裏面です。ベンチマークの指示どおりに直してクラスタを壊すと、ベンチマークのスコアはむしろ上がります。 自動評価ツールは「設定文字列があるか」しか見ておらず、その設定でクラスタが動くかどうかは見ていません。
ステップ5:復旧する(必須)
ステップ0 のバックアップから戻します。
実行コマンド(k8s-cp-01 上・root):
# cp /root/kube-apiserver.yaml.bak-05 /etc/kubernetes/manifests/kube-apiserver.yaml
復帰の確認は crictl で行ってください。kubectl は k8s-cp-02 / k8s-cp-03 が答えてしまうので、k8s-cp-01 が直ったことの証拠になりません。
実行コマンド(k8s-cp-01 上・root):
# crictl ps | grep kube-apiserver
# curl -s -o /dev/null -w '%{http_code}\n' -k --noproxy '*' https://192.168.1.125:6443/livez
実行結果(2 コマンド目):
200
crictl ps(-a なし)の出力に ATTEMPT が 0 の新しいコンテナが出ていて、匿名 /livez が 200 に戻っていれば復旧です。kubectl --server=https://192.168.1.125:6443 get --raw=/healthz で ok を確認してもかまいません(--server で名指ししているので、この形なら k8s-cp-01 の答えになります)。
正しい締め方 ——--authentication-config で匿名を probe に限定する
前節で分かったのは「匿名を全部切ると probe が死ぬ」ということでした。かといって全部許したままではバージョンが漏れます。Kubernetes には第 3 の選択肢があります。
第2回の予告を、ここで回収します。
第2回の鉄則4 では「実際に 3 点セットを組むのは第5回が最初で、第9回・第16回でも同じ形を使う」と書きました。その「最初」がここです。 匿名アクセスを正しく締める手段がファイル参照フラグなので、フラグだけでは足りません。第9回(etcd 暗号化)と第16回(監査ログ)でも同じ形が出てくるので、ここで型を作っておいてください。
AuthenticationConfiguration という設定ファイル
--anonymous-auth は true か false の 2 択でした。これに対して --authentication-config は、匿名を許すパスを列挙する方式です。設定ファイルの全量は次のとおりです。
/etc/kubernetes/auth/anonymous.yaml:
apiVersion: apiserver.config.k8s.io/v1
kind: AuthenticationConfiguration
anonymous:
enabled: true
conditions:
- path: /livez
- path: /readyz
- path: /healthz
読み方は「匿名認証は有効だが、通用するのは conditions に挙げた 3 パスに対してだけ」です。それ以外のパスに匿名で来たリクエストは、認証層で 401 になります。/version も 401 になるので、バージョン漏えいが止まります。そして /livez と /readyz は許されているので、probe は通ります。
conditions のパス指定は完全一致です。/livez/etcd のようなサブパスは /livez の指定では通りません。必要なら個別に列挙します。
バージョンの前提 ——feature gate は要らない
この機能(匿名エンドポイントの限定)は AnonymousAuthConfigurableEndpoints、設定ファイル方式そのものは StructuredAuthenticationConfiguration という feature gate で導入されました。前者は v1.31 で alpha、v1.32 で beta として入っています。
そこで気になるのは「ラボと試験環境で feature gate の指定が要るのか」です。実機で確かめました。
実行コマンド(k8s-cp-01 上・root):
# crictl ps | grep kube-apiserver
# crictl exec <kube-apiserver のコンテナ ID> kube-apiserver --help | grep -i 'AnonymousAuth\|StructuredAuthentication'
結果は次のとおりでした。ラボの v1.36.2 でも、CKS 試験環境と同じ v1.35 系(registry.k8s.io/kube-apiserver:v1.35.6 のイメージで確認)でも、--feature-gates の一覧に AnonymousAuthConfigurableEndpoints と StructuredAuthenticationConfiguration は載っていません(残っているのは StructuredAuthenticationConfigurationEgressSelector と StructuredAuthenticationConfigurationJWKSMetrics の BETA 2 件だけです)。一覧から外れているのは GA 済みだからで、どちらのバージョンでも feature gate の指定なしに --authentication-config が使えます。
本書は「GA した版番号」を断定しません。確認したのは「v1.35 と v1.36 の実機で、feature gate を指定せずに動く」という事実だけです。feature gate の状態はマイナーごとに変わるので、自分のクラスタで --help を見て確かめる手順のほうを覚えてください。試験環境でも同じ手が使えます。
排他制約 ——2 つを両方書いてはいけない
kube-apiserver --help の --authentication-config の説明には、次の一文があります。
--authentication-config string File with Authentication Configuration to configure the JWT Token
authenticator or the anonymous authenticator. Requires the
StructuredAuthenticationConfiguration feature gate. This flag is
mutually exclusive with the --oidc-* flags if the file configures the
JWT Token authenticator. This flag is mutually exclusive with
--anonymous-auth if the file configures the Anonymous authenticator.
最後の 1 文が該当します。設定ファイルが匿名認証を構成している場合、--anonymous-auth とは排他です。したがって前節で --anonymous-auth=false を入れたままなら必ず消してください。両方書くと API Server が起動しません。
ヘルプ本文には Requires the StructuredAuthenticationConfiguration feature gate とも書かれています。GA 後もヘルプ文言が残っている例で、ヘルプの文言と feature gate 一覧が食い違うことがあるという点も覚えておくと役に立ちます。判断材料としては --feature-gates の一覧のほうが確実です。
3 点セットが必要な理由(鉄則4 の実践)
設定ファイルは /etc/kubernetes/auth/ に置きます。ここで鉄則4 が効きます。static Pod はコンテナなので、マウントしていないホストのディレクトリは見えません。 フラグだけ足しても、コンテナの中には /etc/kubernetes/auth/anonymous.yaml が存在せず、API Server は起動できません。
そこで 3 つを同時に足します。編集後の該当箇所(3 か所):
- --authentication-config=/etc/kubernetes/auth/anonymous.yaml
volumeMounts:
- mountPath: /etc/kubernetes/auth
name: auth-config
readOnly: true
volumes:
- hostPath:
path: /etc/kubernetes/auth
type: DirectoryOrCreate
name: auth-config
3 つの対応関係は次のとおりです。
| # | 何を書くか | 役割 |
|---|---|---|
| ① | command にフラグ | 「このパスのファイルを読め」と API Server に指示する |
| ② | volumeMounts | コンテナの中のどこにそのディレクトリを見せるか |
| ③ | volumes(hostPath) | ホストのどのディレクトリを持ち込むか |
name(ここでは auth-config)が ② と ③ を結ぶキーです。綴りが 1 文字でも違うと Pod が起動しません。type: DirectoryOrCreate は「ディレクトリが無ければ作る」という指定で、kubeadm が既存の volumes に使っているのと同じ書き方です。
3 点セットが要らないケースと対比する
ここは第2回で予告した対比です。第2回 やってみよう③ で足した --kubelet-certificate-authority=/etc/kubernetes/pki/ca.crt(本回の やってみよう④ で恒久的に足すのと同じフラグです)は、明らかにホスト上のファイルを参照するのに ② と ③ が要りませんでした。
理由は /etc/kubernetes/pki が k8s-certs という名前で既にマウント済みだからです。参照先がその配下にあるなら、フラグを足すだけでコンテナから見えます。
| フラグ | 参照先 | 既存マウントの | 必要なもの |
|---|---|---|---|
--kubelet-certificate-authority(第2回・本回 やってみよう④) | /etc/kubernetes/pki/ca.crt | 内(k8s-certs) | フラグのみ |
--authentication-config(本回 やってみよう②) | /etc/kubernetes/auth/anonymous.yaml | 外 | 3 点セット |
3 点セットは「常に 3 つ書く」ではありません。
正しい使い方は「参照先が既存の volumeMounts に含まれているかを先に確認し、含まれていなければ 3 つ足す」です。既存のマウントは grep -A 3 volumeMounts /etc/kubernetes/manifests/kube-apiserver.yaml で確認できます。本書ラボの API Server にあるのは k8s-certs(/etc/kubernetes/pki)・ca-certs・etc-pki-ca-trust・etc-pki-tls-certs の 4 つです。
適用後に何が変わるか
| パス | 適用前 | 適用後 | 意味 |
|---|---|---|---|
/livez | 200 | 200 | probe が通る |
/readyz | 200 | 200 | probe が通る |
/healthz | 200 | 200 | conditions に入れているので通る |
/version | 200 | 401 | バージョン漏えいが止まる |
/api | 403 | 401 | 認可層まで届かなくなった |
/api/v1/secrets | 403 | 401 | 同上 |
403 が 401 に変わったことが、この設定の効き目そのものです。適用前は「認証は通っていて、RBAC で落とされていた」。適用後は「認証の時点で落ちている」。関門が 1 つ手前に移りました。前節で確認した「403 は安全を意味しない」という懸念が、ここで解消されます。
そして --anonymous-auth=false のときと違い、API Server は再起動ループに入りません。probe が使う 2 パスは開いているからです。同じ防御効果を、probe を壊さずに得られます。
締めたあとに、同じ物差しをもう一度当てる
この節の冒頭で「切る前に何が取れるかを測る」と決めました。締めたあとにも同じことをします。 開けたままにした 3 パスから、攻撃者は何を読めるのでしょうか。
実行コマンド(k8s-cp-01 上・root):
# curl -sk --noproxy '*' 'https://192.168.1.125:6443/readyz?verbose'
実行結果(先頭 15 行・実際は 30 行前後):
[+]ping ok
[+]log ok
[+]etcd ok
[+]etcd-readiness ok
[+]informer-sync ok
[+]poststarthook/start-apiserver-admission-initializer ok
[+]poststarthook/generic-apiserver-start-informers ok
[+]poststarthook/priority-and-fairness-config-consumer ok
[+]poststarthook/priority-and-fairness-filter ok
[+]poststarthook/storage-object-count-tracker-hook ok
[+]poststarthook/start-apiextensions-informers ok
[+]poststarthook/start-apiextensions-controllers ok
[+]poststarthook/crd-informer-synced ok
[+]poststarthook/start-system-namespaces-controller ok
[+]poststarthook/peer-endpoint-reconciler-controller ok
通ります。 ?verbose はクエリ文字列であってパスではないので、conditions の path: /readyz という完全一致には引っかからず、そのまま匿名で読めます。/livez?verbose も同じです。
読めるのは各内部チェックの名前と成否です。バージョン番号ほど直接的ではありませんが、etcd が同居しているのか、どの aggregator が動いているのか、といった構成の手がかりにはなります。
本回はこれを許容します。 probe と LB のために開けざるを得ないパスであり、ここを閉じると起動できなくなるからです。大事なのは「残っているリスクを把握したうえで許容する」ことで、「締めたから安全」で思考を止めないことです。 第2回で「ベンチマークを通しても安全とは限らない」と学んだのと同じ姿勢を、自分の設定にも向けます。
conditions に何を残すかは「匿名で叩いている全員」を洗い出してから決める
本番ガードレール:conditions を削る前に「匿名で叩いている全員」を洗い出してください
/livez と /readyz は kubeadm の 3 つの probe が使うと分かりました。では /healthz は誰が使うのでしょうか。真っ先に疑うのはロードバランサです。本書ラボの k8s-lb を確認します。
実行コマンド(k8s-lb 上・root):
# grep -nE 'httpchk|tcp-check|backend|server' /etc/haproxy/haproxy.cfg
実行結果:
22:backend k8s-cp-api
24: option tcp-check
26: server k8s-cp-01 192.168.1.125:6443 check
27: server k8s-cp-02 192.168.1.126:6443 check
28: server k8s-cp-03 192.168.1.127:6443 check
35:backend traefik-https
37: server traefik 192.168.1.200:443 check
本書ラボの HAProxy は option tcp-check、つまり L4(TCP 接続が張れるか)でしか見ていません。 HTTP パスは叩いていないので、/healthz を conditions から外しても本ラボの LB は壊れません。
ただし それは「確認したから分かったこと」であって、外してよい一般則ではありません。HAProxy で option httpchk GET /healthz を使う構成は広く使われており、その場合のヘルスチェックは資格情報を持たない匿名リクエストです。確認せずに外していれば、API Server 自体は健全なのに LB が「全台 down」と判断する、切り分けの難しい障害を作っていました。
conditions を削るときの洗い出し先は 4 か所です。 ①static Pod の probe 定義、②LB の設定(HAProxy なら option httpchk / option tcp-check の行)、③監視のスクレイプ設定、④外形監視サービスの設定。本回では /healthz を残す判断をします。実害がなく、将来 LB を HTTP チェックへ変えたときに壊れないためです。「消せると分かったうえで残す」と「確認せずに残す」は別物で、CKS が問うのは前者です。
安全にしてもベンチマークは通らない
この設定を入れたあとに kube-bench を打つと、1.2.1 は WARN のままです。1.2.1 が探しているのは --anonymous-auth=false というフラグの文字列であって、--authentication-config による限定は見ていないからです。
前節と合わせると、ベンチマークの限界が両方向から見えます。
| やったこと | 実際のクラスタ | kube-bench 1.2.1 |
|---|---|---|
--anonymous-auth=false を素直に適用 | API Server が再起動ループ | PASS へ変わる(44/5/11 → 45/5/10) |
--authentication-config で限定 | 匿名を締めたうえで正常稼働 | WARN のまま(変化なし) |
危険にするとスコアは上がり、安全にしてもスコアは動きません。 ——順序が逆です。ベンチマークは出発点であって到達点ではない、という第2回の主張が、本回でより強い形で確かめられます。スコアの改善を「安全になった証拠」として報告すると、壊れたクラスタを成果として提出することになります。 実務では「WARN のまま残した項目について、なぜその判断をしたのかを記録に残す」ことが対応そのものになります。

やってみよう②:匿名エンドポイント限定を 3 台へ展開する
この演習を始める前に
- 前節のステップ5(復旧)が完了していることを確認してください。
--anonymous-auth=falseが残っていると排他制約に抵触して起動しません - 3 台同時に変更しないでください。 1 台ずつ、確認してから次へ進みます
- ラボ所要時間の目安は 15〜20 分です
- 試験ではここが 1 問(8〜10 分)になります。「指定された Control Plane の API Server に匿名アクセスの制限を設定せよ」という形です。試験は 1 台ですが、本回は 3 台に展開します
ステップ1:バックアップを確認する
k8s-cp-01 については、前節のステップ0 で取った /root/kube-apiserver.yaml.bak-05 をそのまま流用します(取り直し不要)。k8s-cp-02 / k8s-cp-03 では、それぞれの台で新たに取ります。
実行コマンド(k8s-cp-02 上・root / k8s-cp-03 でも同じ):
# cp /etc/kubernetes/manifests/kube-apiserver.yaml /root/kube-apiserver.yaml.bak-05
ステップ2:設定ファイルを置く
実行コマンド(k8s-cp-01 上・root):
# mkdir -p /etc/kubernetes/auth
# vi /etc/kubernetes/auth/anonymous.yaml
内容(全量):
apiVersion: apiserver.config.k8s.io/v1
kind: AuthenticationConfiguration
anonymous:
enabled: true
conditions:
- path: /livez
- path: /readyz
- path: /healthz
保存したら権限を絞ります。
実行コマンド(k8s-cp-01 上・root):
# chmod 600 /etc/kubernetes/auth/anonymous.yaml
# ls -l /etc/kubernetes/auth/
実行結果:
合計 4
-rw-------. 1 root root 168 7月 26 21:44 anonymous.yaml
API Server の設定ファイルは 600・所有者 root が既定の作法です。CIS ベンチマークにも /etc/kubernetes 配下のファイル権限を確認する項目群があり、第2回で kubelet 側の 2 件を修復しました。自分で足すファイルも同じ基準に合わせてください。
ステップ3:マニフェストに 3 点セットを足す
実行コマンド(k8s-cp-01 上・root):
# vi /etc/kubernetes/manifests/kube-apiserver.yaml
3 か所を編集します。まず command にフラグを追加します(アルファベット順で --authorization-mode の前です)。編集後の該当箇所:
- --authentication-config=/etc/kubernetes/auth/anonymous.yaml
- --authorization-mode=Node,RBAC
前節で --anonymous-auth=false を入れたまま復旧していない場合は、この行を削除してください。 排他制約により、両方あると起動しません。
次にコンテナの volumeMounts に 1 ブロック足します。既存のエントリ(k8s-certs など 4 つ)はそのまま残し、その並びに追加します。追加するブロック:
- mountPath: /etc/kubernetes/auth
name: auth-config
readOnly: true
最後に Pod の volumes に 1 ブロック足します。こちらも既存のエントリを残したまま追加します。追加するブロック:
- hostPath:
path: /etc/kubernetes/auth
type: DirectoryOrCreate
name: auth-config
インデントに注意してください。
volumeMounts は spec.containers[0] の下(半角スペース 4 個の階層)、volumes は spec の直下(半角スペース 2 個の階層)です。この 2 つは階層が違います。既にある ca-certs のブロックを目印にして、同じ深さに並べるのが確実です。vi で貼り付けるときは :set paste を先に打つと、自動インデントによる崩れを防げます。
ステップ4:k8s-cp-01 で効き目を確認する
実行コマンド(k8s-cp-01 上・root):
# crictl ps | grep kube-apiserver
ATTEMPT が 0 の新しいコンテナに入れ替わっていることを確認してから、4 つのパスを測ります。
実行コマンド(k8s-cp-01 上・root):
# curl -s -o /dev/null -w '%{http_code}\n' -k --noproxy '*' https://192.168.1.125:6443/livez
# curl -s -o /dev/null -w '%{http_code}\n' -k --noproxy '*' https://192.168.1.125:6443/readyz
# curl -s -o /dev/null -w '%{http_code}\n' -k --noproxy '*' https://192.168.1.125:6443/healthz
# curl -s -o /dev/null -w '%{http_code}\n' -k --noproxy '*' https://192.168.1.125:6443/version
実行結果:
200
200
200
401
/version が 401 になり、/livez と /readyz は 200 のまま——これが本演習の合格条件です。RESTARTS が増えていないことも確認してください。
もし API Server が起動しない場合は、鉄則3 のとおりバックアップから戻してから差分を見直します。原因の大半は次の 3 つです。
volumeMountsとvolumesのnameの綴り違い- インデントの階層違い(
volumesをcontainersの下に書いてしまう) --anonymous-auth=falseの消し忘れ(排他制約)
ステップ5:k8s-cp-02 / k8s-cp-03 へ展開する
本番ガードレール:1 台ずつ・確認してから次へ
k8s-cp-01 が安定したことを確認してから k8s-cp-02 に着手し、k8s-cp-02 が安定してから k8s-cp-03 に進んでください。3 台同時の変更は本書ラボでも本番でも禁止です。 前節で見たとおり、この変更を誤ると API Server は起動しません。3 台同時に誤れば、クラスタ全体の API が同時に停止します。1 台ずつなら、残る 2 台が動いている間に落ち着いて戻せます。
k8s-cp-02(<cp-02 の IP> = 192.168.1.126)・k8s-cp-03(<cp-03 の IP> = 192.168.1.127)で、ステップ1 から ステップ4 を同じ順に繰り返します。curl の宛先 IP だけがその台のものに変わります。
ステップ6:クラスタ全体の健全性を確認する
実行コマンド(k8s-ops 上・developer):
$ kubectl get nodes
$ kubectl get pod -n kube-system -o wide | grep apiserver
$ curl -sk --noproxy '*' -o /dev/null -w '%{http_code}\n' https://fanclub.local/
実行結果:
NAME STATUS ROLES AGE VERSION
k8s-cp-01 Ready control-plane 26h v1.36.2
k8s-cp-02 Ready control-plane 23h v1.36.2
k8s-cp-03 Ready control-plane 23h v1.36.2
k8s-wl-01 Ready <none> 23h v1.36.2
k8s-wl-02 Ready <none> 23h v1.36.2
kube-apiserver-k8s-cp-01 1/1 Running 0 4m22s 192.168.1.125 k8s-cp-01 <none> <none>
kube-apiserver-k8s-cp-02 1/1 Running 0 2m2s 192.168.1.126 k8s-cp-02 <none> <none>
kube-apiserver-k8s-cp-03 1/1 Running 0 48s 192.168.1.127 k8s-cp-03 <none> <none>
200
AGE が 3 台とも短く、しかもバラバラであることに注目してください。1 台ずつ間を空けて適用した時間差が、そのまま AGE に現れています。 RESTARTS はいずれも 0 です。
k8s-ops からの kubectl が普段どおり動いていることも確認してください。kubectl はクライアント証明書を持っているので匿名ではありません。本演習の変更は、正規の資格情報を持つリクエストには何の影響も与えません。
到達点
3 台すべてで匿名アクセスが /livez /readyz /healthz の 3 パスだけになり、クラスタは無停止のままです。HAProxy がどの台へ振り分けても、/version は 401 を返します。第4回で学んだ「HA では 3 台に揃えて初めて設定になる」が、ここでもそのまま効いています。
ここまでのまとめ。
① 認証の関門を締め終えました。匿名は「全部許す」でも「全部切る」でもなく、probe とロードバランサが必要とするパスだけを開ける形に落ち着いています。次からは残る 2 つの関門——② 認可と ③ Admission——を見ます。この 2 つは kubeadm が既定で締めているので、本回では「設定する」のではなく「何をどこまで止めているのかを測る」ことに時間を使います。
試験相当の粒度
「指定された Control Plane の API Server に匿名アクセスの制限を設定せよ」という 1 問で 8〜10 分が目安です。試験で問われるのは 1 台分なので、本演習のステップ2〜4 だけを 1 セット打つ形になります。AuthenticationConfiguration の YAML は kubernetes.io/docs の Authenticating ページから取れるので、暗記より「どのページにあるか」を覚えるほうが有効です。時間を落とさない要点は、3 点セットを 1 回で書き切ることと、保存後に crictl ps で再起動を確かめてから次の設問へ移ることの 2 つです。
認可の関門 ——Node 認可と RBAC の二本立て
② の関門です。ここは kubeadm が既定で締めているので、読み解きに徹します。
実行コマンド(k8s-cp-01 上・root):
# grep authorization-mode /etc/kubernetes/manifests/kube-apiserver.yaml
実行結果:
- --authorization-mode=Node,RBAC
kubeadm init が最初から書いている行です。2 つのモードが左から順に評価され、どちらかが許可すればリクエストは通ります(拒否は加算されません。第4回で扱った「RBAC は許可のみ」と同じ考え方です)。
| モード | 対象 | 役割 |
|---|---|---|
| Node | system:nodes グループの kubelet | 「自分のノードと、自分に割り当てられた Pod に関係するもの」だけを読み書きできるよう限定する |
| RBAC | それ以外すべて | Role / ClusterRole による通常の権限判定(第4回) |
なぜ Node 認可が必要なのか
kubelet は Pod を起動するために、その Pod が参照する Secret・ConfigMap・PersistentVolume を読む必要があります。これを RBAC だけで表現しようとすると「全 Namespace の全 Secret を読める」という、極端に広い ClusterRole になってしまいます。ノードが 1 台侵害されただけでクラスタ全体の Secret が漏れる構図です。
Node 認可は、この判断を静的な権限表ではなく、その時点のスケジューリング結果に基づいて行います。「その kubelet のノードで動いている Pod が参照しているもの」だけに動的に絞るため、Pod が別ノードへ移れば参照できる範囲も変わります。RBAC では書けない種類の絞り方です。
これは第4回で扱った「最小権限」の考え方の延長にありますが、実現手段が RBAC ではない点が押さえどころです。CKS では「kubelet の権限をどう絞るか」という問いに対して、RBAC ではなく Node 認可を答えられる必要があります。
CIS でも確認できる
この設定は kube-bench の 3 項目に対応します。
実行コマンド(k8s-cp-01 上・root):
# kube-bench run --targets master --check 1.2.6,1.2.7,1.2.8
実行結果:
[INFO] 1 Control Plane Security Configuration
[INFO] 1.2 API Server
[PASS] 1.2.6 Ensure that the --authorization-mode argument is not set to AlwaysAllow (Automated)
[PASS] 1.2.7 Ensure that the --authorization-mode argument includes Node (Automated)
[PASS] 1.2.8 Ensure that the --authorization-mode argument includes RBAC (Automated)
== Summary master ==
3 checks PASS
0 checks FAIL
0 checks WARN
0 checks INFO
| ID | 内容 | 判定 |
|---|---|---|
| 1.2.6 | --authorization-mode が AlwaysAllow になっていないこと | PASS |
| 1.2.7 | --authorization-mode に Node を含むこと | PASS |
| 1.2.8 | --authorization-mode に RBAC を含むこと | PASS |
3 件とも既定で PASS します。第2回で「44 PASS」と数えたうちの 3 件は、自分では何もしていない PASSです。ベンチマークのスコアには、こうした「最初から満たしていた項目」が多く含まれます。
アンチパターン:AlwaysAllow
--authorization-mode=AlwaysAllow は、認証を通ったすべてのリクエストを無条件に許可します。認可の関門が実質的に無くなるので、匿名アクセスが有効なままこれを設定すると、資格情報なしで何でもできるクラスタになります。
学習用に立てたクラスタや、古い手順書のまま構築された環境で見かける設定です。1.2.6 が一発で FAIL になるので、kube-bench を打てばすぐ分かります。監査を任されたクラスタでは、まずここを見てください。
逆に、モードの並びを RBAC,Node と書いても動作します。順序が結果を変えるのは「どちらが先に許可を返すか」だけで、最終的な可否は変わりません。順序より、両方が入っていることのほうが重要です。
Admission の関門 ——既定の 27 個と kubeadm が足す NodeRestriction
③ の関門です。ここも kubeadm が既定で締めています。
実行コマンド(k8s-cp-01 上・root):
# grep enable-admission-plugins /etc/kubernetes/manifests/kube-apiserver.yaml
実行結果:
- --enable-admission-plugins=NodeRestriction
この書き方には落とし穴がある
この 1 行を素直に読むと「有効な Admission プラグインは NodeRestriction だけ」に見えます。そうではありません。 --enable-admission-plugins は「既定で有効なものに追加する」フラグです。
本書ラボ(v1.36.2)で既定有効だったのは次の 27 個です。
NamespaceLifecycle, LimitRanger, ServiceAccount, TaintNodesByCondition, PodSecurity, Priority,
DefaultTolerationSeconds, DefaultStorageClass, StorageObjectInUseProtection, PodGroupProtection,
PersistentVolumeClaimResize, RuntimeClass, CertificateApproval, CertificateSigning,
ClusterTrustBundleAttest, CertificateSubjectRestriction, DefaultIngressClass, PodTopologyLabels,
PodGroupWorkloadExists, NodeDeclaredFeatureValidator, JobValidation, PodResizeValidator,
MutatingAdmissionPolicy, MutatingAdmissionWebhook, ValidatingAdmissionPolicy,
ValidatingAdmissionWebhook, ResourceQuota
この 27 個を暗記しないでください
これは本書ラボ(v1.36.2)で実測した顔ぶれです。既定リストはマイナーごとに増減するので、CKS 試験環境の v1.35 では顔ぶれが違う可能性があります。 上の一覧にある PodGroupProtection / PodTopologyLabels / NodeDeclaredFeatureValidator のような新しいものは、その典型です。
数や名前を暗記せず、「その場で --help を読んで確かめる」手順のほうを覚えてください。 覚えるべきは「--enable-admission-plugins は既定への追加である」という性質と、確かめ方の 2 点だけです。
その場で確かめる手順(試験でも使える)
kube-apiserver --help の --enable-admission-plugins の説明に、括弧書きで既定一覧が載っています。ただし kubeadm クラスタでは kube-apiserver のバイナリがホストに無く、static Pod のコンテナの中にあります。そこで crictl exec で読みます。
実行コマンド(k8s-cp-01 上・root):
# crictl ps | grep kube-apiserver
# crictl exec <kube-apiserver のコンテナ ID> kube-apiserver --help | grep -A 6 'enable-admission-plugins'
1 つ目のコマンドで表示された CONTAINER 列の ID を 2 つ目に渡します。この手順は Admission 以外にも効きます。本回では feature gate の確認にも同じ形を使いました。「フラグの既定値を知りたいときは crictl exec で --help」という手筋を覚えておくと、試験でドキュメントを開く回数が減ります。crictl の使い方そのものは kubernetes.io/docs にページがあるので、試験中に参照できます(後述の参照可否表を参照してください)。
一覧から読み取る 4 点
| プラグイン | 既定 | 意味 |
|---|---|---|
PodSecurity | 有効 | Pod Security Standards の実行主体。第8回は「有効化する回」ではなく「使う回」になる |
NodeRestriction | 無効 | 既定に入っていない。kubeadm が意図的に足している |
AlwaysPullImages | 無効 | kube-bench 1.2.11 が WARN な理由。第14回(許可レジストリ制限)への伏線 |
EventRateLimit | 無効 | kube-bench 1.2.9 が WARN。設定ファイルが必要なので本回では入れない |
EventRateLimit は「イベントの大量生成による API Server への負荷を制限する」プラグインです。有効化には --admission-control-config-file で設定ファイルを渡す必要があり、3 点セットの出番になります。同じ --admission-control-config-file は第14回の ImagePolicyWebhook でも使うので、そちらでまとめて扱います。
NodeRestriction が止めているもの
NodeRestriction は、kubelet が自分の Node オブジェクトの一部フィールドを書き換えるのを拒否します。対象は node-restriction.kubernetes.io/ で始まるラベルや taints などです。
ここで層の違いが出ます。Node 認可は「他人のものを触らせない」、NodeRestriction は「自分のものでも触らせないフィールドがある」という線引きです。「自分の Node オブジェクトだから何をしてもよい」わけではない、というのが Admission 層の主張になります。
node-restriction.kubernetes.io/ というプレフィックスのラベルは、「kubelet が自分で名乗れない」ことが保証されているラベルです。この保証があるので、nodeSelector や nodeAffinity の条件に使えば、ノードが乗っ取られても Pod の配置先を偽装されません。「機密データを扱う Pod は node-restriction.kubernetes.io/tier=secure のノードにだけ置く」という設計が成立するのは、このプレフィックスが kubelet から書けないからです。
逆に言えば、普通のラベル(例: tier=secure)で同じ設計をしても、ノードを取られた時点で破れます。kubelet は自分の Node に任意のラベルを付けられるからです。次の演習でこの差を実際に確認します。
やってみよう③:越権を試して「どの関門が止めたか」を読む
3 層モデルを 1 つのコマンド列で確認します。本回の概念部分の総仕上げです。
この演習を始める前に
- k8s-cp-01 上で root(
#プロンプト)で実行してください。kubelet.confが指すクライアント証明書/var/lib/kubelet/pki/kubelet-client-current.pemは root しか読めないため、developerの$では失敗します - 全ステップで
kubectl --kubeconfig=/etc/kubernetes/kubelet.confを使います。長いのでalias kk='kubectl --kubeconfig=/etc/kubernetes/kubelet.conf'を先に張ってもかまいません(本文は省略せずに書きます) - この演習はノードが乗っ取られ、kubelet の鍵を奪われた状況の再現です
- ステップ1 で付けたラベルはステップ5 で必ず外してください
- ラボ所要時間の目安は 5〜8 分です
- 試験ではここが 1 問(5〜8 分)になります。「あるノードの kubelet 資格情報でできること・できないことを調べよ」という形です
ステップ0:誰として認証されているかを確かめる
第4回で使った auth whoami を、kubelet の kubeconfig で打ちます。
実行コマンド(k8s-cp-01 上・root):
# kubectl --kubeconfig=/etc/kubernetes/kubelet.conf auth whoami
実行結果:
ATTRIBUTE VALUE
Username system:node:k8s-cp-01
Groups [system:nodes system:authenticated]
Extra: authentication.kubernetes.io/credential-id [X509SHA256=ccce317f7c550f98dd7dd8c5e0716596aed22aa46df4497c737647cc408d6a0c]
ここを確認してから先へ進んでください。 system:node:<ノード名> というユーザー名と system:nodes というグループが、Node 認可の判定対象そのものです。KUBECONFIG を渡し忘れると kubernetes-admin のまま打つことになり、以降のステップが全部成功してしまって演習になりません。
ステップ1:自ノードに任意ラベルを付ける
実行コマンド(k8s-cp-01 上・root):
# kubectl --kubeconfig=/etc/kubernetes/kubelet.conf label node k8s-cp-01 cks-test=self
実行結果:
node/k8s-cp-01 labeled
成功します。 kubelet は自分の Node オブジェクトに任意のラベルを付けられます。前節の最後で触れたとおり、普通のラベルによるノード選別は、kubelet の資格情報を持つ者に対しては防御になりません。
ステップ2:他ノードにラベルを付ける
実行コマンド(k8s-cp-01 上・root):
# kubectl --kubeconfig=/etc/kubernetes/kubelet.conf label node k8s-cp-02 cks-test=other
実行結果:
Error from server (Forbidden): nodes "k8s-cp-02" is forbidden: User "system:node:k8s-cp-01"
cannot get resource "nodes" in API group "" at the cluster scope:
node 'k8s-cp-01' cannot read 'k8s-cp-02', only its own Node object
拒否されました。メッセージの後半、only its own Node object が Node 認可の言い回しです。
ステップ3:他 Namespace の Secret を一覧する
実行コマンド(k8s-cp-01 上・root):
# kubectl --kubeconfig=/etc/kubernetes/kubelet.conf get secrets -n fanclub
実行結果:
Error from server (Forbidden): secrets is forbidden: User "system:node:k8s-cp-01"
cannot list resource "secrets" in API group "" in the namespace "fanclub": No Object name found
No Object name found は Node 認可の特徴的なメッセージです。Node 認可は「その kubelet の Pod が参照している、名前を指定された特定の Secret」しか許可しません。名前を指定しない list は、原理的に許可対象にならないため、こういう文面になります。
裏を返せば、そのノードで動いている Pod が使っている Secret は、名前を指定すれば読めます。ノードを取られた場合に守れるのは「そのノードに載っていない分」だけです。第9回の etcd 暗号化や、Secret をノードに置かない設計が意味を持つのはこのためです。
ステップ4:自ノードに node-restriction ラベルを付ける
ステップ1 では自分の Node にラベルを付けられました。今度はプレフィックス付きのラベルで同じことを試します。
実行コマンド(k8s-cp-01 上・root):
# kubectl --kubeconfig=/etc/kubernetes/kubelet.conf label node k8s-cp-01 node-restriction.kubernetes.io/owner=attacker
実行結果:
Error from server (Forbidden): nodes "k8s-cp-01" is forbidden:
is not allowed to modify labels: node-restriction.kubernetes.io/owner
対象は自分の Node です。 ステップ2 と違い「読めない」とは言われていません。認可は通っており、止めたのは Admission 層です。
ステップ5:後片付け
実行コマンド(k8s-cp-01 上・root):
# kubectl --kubeconfig=/etc/kubernetes/kubelet.conf label node k8s-cp-01 cks-test-
実行結果:
node/k8s-cp-01 unlabeled
ラベル名の末尾に - を付けると削除です。検証で付けたものはその場で外す——第4回の後片付けと同じ作法です。
読み解き ——同じ 403 でも止めた層が違う
ステップ2・3・4 はすべて Error from server (Forbidden)、つまり HTTP 403 です。しかし止めた関門は同じではありません。
| ステップ | 止めた関門 | 見分け方 |
|---|---|---|
| 2・3 | 認可(Node 認可) | cannot get resource ... at the cluster scope / only its own Node object / No Object name found という権限の言い回し |
| 4 | Admission(NodeRestriction) | is not allowed to modify labels: ... という内容への言及。ステップ2 と違い「読めない」とは言っていない=認可は通っている |
拒否メッセージが「主体の権限」を語っているか、「リクエストの中身」を語っているかで見分けます。前者が認可層、後者が Admission 層です。この読み分けは、Gatekeeper や ValidatingAdmissionPolicy を扱う第8回でも、そのまま使います。
よくある誤解を 1 つ訂正しておきます。
「NodeRestriction は kubelet が他ノードの Node / Pod を改変するのを防ぐ機能である」という説明を見かけますが、実機ではそれを止めているのは Node 認可です(ステップ2 と ステップ3)。NodeRestriction が見るのは、認可を通ったリクエストの中身です。両者は補い合う関係で、片方だけでは成立しません。試験でも実務でも、拒否メッセージを読めばどちらの仕事かは判別できます。
試験相当の粒度
「あるノードの kubelet 資格情報でできること・できないことを調べよ」という 1 問で 5〜8 分が目安です。時間を落とさない要点は 2 つです。--kubeconfig=/etc/kubernetes/kubelet.conf の綴りを覚えていることと、最初に auth whoami で誰になっているかを確かめること。この 2 つを飛ばすと、admin のまま検証して全部成功し、答えを間違えます。
第2回の宿題を回収する ——CIS 1.2.5 を恒久的に直す
本回のもう 1 つの主題です。第2回で保留した 1.2.5(--kubelet-certificate-authority) を、今度は壊さずに PASS にします。
第2回で何が起きたか
第2回では、1.2.5 の Remediation に従って --kubelet-certificate-authority=/etc/kubernetes/pki/ca.crt を足しました。API Server は正常に起動し、/healthz も ok を返しました。ところが kubectl logs と kubectl exec が x509 エラーで壊れ、しかも HA なので「たまに失敗する」という形で現れました。最終的にバックアップから戻し、恒久対応を本回へ送っています。
CIS の主張自体は正しいという点を、もう一度確認しておきます。既定では API Server は kubelet のサーバ証明書を検証していません。中間者攻撃で kubelet になりすまされると、kubectl exec の内容や Pod のログが盗まれます(成立するには、Control Plane からノードの 10250 へ向かう経路に割り込める位置を取られている必要があります。ノード間ネットワークが信頼できない環境ほど効いてくる話です)。検証を有効にすべきだ、という主張には理があります。
なぜ壊れたのか
kubelet は 10250 番ポートで HTTPS を待ち受けています。API Server が kubectl logs を中継するとき、この 10250 へ接続します。そこで提示される kubelet のサーバ証明書が自己署名である、というのが原因です。
証明書の中身は、第2回 やってみよう③ と同じやり方で確認できます(openssl は k8s-ops にも Control Plane Node にも入っていないため、curl -kv の出力を読みます)。
実行コマンド(k8s-cp-01 上・root):
# curl -kv --noproxy '*' https://192.168.1.125:10250/healthz 2>&1 | grep -E 'subject:|issuer:|verify result'
実行結果:
* subject: CN=k8s-cp-01@1784976573
* issuer: CN=k8s-cp-01-ca@1784976573
* SSL certificate verify result: self-signed certificate in certificate chain (19)
読み取れることは 3 つです。
issuerがCN=k8s-cp-01-ca@...——kubelet が自分で作った CA が発行元になっている- したがってクラスタ CA(
CN=kubernetes)とはまったく無関係である - 末尾の数字は生成時刻(UNIX 時間)なので、読者の環境では別の値になる
--kubelet-certificate-authority=/etc/kubernetes/pki/ca.crt は「クラスタ CA で検証しろ」という指示です。無関係な CA が発行した証明書は当然通りません。フラグが悪いのではなく、kubelet 側の証明書運用が整っていないのが原因です。
ツールが無い前提で手が動くようにしておいてください。
証明書の中身を読む標準的な方法は openssl x509 -text ですが、本書ラボにも試験環境にも openssl があるとは限りません。curl -kv は subject: と issuer: の 2 行を出してくれるので、発行元を確かめる用途なら十分です。第4回で「試験環境に jq は無いので当てにしない」と置いたのと同じ考え方で、手元にある道具で答えを出す訓練になります。
直し方 ——serverTLSBootstrap
kubelet に「サーバ証明書も自分で作らず、クラスタ CA に発行してもらえ」と指示する設定が serverTLSBootstrap: true です。これを入れると、kubelet は起動時に CSR(証明書署名要求)を API Server へ提出します。
承認されると、クラスタ CA が署名したサーバ証明書が払い出され、kubelet はそれを 10250 で提示するようになります。発行元がクラスタ CA になるので、--kubelet-certificate-authority による検証が通ります。
この CSR は自動承認されない
kubeadm クラスタには CSR の自動承認が仕込まれていますが、対象は 1 種類だけです。
実行コマンド(k8s-ops 上・developer):
$ kubectl get clusterrolebinding | grep autoapprove
実行結果:
kubeadm:node-autoapprove-certificate-rotation ClusterRole/system:certificates.k8s.io:certificatesigningrequests:selfnodeclient
参照されている ClusterRole の名前の末尾に注目してください。selfnodeclient——つまりクライアント証明書のローテーションだけが自動承認されます。サーバ証明書に対応する selfnodeserver の ClusterRole は、そもそもクラスタに存在しません。
したがって kubernetes.io/kubelet-serving の CSR は、必ず人間が承認します。
なぜ自動承認しない設計なのか
サーバ証明書には、ノードが自己申告した IP とホスト名が SAN として入ります。これを無条件に自動承認すると、乗っ取られたノードが他ノードの IP を名乗る証明書を正規に手に入れられます。その証明書があれば、API Server の kubectl exec 中継を横取りできる可能性が生じます。
やってみよう③ で確認したとおり、kubelet の資格情報は「自分の Node は触れる」立場にあります。自己申告の内容を検証せずに署名すると、その立場が他ノードのなりすましにまで広がります。 だから人間が確認する設計になっています。
運用上の注意 ——承認は 1 回で終わらない
本番ガードレール:証明書には期限があります
本ラボで払い出されるサーバ証明書の期限は 1 年です。期限が来るたびに CSR の承認が必要になります。放置すると、期限切れの日に kubectl logs と kubectl exec が突然使えなくなります。しかも --kubelet-certificate-authority を入れている状態では、失敗の理由が x509 エラーとして現れるだけなので、1 年後の担当者にとっては原因不明の障害になります。
本番での定石は、承認条件を検証したうえで自動承認するコントローラー(kubelet-csr-approver など)を入れることです(サードパーティのコントローラーで、CKS 試験には出ませんし本書ラボにも入れません。本番の設計選択肢として名前だけ覚えておいてください)。ノード名と申告 IP の対応をクラスタ側の情報と突き合わせてから署名するので、人手を掛けずに上の risk を潰せます。
一方、selfnodeserver の ClusterRole を自分で作って無条件に自動承認するのはアンチパターンです。手間は消えますが、「ノードが他ノードの IP を名乗る証明書を手に入れられる」というリスクをそのまま受け入れることになります。設計の論点は「自動承認するかどうか」ではなく「何を検証してから承認するか」です。
アップグレードとの順序制約
本回固有の落とし穴です。serverTLSBootstrap: true を /var/lib/kubelet/config.yaml に直接書くだけでは足りません。
この設定の SSOT(単一の真実)は、kube-system Namespace の kubelet-config ConfigMap です。kubeadm upgrade node は、ローカルの /var/lib/kubelet/config.yaml を ConfigMap の内容で丸ごと上書きします。dry-run で確認できます。
実行コマンド(k8s-wl-02 上・root):
# kubeadm upgrade node phase kubelet-config --dry-run
実行結果(抜粋):
[dryrun] Would back up kubelet config file to /etc/kubernetes/tmp/.../config.yaml
[dryrun] Would write file "/var/lib/kubelet/config.yaml" with content:
apiVersion: kubelet.config.k8s.io/v1beta1
Would write file——書き換えるのではなく、書き出すと言っています。ローカルファイルだけを編集していると、次の順で事故が起きます。
- ローカルの
/var/lib/kubelet/config.yamlにだけserverTLSBootstrap: trueを書く - CSR を承認し、
kubectl logsが動くようになる。API Server に--kubelet-certificate-authorityを入れる - 後日
kubeadm upgrade nodeを実行する - ローカルファイルが ConfigMap の内容で上書きされ、
serverTLSBootstrapが消える - kubelet が自己署名証明書に戻る。API Server 側のフラグは残っているので、
kubectl logsが再び壊れる
ConfigMap を先に直してください。 次の演習ではこの順序を守ります。第4回の最後で「手で締めた設定は helm upgrade や kubeadm upgrade で巻き戻る。恒久対応は生成する側に書く」と書いたのと同じ構図です。今回の「生成する側」が kubelet-config ConfigMap にあたります。
同じ問題が API Server 側にもあります
ここまで本回では /etc/kubernetes/manifests/kube-apiserver.yaml を vi で直接編集してきました。そちらにもまったく同じ落とし穴があります。 kubeadm upgrade apply は、このマニフェストも kubeadm-config ConfigMap から作り直すからです。
つまり 本回で足した --authentication-config と --kubelet-certificate-authority も、そのままではアップグレードで消えます。対処は やってみよう⑤ のステップ1-b で扱います。ここでは「kubeadm クラスタで設定を恒久化する場所は、ノード上のファイルではなく kube-system の ConfigMap である」という原則だけ持って先へ進んでください。
やってみよう④:kubelet サービング証明書をクラスタ CA 署名へ切り替える
この演習を始める前に
- 前提は やってみよう② の完了状態です
- 5 ノードすべて(k8s-cp-01〜03・k8s-wl-01〜02)を触ります。1 ノードでも CSR を承認し忘れると、そのノードの Pod に対する
kubectl logsだけが壊れます - ステップ1 の ConfigMap 編集はインデントを 1 段間違えると 5 ノードすべての kubelet が起動しなくなります。バックアップを取ってから編集してください
- ステップ2 の 5 台は 1 台ずつ(k8s-cp-01 → k8s-cp-02 → k8s-cp-03 → k8s-wl-01 → k8s-wl-02)進めます。1 台ごとに
systemctl is-active kubeletがactiveであることを確認してから次へ移ってください - ラボ所要時間の目安は 25〜35 分です
- 試験ではここが 1 問(5〜8 分)になります。「Pending になっている kubelet-serving の CSR を承認せよ」という形です。試験は承認だけですが、本回は設定 → CSR 発生 → 承認 → API Server 側のフラグまで通しでやります(なぜその CSR が出ているのかを理解するためです)
ステップ1:kubelet-config ConfigMap を直す(先にこちら)
実行コマンド(k8s-ops 上・developer):
$ kubectl get cm kubelet-config -n kube-system -o yaml > ~/kubelet-config.bak-05.yaml
$ kubectl edit cm kubelet-config -n kube-system
インデントに注意してください(最も事故る箇所です)
編集するのは data.kubelet の文字列に埋め込まれた YAML です。ConfigMap としては 1 個の文字列ですが、中身は KubeletConfiguration の YAML になっています。apiVersion: kubelet.config.k8s.io/v1beta1 と同じインデント段に 1 行足してください。1 段深く書くと直前のキーの子要素になり、KubeletConfiguration として不正になります。
編集箇所(前後の数行だけを抜き出しています。+ の行が追加分です):
kubelet: |
apiVersion: kubelet.config.k8s.io/v1beta1
rotateCertificates: true
+ serverTLSBootstrap: true
staticPodPath: /etc/kubernetes/manifests
実際に打ち込むのは serverTLSBootstrap: true の 1 行だけで、行頭の + は差分を示す記号です。保存したら反映を確認します。
実行コマンド(k8s-ops 上・developer):
$ kubectl get cm kubelet-config -n kube-system -o yaml | grep serverTLSBootstrap
実行結果:
serverTLSBootstrap: true
ステップ2:5 ノードのローカル設定にも同じ行を足す
ConfigMap を直しても、kubelet が今読んでいるのはローカルファイルです。5 ノードすべてで同じ行を足し、kubelet を再起動します。
実行コマンド(k8s-cp-01 上・root / 残る 4 ノードでも同じ):
# cp /var/lib/kubelet/config.yaml /root/kubelet-config.yaml.bak-05
# echo "serverTLSBootstrap: true" >> /var/lib/kubelet/config.yaml
# systemctl restart kubelet
>> による追記なのでファイル末尾に入ります。KubeletConfiguration はトップレベルのキーの並びなので、末尾追記で問題ありません(ConfigMap 側と違い、埋め込み文字列ではないためインデントは不要です)。
再起動後、kubelet が起動していることを確認してから次のノードへ進んでください。
実行コマンド(各ノード上・root):
# systemctl is-active kubelet
実行結果:
active
なぜ ConfigMap とローカルの両方を直すのか。
ConfigMap は SSOT(kubeadm upgrade node が読む元)、ローカルファイルは kubelet が実際に読む先です。ConfigMap だけでは今すぐ効かず、ローカルだけではアップグレードで消えます。 「今の状態」と「将来の状態」を別々に管理する必要がある——これが前節の順序制約の実務的な意味です。
ステップ3:CSR が Pending で出ていることを確認する
実行コマンド(k8s-ops 上・developer):
$ kubectl get csr
実行結果:
NAME AGE SIGNERNAME REQUESTOR REQUESTEDDURATION CONDITION
csr-68sh9 50s kubernetes.io/kubelet-serving system:node:k8s-cp-02 <none> Pending
csr-6r2pv 54s kubernetes.io/kubelet-serving system:node:k8s-cp-01 <none> Pending
csr-6sqhv 43s kubernetes.io/kubelet-serving system:node:k8s-cp-03 <none> Pending
csr-b8sbc 31s kubernetes.io/kubelet-serving system:node:k8s-wl-02 <none> Pending
csr-vv28z 37s kubernetes.io/kubelet-serving system:node:k8s-wl-01 <none> Pending
NAME はランダムなので、読者の環境では別の文字列になります。見るべき列は SIGNERNAME と CONDITION の 2 つです。Pending が 5 件(5 ノード分)出ていることを確認してください。
signerName は、その CSR を誰が署名するかの種別を表します。押さえるのは 2 つです。
signerName | 用途 | 自動承認 |
|---|---|---|
kubernetes.io/kubelet-serving | kubelet のサーバ証明書(10250 で提示する) | されない |
kubernetes.io/kube-apiserver-client-kubelet | kubelet のクライアント証明書(API Server へ接続する) | される(selfnodeclient) |
自動承認の対象が違うのは、この種別による区別です。試験で「Pending の CSR を承認せよ」と問われたとき、なぜそれが Pending のまま残っているのかの答えがここにあります。
ステップ4:中身を確認してから承認する
前節で説明したとおり、この承認は「自己申告の内容を人間が確かめる」工程です。確かめずに承認する癖を付けないでください。
実行コマンド(k8s-ops 上・developer):
$ kubectl get csr <CSR 名> -o jsonpath='{.spec.username}{"\n"}'
$ kubectl certificate approve <CSR 名>
spec.username が system:node:<ノード名> になっていて、それが自分の管理下のノード名と一致することを確認します。5 件すべてを 1 件ずつ確認したうえで、まとめて承認する形も使えます。
実行コマンド(k8s-ops 上・developer):
$ kubectl get csr --field-selector spec.signerName=kubernetes.io/kubelet-serving -o name | xargs kubectl certificate approve
実行結果:
certificatesigningrequest.certificates.k8s.io/csr-68sh9 approved
certificatesigningrequest.certificates.k8s.io/csr-6r2pv approved
certificatesigningrequest.certificates.k8s.io/csr-6sqhv approved
certificatesigningrequest.certificates.k8s.io/csr-b8sbc approved
certificatesigningrequest.certificates.k8s.io/csr-vv28z approved
--field-selector で kubelet-serving のものだけに絞っているのが要点です。絞らずに全 CSR を承認すると、意図しない証明書まで発行してしまいます。 承認後の状態を確認します。
実行コマンド(k8s-ops 上・developer):
$ kubectl get csr --field-selector spec.signerName=kubernetes.io/kubelet-serving
実行結果:
NAME AGE SIGNERNAME REQUESTOR REQUESTEDDURATION CONDITION
csr-68sh9 57s kubernetes.io/kubelet-serving system:node:k8s-cp-02 <none> Approved,Issued
csr-6r2pv 61s kubernetes.io/kubelet-serving system:node:k8s-cp-01 <none> Approved,Issued
csr-6sqhv 50s kubernetes.io/kubelet-serving system:node:k8s-cp-03 <none> Approved,Issued
csr-b8sbc 38s kubernetes.io/kubelet-serving system:node:k8s-wl-02 <none> Approved,Issued
csr-vv28z 44s kubernetes.io/kubelet-serving system:node:k8s-wl-01 <none> Approved,Issued
CONDITION が 5 件とも Approved,Issued になりました。Approved だけで Issued が付いていない場合は、署名が済んでいません——その状態では証明書はまだ差し替わっていないので、少し待ってから見直してください。
Approved,Issued の 2 語が並んでいれば完了です。Approved だけで Issued が付かない場合は、署名側(kube-controller-manager)で問題が起きています。
ステップ5:証明書が差し替わったことを確認する
実行コマンド(k8s-cp-01 上・root):
# ls -l /var/lib/kubelet/pki/
実行結果(kubelet-server 関連の行の抜粋):
-rw-------. 1 root root 1151 7月 26 21:53 kubelet-server-2026-07-26-21-53-02.pem
lrwxrwxrwx. 1 root root 59 7月 26 21:53 kubelet-server-current.pem -> /var/lib/kubelet/pki/kubelet-server-2026-07-26-21-53-02.pem
日時入りの実体ファイルと、それを指すシンボリックリンク kubelet-server-current.pem が作られます。ファイル名の日時は読者の環境では別の値になります。次回のローテーション時には新しい実体ファイルができ、リンクの向き先が変わります。
提示される証明書そのものも確認します。第2回と同じやり方です。
実行コマンド(k8s-cp-01 上・root):
# curl -kv --noproxy '*' https://192.168.1.125:10250/healthz 2>&1 | grep -E 'subject:|issuer:'
実行結果:
* subject: O=system:nodes; CN=system:node:k8s-cp-01
* issuer: CN=kubernetes
issuer が CN=kubernetes になりました。 クラスタ CA が署名した証明書です。subject も CN=k8s-cp-01@<数字> という自己流の名前ではなく、O=system:nodes / CN=system:node:k8s-cp-01 という Kubernetes の識別子の形になっています。証明書だけを見て「このノードは誰か」が分かる状態です。
5 ノードすべてで同じ確認をしてください。ここで 1 台でも自己署名のままなら、次のステップで kubectl logs が壊れます。
ステップ6:k8s-cp-01 の API Server にフラグを足す
ここでようやく、第2回で壊れたフラグを入れ直します。
実行コマンド(k8s-cp-01 上・root):
# cp /etc/kubernetes/manifests/kube-apiserver.yaml /root/kube-apiserver.yaml.bak-05b
# vi /etc/kubernetes/manifests/kube-apiserver.yaml
挿入位置は --kubelet-client-certificate の直前です。編集後の該当箇所:
- --kubelet-certificate-authority=/etc/kubernetes/pki/ca.crt
- --kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt
- --kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key
3 点セットの判断:ここはフラグだけで足ります。
参照先の /etc/kubernetes/pki/ca.crt は k8s-certs として既にマウント済みだからです。やってみよう② の /etc/kubernetes/auth は既存マウントの外だったので 3 つとも要りました。同じ「ファイル参照フラグ」でも、参照先の位置で要否が変わります。
保存すると kubelet が static Pod を作り直します(鉄則2)。次のステップへ進む前に、再起動が終わったことを確認してください。 再起動中の k8s-cp-01 は 6443 番に応答しないので、そのまま ステップ7 を打つと接続エラーになり、「フラグを入れたから壊れた」と誤解します。
実行コマンド(k8s-cp-01 上・root):
# crictl ps | grep kube-apiserver
# curl -s -o /dev/null -w '%{http_code}\n' -k --noproxy '*' https://192.168.1.125:6443/livez
ATTEMPT が 0 の新しいコンテナに入れ替わり、/livez が 200 を返してから次へ進みます。本書ラボではおよそ 1 分でした。ATTEMPT が増えていく場合は起動に失敗しているので、/root/kube-apiserver.yaml.bak-05b から戻して差分を見直してください。
ステップ7:第2回で壊れた 3 つを全部試す
必ず --server で k8s-cp-01 を名指ししてください
この時点でフラグを足したのは k8s-cp-01 だけです。名指しをやめると HAProxy が k8s-cp-02 / k8s-cp-03 へ流し、フラグの入っていない API Server が答えて成功してしまいます。検証になりません。第2回 やってみよう③ で確認した「名指しをやめた途端に成否が揺れる」の裏返しです。
実行コマンド(k8s-ops 上・developer):
$ kubectl --server=https://192.168.1.125:6443 --insecure-skip-tls-verify logs -n fanclub -l app=fanclub-frontend --tail=3
$ kubectl --server=https://192.168.1.125:6443 --insecure-skip-tls-verify exec -n fanclub deploy/fanclub-frontend -- echo EXEC_OK
$ kubectl --server=https://192.168.1.125:6443 --insecure-skip-tls-verify top nodes
実行結果:
10.244.119.164 - - [26/Jul/2026:12:31:10 +0000] "GET / HTTP/1.1" 200 2600 "-" "curl/8.12.1" "192.168.1.129"
10.244.119.164 - - [26/Jul/2026:12:33:53 +0000] "GET / HTTP/1.1" 200 2600 "-" "curl/8.12.1" "192.168.1.129"
10.244.119.164 - - [26/Jul/2026:12:49:42 +0000] "GET / HTTP/1.1" 200 2600 "-" "curl/8.12.1" "192.168.1.129"
2026/07/26 12:27:52 [notice] 1#1: start worker process 33
10.244.119.164 - - [26/Jul/2026:12:33:53 +0000] "GET / HTTP/1.1" 200 2600 "-" "curl/8.12.1" "192.168.1.129"
10.244.119.164 - - [26/Jul/2026:12:36:25 +0000] "GET / HTTP/1.1" 200 2600 "-" "curl/8.12.1" "192.168.1.129"
EXEC_OK
NAME CPU(cores) CPU(%) MEMORY(bytes) MEMORY(%)
k8s-cp-01 106m 5% 1956Mi 55%
k8s-cp-02 135m 6% 2213Mi 62%
k8s-cp-03 144m 7% 2208Mi 62%
k8s-wl-01 176m 4% 5079Mi 67%
k8s-wl-02 141m 3% 3854Mi 50%
-l app=fanclub-frontend は 2 つの Pod にまたがるので、ログが Pod 単位で続けて出ます(時刻が前後して見えるのはこのためです)。第2回で x509 エラーになった 3 つが、すべて通りました。
--insecure-skip-tls-verify を付けているのは、admin.conf の server: が k8s-lb を指しているためです(--server で IP に上書きすると、証明書の SAN との突き合わせで警告が出る場合があります)。ここで確かめたいのは API Server と kubelet の間の TLS であり、kubectl と API Server の間の TLS ではありません。
top nodes も試すのは、metrics-server と同じ経路(API Server から kubelet の 10250)を使うためです。3 つとも成功すれば、第2回で壊れたものはすべて直っています。
ステップ8:k8s-cp-02 / k8s-cp-03 にも同じフラグを足す
ステップ6 と同じ編集を、1 台ずつ・確認してから次への原則で行います。編集後は各台で crictl ps と --server 名指しの確認を行ってください。
ステップ8-b:名指しを外して 6 回連続で成功することを確認する
3 台に入れ終えたら、--server を外して同じコマンドを繰り返します。第2回が読者に課したのと同じ確認形式です。
実行コマンド(k8s-ops 上・developer):
$ for i in 1 2 3 4 5 6; do kubectl logs -n fanclub -l app=fanclub-frontend --tail=1 > /dev/null && echo "OK $i" || echo "NG $i"; done
実行結果:
OK 1
OK 2
OK 3
OK 4
OK 5
OK 6
HAProxy が 3 台へ振り分けるので、6 回連続の成功が「3 台すべてで正しく動いている」ことの実用的な証拠になります。どれか 1 台でも取りこぼしがあると、ここで断続的に NG が混じります。混じったら止めて、どの台の設定が抜けているかを探してください。
ステップ9:kube-bench で 1.2.5 が PASS になったことを確認する
実行コマンド(k8s-cp-01 上・root):
# export KUBECONFIG=/etc/kubernetes/admin.conf
# kube-bench run --targets master --check 1.2.5
実行結果:
[INFO] 1 Control Plane Security Configuration
[INFO] 1.2 API Server
[PASS] 1.2.5 Ensure that the --kubelet-certificate-authority argument is set as appropriate (Automated)
== Summary master ==
1 checks PASS
0 checks FAIL
0 checks WARN
0 checks INFO
全体のスコアも取り直します。
実行コマンド(k8s-cp-01 上・root):
# kube-bench run --targets master | tail -11
実行結果:
== Summary master ==
45 checks PASS
4 checks FAIL
11 checks WARN
0 checks INFO
== Summary total ==
45 checks PASS
4 checks FAIL
11 checks WARN
0 checks INFO
第1回から本回までの推移は次のとおりです。
| 時点 | PASS | FAIL | WARN |
|---|---|---|---|
| 第1回(ベースライン) | 39 | 10 | 11 |
| 第2回 完了後 | 44 | 5 | 11 |
| 本回 完了後 | 45 | 4 | 11 |
残る FAIL 4 件はすべて監査ログ(1.2.16〜1.2.19)で、第16回で回収します。 WARN 11 件が動いていない点にも注目してください。本回で匿名アクセスを締めても、--anonymous-auth=false という文字列を書かない限り 1.2.1 は WARN のままです。スコアの改善と実際の堅牢化は、別々に評価する必要があります。
試験相当の粒度
「Pending になっている kubelet-serving の CSR を承認せよ」という 1 問で 5〜8 分が目安です。試験で問われるのは本演習のステップ3〜4 だけで、設定の投入や API Server 側のフラグまでは求められないことが多くなります。時間を落とさない要点は、kubectl get csr の SIGNERNAME 列を見て対象を絞ることと、kubectl certificate approve の綴り(approve であって approval ではありません)です。拒否は kubectl certificate deny です。

やってみよう⑤:脆弱性を塞ぐ ——v1.36.2 から v1.36.3 へ
CKS D2 の「Upgrade Kubernetes to avoid vulnerabilities」に対応する節です。
この演習を始める前に
- ラボ所要時間は 60 分以上です。5 ノード分の作業になります。時間を確保してから着手してください
- 試験ではここが 1 問(8〜12 分)になります。「指定された 1 ノードを vX.Y.Z へアップグレードせよ」という形です。本回は 5 ノード全部をやるので、試験の 5 倍の分量になります
- アップグレードは片道です。
kubeadm upgradeにダウングレードの手段はありません - 本番で実施する場合は、必ず etcd のバックアップ(第2巻第5回)を取ってから着手してください
- やってみよう④ まで完了していることが前提です
アップグレードを「運用」ではなく「防御」として打つ
Kubernetes のパッチリリースには、公表済み脆弱性(CVE)の修正が入ります。古いパッチに留まることは、攻撃者が CVE 番号で検索できる穴を開けたままにしておくのと同じです。
本回の前半で確認したことを思い出してください。匿名アクセスが有効なクラスタでは /version からバージョンが読めました。 攻撃者から見れば「このクラスタは v1.36.2 だ」という情報は、どの CVE を試すかを決める材料です。匿名を締めることと、パッチを当てることは、同じリスクへの 2 方向からの対処です。
実務では「動いているものは触らない」という判断が働きがちですが、セキュリティの観点では「触らないこと」がリスクの蓄積になります。パッチ適用を定期作業として組み込めるかどうかが、運用設計の分かれ目になります。
バージョンスキュー ——順序を決めているのは 1 つの原則
| 組み合わせ | 許容 |
|---|---|
| kubelet と kube-apiserver | kubelet は API Server より新しくしてはいけない(最大 3 マイナー古いまで) |
| kube-controller-manager / kube-scheduler と kube-apiserver | API Server と同じか、最大 1 マイナー古いまで |
| kubectl | API Server の ±1 マイナー |
| マイナーの上げ方 | 1 つずつ上げる(飛ばさない) |
| ノードの順序 | Control Plane → Workload Node |
この表は暗記事項ですが、丸暗記する必要はありません。「API Server が最も新しくなければならない」という 1 点から、ほぼすべてが導けます。API Server が最新であるべきなら、上げる順序は API Server が先——つまり Control Plane が先で Workload Node が後になります。
スキュー表を実物で確かめてください。
本書ラボの作業端末 k8s-ops の kubectl は v1.35.6 のままです。API Server が v1.36.3 になっても ±1 マイナーの範囲に収まるので問題ありません。逆に言えば、作業端末だけを先に上げると、先にスキューを外します。「バージョンを上げる順序は API Server から」という原則は、クラスタの外にある道具にも効きます。
実行コマンド(k8s-ops 上・developer):
$ kubectl version
Client Version と Server Version の両方が出るので、スキューの現状はこの 1 コマンドで確認できます。
このスキュー表は試験中に開けません
バージョンスキューの一覧は kubernetes.io/releases/version-skew-policy/ にありますが、/releases/ は CKS の参照許可 8 件(kubernetes.io/docs と kubernetes.io/blog ほか)のサブパス外なので、試験中は開けません。上の表は暗記対象です。ただし kubeadm upgrade のタスクページ(/docs/tasks/administer-cluster/kubeadm/kubeadm-upgrade/)には要点が書かれているので、そこから辿る手はあります。
パッチ更新とマイナー更新の違い
| パッチ(v1.36.2 → v1.36.3) | マイナー(v1.35 → v1.36) | |
|---|---|---|
| リポジトリ URL | 変更不要(.../stable:/v1.36/rpm/ のまま) | 変更が必要 |
| API の非推奨・削除 | 基本的に無し | あり(事前調査が要る) |
| CoreDNS / etcd | 据え置きのことが多い | 上がることが多い |
| 動機 | CVE 修正 | 機能追加・サポート期限 |
第2巻第6回で扱ったのは右の列でした。本回は左の列です。 リポジトリ URL を触らない分だけ手数は少なくなりますが、1 ノードあたりの手順そのものは同じです。
なお、v1.37 のリポジトリは本記事の執筆時点で存在しません(アクセスすると 403 が返ります)。v1.36 系で到達できる最新は v1.36.3 です。「1 マイナー上げる」ができないときにパッチ更新を選ぶ——という判断も、実務ではよくあります。
ステップ1:計画を見る
kubeadm upgrade plan は、kubeadm 自体を上げる前に打てます。何がどこまで上がるのかを先に把握してください。
実行コマンド(k8s-cp-01 上・root):
# kubeadm upgrade plan
実行結果(要点の抜粋):
[upgrade/versions] Cluster version: 1.36.2
[upgrade/versions] kubeadm version: v1.36.2
[upgrade/versions] Target version: v1.36.3
[upgrade/versions] Latest version in the v1.36 series: v1.36.3
Components that must be upgraded manually after you have upgraded the control plane with 'kubeadm upgrade apply':
COMPONENT NODE CURRENT TARGET
kubelet k8s-cp-01 v1.36.2 v1.36.3
Upgrade to the latest version in the v1.36 series:
COMPONENT NODE CURRENT TARGET
kube-apiserver k8s-cp-01 v1.36.2 v1.36.3
kube-controller-manager k8s-cp-01 v1.36.2 v1.36.3
kube-scheduler k8s-cp-01 v1.36.2 v1.36.3
kube-proxy 1.36.2 v1.36.3
CoreDNS v1.14.2 v1.14.2
etcd k8s-cp-01 3.6.8-0 3.6.8-0
You can now apply the upgrade by executing the following command:
kubeadm upgrade apply v1.36.3
Note: Before you can perform this upgrade, you have to update kubeadm to v1.36.3.
実際の出力では kubelet の行が 5 ノード分並びます。読み取るのは 3 点です。
Target version: v1.36.3——到達先が確定した- CoreDNS と etcd の CURRENT と TARGET が同じ——パッチ更新なので据え置き。マイナー更新では、ここが上がることが多い
kubeletは手動——kubeadm upgrade applyは Control Plane のコンポーネントを入れ替えるだけで、kubelet のパッケージは自分で上げる
最後の Note も重要です。kubeadm 自体を先に v1.36.3 へ上げないと、apply は実行できません。
1 ノードあたりの手順は 5 段階
この 5 段階を、k8s-cp-01 → k8s-cp-02 → k8s-cp-03 → k8s-wl-01 → k8s-wl-02 の順に 1 台ずつ繰り返します。 k8s-cp-01 とそれ以外の違いは、③ が kubeadm upgrade apply v1.36.3 になるか kubeadm upgrade node になるかの 1 点だけです。
| # | 内容 | 実行場所 |
|---|---|---|
| ① | dnf versionlock delete kubeadm kubelet kubectl | 当該ノード・root |
| ② | dnf install -y kubeadm-1.36.3-150500.1.1 | 当該ノード・root |
| ③ | 1 台目のみ kubeadm upgrade apply v1.36.3 / 2 台目以降 kubeadm upgrade node | 当該ノード・root |
| ④ | kubectl drain <node> --ignore-daemonsets --delete-emptydir-data → dnf install -y kubelet-1.36.3-150500.1.1 kubectl-1.36.3-150500.1.1 → systemctl daemon-reload → systemctl restart kubelet → kubectl uncordon <node> | drain / uncordon は k8s-ops・パッケージは当該ノード |
| ⑤ | dnf versionlock add kubeadm kubelet kubectl(新しい版で貼り直す) | 当該ノード・root |
①〜⑤ は 1 ノードの中で閉じます。次のノードへ移る前に、必ず ⑤ の貼り直しまで終えてください。 ① を 5 台分まとめて外してから ② へ進む、という進め方はしないでください(本節末尾の本番ガードレール③ で理由を書きます)。
dnf の出力は日本語ロケールだと和文になります。本文は LC_ALL=C を前置した英語表示で統一します(第2回の sha256sum --check と同じ方針です)。
④ の drain について、Control Plane Node ではほとんど何も退避しません。node-role.kubernetes.io/control-plane:NoSchedule の taint があるため、載っているのは DaemonSet と static Pod が中心だからです(--ignore-daemonsets は必要です)。退避先の余裕を気にするのは Workload Node の 2 台だけで、その手順は後述します。
パッケージ名に付けるバージョン文字列は、リポジトリの実物を見て確定します。versionlock が効いていると通常の list では候補が見えないので、プラグインを一時的に無効化して問い合わせます。
実行コマンド(当該ノード上・root):
# dnf --disableplugin=versionlock list --showduplicates kubeadm kubelet kubectl | grep 1.36.3
実行結果(x86_64 の行のみ抜粋):
kubeadm.x86_64 1.36.3-150500.1.1 kubernetes
kubectl.x86_64 1.36.3-150500.1.1 kubernetes
kubelet.x86_64 1.36.3-150500.1.1 kubernetes
1.36.3-150500.1.1 の後半(-150500.1.1)はパッケージのリリース番号で、Kubernetes のバージョンとは別物です。読者の環境では別の値になることがあるので、本文の値を写すのではなく、必ず自分の環境で確認した値を使ってください。
versionlock は dnf のプラグインで、指定パッケージの更新を止める仕組みです。第2巻でピン留めのために設定済みで、本書ラボには kubeadm / kubelet / kubectl の 3 行が入っています。外さないと dnf install が拒否されます。 ロックの現状は次で確認できます。
実行コマンド(当該ノード上・root):
# LC_ALL=C dnf versionlock list
実行結果:
Last metadata expiration check: 0:01:07 ago on Sun Jul 26 21:59:19 2026.
kubeadm-0:1.36.2-150500.2.1.*
kubelet-0:1.36.2-150500.2.1.*
kubectl-0:1.36.2-150500.2.1.*
解除すると次のように出ます。
実行コマンド(当該ノード上・root):
# LC_ALL=C dnf versionlock delete kubeadm kubelet kubectl
実行結果:
Deleting versionlock for: kubeadm-0:1.36.2-150500.2.1.*
Deleting versionlock for: kubelet-0:1.36.2-150500.2.1.*
Deleting versionlock for: kubectl-0:1.36.2-150500.2.1.*
LC_ALL=C を付けているのは、日本語ロケールだと「versionlock を削除:」のように和文で出るためです。第2回で sha256sum --check に付けたのと同じ理由で、試験環境は英語なので英語の文面を見慣れておくほうが有利です。
なお 本書ラボでは --disableexcludes=kubernetes は不要です。公式手順にはこのオプションが出てきますが、これは /etc/yum.repos.d/kubernetes.repo に exclude= 行がある構成向けのものです。第2巻で作った本書のリポジトリ定義には exclude= が無いため、付けても付けなくても結果は同じです。自分のリポジトリ定義を見て要否を判断してください。
バージョン指定について、第2巻から引き継いだ原則を再掲します。リポジトリ URL の v1.36 はマイナーまでの指定なので、これだけではパッチが固定されません。dnf install でパッチまで明示し、kubeadm upgrade apply にも v1.36.3 と明示します。両方に書いてはじめてバージョンが決まります。
ステップ1-b:手動で足したフラグを kubeadm-config へ移す
ここでの道順
このステップでは kubeadm upgrade apply --dry-run を使います。apply は kubeadm 自体が新しくなっていないと打てません(--dry-run でも同じです)。そこで、k8s-cp-01 でだけ、上の 5 段階のうち ① と ② を先に済ませます。③ 以降には進みません。
実行コマンド(k8s-cp-01 上・root):
# LC_ALL=C dnf versionlock delete kubeadm kubelet kubectl
# LC_ALL=C dnf install -y kubeadm-1.36.3-150500.1.1
# kubeadm version -o short
実行結果(3 コマンド目):
v1.36.3
この時点で更新したのは kubeadm というコマンドだけです。クラスタはまだ v1.36.2 のまま動いています。kubeadm を先に上げないまま --dry-run を打つと、次のエラーで止まります——同じ記事の前の段落で予告したとおりです。
error: error execution phase preflight: the version argument is invalid due to these errors:
- Specified version to upgrade to "v1.36.3" is higher than the kubeadm version "v1.36.2". Upgrade kubeadm first using the tool you used to install kubeadm
なお kubeadm upgrade plan(ステップ1)は kubeadm が古くても打てます。「計画は古い kubeadm でも見られるが、適用(とその dry-run)は新しい kubeadm が要る」という違いです。
このステップを飛ばすと、本回でやった作業が全部消えます
kubeadm upgrade apply は /etc/kubernetes/manifests/kube-apiserver.yaml を作り直します。作り直す元になるのは kube-system の kubeadm-config ConfigMap であって、いま動いているマニフェストではありません。手で書き足したフラグは、その ConfigMap に載っていない限り消えます。
本当にそうなるのかを、実行する前に確かめます。kubeadm upgrade apply には --dry-run があり、「どんなファイルを書こうとしているか」を実行せずに全文表示できます。
実行コマンド(k8s-cp-01 上・root):
# kubeadm upgrade apply v1.36.3 --dry-run 2>&1 | grep -A 40 'Would write file "/etc/kubernetes/manifests/kube-apiserver.yaml"'
実行結果(command の部分の抜粋):
[dryrun] Would write file "/etc/kubernetes/manifests/kube-apiserver.yaml" with content:
apiVersion: v1
kind: Pod
...
spec:
containers:
- command:
- kube-apiserver
- --advertise-address=192.168.1.125
- --allow-privileged=true
- --authorization-mode=Node,RBAC
- --client-ca-file=/etc/kubernetes/pki/ca.crt
- --enable-admission-plugins=NodeRestriction
- --enable-bootstrap-token-auth=true
- --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt
- --etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt
- --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key
- --etcd-servers=https://127.0.0.1:2379
- --kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt
- --kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key
- --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
- --proxy-client-cert-file=/etc/kubernetes/pki/front-proxy-client.crt
- --proxy-client-key-file=/etc/kubernetes/pki/front-proxy-client.key
- --requestheader-allowed-names=front-proxy-client
- --requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt
- --requestheader-extra-headers-prefix=X-Remote-Extra-
- --requestheader-group-headers=X-Remote-Group
- --requestheader-username-headers=X-Remote-User
- --secure-port=6443
- --service-account-issuer=https://kubernetes.default.svc.cluster.local
- --service-account-key-file=/etc/kubernetes/pki/sa.pub
- --service-account-signing-key-file=/etc/kubernetes/pki/sa.key
本回で足した --authentication-config も --kubelet-certificate-authority も、第2回の --profiling=false も、第4回の --service-account-max-token-expiration=24h も、1 つも入っていません。 volumeMounts と volumes の auth-config も消えています。このまま upgrade apply を実行すれば、第2回から積み上げた設定がすべて失われます。
正しい置き場所は kubeadm-config ConfigMap の ClusterConfiguration です。現状を見ます。
実行コマンド(k8s-ops 上・developer):
$ kubectl get cm kubeadm-config -n kube-system -o jsonpath='{.data.ClusterConfiguration}'
実行結果(全量):
apiServer: {}
apiVersion: kubeadm.k8s.io/v1beta4
caCertificateValidityPeriod: 87600h0m0s
certificateValidityPeriod: 8760h0m0s
certificatesDir: /etc/kubernetes/pki
clusterName: kubernetes
controlPlaneEndpoint: k8s-lb:6443
controllerManager: {}
dns: {}
encryptionAlgorithm: RSA-2048
etcd:
local:
dataDir: /var/lib/etcd
imageRepository: registry.k8s.io
kind: ClusterConfiguration
kubernetesVersion: v1.36.2
networking:
dnsDomain: cluster.local
podSubnet: 10.244.0.0/16
serviceSubnet: 10.96.0.0/12
proxy: {}
scheduler: {}
apiServer: {} ——空です。 手で足したフラグは、kubeadm から見れば「存在しない設定」でした。ここに extraArgs(フラグ)と extraVolumes(3 点セットの ②③ に相当)を書きます。
空なのは apiServer だけではありません。 controllerManager: {} と scheduler: {} も同じく空です。第2回で --profiling=false を入れたのは 3 コンポーネント(apiserver / controller-manager / scheduler)なので、この 2 つも一緒に移さないと、アップグレードで controller-manager と scheduler のフラグだけが消えます——kubeadm はこの 3 つのマニフェストをすべて作り直すからです。
実行コマンド(k8s-ops 上・developer):
$ kubectl get cm kubeadm-config -n kube-system -o yaml > ~/kubeadm-config.bak-05.yaml
$ kubectl edit cm kubeadm-config -n kube-system
apiServer: {} の行を、次の内容に置き換えます(全量)。
apiServer:
extraArgs:
- name: authentication-config
value: /etc/kubernetes/auth/anonymous.yaml
- name: kubelet-certificate-authority
value: /etc/kubernetes/pki/ca.crt
- name: profiling
value: "false"
- name: service-account-extend-token-expiration
value: "false"
- name: service-account-max-token-expiration
value: 24h
extraVolumes:
- name: auth-config
hostPath: /etc/kubernetes/auth
mountPath: /etc/kubernetes/auth
readOnly: true
pathType: DirectoryOrCreate
あわせて controllerManager: {} と scheduler: {} の 2 行も置き換えます(全量)。
controllerManager:
extraArgs:
- name: profiling
value: "false"
scheduler:
extraArgs:
- name: profiling
value: "false"
この 2 つを忘れると、アップグレードで --profiling=false が静かに消えます。
kubeadm upgrade は apiserver / controller-manager / scheduler の 3 つの static Pod マニフェストをすべて作り直します。ClusterConfiguration に書いていないフラグは、そのとき再現されません。apiServer だけを移して残り 2 つを放置すると、アップグレード後に grep -c profiling が controller-manager と scheduler の両方で 0 になり、第2回で PASS にした 1.3.2(controller-manager)と 1.4.1(scheduler)が FAIL へ戻ります(master スコアが 45 PASS / 4 FAIL → 43 PASS / 6 FAIL)。「手で入れた設定は、次のアップグレードで消える」——本節の主題そのものが、3 コンポーネント分あるということです。
書式の注意点が 3 つあります。
①extraArgs は kubeadm.k8s.io/v1beta4 から name / value のリスト形式になりました(v1beta3 まではマップ形式でした。古い記事の書き方をそのまま貼ると弾かれます)。②フラグ名の先頭の -- は書きません。③false や true は引用符で囲みます(囲まないと真偽値として解釈され、文字列を要求する箇所で型が合いません)。24h のように数字で始まらない文字列は囲まなくてかまいません。
本番ガードレール:ClusterConfiguration は 3 台すべてに効きます
kubeadm-config はクラスタで 1 つです。ここに書いた extraArgs と extraVolumes は、アップグレードした Control Plane すべてに適用されます。つまり 3 台すべてに /etc/kubernetes/auth/anonymous.yaml が置かれている必要があります。
pathType: DirectoryOrCreate はディレクトリを作るだけで、中のファイルは作りません。ファイルを置き忘れた Control Plane は、アップグレード後に --authentication-config の指す先が無い状態で起動しようとして失敗します。しかも本回の H2「--anonymous-auth=false を素直に入れるとどうなるか」で見たとおり、HA では 1 台落ちても気づきません。
本書ラボでは やってみよう② のステップ5 で 3 台に配ってあるので、そのまま進めて問題ありません。先へ進む前に 3 台とも存在することを確かめてください。
実行コマンド(k8s-cp-01 / k8s-cp-02 / k8s-cp-03 上・root):
# ls -l /etc/kubernetes/auth/
実行結果:
合計 4
-rw-------. 1 root root 168 7月 26 21:44 anonymous.yaml
ここには 第2回・第4回で足したフラグも一緒に書いています。手で足したものはすべて対象だからです。--profiling=false は第2回で k8s-cp-01 にだけ入れて、残り 2 台は読者に委ねていました。これを ClusterConfiguration に書くと、アップグレードのタイミングで 3 台に自動で揃います。
編集したら、もう一度 --dry-run で確かめます。確認せずに本番の apply を打たないでください。
実行コマンド(k8s-cp-01 上・root):
# kubeadm upgrade apply v1.36.3 --dry-run 2>&1 | grep -A 130 'Would write file "/etc/kubernetes/manifests/kube-apiserver.yaml"' | grep -E 'authentication-config|kubelet-certificate-authority|profiling|service-account-max|auth-config|kubernetes/auth|image:'
実行結果:
- --authentication-config=/etc/kubernetes/auth/anonymous.yaml
- --kubelet-certificate-authority=/etc/kubernetes/pki/ca.crt
- --profiling=false
- --service-account-extend-token-expiration=false
- --service-account-max-token-expiration=24h
image: registry.k8s.io/kube-apiserver:v1.36.3
- mountPath: /etc/kubernetes/auth
name: auth-config
path: /etc/kubernetes/auth
name: auth-config
5 つのフラグと 3 点セットのマウントが、新しいマニフェスト(イメージは v1.36.3)に載りました。 extraArgs で足したフラグは kubeadm 既定のフラグ群の後ろに追加されるため、アルファベット順にはなりません。これは正常です。
本回でいちばん覚えて帰ってほしいこと
kubeadm クラスタでは、ノード上のファイルは「結果」であって「正本」ではありません。 正本は kube-system にある 2 つの ConfigMap です。
| 正本(ConfigMap) | そこから作られるファイル | 直し忘れると |
|---|---|---|
kubeadm-config(ClusterConfiguration) | /etc/kubernetes/manifests/kube-apiserver.yaml ほか Control Plane の static Pod | kubeadm upgrade apply / node でフラグが消える |
kubelet-config | /var/lib/kubelet/config.yaml | kubeadm upgrade node で設定が消える |
やってみよう④ のステップ1 で kubelet-config を先に直したのも、いまここで kubeadm-config を直しているのも、同じ 1 つの原則です。ノード上のファイルだけを直す運用は、アップグレードの日まで問題が表面化しません。「動いているのに、次のアップグレードで壊れる」種類の欠陥で、CKS でも実務でも、これを見抜けるかどうかが差になります。
ステップ2〜6:5 台を順に上げる
上の 5 段階を、k8s-cp-01 → k8s-cp-02 → k8s-cp-03 → k8s-wl-01 → k8s-wl-02 の順に実施します。各ノードの完了後に、そのノードが Ready かつ新バージョンになったことを確認してから次へ進んでください。
k8s-cp-01 については、ステップ1-b で ① と ② を済ませてあります。③ から始めてください。 k8s-cp-02 以降は ① から順に打ちます。
実行コマンド(k8s-ops 上・developer):
$ kubectl get nodes
VERSION 列が対象ノードだけ v1.36.3 になり、他は v1.36.2 のまま——という混在状態が正常です。混在に耐えられるのは、前述のスキュー許容範囲に収まっているからです。
1 台目(k8s-cp-01)の ③ の実出力は次のとおりです。
実行コマンド(k8s-cp-01 上・root):
# kubeadm upgrade apply v1.36.3
実行結果:
[upgrade/staticpods] Preparing for "kube-controller-manager" upgrade
[upgrade/staticpods] Renewing controller-manager.conf certificate
[upgrade/staticpods] Moving new manifest to "/etc/kubernetes/manifests/kube-controller-manager.yaml" and backing up old manifest to "/etc/kubernetes/tmp/kubeadm-backup-manifests-2026-07-26-22-03-57/kube-controller-manager.yaml"
[upgrade/staticpods] Waiting for the kubelet to restart the component
[upgrade/staticpods] This can take up to 5m0s
[apiclient] Found 3 Pods for label selector component=kube-controller-manager
[upgrade/staticpods] Component "kube-controller-manager" upgraded successfully!
[upgrade/staticpods] Preparing for "kube-scheduler" upgrade
[upgrade/staticpods] Renewing scheduler.conf certificate
[upgrade/staticpods] Moving new manifest to "/etc/kubernetes/manifests/kube-scheduler.yaml" and backing up old manifest to "/etc/kubernetes/tmp/kubeadm-backup-manifests-2026-07-26-22-03-57/kube-scheduler.yaml"
[upgrade/staticpods] Waiting for the kubelet to restart the component
[upgrade/staticpods] This can take up to 5m0s
[apiclient] Found 3 Pods for label selector component=kube-scheduler
[upgrade/staticpods] Component "kube-scheduler" upgraded successfully!
[upgrade/control-plane] The control plane instance for this node was successfully upgraded!
[upload-config] Storing the configuration used in ConfigMap "kubeadm-config" in the "kube-system" Namespace
[kubelet] Creating a ConfigMap "kubelet-config" in namespace kube-system with the configuration for the kubelets in the cluster
[upgrade/kubeconfig] The kubeconfig files for this node were successfully upgraded!
[upgrade] Backing up kubelet config file to /etc/kubernetes/tmp/kubeadm-kubelet-config-2026-07-26-22-05-19/config.yaml
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[upgrade/kubelet-config] The kubelet configuration for this node was successfully upgraded!
[upgrade/bootstrap-token] Configuring bootstrap token and cluster-info RBAC rules
[upgrade/addon] Skipping upgrade of addons because control plane instances [k8s-cp-02 k8s-cp-03] have not been upgraded
[upgrade] SUCCESS! A control plane node of your cluster was upgraded to "v1.36.3".
[upgrade] Now please proceed with upgrading the rest of the nodes by following the right order.
本書ラボでは 約 2 分で完了しました。読み取っておきたい行が 4 つあります。
Moving new manifest to ... and backing up old manifest to /etc/kubernetes/tmp/kubeadm-backup-manifests-<日時>/——kubeadm は古いマニフェストを自分で退避します。ステップ1-b で確認したとおり内容は作り直されるので、この退避先が「元の姿」を残す唯一の場所になります[upload-config] Storing the configuration used in ConfigMap "kubeadm-config"——ClusterConfigurationのkubernetesVersionがv1.36.3へ書き戻されます[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"——ここで kubelet 設定も ConfigMap から書き直されます。やってみよう④ のステップ1 が効くのはこの瞬間です[upgrade/addon] Skipping upgrade of addons because control plane instances [k8s-cp-02 k8s-cp-03] have not been upgraded——CoreDNS と kube-proxy の更新は最後の Control Plane を上げるまで保留されます
2 台目以降(k8s-cp-02 / k8s-cp-03 / k8s-wl-01 / k8s-wl-02)は node です。バージョン指定は要りません(クラスタの目標バージョンを参照するためです)。
実行コマンド(当該ノード上・root):
# kubeadm upgrade node
実行結果:
[apiclient] Found 3 Pods for label selector component=kube-apiserver
[upgrade/staticpods] Component "kube-apiserver" upgraded successfully!
[upgrade/staticpods] Preparing for "kube-controller-manager" upgrade
[upgrade/staticpods] Renewing controller-manager.conf certificate
[upgrade/staticpods] Moving new manifest to "/etc/kubernetes/manifests/kube-controller-manager.yaml" and backing up old manifest to "/etc/kubernetes/tmp/kubeadm-backup-manifests-2026-07-26-22-06-35/kube-controller-manager.yaml"
[upgrade/staticpods] Component "kube-controller-manager" upgraded successfully!
[upgrade/staticpods] Preparing for "kube-scheduler" upgrade
[upgrade/staticpods] Component "kube-scheduler" upgraded successfully!
[upgrade/control-plane] The control plane instance for this node was successfully upgraded!
[upgrade/kubeconfig] The kubeconfig files for this node were successfully upgraded!
[upgrade] Backing up kubelet config file to /etc/kubernetes/tmp/kubeadm-kubelet-config-2026-07-26-22-08-12/config.yaml
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[upgrade/kubelet-config] The kubelet configuration for this node was successfully upgraded!
[upgrade/addon] Skipping upgrade of addons because control plane instances [k8s-cp-03] have not been upgraded
最後の Control Plane(k8s-cp-03)だけは末尾が変わります。 保留されていたアドオンがここで適用されるためです。
実行結果(k8s-cp-03 の末尾):
[upgrade/kubelet-config] The kubelet configuration for this node was successfully upgraded!
[addons] Applied essential addon: CoreDNS
[addons] Applied essential addon: kube-proxy
Workload Node(k8s-wl-01 / k8s-wl-02)では、Control Plane 向けのフェーズが丸ごと飛ばされます。
実行結果(k8s-wl-01):
[upgrade/preflight] Running pre-flight checks
[upgrade/preflight] Skipping prepull. Not a control plane node.
[upgrade/control-plane] Skipping phase. Not a control plane node.
[upgrade/kubeconfig] Skipping phase. Not a control plane node.
[upgrade] Backing up kubelet config file to /etc/kubernetes/tmp/kubeadm-kubelet-config-2026-07-26-22-13-06/config.yaml
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[upgrade/kubelet-config] The kubelet configuration for this node was successfully upgraded!
[upgrade/addon] Skipping the addon/coredns phase. Not a control plane node.
[upgrade/addon] Skipping the addon/kube-proxy phase. Not a control plane node.
Workload Node で kubeadm upgrade node が実際にやることは、kubelet 設定の書き直しだけです。 それでもこのコマンドが要るのは、/var/lib/kubelet/config.yaml を新しいバージョンの形式へ合わせるためです。
Workload Node を drain するときの手順
本書ラボの Workload Node は 2 台しかありません。片方を drain すると fanclub 一式がもう片方に集中します。 次の順で判断してください。
kubectl get pod -A -o wide | grep -E 'fanclub|longhorn'でfanclub-db-0がどちらのノードに載っているかを先に確認する- db-0 が載っていない側のノードから先に drain する
kubectl drain <node> --ignore-daemonsets --delete-emptydir-dataを使う(--delete-emptydir-dataが無いと emptyDir を持つ Pod で止まります)- drain が進まないときは
--forceを打たずに原因を調べる。kubectl get pdb -Aを見てください(Longhorn のinstance-managerは PodDisruptionBudget を持つため、evict できずに待ち続けることがあります) uncordonしたあと、fanclub-backendが2/2に戻ってから次のノードへ進む
本書ラボで実際に起きたことを載せておきます。PDB による待ちは確かに発生しますが、待っていれば解消します。
実行結果(k8s-wl-01 の drain・抜粋):
evicting pod longhorn-system/instance-manager-fab969fd0e41cf59df06951809055bda
error when evicting pods/"instance-manager-fab969fd0e41cf59df06951809055bda" -n "longhorn-system" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
evicting pod longhorn-system/instance-manager-fab969fd0e41cf59df06951809055bda
error when evicting pods/"instance-manager-fab969fd0e41cf59df06951809055bda" -n "longhorn-system" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
evicting pod longhorn-system/instance-manager-fab969fd0e41cf59df06951809055bda
pod/instance-manager-fab969fd0e41cf59df06951809055bda evicted
pod/fanclub-backend-788b7bbd7c-tfzv8 evicted
pod/fanclub-backend-788b7bbd7c-mmhrr evicted
node/k8s-wl-01 drained
Cannot evict pod as it would violate the pod's disruption budget. が 5 秒おきに数回繰り返されたあと、evict が通って drain が完了しました。PDB の状態は次で確認できます。
実行コマンド(k8s-ops 上・developer):
$ kubectl get pdb -A
実行結果:
NAMESPACE NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE
kube-system calico-kube-controllers N/A 1 1 26h
longhorn-system csi-attacher 1 N/A 2 8h
longhorn-system csi-provisioner 1 N/A 2 8h
longhorn-system instance-manager-c36384cab41c5643fb3ac92529788a0c 1 N/A 0 20h
longhorn-system instance-manager-fab969fd0e41cf59df06951809055bda 1 N/A 0 20h
instance-manager の ALLOWED DISRUPTIONS が 0 になっています。Longhorn はボリュームのレプリカを退避し終えるまで、そのノードの instance-manager を落とさせません。退避が済むと ALLOWED DISRUPTIONS が上がり、evict が通ります。これは異常ではなく、データを守るための正常な待ちです。ここで --force を打つと、Longhorn の保護をすり抜けてボリュームを壊しかねません。
fanclub-db-0 が載っている側(本書ラボでは k8s-wl-02)の drain は、Workload Node が 2 台しかないので最後に必ず来ます。db-0 は反対側のノードで作り直され、backend はそれに合わせて入れ替わります。本書ラボでは drain が約 40 秒、その後 backend が 3 つとも 2/2 に戻るまで約 5 分でした。その間も https://fanclub.local は 200 を返し続けました(frontend が先に反対側へ退避され、DB を必要としないトップページは応答し続けるためです)。
本番ガードレール(4 件)
① drain は退避であって無停止の保証ではありません
fanclub-db は StatefulSet で Longhorn ボリュームを持ちます。db-0 が載っているノードを drain すると DB が別ノードで作り直され、backend が 1/2 のまま自力で戻らないことがあります(第2巻第12回・第3巻第4回で実測しています)。「drain したから安全」ではなく、drain したあとに何が起きるかを知ったうえで順序を決めるのが正しい手順です。
② 設計判断として「drain しない」選択もあります
パッチアップグレードでは kubelet の再起動が数秒で済むため、本番ではローリング再起動だけで済ませる判断もありえます。drain は安全側に倒す選択であって必須ではありません——どちらを選ぶかは、そのワークロードが数秒の kubelet 停止に耐えるかで決まります。kubelet が止まっている間も既存のコンテナは動き続けるので、停止するのは「Pod の管理」であって「Pod の実行」ではない、という理解が判断の土台になります。
③ versionlock を外したまま忘れないでください
外したままにすると、次に誰かが dnf update を打った瞬間に kubelet だけが勝手に上がります。API Server より新しい kubelet はスキュー違反です。本書はノードごとに「外す → 上げる → 貼り直す」を閉じています。本番でもこの単位で閉じるのが安全で、5 台分をまとめて外しっぱなしにしないでください。
④ ダウングレードはできません
kubeadm upgrade は片道です。上げたあとで問題が判明しても、バージョンを戻す正規の手段はありません。本番では必ず etcd バックアップ(第2巻第5回)を取ってから実施してください。 本書ラボでは 9 VM を停止させてから取るスナップショットが同じ役割を果たします(稼働中に順次取ると、復元したときに etcd の状態がノード間でずれます)。「戻せない操作の前に戻せる状態を作る」という点では、第2回の鉄則1(マニフェストのバックアップ)と同じ考え方です。
ステップ7:全ノードのバージョンを確認する
実行コマンド(k8s-ops 上・developer):
$ kubectl get nodes
$ kubectl version
$ curl -sk --noproxy '*' -o /dev/null -w '%{http_code}\n' https://fanclub.local/
実行結果:
NAME STATUS ROLES AGE VERSION
k8s-cp-01 Ready control-plane 26h v1.36.3
k8s-cp-02 Ready control-plane 23h v1.36.3
k8s-cp-03 Ready control-plane 23h v1.36.3
k8s-wl-01 Ready <none> 23h v1.36.3
k8s-wl-02 Ready <none> 23h v1.36.3
Client Version: v1.35.6
Kustomize Version: v5.7.1
Server Version: v1.36.3
200
kubectl version の Client Version が v1.35.6 のままである点を確認してください。API Server は v1.36.3 なので、±1 マイナーの範囲に収まっています。上げなくてよいものは上げない——これも判断のうちです。
ここで本書ラボと CKS 試験環境の差が変わりました。
本回の前は「ラボ v1.36.2 / 試験 v1.35」でしたが、いまは 「ラボ v1.36.3 / 試験 v1.35」= 1 マイナー + パッチ分の差になっています。第6回以降の記事冒頭にある「動作確認バージョン」行は v1.36.3 で書かれます。
差が開くこと自体は問題ありません。本書は差が出る箇所(Admission プラグインの既定一覧など)をそのつど名指しで注記します。 逆に、注記が無い箇所は「このマイナー差では変わらない」と判断してよい、という読み方をしてください。
ステップ8:本回の設定が生き残っているかを検算する
ここが本回の締めです。 やってみよう④ のステップ1 で kubelet-config ConfigMap を先に直したことの効果を確認します。
実行コマンド(k8s-cp-01 上・root):
# grep serverTLSBootstrap /var/lib/kubelet/config.yaml
# grep authentication-config /etc/kubernetes/manifests/kube-apiserver.yaml
# grep kubelet-certificate-authority /etc/kubernetes/manifests/kube-apiserver.yaml
実行結果:
serverTLSBootstrap: true
- --authentication-config=/etc/kubernetes/auth/anonymous.yaml
- --kubelet-certificate-authority=/etc/kubernetes/pki/ca.crt
読み取るべき点は 2 つで、性質がまったく違います。
| 設定 | どこにあるか | なぜ残るのか |
|---|---|---|
serverTLSBootstrap: true | /var/lib/kubelet/config.yaml(kubeadm upgrade node が上書きする) | ConfigMap を先に直したから。直していなければ消えていた |
--authentication-config / --kubelet-certificate-authority | /etc/kubernetes/manifests/kube-apiserver.yaml(kubeadm upgrade apply / node が作り直す) | ステップ1-b で kubeadm-config の extraArgs に移したから。移していなければ消えていた |
最後に、やってみよう④ のステップ7 と同じ 3 コマンドをもう一度打って、logs / exec / top が動くことを確かめてください。アップグレードのあとに動作確認まで行って、はじめて 1 サイクル完了です。
実行コマンド(k8s-ops 上・developer):
$ kubectl logs -n fanclub -l app=fanclub-frontend --tail=3
$ kubectl exec -n fanclub deploy/fanclub-frontend -- echo EXEC_OK
$ kubectl top nodes
実行結果:
2026/07/26 13:16:10 [notice] 1#1: start worker process 29
2026/07/26 13:16:10 [notice] 1#1: start worker process 30
2026/07/26 13:16:10 [notice] 1#1: start worker process 31
10.244.215.247 - - [26/Jul/2026:13:21:19 +0000] "GET / HTTP/1.1" 200 2600 "-" "curl/8.12.1" "10.244.119.128"
EXEC_OK
NAME CPU(cores) CPU(%) MEMORY(bytes) MEMORY(%)
k8s-cp-01 597m 29% 1956Mi 55%
k8s-cp-02 979m 48% 2310Mi 65%
k8s-cp-03 499m 24% 2231Mi 62%
k8s-wl-01 610m 15% 4781Mi 63%
k8s-wl-02 326m 8% 1396Mi 18%
アップグレード直後は Pod の再配置が続いているため、CPU 使用率は普段より高く出ます。数分待つと落ち着きます。
ステップ9:パッケージが上書きしたファイル権限を戻す
ConfigMap に移せるのは kubeadm が管理するものだけです。 dnf が置き換えるファイルは、その外側にあります。第2回で 600 にした /usr/lib/systemd/system/kubelet.service は kubelet パッケージに含まれるファイルなので、アップグレードで 644 に戻ります。
実行コマンド(k8s-cp-01〜03 / k8s-wl-01〜02 上・root):
# ls -l /usr/lib/systemd/system/kubelet.service
実行結果(644 に戻っています):
-rw-r--r--. 1 root root 494 8月 1 00:00 /usr/lib/systemd/system/kubelet.service
タイムスタンプがパッケージのものに変わっていることが証拠です。5 台とも戻します。
実行コマンド(k8s-cp-01〜03 / k8s-wl-01〜02 上・root):
# chmod 600 /usr/lib/systemd/system/kubelet.service
# kube-bench run --targets node --check 4.1.1 --benchmark cis-1.12
実行結果:
[PASS] 4.1.1 Ensure that the kubelet service file permissions are set to 600 or more restrictive
--benchmark cis-1.12 を省くと、Workload Node では判定基準が変わります。
このコマンドは 5 台すべてで打ちますが、Workload Node には kubeconfig がありません。 第2回の やってみよう② で確認したとおり、kube-bench はクラスタのバージョンを判定できないと v1.18 相当のプロファイルへフォールバックします。その状態で打つと、同じ 4.1.1 が「644 or more restrictive」という緩い基準で評価され、600 にしていなくても PASS になります。
上の出力(「600 or more restrictive」)は --benchmark cis-1.12 を付けたときのものです。 手元の出力が「644」になっていたら、基準が落ちているので付け忘れを疑ってください。 第2回で「FAIL が PASS に化ける」と書いた罠は、確認コマンドの側にも同じように潜みます。
これを飛ばすと、node ターゲットが 19 PASS / 0 FAIL から 18 PASS / 1 FAIL に戻ります。
第2回の本文が「第5回でクラスタを更新したあとは、本回と同じ確認をもう一度実施することになります」と予告していたのが、ここです。設定が消える経路は 3 つあり、対処もそれぞれ違います——①static Pod マニフェスト(ClusterConfiguration に移す)②kubelet の設定(kubelet-config ConfigMap を先に直す)③パッケージが持つファイル(アップグレードのたびに手で戻す)。③だけは宣言的に持てないので、アップグレード手順書のチェックリストに載せます。
試験相当の粒度
「指定された 1 ノードを vX.Y.Z へアップグレードせよ」という 1 問で 8〜12 分が目安です。試験では 1 ノード分なので、本演習の 5 段階を 1 セット打つ形になります。時間を落とさない要点は 3 つです。
- 1 台目かどうかで
applyとnodeを使い分ける(間違えるとエラーで数分を失います) drain→ パッケージ更新 →daemon-reload→restart kubelet→uncordonの順序を手に覚える- 手順そのものは
kubernetes.io/docsの kubeadm upgrade ページにあるので、暗記より「開いてなぞる」ほうが確実。ただしページを開く 1 分を惜しむなら順序だけは覚えておく
暗記必須コマンド(第5回)
| 用途 | コマンド | 補足 |
|---|---|---|
| 匿名で API を叩く | curl -sk --noproxy '*' https://<CP の IP>:6443/version | プロキシ環境では --noproxy '*' が必須 |
| 匿名の権限を調べる | SubjectAccessReview を kubectl create -f - で投げる | auth can-i --as=system:anonymous は使えない(匿名に SelfSubjectAccessReview の作成権限が無い) |
| 特定 API Server の健全性 | kubectl --server=https://<CP の IP>:6443 --insecure-skip-tls-verify get --raw=/healthz | HA では --server で固定する |
| static Pod の起動確認 | crictl ps | grep kube-apiserver | API が落ちている間はこれだけが真実 |
| static Pod のバックアップ | cp /etc/kubernetes/manifests/kube-apiserver.yaml /root/kube-apiserver.yaml.bak-05 | manifests/ の外へ置く |
| Admission 既定一覧を見る | crictl exec <apiserver の CID> kube-apiserver --help | --enable-admission-plugins の括弧内 |
| 証明書の発行者を見る | curl -kv --noproxy '*' https://<node IP>:10250/healthz | openssl が無い環境向け |
| CSR 一覧 | kubectl get csr | SIGNERNAME と CONDITION を見る |
| CSR 承認 | kubectl certificate approve <名前> | 拒否は deny |
| kubelet-serving の CSR だけ | kubectl get csr --field-selector spec.signerName=kubernetes.io/kubelet-serving | まとめ承認に使う |
| アップグレード計画 | kubeadm upgrade plan | kubeadm を上げる前に打てる |
| 1 台目の Control Plane | kubeadm upgrade apply v1.36.3 | apply は 1 台目だけ |
| 2 台目以降・Workload | kubeadm upgrade node | バージョン指定は不要 |
| ノード退避 / 復帰 | kubectl drain <node> --ignore-daemonsets / kubectl uncordon <node> | kubelet 更新の前後 |
| versionlock 解除 / 追加 | dnf versionlock delete kubeadm kubelet kubectl / dnf versionlock add ... | 上げ終えたら貼り直す |
| アップグレードが何を書くか先に見る | kubeadm upgrade apply <version> --dry-run | 手で足したフラグが消えないかを事前に確認する |
| Control Plane 設定の正本を読む | kubectl get cm kubeadm-config -n kube-system -o jsonpath='{.data.ClusterConfiguration}' | フラグの恒久化先は apiServer.extraArgs |
| kubelet 設定の正本を読む | kubectl get cm kubelet-config -n kube-system -o yaml | /var/lib/kubelet/config.yaml はここから作られる |
パッケージ管理コマンドは、本書ラボと試験環境で違います
本書ラボは AlmaLinux なので dnf と dnf versionlock を使います。CKS の試験環境は Debian 系(apt)です。 コマンドをそのまま暗記すると、試験当日に手が止まります。
| やること | 本書ラボ(AlmaLinux / dnf) | Debian 系(apt) |
|---|---|---|
| ピン留めを外す | dnf versionlock delete kubeadm kubelet kubectl | apt-mark unhold kubeadm kubelet kubectl |
| 版を指定して入れ替える | dnf install -y kubeadm-1.36.3-150500.1.1 | apt-get install -y kubeadm=1.36.3-1.1 |
| ピン留めし直す | dnf versionlock add ... | apt-mark hold kubeadm kubelet kubectl |
| 入手できる版を調べる | dnf --disableplugin=versionlock list --showduplicates kubeadm | apt-cache madison kubeadm |
暗記するのはコマンド文字列ではなく「ロックを外す → 版を指定して入れ替える → ロックし直す」という型です。 ディストロが変わっても型は同じで、対応するコマンドは kubernetes.io/docs の kubeadm upgrade のページに両方載っています(試験中に参照できます)。kubeadm upgrade apply / upgrade node / drain / uncordon の 4 つはディストロに依存しないので、そちらは丸ごと覚えて差し支えありません。
試験中に参照できるか
| 内容 | 参照可否 |
|---|---|
AuthenticationConfiguration の YAML | 可(kubernetes.io/docs — Authenticating) |
serverTLSBootstrap / CSR 承認 | 可(kubernetes.io/docs — TLS bootstrapping) |
| バージョンスキュー | 不可(kubernetes.io/releases/ は許可 8 件のサブパス外)→ 暗記対象。kubeadm upgrade のタスクページから要点を辿ることはできる |
kubeadm-config の extraArgs / extraVolumes の書式(v1beta4) | 可(kubernetes.io/docs — kubeadm Configuration (v1beta4))。書式を暗記する必要はありません——リスト形式かマップ形式かで迷ったらここを開きます |
kubeadm upgrade の手順 | 可(kubernetes.io/docs — Upgrading kubeadm clusters) |
| kube-bench の使い方 | 不可(許可 8 件に無い)→ 第2回で暗記済み |
crictl の使い方 | 可(kubernetes.io/docs/tasks/debug/debug-cluster/crictl/)。ただし github.com/kubernetes-sigs/cri-tools は不可 |
この表そのものが教材です。 crictl は「Kubernetes 本体ではない道具」なので参照できないと思いがちですが、実際には kubernetes.io/docs にページがあります。逆にバージョンスキューは「公式の重要文書」であるにもかかわらず、/docs/ ではなく /releases/ にあるので開けません。
参照可否は、製品名や重要度で判断してはいけません。「そのページが許可された URL のサブパスにあるか」だけで決まります。 第1回で立てたこの基準を、本回で実地に落としました。試験前には、自分がよく使うページの URL を一度確認しておいてください。
最後に道具の話です。本文は読みやすさのため kubectl を全綴りで書いていますが、試験では alias k=kubectl を最初に張って k で打ってください。本回は static Pod の直接編集が中心なので --dry-run=client -o yaml の出番は少ないものの、kubectl certificate approve のようなワンライナーは k で打てると時間が浮きます。
まとめ
本回では次の点を確認しました。
- API Server は 認証 → 認可 → Admission の 3 つの関門でリクエストを処理する。401 なら認証層、403 なら認可層か Admission 層で、後者 2 つは拒否メッセージの文面で見分ける
- 匿名アクセスは既定で有効だが、RBAC(
system:public-info-viewer)により/healthz/livez/readyz/versionの 4 パス(/version/を含めて 5 エントリ)だけが許されている。取れるのはバージョン情報であり、それは CVE の狙い撃ちに使える --anonymous-auth=falseを素直に入れると kubeadm の 3 つの probe がすべて 401 になり、API Server が再起動ループに入る。 HA では他の 2 台が応答するため気づきにくく、CIS 1.2.1 は Manual なのでスコアも動かない- 正しい締め方は
--authentication-configで/livez/readyz/healthzだけ匿名を許すこと。--anonymous-authとは排他で、両方は書けない - 3 点セットは「常に 3 つ」ではない。
/etc/kubernetes/pkiの中なら既存マウントで足りる(フラグのみ)、外なら 3 つとも要る --authorization-mode=Node,RBACとNodeRestrictionは kubeadm が既定で入れている。Node 認可は「他ノードのものを読ませない」、NodeRestriction は「自分のものでも触らせないフィールドがある」--enable-admission-pluginsは既定への追加である。ラボの既定は 27 個だったが、マイナーごとに変わるので暗記せず--helpで確かめる- CIS 1.2.5 の恒久対応は
serverTLSBootstrap: true+ CSR の手動承認 +--kubelet-certificate-authorityの 3 点。kubelet-servingの CSR は自動承認されない(自動承認はselfnodeclientだけ) - kubeadm クラスタでは、ノード上のファイルは「結果」であって「正本」ではない。
kubeadm upgradeは/var/lib/kubelet/config.yamlをkubelet-configConfigMap から、kube-apiserver.yamlをkubeadm-configConfigMap から作り直す。手で足した設定は、この 2 つの ConfigMap(apiServer.extraArgs/extraVolumesを含む)に移していなければ消える - 実行前に
kubeadm upgrade apply --dry-runで「何を書こうとしているか」を確認する。 消えるかどうかは、打つ前に分かる - 古いパッチに留まることは CVE を放置すること。アップグレードは Control Plane → Workload、1 台目は
apply・以降はnode、マイナーは 1 つずつ - kube-bench は 44 PASS / 5 FAIL → 45 PASS / 4 FAIL になった。残る FAIL 4 件は監査ログで第16回に送る。WARN 11 件は本回の対処では動かない
理解度チェック(○×形式・全 9 問)
次の各文が正しいか(○)誤りか(×)を判断してください。下の「解答と解説」を開くと答え合わせができます。
kubelet-configConfigMap を直さずに/var/lib/kubelet/config.yamlだけへserverTLSBootstrap: trueを書いた場合、kubeadm upgrade nodeを実行してもその設定は残る- 認証に失敗したリクエストには 403 が返る
- 既定の設定では、匿名リクエストは認証を通過して
system:anonymousとして扱われる --anonymous-auth=falseを kubeadm クラスタの API Server に足すと、既定の probe が 401 になり再起動ループに入る--anonymous-authと--authentication-config(匿名を設定する場合)は同時に指定できる- static Pod にファイル参照フラグを足すときは、参照先が既存の
volumeMountsに含まれていればvolumesの追加は不要である --enable-admission-plugins=NodeRestrictionと書くと、有効な Admission プラグインは NodeRestriction だけになる- kubelet が出す
kubernetes.io/kubelet-servingの CSR は kubeadm クラスタでは自動承認される - パッチアップグレード(v1.36.2 → v1.36.3)ではリポジトリ URL の変更が必要である
解答と解説
1=×/2=×/3=○/4=○/5=×/6=○/7=×/8=×/9=×
問1 は誤りです。kubeadm upgrade node はローカルの /var/lib/kubelet/config.yaml を kubelet-config ConfigMap の内容で丸ごと上書きします。ローカルだけを直していると、アップグレードの瞬間に serverTLSBootstrap が消え、kubelet が自己署名証明書に戻ります。--kubelet-certificate-authority は API Server 側に残るので、kubectl logs が再び壊れます。ConfigMap を先に直してください。
問2 は誤りで、認証失敗は 401 です。403 は認可層か Admission 層で落ちたことを示します。問3 は正しく、既定では匿名リクエストは拒否されずに system:anonymous として認証を通過します。403 が返っていても「認証は通っている」という点が、本回の出発点でした。
問4 は正しく、kubeadm が生成する 3 つの probe はいずれも資格情報を持たない匿名リクエストです。startupProbe が failureThreshold: 24 × periodSeconds: 10 で約 240 秒後に kill するため、約 4 分周期の再起動ループになります。HA だと他の 2 台が応答するので、kubectl get nodes は 5/5 Ready のまま見えます。
問5 は誤りです。kube-apiserver --help に This flag is mutually exclusive with --anonymous-auth if the file configures the Anonymous authenticator と明記されています。両方書くと起動しません。 --authentication-config を入れるときは --anonymous-auth を消してください。
問6 は正しく、第2回 やってみよう③ の --kubelet-certificate-authority が /etc/kubernetes/pki(k8s-certs でマウント済み)を指していたのがこのケースです。本回の /etc/kubernetes/auth は既存マウントの外なので、3 つとも必要でした。「常に 3 つ」でも「常に 1 つ」でもなく、参照先の位置で決まります。
問7 は誤りです。--enable-admission-plugins は既定に追加するフラグで、ラボでは既定だけで 27 個が有効でした。PodSecurity が既定で有効である点は第8回の前提になり、AlwaysPullImages と EventRateLimit が既定で無効である点は kube-bench の WARN の理由になります。
問8 は誤りです。自動承認されるのは selfnodeclient(クライアント証明書のローテーション)だけで、selfnodeserver の ClusterRole はクラスタに存在しません。サーバ証明書は人間が承認します。 自己申告の IP とホスト名が入るため、検証せずに署名するとなりすましを許すからです。
問9 は誤りです。パッチは同一マイナー内の更新なので、リポジトリ URL は .../stable:/v1.36/rpm/ のままです。URL の変更が必要になるのはマイナーを上げるとき(第2巻第6回でやったケース)です。ただし URL がマイナーまでの指定である以上、dnf install と kubeadm upgrade apply の両方でパッチまで明示する必要がある点は変わりません。
次回予告
第6回「カーネル層の強制アクセス制御(SELinux / seccomp / capabilities / AppArmor)」では、D3(System Hardening)に入り、クラスタ設定から OS・ノードの層へ降ります。本回まではすべて「API Server に届いたあと」の話でしたが、次回はコンテナがカーネルに対して何を要求できるかを絞ります。SELinux のラベルを読み、seccomp で syscall を制限し、capabilities を落とします。
本回で扱った「ベンチマークが通ることと安全であることは別」という論点は、次回でもう一度別の形で出てきます。ホストの SELinux が Enforcing であっても、コンテナが拘束されているとは限らないからです。
→ 詳しくは 第6回 カーネル層の強制アクセス制御(SELinux / seccomp / capabilities / AppArmor)
