3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Signerになって理解するPodCertificate

3
Last updated at Posted at 2025-09-21

⚠️ Alpha API使用上の注意:

  • 本記事のcertificates.k8s.io/v1alpha1 APIおよびPodCertificate ProjectedVolumeは、Kubernetes v1.34時点でAlphaステージです
  • Alpha APIは破壊的変更(breaking changes)が行われる可能性があり、予告なく削除される場合もあります
  • 本番環境での使用は推奨されません
  • GA化に向けてフィールド名、動作、検証ロジックなどが変更される可能性があります

背景

K8s v1.34でPodCertificate フィーチャーがalphaリリースされました。
PodCertificate フィーチャーはcertificates.k8s.io/v1alpha1PodCertificateRequestリソースを定義することで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状態のまま起動しません。

※ 筆者が確認した限りでは存在しませんでしたが間違っていたら申し訳ありません :bow:

やりたいこと

そこで、Signerが存在しないなら自分がSignerとして振る舞い、PodCertificateの仕組みもついでに理解してしまいたいと思いました。

⚠️ 注意:
この記事で実装するSignerは概念実証(PoC)レベルのものです。本番環境での使用は想定しておらず、セキュリティ上の考慮が不十分な部分があります。

kEP-4317に記載してあるシーケンスを見ると、Signerの役割は PodCertificateRequestを読んで証明書を発行し、PodCertificateRequestリソースのstatusを更新すればよさそうです。

やってみた

前提条件

  • Kubernetes クラスターでPodCertificate機能が有効(feature gate enabled)
  • kubectl
  • openssl x509コマンドで-force_pubkeyが使えるバージョン
  • date GNU版
  • jq

全体の流れ

  1. Pod作成: PodCertificate projected volumeを持つPodを作成
  2. PodCertificateRequest自動生成: kubeletがPodの秘密鍵を生成し、PodCertificateRequestを作成
  3. 証明書発行: Certificate Signerが証明書を発行する
  4. PodCertificateRequestのstatusを更新: Certificte Signerが発行した証明書をstatusにセット
  5. 証明書マウント: kubeletが証明書バンドルをPodにマウント
  6. 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ステータス更新

$ 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への一歩を踏み出せたような気がします。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?