機密 GKE 節點的遠端認證和密封功能 (vTPM、SEV-SNP、TDX)

1. 總覽

Confidential GKE (CGKE) 節點可確保工作負載中的資料在使用時經過加密。將 vTPM 裝置公開給 CGKE 工作負載,可讓工作負載使用 vTPM 功能。在本程式碼研究室中,您將瞭解 vTPM 的功能,以及如何使用 cc-device-plugin 運用 Intel TDX 和 AMD SEV-SNP 硬體驗證。

  • 透過 vTPM 遠端認證,遠端人員可以驗證代管工作負載的 CGKE 節點是否在機密虛擬機器 (CVM) 上執行。
  • vTPM 授權和 vTPM 封存。
  • 使用公開的 Intel TDX 和 AMD SEV-SNP 客戶端裝置進行硬體認證。

683a3b43587ef69f.png

如上圖所示,本程式碼研究室的第一部分包含下列步驟:

  • 設定 CGKE 節點,並向所選工作負載公開 vTPM 裝置。
  • 部署工作負載,並遠端驗證代管工作負載的 CGKE 節點。
  • 部署工作負載,直接從 TDX 或 SEV-SNP 裝置擷取硬體驗證報價。
  • 設定 Secret Release 網路伺服器。

8f6e80c762a5d911.png

如上圖所示,本程式碼研究室的第二部分包含:

  • 在 CGKE 節點上設定 vTPM 授權和 vTPM 密封功能。

cc_device_plugin_concept.png

如上圖所示,本程式碼研究室的第三部分包含:

  • cc-device-plugin 如何在設定階段安全地對應硬體裝置。
  • 在執行階段將工作負載部署至 TDX 或 SEV-SNP 裝置,直接擷取硬體認證引文,完全不會產生額外負擔。

課程內容

  • 如何向 CGKE 工作負載公開 vTPM 裝置。
  • 如何透過 CGKE 工作負載上的 機密運算 API (認證驗證器服務) 進行遠端認證。
  • 如何設定 vTPM 授權,以及執行 vTPM 封存。
  • 如何使用 cc-device-plugin 存取 CGKE 工作負載中的 SEV-SNP 和 TDX 裝置。
  • 如何使用 go-tdx-guestgo-sev-guest 擷取硬體驗證引文。

軟硬體需求

  • Google Cloud Platform 專案
  • 瀏覽器,例如 Chrome 或 Firefox
  • Google Compute Engine (程式碼研究室)、機密 VM機密 GKE 節點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 Shell 或本機開發環境中執行下列指令:

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 叢集 (同時包含機密和非機密 GKE 工作站節點),建議運算子只將 cc-device-plugin 部署到機密 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。應用程式容器映像檔包含 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。
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)。設定 Kubernetes 服務帳戶,在 GCP 資源上沿用 GCP 服務帳戶的權限。

  1. 建立 Kubernetes 服務帳戶 codelab-ksa
kubectl create serviceaccount codelab-ksa \
    --namespace default
  1. 建立角色 Confidential_Computing_Workload_User,並授予該角色存取機密運算 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 才能取得存取機密運算 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 才能取得存取機密運算 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. 設定 Secret Release 網路伺服器

在這個步驟中,您會結束先前的 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)。透過 SSH 連線至新的 VM。

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)。建立下列兩個檔案,儲存密鑰發布網路伺服器的原始碼。

建立 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 Shell 分頁或本機開發環境工作階段,然後執行下列指令。這樣就能取得 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-ipcgke-attestation-codelab-web-server VM 執行個體的內部 IP。

7. CGKE 節點的 vTPM 密封功能

在這個步驟中,您會在 CGKE 節點上設定 vTPM 擁有者授權,並使用 vTPM 擁有者密碼片語部署工作負載。接著,您會建立 vTPM 主金鑰,透過 vTPM 封存功能封存及解除封存工作負載中的資料。

1)。在 CGKE 節點上設定 vTPM 擁有者授權。

  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。
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 工作執行一次性工作。(警告:這項工作會清除每個 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. 啟動一次性工作。這項工作會在所有 worker 節點上設定 vTPM 擁有者通關密語。
kubectl apply -f tpm-tools-task.yaml

2)。建立 Kubernetes 密鑰,保存 vTPM 擁有者通關密語。

kubectl create secret generic tpm-secret --from-literal=passphrase='this_is_passphrase'

3). 建立示範應用程式容器,並將通關密語傳遞至該容器。這個範例應用程式容器包含 tpm2-tools,可與 vTPM 互動。

  1. 為範例應用程式容器建立部署作業 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.ctxsealed.priv 檔案只能用於一部 vTPM 裝置。只要有權存取 vTPM 裝置和這些檔案,就能存取密封資料。您可以進一步使用 PCR 值政策密封資料,但這不在本程式碼研究室的範圍內。

8. 部署工作負載以執行硬體驗證:Intel TDX 和 AMD SEV-SNP

cc_device_plugin_arch.png

在這個步驟中,您會建立以 Intel TDX 或 AMD SEV-SNP 為基礎的工作負載,並在機密 GKE 節點上執行硬體層級的認證。如上方的架構圖所示,我們會使用訪客裝置外掛程式,擷取 Intel TDX 和 AMD SEV-SNP 架構的認證引文。前往 Cloud 控制台或本機開發環境,執行指令。

挑選要測試的機密運算架構,例如 Intel 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)。與這些資源相關聯的任何工作負載,都能存取 worker 節點上的訪客裝置。

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 叢集 (同時包含機密和非機密 GKE 工作站節點),建議運算子只將 cc-device-plugin 部署至機密 GKE 工作站節點。

9. 硬體驗證:Intel TDX 和 AMD SEV-SNP

在本節中,您將瞭解如何使用客層裝置外掛程式,擷取 Intel TDX 和 AMD SEV-SNP 架構的驗證引號,在 Confidential GKE 節點內執行硬體層級的驗證。

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:

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)。為 Intel TDX 和 AMD SEV-SNP 工作負載建立應用程式部署作業 YAML,以公開硬體裝置。

  • 如為 Intel TDX 工作負載,請建立下列部署作業 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 工作負載,請建立下列部署 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)。將部署項目套用至 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 帳戶收費,請務必刪除本程式碼研究室建立的資源。

在 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. 後續步驟

進一步瞭解機密 GKE 節點