⚠️ Alpha API使用上の注意:
- 本記事の
certificates.k8s.io/v1alpha1APIおよびPodCertificateProjectedVolumeは、Kubernetes v1.34時点でAlphaステージです - Alpha APIは破壊的変更(breaking changes)が行われる可能性があり、予告なく削除される場合もあります
- 本番環境での使用は推奨されません
- GA化に向けてフィールド名、動作、検証ロジックなどが変更される可能性があります
背景
K8s v1.34でPodCertificate フィーチャーがalphaリリースされました。
PodCertificate フィーチャーはcertificates.k8s.io/v1alpha1のPodCertificateRequestリソースを定義することでPodに対して証明書を発行するための仕組みです。
一般的なユースケースとしては、ユーザーが直接PodCertificateRequestを作成する必要はなく、ProjectedVolumeのソースとして podCertificate が利用できるようになるので、PodのSpecに指定することでkubeletがいい感じに鍵ペアを作成〜証明書の要求、Podへマウントしてくれます。
apiVersion: v1
kind: Pod
metadata:
namespace: default
name: podcertificate-pod
spec:
serviceAccountName: default
containers:
- image: debian
name: main
command: ["sleep", "infinity"]
volumeMounts:
- name: my-x509-credentials
mountPath: /var/run/my-x509-credentials
volumes:
- name: my-x509-credentials
projected:
defaultMode: 420
sources:
- podCertificate:
keyType: ED25519
signerName: coolcert.example.com/foo
credentialBundlePath: credentialbundle.pem
しかし、alphaリリース時点でPodCertificateをサポートしているSignerが実装されておらず(※)、ProjectedVolumeで指定しても証明書待ちになり、PodはPending状態のまま起動しません。
※ 筆者が確認した限りでは存在しませんでしたが間違っていたら申し訳ありません ![]()
やりたいこと
そこで、Signerが存在しないなら自分がSignerとして振る舞い、PodCertificateの仕組みもついでに理解してしまいたいと思いました。
⚠️ 注意:
この記事で実装するSignerは概念実証(PoC)レベルのものです。本番環境での使用は想定しておらず、セキュリティ上の考慮が不十分な部分があります。
kEP-4317に記載してあるシーケンスを見ると、Signerの役割は PodCertificateRequestを読んで証明書を発行し、PodCertificateRequestリソースのstatusを更新すればよさそうです。
やってみた
前提条件
- Kubernetes クラスターでPodCertificate機能が有効(feature gate enabled)
kubectl-
opensslx509コマンドで-force_pubkeyが使えるバージョン -
dateGNU版 jq
全体の流れ
- Pod作成: PodCertificate projected volumeを持つPodを作成
- PodCertificateRequest自動生成: kubeletがPodの秘密鍵を生成し、PodCertificateRequestを作成
- 証明書発行: Certificate Signerが証明書を発行する
- PodCertificateRequestのstatusを更新: Certificte Signerが発行した証明書をstatusにセット
- 証明書マウント: kubeletが証明書バンドルをPodにマウント
- Pod起動: 証明書が利用可能になりPodが起動
事前準備
今回、発行する証明書は独自のPKIツリーとするため、CA証明書を準備します。
# CA秘密鍵の生成
$ openssl ecparam -genkey -name prime256v1 -out ca.key
# CA証明書の作成
$ openssl req -new -x509 -key ca.key -out ca.crt -days 3650 \
-subj "/CN=kubernetes-pod-certificate-ca/O=system:masters"
# CA証明書の確認
$ openssl x509 -in ca.crt -text -noout
Pod リソース準備
cat > pod-with-cert.yaml <<EOF
apiVersion: v1
kind: Pod
metadata:
name: pod-cert-test
namespace: default
spec:
containers:
- name: test-container
image: alpine:3.18
command: ["/bin/sh"]
args: ["-c", "while true; do echo 'Waiting for certificates...'; ls -la /etc/certs/; sleep 30; done"]
volumeMounts:
- name: pod-certs
mountPath: /etc/certs
readOnly: true
volumes:
- name: pod-certs
projected:
sources:
- podCertificate:
signerName: kubernetes.io/kube-apiserver-client-kubelet # この例では予約済みの名前を使っているが、example.com/hiyosi-signer みたいなのでもOK
keyType: ECDSAP256
maxExpirationSeconds: 86400
credentialBundlePath: credential-bundle.pem
EOF
補足:
-
signerName: 証明書署名者の識別名 -
keyType: 生成する鍵のタイプ(ECDSAP256, RSA4096等) -
maxExpirationSeconds: 証明書の最大有効期間(秒) -
credentialBundlePath: 証明書バンドルファイルのパス
発行処理
1. Pod作成
最初に、先ほど用意したProvjected Volumeを設定したPodリソースを作成します。
$ kubectl apply -f pod-with-cert.yaml
※ これより先は実際にコマンドを実行しながら確認する手順になっていますが、手順をスクリプト化したものが以下にありますので、とりあえず結果だけみたい場合はスクリプトを実行してください。
証明書発行〜PodCertificateRequestステータス更新
-
https://gist.github.com/hiyosi/0bbe68c2f3a5de37ab36e5a86f7887a2
Podにマウントされた証明書のIdentityをデコードして確認 - https://gist.github.com/hiyosi/861c70c014ebd58f63675c726797a93d
$ bash pod-certificate-issuer.sh
$ python3 extract-and-decode-pod-identity.py
2. PodCertificateRequest自動生成
podCertificate ProjectedVolumeが要求されたPodが作成されるとkubeletがPodCertificateRequestを作成してくれるはずなので、リソースが存在することを確認します。
$ NAMESPACE=default
$ kubectl get podcertificaterequest -n "$NAMESPACE"
$ PCR_NAME=xxxx # オブジェクト名
$ kubectl get podcertificaterequest "$PCR_NAME" -n "$NAMESPACE" -o yaml
e.g. 作成されたリソース
apiVersion: certificates.k8s.io/v1alpha1
kind: PodCertificateRequest
metadata:
creationTimestamp: "2025-09-20T07:50:30Z"
generateName: req-
name: req-sxs5l
namespace: default
ownerReferences:
- apiVersion: core/v1
kind: Pod
name: pod-cert-test
uid: 9c13f68b-874a-4c24-adba-f7213a5fcf0c
resourceVersion: "551173"
uid: 40cca1e5-b3a4-4978-b548-a5a1b1f27304
spec:
maxExpirationSeconds: 86400
nodeName: pod-cert-test-worker
nodeUID: 68576cf7-35e5-4d7c-8e78-f4bffe787b24
pkixPublicKey: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEkLGcI0oKCueGgR1XDmWQEtVeCnjRPNQXpoadijHs8lomWNtf6iw0Y92YnJfmhzhbE9z98y9ElBHFqoTbI2fecA==
podName: pod-cert-test
podUID: 9c13f68b-874a-4c24-adba-f7213a5fcf0c
proofOfPossession: MEYCIQCKufl1AuR1D89dFqBLGwTsPxgkk5E/bUBoaXM5W83F7wIhAMyq2ED/KYLJgji42dneVMqZEk+unOm9Aarblpm94XeY
serviceAccountName: default
serviceAccountUID: e2c917ba-3410-4cb2-82ae-b65bc19a5c45
signerName: kubernetes.io/kube-apiserver-client-kubelet
-
proofOfPossession: ProofOfPossessionはリソースの作成者が正当であることを確認するためのもの。- Pod UIDを秘密鍵で署名したASCII文字列
-
PodCertificateリソースを受け取ったkube-apiserver(Admission Controller)で検証 - kubeletの認証とRBACによる認可(kubeletのみリソースの作成可能)、Node Restriction(kubeletは自分が管理するNodeのPodに対してのみPodCertificateRequestを作成できる)などとあわせて多層防御
この時点ではPodはPending状態で証明書のマウントを待っています。
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
pod-cert-test 0/1 ContainerCreating 0 5s
3. 証明書発行
SignerはPodCertificateRequestのspecにしたがって証明書を発行します。
今回は opensslコマンドを使って証明書を発行してみます。(が、このあと色々面倒で後悔することになった)
証明書をPEM形式に変換
$ PKIX_PUBLIC_KEY=xxx # PodCertificateRequestの .spec.pkixPublicKey の値
$ echo "$PKIX_PUBLIC_KEY" | base64 -d > pod-public.der
$ openssl ec -pubin -inform DER -in pod-public.der -outform PEM -out pod-public.pem
Pod証明書のためのopenssl configの作成します。
cat > cert.cnf <<EOF
[req]
distinguished_name = req_dn
prompt = no
[req_dn]
CN = system:pod:${NAMESPACE}:${POD_NAME}
O = system:pods
EOF
extentionの設定
(Pod Identityの拡張のあたりはこんな感じであっている? このあたり詳しくないので Claude Codeで作ってもらった)
⚠️ 注意:
最新のKEP (v1.35 beta時点)では Pod Identity Extension の表現例はKEPから消えています。
こちらはあくまで参考となります。
$ kubectl get podcertificaterequest "$PCR_NAME" -n "$NAMESPACE" -o json > pcr.json
$ PUBLIC_KEY_B64=$(jq -r '.spec.pkixPublicKey' pcr.json)
$ POD_NAME=$(jq -r '.spec.podName' pcr.json)
$ POD_UID=$(jq -r '.spec.podUID' pcr.json)
$ NODE_NAME=$(jq -r '.spec.nodeName' pcr.json)
$ NODE_UID=$(jq -r '.spec.nodeUID' pcr.json)
$ SERVICE_ACCOUNT=$(jq -r '.spec.serviceAccountName' pcr.json)
$ SERVICE_ACCOUNT_UID=$(jq -r '.spec.serviceAccountUID' pcr.json)
$ MAX_EXPIRATION_SECONDS=$(jq -r '.spec.maxExpirationSeconds // 86400' pcr.json)
$ NAMESPACE_UID=$(kubectl get namespace "$NAMESPACE" -o jsonpath='{.metadata.uid}')
cat > ext.cnf <<EOF
basicConstraints = CA:FALSE
keyUsage = critical,digitalSignature,keyEncipherment
extendedKeyUsage = clientAuth
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
# Subject Alternative Names
subjectAltName = @alt_names
# Pod Identity Extension - Official Kubernetes OID (KEP-4317)
1.3.6.1.4.1.57683.1 = critical,ASN1:SEQUENCE:pod_identity_section
[alt_names]
DNS.1 = ${POD_NAME}
DNS.2 = ${POD_NAME}.${NAMESPACE}
DNS.3 = ${POD_NAME}.${NAMESPACE}.pod.cluster.local
[pod_identity_section]
# Fields with implicit tags as per KEP-4317 specification
field0 = IMP:0,UTF8:${NAMESPACE}
field1 = IMP:1,UTF8:${NAMESPACE_UID}
field2 = IMP:2,UTF8:${SERVICE_ACCOUNT}
field3 = IMP:3,UTF8:${SERVICE_ACCOUNT_UID}
field4 = IMP:4,UTF8:${POD_NAME}
field5 = IMP:5,UTF8:${POD_UID}
field6 = IMP:6,UTF8:${NODE_NAME}
field7 = IMP:7,UTF8:${NODE_UID}
EOF
ここで、opensslは証明書発行にCSRが必須なようなのですが(おそらく)、今回の仕組み上秘密鍵は渡されてこないのでCSRを作成するのにワークアラウンドで秘密鍵を作成して、CSRを作成します。
その後、以下のopensslのバージョンでは x509 コマンドに -force_pubkeyオプションが存在するのでそれを利用して鍵を置き換えます。
$ openssl version
OpenSSL 3.5.0 8 Apr 2025 (Library: OpenSSL 3.5.0 8 Apr 2025
$ openssl x509 -help 2>&1 | grep -i force
-key val Key for signing, and to include unless using -force_pubkey
-force_pubkey infile Key to be placed in new certificate or certificate request
$ openssl ecparam -genkey -name prime256v1 -out temp-private.pem
$ openssl req -new -key temp-private.pem -out temp.csr -config cert.cnf
$ MAX_EXPIRATION_SECONDS=$(jq -r '.spec.maxExpirationSeconds // 86400' pcr.json)
$ DAYS=$((MAX_EXPIRATION_SECONDS / 86400))
# -force_pubkeyで置き換え
$ openssl x509 -req -in temp.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-out pod-cert.crt -days $DAYS -extfile ext.cnf \
-force_pubkey pod-public.pem
確認
$ openssl x509 -in pod-cert.crt -text -noout | grep -A 20 "1.3.6.1.4.1.57683.1"
# PodCertificateRequestの公開鍵
$ openssl ec -pubin -in pod-public.pem -text -noout | grep -A 10 "pub:" | grep -E "^\s*[0-9a-f]{2}:" | tr -d ' :' | tr -d '\n'
# 発行した証明書の公開鍵
$ openssl x509 -in pod-cert.crt -pubkey -noout | openssl ec -pubin -text -noout | grep -A 10 "pub:" | grep -E "^\s*[0-9a-f]{2}:" | tr -d ' :' | tr -d '\n'
証明書バンドルの作成
$ cat pod-cert.crt > credential-bundle.pem
$ cat ca.crt >> credential-bundle.pem
4. PodCertificateRequestのstatusを更新
証明書が発行できたので、それをPodCertificateRequestリソースのstatusにセットしてステータスを更新します。
※ ここではGNU dateを使ってます
# 証明書から実際のnotBeforeを取得(必須)
$ NOT_BEFORE=$(openssl x509 -in pod-cert.crt -noout -startdate | cut -d= -f2)
$ NOT_BEFORE=$(gdate -u -d "$NOT_BEFORE" +"%Y-%m-%dT%H:%M:%SZ")
# maxExpirationSecondsを取得
$ MAX_EXPIRATION_SECONDS=$(jq -r '.spec.maxExpirationSeconds // 86400' pcr.json)
# notAfterを計算(notBefore + maxExpirationSeconds)
$ NOT_BEFORE_EPOCH=$(gdate -u -d "$NOT_BEFORE" +%s)
$ NOT_AFTER=$(gdate -u -d "@$((NOT_BEFORE_EPOCH + MAX_EXPIRATION_SECONDS))" +"%Y-%m-%dT%H:%M:%SZ")
# リフレッシュ時間を計算(有効期限の80%経過後)
$ REFRESH_SECONDS=$((MAX_EXPIRATION_SECONDS * 80 / 100))
$ BEGIN_REFRESH_AT=$(gdate -u -d "@$((NOT_BEFORE_EPOCH + REFRESH_SECONDS))" +"%Y-%m-%dT%H:%M:%SZ")
# 証明書チェーンをJSON文字列に変換
$ CREDENTIAL_BUNDLE=$(cat credential-bundle.pem | jq -Rs '.')
# patch.jsonを作成
$ cat > patch.json <<EOF
{
"status": {
"certificateChain": $CREDENTIAL_BUNDLE,
"notBefore": "$NOT_BEFORE",
"notAfter": "$NOT_AFTER",
"beginRefreshAt": "$BEGIN_REFRESH_AT",
"conditions": [
{
"type": "Issued",
"status": "True",
"reason": "Issued",
"message": "Certificate issued with KEP-4317 Pod Identity extension",
"lastTransitionTime": "$NOT_BEFORE"
}
]
}
}
EOF
kubectl patchでパッチを適用
$ kubectl patch podcertificaterequest "$PCR_NAME" -n "$NAMESPACE" \
--type=merge --subresource=status -p "$(cat patch.json)"
podcertificaterequest.certificates.k8s.io/req-xxx patched
5. 証明書マウント〜Pod起動
PodCertificateRequestが正常に更新され、Podに証明書がマウントされて正常に起動することを確認。
$ kubectl get podcertificaterequest "$PCR_NAME" -n "$NAMESPACE"
NAME PODNAME SERVICEACCOUNTNAME NODENAME SIGNERNAME
req-xxx pod-cert-test default pod-cert-test-worker kubernetes.io/kube-apiserver-client-kubelet Issued
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
pod-cert-test 1/1 Running 0 44s
証明書の中身を確認してみます。
こちらにスクリプトを用意しておきました。
https://gist.github.com/hiyosi/861c70c014ebd58f63675c726797a93d
$ python3 extract-and-decode-pod-identity.py
...
4. Pod Identity Extension Contents:
============================================================
Namespace : default
NamespaceUID : 3c88b151-3b1e-4641-a915-12202e80751a
ServiceAccountName : default
ServiceAccountUID : e2c917ba-3410-4cb2-82ae-b65bc19a5c45
PodName : pod-cert-test
PodUID : 1b9469ec-9590-4a6b-9f65-7ba55a12fe78
NodeName : pod-cert-test-worker
NodeUID : 68576cf7-35e5-4d7c-8e78-f4bffe787b24
============================================================
...
無事Podに証明書がマウントされて、Identityもそれっぽく設定されているように見えます。
これで私もSignerへの一歩を踏み出せたような気がします。