1. 概要
Confidential GKE(CGKE)ノードは、ワークロード内のデータが使用中に暗号化されることを保証します。vTPM デバイスを CGKE ワークロードに公開すると、ワークロードで vTPM 機能を使用できるようになります。この Codelab では、vTPM の機能と、cc-device-plugin を使用して Intel TDX と AMD SEV-SNP のハードウェア構成証明を活用する方法について学びます。
- vTPM リモート認証により、リモート当事者は、ワークロードをホストする CGKE ノードが Confidential VMs(CVM)で実行されていることを確認できます。
- vTPM 認証と vTPM シーリング。
- 公開されたインテル TDX と AMD SEV-SNP ゲスト デバイスを使用したハードウェア認証。

上の図に示すように、この Codelab の最初の部分には次の手順が含まれています。
- CGKE ノードは、vTPM デバイスを設定して、選択したワークロードに公開します。
- ワークロードをデプロイし、ワークロードをホストする CGKE ノードをリモートで証明します。
- ワークロードをデプロイして、TDX または SEV-SNP デバイスからハードウェア構成証明引用符を直接取得します。
- Secret Release Web Server の設定。

上の図に示すように、この Codelab の第 2 部には次のものが含まれています。
- CGKE ノードでの vTPM 認可の設定と vTPM シーリング。

上の図に示すように、この Codelab の第 3 部には次のものが含まれています。
cc-device-pluginがセットアップ フェーズでハードウェア デバイスを安全にマッピングする方法。- ワークロードをデプロイして、ランタイム フェーズ中に TDX または SEV-SNP デバイスからハードウェア構成証明引用符を直接取得します。オーバーヘッドはゼロです。
学習内容
- vTPM デバイスを CGKE ワークロードに公開する方法。
- CGKE ワークロードで Confidential Computing API(Attestation Verifier サービス)を使用してリモート認証を行う方法。
- vTPM 認証を設定して vTPM シーリングを実行する方法。
cc-device-pluginを使用して CGKE ワークロードで SEV-SNP デバイスと TDX デバイスにアクセスする方法。- go-tdx-guest と go-sev-guest を使用してハードウェア構成証明引用を取得する方法。
必要なもの
- Google Cloud Platform プロジェクト
- ブラウザ(Chrome、Firefox など)
- Google Compute Engine(codelab)、Confidential VM、Confidential GKE Node、Artifact Registry に関する基本的な知識
- Intel TDX または AMD SEV-SNP の場合: 最小 GKE バージョン v1.33.5-gke.1697000+ または v1.34.1-gke.2909000+ と適切なマシン ファミリー(TDX の場合は c3、SNP の場合は n2d)。
2. 設定と要件
必要な API を有効にするには、Cloud コンソールまたはローカル開発環境で次のコマンドを実行します。
gcloud auth login
gcloud services enable \
cloudapis.googleapis.com \
cloudshell.googleapis.com \
container.googleapis.com \
artifactregistry.googleapis.com \
confidentialcomputing.googleapis.com \
iamcredentials.googleapis.com \
compute.googleapis.com
3. 共有 CGKE コントロール プレーンの設定
このステップでは、共有 GKE コントロール プレーンと Artifact Registry Docker リポジトリを設定します。
1)。環境変数を設定し、共有 GKE クラスタを作成します。
your-project-id を実際のプロジェクト ID に置き換えます。us-central1-c は、目的のゾーンに置き換えます。(リージョンとゾーンをご覧ください)
export PROJECT_ID="your-project-id"
export ZONE="us-central1-c"
export CLUSTER_NAME="cgke-attestation-codelab"
gcloud config set project ${PROJECT_ID}
gcloud container clusters create ${CLUSTER_NAME} \
--zone=${ZONE} \
--num-nodes=1 \
--machine-type=e2-medium \
--workload-pool=${PROJECT_ID}.svc.id.goog \
--workload-metadata=GKE_METADATA
gcloud container clusters get-credentials ${CLUSTER_NAME} --zone=${ZONE}
2)。ワークロード コンテナ イメージを保存する Artifact Registry Docker リポジトリを作成します。
gcloud artifacts repositories create codelab-repo \
--repository-format=docker \
--location=us
プログラムの選択
共有 GKE コントロール プレーンと Artifact Registry リポジトリの準備が整いました。方針の選択:
- オプション 1: vTPM リモート認証とシーリング(AMD SEV)- CGKE ノードの設定と vTPM デバイスの公開に直接進みます。
- オプション 2: ハードウェア認証(インテル TDX と AMD SEV-SNP) - ハードウェア認証を実行するワークロードのデプロイに直接移動します。(すべてを試してみますか?オプション 1 を完了し、オプション 2 に順番に進みます)
4. CGKE ノードを設定し、選択したワークロードに vTPM デバイスを公開する
このステップでは、CGKE に vTPM ノードプールを作成し、デバイス プラグインを適用して、CVM vTPM デバイスをワークロードに公開します。Cloud コンソールまたはローカル開発環境に移動して、コマンドを実行します。
1)。AMD SEV ノードプールを追加する
gcloud container node-pools create sev-pool \
--cluster=${CLUSTER_NAME} \
--zone=${ZONE} \
--machine-type=n2d-standard-2 \
--num-nodes=1 \
--enable-confidential-nodes \
--confidential-node-type=sev
2)。デフォルトのノードプールを削除する(省略可。コストとノイズを削減するためにおすすめ)
gcloud container node-pools delete default-pool \
--cluster=${CLUSTER_NAME} \
--zone=${ZONE} \
--quiet
3)。デバイス プラグインを起動して、CGKE クラスタが vTPM デバイスをワークロードに公開できるようにします。Kubernetes デバイス プラグインを使用して新しいリソース(google.com/cc)を作成します。新しいリソースに関連付けられたワークロードは、ワーカーノード上のデバイスを確認できます。
gcloud container clusters get-credentials ${CLUSTER_NAME} --zone ${ZONE} --project ${PROJECT_ID}
kubectl apply -f https://raw.githubusercontent.com/google/cc-device-plugin/main/manifests/cc-device-plugin.yaml
次のコマンドを使用すると、デプロイされた cc-device-plugin を確認できます。
kubectl get pods -A | grep "cc-device-plugin"
注: 混合モードの GKE クラスタ(Confidential GKE ワーカーノードと非 Confidential GKE ワーカーノードの両方を含む)の場合は、オペレーターが cc-device-plugin を Confidential GKE ワーカーノードにのみデプロイすることをおすすめします。
(省略可)CGKE Pod の Prometheus モニタリングを適用します。モニタリングをオンにすると、デバイス プラグインのステータスを確認できます。
kubectl apply -f https://raw.githubusercontent.com/google/cc-device-plugin/main/manifests/cc-device-plugin-pod-monitoring.yaml
https://console.cloud.google.com/monitoring/metrics-explorer に移動して cc-device-plugin 指標を見つけるか、PROMQL を使用します。たとえば、次の PROMQL コマンドは、各 cc-device-plugin プロセスの CPU 秒数を示します。
rate(process_cpu_seconds_total[${__interval}])
5. ワークロードのデプロイとワークロードでのリモート証明書の実行(vTPM)
このステップでは、前のステップで作成した CGKE クラスタにワークロードを作成してデプロイし、vTPM リモート証明書を取得して、ワーカーノードで証明書トークン(OIDC トークン)を取得します。
1)。アプリケーション コンテナ イメージを作成して Artifact Registry に push します。アプリケーション コンテナ イメージには go-tpm ツールが含まれています。このツールは、証明書を収集して証明書検証サービスに送信し、証明書トークン(OIDC トークン)を取得できます。
- アプリケーション コンテナ イメージの
Dockerfileを作成します。
cat << 'EOF' > Dockerfile FROM golang:1.26.4 as builder WORKDIR / RUN git clone https://github.com/google/go-tpm-tools.git WORKDIR /go-tpm-tools/cmd/gotpm RUN CGO_ENABLED=0 GOOS=linux go build -o /gotpm FROM debian:trixie WORKDIR / RUN apt-get update -y RUN DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl RUN rm -rf /etc/apt/sources.list.d COPY --from=builder /gotpm /gotpm CMD ["tail", "-f", "/dev/null"] EOF
- アプリケーション コンテナ イメージを Artifact Registry に push します。
docker build -t us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/go-tpm:latest .
docker push us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/go-tpm:latest
2)。GCP リソースに対する GCP サービス アカウントの権限を継承するように Kubernetes サービス アカウントを設定します。
- Kubernetes サービス アカウント
codelab-ksaを作成します。
kubectl create serviceaccount codelab-ksa \
--namespace default
- ロール
Confidential_Computing_Workload_Userを作成し、Confidential Computing API にアクセスするロール権限を付与します。
gcloud iam roles create Confidential_Computing_Workload_User --project=${PROJECT_ID} \
--title="CGKE Workload User" --description="Grants the ability to generate an attestation token in a GKE workload." \
--permissions="confidentialcomputing.challenges.create,confidentialcomputing.challenges.verify,confidentialcomputing.locations.get,confidentialcomputing.locations.list" --stage=GA
- GCP サービス アカウント
codelab-csaを作成し、ロールConfidential_Computing_Workload_Userとバインドします。これにより、codelab-csaに Confidential Computing API へのアクセス権が付与されます。
gcloud iam service-accounts create codelab-csa \
--project=${PROJECT_ID}
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
--member "serviceAccount:codelab-csa@${PROJECT_ID}.iam.gserviceaccount.com" \
--role "projects/${PROJECT_ID}/roles/Confidential_Computing_Workload_User"
gcloud iam service-accounts add-iam-policy-binding codelab-csa@${PROJECT_ID}.iam.gserviceaccount.com \
--role roles/iam.workloadIdentityUser \
--member "serviceAccount:${PROJECT_ID}.svc.id.goog[default/codelab-ksa]"
- Kubernetes サービス アカウント
codelab-ksaを GCP サービス アカウントcodelab-csaにバインドします。これにより、codelab-ksaに Confidential Computing API へのアクセス権が付与されます。
kubectl annotate serviceaccount codelab-ksa \
--namespace default \
iam.gke.io/gcp-service-account=codelab-csa@${PROJECT_ID}.iam.gserviceaccount.com
3)。デモ アプリケーションのアプリケーション デプロイ YAML を作成します。選択したワークロードに Kubernetes サービス アカウント codelab-ksa を割り当てます。
cat << EOF > deploy.yaml
apiVersion: v1
kind: Pod
metadata:
name: go-tpm-demo
labels:
app.kubernetes.io/name: go-tpm-demo
spec:
serviceAccountName: codelab-ksa
nodeSelector:
iam.gke.io/gke-metadata-server-enabled: "true"
cloud.google.com/machine-family: "n2d"
cloud.google.com/gke-confidential-nodes-instance-type: "SEV"
containers:
- name: go-tpm
image: us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/go-tpm:latest
resources:
limits:
google.com/cc: 1
EOF
4)。Deployment を CGKE クラスタに適用します。
kubectl apply -f deploy.yaml
5)。ワークロードに接続し、リモート証明書を起動して証明書トークン(OIDC トークン)を取得します。
kubectl exec -it go-tpm-demo -- /bin/bash ./gotpm token --event-log=/run/cc-device-plugin/binary_bios_measurements > attestation_token
6)。トークンを画面に出力して、コピーできるようにします。
cat attestation_token
jwt.io で構成証明トークンをデコードして、クレームを表示できます。
6. シークレット リリース ウェブサーバーの設定
この手順では、前の SSH セッションを終了し、別の VM を設定します。この VM に、シークレット リリース ウェブサーバーを設定します。ウェブサーバーは、受信した証明書トークンとそのクレームを検証します。検証に成功すると、シークレットがリクエスタに渡されます。
1)。Cloud コンソールまたはローカル開発環境に移動します。仮想マシンを作成します。
gcloud config set project ${PROJECT_ID}
gcloud compute instances create cgke-attestation-codelab-web-server \
--machine-type=n2d-standard-2 \
--zone=${ZONE} \
--image-family=ubuntu-2204-lts \
--image-project=ubuntu-os-cloud
2)。新しい VM に SSH で接続します。
gcloud compute ssh --zone ${ZONE} cgke-attestation-codelab-web-server
3)。Go 環境を設定します。
wget https://go.dev/dl/go1.26.4.linux-amd64.tar.gz sudo tar -C /usr/local -xzf go1.26.4.linux-amd64.tar.gz export PATH=$PATH:/usr/local/go/bin
4)。シークレット リリース ウェブサーバーのソースコードを格納する次の 2 つのファイルを作成します。
main.go を作成します。
cat << 'EOF' > main.go
package main
import (
"fmt"
"net/http"
"strings"
"time"
"log"
"github.com/golang-jwt/jwt/v4"
)
const (
theSecret = "This is the super secret information!"
)
func homePage(w http.ResponseWriter, r *http.Request) {
tokenString := r.Header.Get("Authorization")
if tokenString != "" {
tokenString, err := extractToken(tokenString)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
}
tokenBytes := []byte(tokenString)
// A method to return a public key from the well-known endpoint
keyFunc := getRSAPublicKeyFromJWKsFile
token, err := decodeAndValidateToken(tokenBytes, keyFunc)
if err != nil {
http.Error(w, "Invalid JWT Token", http.StatusUnauthorized)
}
if ok, err := isValid(token.Claims.(jwt.MapClaims)); ok {
fmt.Fprintln(w, theSecret)
} else {
if err != nil {
http.Error(w, "Error validating JWT claims: "+err.Error(), http.StatusUnauthorized)
} else {
http.Error(w, "Invalid JWT token Claims", http.StatusUnauthorized)
}
}
} else {
http.Error(w, "Authorization token required", http.StatusUnauthorized)
}
}
func extractToken(tokenString string) (string, error) {
if strings.HasPrefix(tokenString, "Bearer ") {
return strings.TrimPrefix(tokenString, "Bearer "), nil
}
return "", fmt.Errorf("invalid token format")
}
func isValid(claims jwt.MapClaims) (bool, error) {
// 1. Evaluating Standard Claims:
subject, ok := claims["sub"].(string)
if !ok {
return false, fmt.Errorf("missing or invalid 'sub' claim")
}
fmt.Println("Subject:", subject)
issuedAt, ok := claims["iat"].(float64)
if !ok {
return false, fmt.Errorf("missing or invalid 'iat' claim")
}
fmt.Println("Issued At:", time.Unix(int64(issuedAt), 0))
// 2. Evaluating Remote Attestation Claims:
hwModel, ok := claims["hwmodel"].(string)
// Support attestation of both AMD SEV and Intel TDX
if !ok || (hwModel != "GCP_AMD_SEV" && hwModel != "GCP_INTEL_TDX") {
return false, fmt.Errorf("missing or invalid 'hwModel': %v", hwModel)
}
fmt.Println("hwmodel:", hwModel)
swName, ok := claims["swname"].(string)
if !ok || swName != "GCE" {
return false, fmt.Errorf("missing or invalid 'swName'")
}
fmt.Println("swname:", swName)
return true, nil
}
func main() {
http.HandleFunc("/", homePage)
fmt.Println("Server listening on :8080")
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatalf("Server failed to start: %v", err)
}
}
EOF
helper.go を作成します。
cat << 'EOF' > helper.go
package main
import (
"crypto/rsa"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"math/big"
"net/http"
"github.com/golang-jwt/jwt/v4"
)
const (
socketPath = "/run/container_launcher/teeserver.sock"
expectedIssuer = "https://confidentialcomputing.googleapis.com"
wellKnownPath = "/.well-known/openid-configuration"
)
type jwksFile struct {
Keys []jwk `json:"keys"`
}
type jwk struct {
N string `json:"n"` // "nMMTBwJ7H6Id8zUCZd-L7uoNyz9b7lvoyse9izD9l2rtOhWLWbiG-7pKeYJyHeEpilHP4KdQMfUo8JCwhd-OMW0be_XtEu3jXEFjuq2YnPSPFk326eTfENtUc6qJohyMnfKkcOcY_kTE11jM81-fsqtBKjO_KiSkcmAO4wJJb8pHOjue3JCP09ZANL1uN4TuxbM2ibcyf25ODt3WQn54SRQTV0wn098Y5VDU-dzyeKYBNfL14iP0LiXBRfHd4YtEaGV9SBUuVhXdhx1eF0efztCNNz0GSLS2AEPLQduVuFoUImP4s51YdO9TPeeQ3hI8aGpOdC0syxmZ7LsL0rHE1Q",
E string `json:"e"` // "AQAB" or 65537 as an int
Kid string `json:"kid"` // "1f12fa916c3a0ef585894b4b420ad17dc9d6cdf5",
// Unused fields:
// Alg string `json:"alg"` // "RS256",
// Kty string `json:"kty"` // "RSA",
// Use string `json:"use"` // "sig",
}
type wellKnown struct {
JwksURI string `json:"jwks_uri"` // "https://www.googleapis.com/service_accounts/v1/metadata/jwk/signer@confidentialspace-sign.iam.gserviceaccount.com"
// Unused fields:
// Iss string `json:"issuer"` // "https://confidentialcomputing.googleapis.com"
// Subject_types_supported string `json:"subject_types_supported"` // [ "public" ]
// Response_types_supported string `json:"response_types_supported"` // [ "id_token" ]
// Claims_supported string `json:"claims_supported"` // [ "sub", "aud", "exp", "iat", "iss", "jti", "nbf", "dbgstat", "eat_nonce", "google_service_accounts", "hwmodel", "oemid", "secboot", "submods", "swname", "swversion" ]
// Id_token_signing_alg_values_supported string `json:"id_token_signing_alg_values_supported"` // [ "RS256" ]
// Scopes_supported string `json:"scopes_supported"` // [ "openid" ]
}
func getWellKnownFile() (wellKnown, error) {
httpClient := http.Client{}
resp, err := httpClient.Get(expectedIssuer + wellKnownPath)
if err != nil {
return wellKnown{}, fmt.Errorf("failed to get raw .well-known response: %w", err)
}
wellKnownJSON, err := io.ReadAll(resp.Body)
if err != nil {
return wellKnown{}, fmt.Errorf("failed to read .well-known response: %w", err)
}
wk := wellKnown{}
json.Unmarshal(wellKnownJSON, &wk)
return wk, nil
}
func getJWKFile() (jwksFile, error) {
wk, err := getWellKnownFile()
if err != nil {
return jwksFile{}, fmt.Errorf("failed to get .well-known json: %w", err)
}
// Get JWK URI from .wellknown
uri := wk.JwksURI
fmt.Printf("jwks URI: %v\n", uri)
httpClient := http.Client{}
resp, err := httpClient.Get(uri)
if err != nil {
return jwksFile{}, fmt.Errorf("failed to get raw JWK response: %w", err)
}
jwkbytes, err := io.ReadAll(resp.Body)
if err != nil {
return jwksFile{}, fmt.Errorf("failed to read JWK body: %w", err)
}
file := jwksFile{}
err = json.Unmarshal(jwkbytes, &file)
if err != nil {
return jwksFile{}, fmt.Errorf("failed to unmarshall JWK content: %w", err)
}
return file, nil
}
// N and E are 'base64urlUInt' encoded: https://www.rfc-editor.org/rfc/rfc7518#section-6.3
func base64urlUIntDecode(s string) (*big.Int, error) {
b, err := base64.RawURLEncoding.DecodeString(s)
if err != nil {
return nil, err
}
z := new(big.Int)
z.SetBytes(b)
return z, nil
}
func getRSAPublicKeyFromJWKsFile(t *jwt.Token) (any, error) {
keysfile, err := getJWKFile()
if err != nil {
return nil, fmt.Errorf("failed to fetch the JWK file: %w", err)
}
// Multiple keys are present in this endpoint to allow for key rotation.
// This method finds the key that was used for signing to pass to the validator.
kid := t.Header["kid"]
for _, key := range keysfile.Keys {
if key.Kid != kid {
continue // Select the key used for signing
}
n, err := base64urlUIntDecode(key.N)
if err != nil {
return nil, fmt.Errorf("failed to decode key.N %w", err)
}
e, err := base64urlUIntDecode(key.E)
if err != nil {
return nil, fmt.Errorf("failed to decode key.E %w", err)
}
// The parser expects an rsa.PublicKey: https://github.com/golang-jwt/jwt/blob/main/rsa.go#L53
// or an array of keys. We chose to show passing a single key in this example as its possible
// not all validators accept multiple keys for validation.
return &rsa.PublicKey{
N: n,
E: int(e.Int64()),
}, nil
}
return nil, fmt.Errorf("failed to find key with kid '%v' from well-known endpoint", kid)
}
func decodeAndValidateToken(tokenBytes []byte, keyFunc func(t *jwt.Token) (any, error)) (*jwt.Token, error) {
var err error
fmt.Println("Unmarshalling token and checking its validity...")
token, err := jwt.NewParser().Parse(string(tokenBytes), keyFunc)
fmt.Printf("Token valid: %v\n", token.Valid)
if token.Valid {
return token, nil
}
if ve, ok := err.(*jwt.ValidationError); ok {
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
return nil, fmt.Errorf("token format invalid. Please contact the Confidential Space team for assistance")
}
if ve.Errors&(jwt.ValidationErrorNotValidYet) != 0 {
// If device time is not synchronized with the Attestation Service you may need to account for that here.
return nil, errors.New("token is not active yet")
}
if ve.Errors&(jwt.ValidationErrorExpired) != 0 {
return nil, fmt.Errorf("token is expired")
}
return nil, fmt.Errorf("unknown validation error: %v", err)
}
return nil, fmt.Errorf("couldn't handle this token or couldn't read a validation error: %v", err)
}
EOF
5)。次のコマンドを実行して、ウェブサーバーをビルドして実行します。これにより、ポート :8080 でシークレット リリース ウェブサーバーが起動します。
go mod init google.com/codelab go mod tidy go get github.com/golang-jwt/jwt/v4 go build ./codelab
トラブルシューティング: go mod tidy の実行時に次の警告が表示されることがありますが、無視してかまいません。
go: finding module for package github.com/golang-jwt/jwt/v4 go: downloading github.com/golang-jwt/jwt v3.2.2+incompatible go: downloading github.com/golang-jwt/jwt/v4 v4.5.0 go: found github.com/golang-jwt/jwt/v4 in github.com/golang-jwt/jwt/v4 v4.5.0 go: google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible: import path "google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible" should not have @version go: google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible/cmd/jwt: import path "google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible/cmd/jwt" should not have @version go: google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible/request: import path "google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible/request" should not have @version go: google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible/test: import path "google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible/test" should not have @version
6)。別の Cloud コンソール タブまたはローカル開発環境セッションを開始し、次のコマンドを実行します。これにより、cgke-attestation-codelab-web-server-internal-ip が取得されます。
gcloud compute instances describe cgke-attestation-codelab-web-server \
--format='get(networkInterfaces[0].networkIP)' \
--zone=${ZONE}
7)。CGKE ワークロードに接続し、リモート構成証明を起動して構成証明トークン(OIDC トークン)を取得します。次に、attestation-token と cgke-attestation-codelab-web-server-internal-ip の内容を次のコマンドに埋め込みます。これにより、シークレット リリース ウェブサーバーが保持するシークレットが取得されます。
kubectl exec -it go-tpm-demo -- /bin/bash ./gotpm token --event-log=/run/cc-device-plugin/binary_bios_measurements > attestation_token curl http://<cgke-attestation-codelab-web-server-internal-ip>:8080 -H "Authorization: Bearer $(cat ./attestation_token)"
次のように置き換えます。
cgke-attestation-codelab-web-server-internal-ipは、cgke-attestation-codelab-web-serverVM インスタンスの内部 IP です。
7. CGKE ノードでの vTPM シーリング
このステップでは、CGKE ノードで vTPM オーナー認証を設定し、vTPM オーナー パスフレーズを使用してワークロードをデプロイします。その後、vTPM シーリング機能を使用して、ワークロード内のデータをシールおよびシール解除するための vTPM プライマリ鍵を作成します。
1)。CGKE ノードで vTPM オーナー認証を設定します。
- 1 回限りのジョブ コンテナ イメージを作成します。1 回限りのジョブで、すべての vTPM のオーナー パスワードを設定します。コンテナ イメージを作成する
Dockerfileは次のとおりです。
cat << 'EOF' > Dockerfile FROM debian:latest RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections RUN apt-get update RUN apt -y install \ autoconf-archive \ libcmocka0 \ libcmocka-dev \ net-tools \ build-essential \ git \ pkg-config \ gcc \ g++ \ m4 \ libtool \ automake \ libgcrypt20-dev \ libssl-dev \ uthash-dev \ autoconf \ uuid-dev \ libcurl4-openssl-dev \ libjson-c-dev RUN mkdir /src WORKDIR /src RUN git clone https://github.com/tpm2-software/tpm2-tss WORKDIR /src/tpm2-tss RUN ./bootstrap RUN ./configure --prefix=/usr/local RUN make all install WORKDIR /src RUN git clone https://github.com/tpm2-software/tpm2-tools WORKDIR /src/tpm2-tools RUN apt-get -y install libcurl4 libcurl4-openssl-dev pandoc man-db RUN ./bootstrap RUN ./configure --prefix=/usr/local RUN make all install RUN apt-get -y install vim ENTRYPOINT ["/bin/bash"] EOF
- 1 回限りのジョブ コンテナ イメージをビルドして Artifact Registry に push します。
docker build -t us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/tpm-tools:latest .
docker push us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/tpm-tools:latest
- Kubernetes ジョブを使用して 1 回限りのジョブを実行します。(警告: このジョブは各 CVM の vTPM をクリアします。CVM が vTPM を使用してディスクを暗号化している場合、このジョブを実行すると、再起動後に CVM が使用できなくなります。
lsblk -fコマンドで、ディスクの FSTYPE がcrypto_LUKSかどうかを確認できます。
cat << EOF > tpm-tools-task.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: tpm-tools-task
spec:
template:
spec:
nodeSelector:
cloud.google.com/machine-family: "n2d"
cloud.google.com/gke-confidential-nodes-instance-type: "SEV"
containers:
- name: tpm-tools
image: us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/tpm-tools:latest
command: ["/bin/sh", "-c"]
args: ["tpm2_clear; tpm2_changeauth -c owner this_is_passphrase"]
resources:
limits:
google.com/cc: 1
restartPolicy: Never
EOF
ヒント: tpm-tools-task ジョブを再実行する必要がある場合は、まず既存のジョブを削除してください。
kubectl delete job tpm-tools-task
- 1 回限りのジョブを起動します。このジョブは、すべてのワーカーノードに vTPM 所有者のパスフレーズを設定します。
kubectl apply -f tpm-tools-task.yaml
2)。vTPM 所有者のパスフレーズを保持する Kubernetes Secret を作成します。
kubectl create secret generic tpm-secret --from-literal=passphrase='this_is_passphrase'
3)。デモ アプリケーション コンテナを作成し、パスフレーズを渡します。デモ アプリケーション コンテナには、vTPM とやり取りするための tpm2-tools が含まれています。
- デモ アプリケーション コンテナの Deployment YAML ファイルを作成します。
cat << EOF > deploy_demo.yaml
apiVersion: v1
kind: Pod
metadata:
name: tpm-tools-demo
labels:
app.kubernetes.io/name: tpm-tools-demo
spec:
nodeSelector:
cloud.google.com/machine-family: "n2d"
cloud.google.com/gke-confidential-nodes-instance-type: "SEV"
containers:
- name: tpm-tools
image: us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/tpm-tools:latest
command: ["tail", "-f", "/dev/null"]
resources:
limits:
google.com/cc: 1
volumeMounts:
- name: secret-volume
mountPath: "/etc/tpmsecret"
readOnly: true
volumes:
- name: secret-volume
secret:
secretName: tpm-secret
EOF
- デモ アプリケーションをデプロイします。
kubectl apply -f deploy_demo.yaml
4)。デモ アプリケーション コンテナで vTPM シーリングを実行します。
- デモ アプリケーション コンテナに接続し、パスフレーズでプライマリキーを設定します。
kubectl exec -it tpm-tools-demo -- /bin/bash tpm2_createprimary -C o -c primary.ctx -P $(cat /etc/tpmsecret/passphrase)
tpm2_createprimary は vTPM と連携して、指定された階層とテンプレートに基づいてプライマリ オブジェクトを生成します。
- -C o: プライマリ キーが TPM のオーナー階層の下に作成されることを示します。
- -c
primary.ctx: 作成されたプライマリ オブジェクトのコンテキスト(ハンドルと関連データ)をファイルprimary.ctxに保存します。このコンテキストは、後続のオペレーションに不可欠です。
ワークロードは、間違ったオーナー パスフレーズを使用して主キーを作成できません。
tpm2_createprimary -C o -P wrong_passphrase
このコマンドは次のエラーを返します。
WARNING:esys:src/tss2-esys/api/Esys_CreatePrimary.c:401:Esys_CreatePrimary_Finish() Received TPM Error ERROR:esys:src/tss2-esys/api/Esys_CreatePrimary.c:135:Esys_CreatePrimary() Esys Finish ErrorCode (0x000009a2) ERROR: Esys_CreatePrimary(0x9A2) - tpm:session(1):authorization failure without DA implications ERROR: Unable to run tpm2_createprimary
- 作成されたプライマリキーは、データのシールとシール解除に使用できます。
echo "This is my secret message" > secret.txt tpm2_create -C primary.ctx -u sealed.pub -r sealed.priv -i secret.txt tpm2_load -C primary.ctx -u sealed.pub -r sealed.priv -c sealed.ctx tpm2_unseal -c sealed.ctx -o unsealed.txt cat unsealed.txt
tpm2_create は vTPM と連携して、必要な暗号オブジェクトを生成します。
- -C
primary.ctx: 先ほど作成した主キー コンテキストを使用します。 - -u
sealed.pub:sealed.pubにシーリング鍵の公開部分(アンシーリングに必要)を保存します。 - -r
sealed.priv: シーリング鍵の秘密部分をsealed.privに保存します。 - -i
secret.txt: シールするシークレットを含むファイル。
tpm2_load: 公開部分と非公開部分(sealed.pub、sealed.priv)を使用してシーリングキーを TPM に読み込み、そのコンテキストを sealed.ctx に保存します。
tpm2_unseal: vTPM シーリング オブジェクトを使用して以前に暗号化(シール)されたデータを復号(シール解除)します。
cat unsealed.txt: 封印されていないシークレット メッセージを表示して、プロセスが成功したことを確認します。
primary.ctx ファイルと sealed.priv ファイルは、1 つの vTPM デバイスでのみ使用できます。また、vTPM デバイスとこれらのファイルにアクセスできるユーザーは、封印されたデータにアクセスできます。PCR 値のポリシーを使用してデータを封印することもできますが、この Codelab の範囲外です。
8. ハードウェア認証を実行するワークロードのデプロイ: Intel TDX と AMD SEV-SNP

このステップでは、インテル TDX または AMD SEV-SNP ベースのワークロードを作成し、Confidential GKE Node でハードウェア レベルの認証を行います。上記のアーキテクチャ図に示すように、ゲスト デバイス プラグインを使用して、Intel TDX アーキテクチャと AMD SEV-SNP アーキテクチャの構成証明引用符を取得します。Cloud コンソールまたはローカル開発環境に移動して、コマンドを実行します。
テストする Confidential Computing アーキテクチャ(インテル TDX または AMD SEV-SNP)を選択します。
1)。Intel TDX ノードプールを追加する
gcloud container node-pools create tdx-pool \
--cluster=${CLUSTER_NAME} \
--zone=${ZONE} \
--machine-type=c3-standard-4 \
--num-nodes=1 \
--enable-confidential-nodes \
--confidential-node-type=tdx
2)。AMD SEV-SNP ノードプールを追加する
gcloud container node-pools create snp-pool \
--cluster=${CLUSTER_NAME} \
--zone=${ZONE} \
--machine-type=n2d-standard-2 \
--num-nodes=1 \
--enable-confidential-nodes \
--confidential-node-type=sev_snp \
--min-cpu-platform="AMD Milan"
3)。デフォルトのノードプールを削除する(省略可。コストとノイズを削減するためにおすすめ)
gcloud container node-pools delete default-pool \
--cluster=${CLUSTER_NAME} \
--zone=${ZONE} \
--quiet
(注: vTPM セクションをすでに完了している場合、このプールはすでに削除されています。ここで「見つかりません」というエラーが表示されても、無視してかまいません)。
4)。デバイス プラグインを起動して、CGKE クラスタがハードウェア構成証明ゲスト デバイスをワークロードに公開できるようにします。Kubernetes デバイス プラグインを使用して、新しいリソース(intel.com/tdx と amd.com/sev-snp)を作成します。これらのリソースに関連付けられたワークロードは、ワーカーノード上のゲスト デバイスにアクセスできます。
gcloud container clusters get-credentials ${CLUSTER_NAME} --zone ${ZONE} --project ${PROJECT_ID}
kubectl apply -f https://raw.githubusercontent.com/google/cc-device-plugin/main/manifests/cc-device-plugin.yaml
次のコマンドを使用すると、デプロイされた cc-device-plugin を確認できます。
kubectl get pods -A | grep "cc-device-plugin"
注: 混合モードの GKE クラスタ(Confidential GKE ワーカーノードと非 Confidential GKE ワーカーノードの両方を含む)の場合、オペレーターは cc-device-plugin を Confidential GKE ワーカーノードにのみデプロイすることをおすすめします。
9. ハードウェア認証: Intel TDX と AMD SEV-SNP
このセクションでは、ゲスト デバイス プラグインを使用して Intel TDX アーキテクチャと AMD SEV-SNP アーキテクチャの認証引用符を取得し、Confidential GKE Node 内でハードウェア レベルの認証を行う方法について説明します。
1)。それぞれのゲスト デバイスからハードウェア構成証明引用を取得する Go アプリケーションを作成します。
cat << 'EOF' > main.go
package main
import (
"crypto/rand"
"log"
"os"
"strconv"
"time"
"unsafe"
sevclient "github.com/google/go-sev-guest/client"
"golang.org/x/sys/unix"
)
// TDX Linux ABI Definitions
const (
iocTdxGetReport = 0xc4405401
)
type tdxReportReq struct {
ReportData [64]byte
TdReport [1024]byte
}
func main() {
ccType := os.Getenv("CC_TYPE") // Expected "tdx" or "snp"
iterationsStr := os.Getenv("ITERATIONS")
iterations := 100
if val, err := strconv.Atoi(iterationsStr); err == nil && val > 0 {
iterations = val
}
successCount := 0
failureCount := 0
startTime := time.Now()
log.Printf("Starting Attestation test for CC_TYPE: %s (iterations: %d)", ccType, iterations)
if ccType == "tdx" {
devicePath := "/dev/tdx_guest"
file, err := os.OpenFile(devicePath, os.O_RDWR|os.O_SYNC, 0)
if err != nil {
log.Fatalf("FATAL: Failed to open %s: %v", devicePath, err)
}
defer file.Close()
for i := 1; i <= iterations; i++ {
var reportData [64]byte
rand.Read(reportData[:])
req := tdxReportReq{ReportData: reportData}
_, _, errno := unix.Syscall(unix.SYS_IOCTL, file.Fd(), uintptr(iocTdxGetReport), uintptr(unsafe.Pointer(&req)))
if errno != 0 || (req.TdReport[0] == 0 && req.TdReport[10] == 0) {
failureCount++
} else {
successCount++
}
}
} else if ccType == "snp" {
device, err := sevclient.OpenDevice()
if err != nil {
log.Fatalf("FATAL: Failed to open SEV-SNP device: %v", err)
}
defer device.Close()
for i := 1; i <= iterations; i++ {
var reportData [64]byte
rand.Read(reportData[:])
report, err := sevclient.GetRawReport(device, reportData)
if err != nil || len(report) == 0 {
failureCount++
} else {
successCount++
}
}
}
elapsed := time.Since(startTime)
log.Printf("=== Test Completed ===")
log.Printf("Total Iterations: %d", iterations)
log.Printf("Success: %d, Failures: %d", successCount, failureCount)
log.Printf("Total Time: %s", elapsed)
}
EOF
2)。ハードウェア構成証明テストツールの Dockerfile を作成します。
cat << 'EOF' > Dockerfile FROM golang:1.26.4 AS builder WORKDIR /app COPY main.go . RUN go mod init cc-attestation RUN go mod tidy RUN CGO_ENABLED=0 GOOS=linux go build -o /cc-attestation-check main.go FROM ubuntu:22.04 COPY --from=builder /cc-attestation-check /usr/local/bin/ CMD ["/usr/local/bin/cc-attestation-check"] EOF
3)。イメージをビルドして Artifact Registry に push します。
docker build -t us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/cc-attestation-check:latest .
docker push us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/cc-attestation-check:latest
4)。インテル TDX と AMD SEV-SNP の両方のワークロードのアプリケーション デプロイ YAML を作成して、ハードウェア デバイスを公開します。
- インテル TDX ワークロードの場合、次の Deployment YAML を作成します。
cat << EOF > deploy-tdx.yaml
apiVersion: v1
kind: Pod
metadata:
name: tdx-attestation-pod
spec:
restartPolicy: Never
containers:
- name: tdx-container
image: us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/cc-attestation-check:latest
env:
- name: CC_TYPE
value: "tdx"
- name: ITERATIONS
value: "1000"
resources:
limits:
intel.com/tdx: 1
nodeSelector:
cloud.google.com/gke-confidential-nodes-instance-type: "TDX"
cloud.google.com/machine-family: "c3"
EOF
- AMD SEV-SNP ワークロードの場合、次の Deployment YAML を作成します。
cat << EOF > deploy-snp.yaml
apiVersion: v1
kind: Pod
metadata:
name: snp-attestation-pod
spec:
restartPolicy: Never
containers:
- name: snp-container
image: us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/cc-attestation-check:latest
env:
- name: CC_TYPE
value: "snp"
- name: ITERATIONS
value: "100"
resources:
limits:
amd.com/sev-snp: 1
nodeSelector:
cloud.google.com/gke-confidential-nodes-instance-type: "SEV_SNP"
cloud.google.com/machine-family: "n2d"
EOF
5)。Deployment を CGKE クラスタに適用します。
kubectl apply -f deploy-tdx.yaml kubectl apply -f deploy-snp.yaml
6)。Pod が完了するまで待って、ハードウェア証明書ログを確認します。
まず、Pod のステータスを調べて、Pod が正常にスケジュール設定され、実行されていることを確認します([完了] または [実行中] と表示されるまで待ちます)。
kubectl get pods -w
Pod の準備ができたら、ログを調べて、ハードウェア構成証明引用符が正常に取得されたことを確認します。
# View the TDX attestation logs kubectl logs tdx-attestation-pod # View the SEV-SNP attestation logs kubectl logs snp-attestation-pod
(合計実行時間、合計反復回数、成功/失敗のカウントがログに記録されます)。
10. クリーンアップ
Google Cloud アカウントに継続的に課金されないようにするには、この Codelab で作成したリソースを削除することが重要です。
Cloud Shell またはローカル開発環境で次のコマンドを実行します。
1)。環境変数を設定します。
export PROJECT_ID="your-project-id"
export ZONE="us-central1-c"
export CLUSTER_NAME="cgke-attestation-codelab"
gcloud config set project ${PROJECT_ID}
2)。GKE リソースを削除する
gcloud container clusters delete ${CLUSTER_NAME} \
--zone=${ZONE} \
--quiet
3)。Artifact Registry を削除する
gcloud artifacts repositories delete codelab-repo \
--location=us \
--quiet
4)。ウェブサーバー VM を削除する
gcloud compute instances delete cgke-attestation-codelab-web-server \
--zone=${ZONE}
5)。IAM リソースを削除する
# Delete the GCP service account
gcloud iam service-accounts delete codelab-csa@${PROJECT_ID}.iam.gserviceaccount.com \
--quiet
# Delete the custom IAM role
gcloud iam roles delete Confidential_Computing_Workload_User \
--project=${PROJECT_ID} \
--quiet
11. 次のステップ
Confidential GKE Node の詳細を確認する。