Confidential GKE Node でのリモート認証とシーリング(vTPM、SEV-SNP、TDX)

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 ゲスト デバイスを使用したハードウェア認証。

683a3b43587ef69f.png

上の図に示すように、この Codelab の最初の部分には次の手順が含まれています。

  • CGKE ノードは、vTPM デバイスを設定して、選択したワークロードに公開します。
  • ワークロードをデプロイし、ワークロードをホストする CGKE ノードをリモートで証明します。
  • ワークロードをデプロイして、TDX または SEV-SNP デバイスからハードウェア構成証明引用符を直接取得します。
  • Secret Release Web Server の設定。

8f6e80c762a5d911.png

上の図に示すように、この Codelab の第 2 部には次のものが含まれています。

  • CGKE ノードでの vTPM 認可の設定と vTPM シーリング。

cc_device_plugin_concept.png

上の図に示すように、この 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-guestgo-sev-guest を使用してハードウェア構成証明引用を取得する方法。

必要なもの

  • Google Cloud Platform プロジェクト
  • ブラウザ(Chrome、Firefox など)
  • Google Compute Engine(codelab)、Confidential VMConfidential GKE NodeArtifact 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 リポジトリの準備が整いました。方針の選択:

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 トークン)を取得できます。

  1. アプリケーション コンテナ イメージの 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
  1. アプリケーション コンテナ イメージを 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 サービス アカウントを設定します。

  1. Kubernetes サービス アカウント codelab-ksa を作成します。
kubectl create serviceaccount codelab-ksa \
    --namespace default
  1. ロール 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
  1. 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]"
  1. 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-tokencgke-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-server VM インスタンスの内部 IP です。

7. CGKE ノードでの vTPM シーリング

このステップでは、CGKE ノードで vTPM オーナー認証を設定し、vTPM オーナー パスフレーズを使用してワークロードをデプロイします。その後、vTPM シーリング機能を使用して、ワークロード内のデータをシールおよびシール解除するための vTPM プライマリ鍵を作成します。

1)。CGKE ノードで vTPM オーナー認証を設定します。

  1. 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. 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
  1. 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. 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 が含まれています。

  1. デモ アプリケーション コンテナの 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
  1. デモ アプリケーションをデプロイします。
kubectl apply -f deploy_demo.yaml

4)。デモ アプリケーション コンテナで vTPM シーリングを実行します。

  1. デモ アプリケーション コンテナに接続し、パスフレーズでプライマリキーを設定します。
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
  1. 作成されたプライマリキーは、データのシールとシール解除に使用できます。
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.pubsealed.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

cc_device_plugin_arch.png

このステップでは、インテル 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/tdxamd.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 の詳細を確認する。