机密 GKE 节点上的远程证明和密封 (vTPM、SEV-SNP、TDX)

1. 概览

Confidential GKE (CGKE) 节点可确保工作负载中的数据在使用中加密。将 vTPM 设备公开给 CGKE 工作负载,可让工作负载使用 vTPM 功能。在此 Codelab 中,您将了解 vTPM 的功能,以及如何使用 cc-device-plugin 来利用 Intel TDX 和 AMD SEV-SNP 硬件证明。

  • 通过 vTPM 远程证明,远程方可以验证托管工作负载的 CGKE 节点是否在机密虚拟机 (CVM) 上运行。
  • vTPM 授权和 vTPM 密封。
  • 使用公开的 Intel TDX 和 AMD SEV-SNP 客户机设备进行硬件证明。

683a3b43587ef69f.png

如上图所示,本 Codelab 的第一部分包含以下步骤:

  • CGKE 节点设置并向所选工作负载公开 vTPM 设备。
  • 部署工作负载并远程证明托管该工作负载的 CGKE 节点。
  • 部署工作负载以直接从 TDX 或 SEV-SNP 设备获取硬件证明引用。
  • Secret Release Web 服务器设置。

8f6e80c762a5d911.png

如上图所示,此 Codelab 的第二部分包含以下内容:

  • CGKE 节点上的 vTPM 授权设置和 vTPM 密封。

cc_device_plugin_concept.png

如上图所示,此 Codelab 的第三部分包含:

  • 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 (Codelab)、机密虚拟机机密 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 控制台或本地开发环境中运行以下命令:

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 工作器节点和非机密 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)即可。将部署应用于 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 Web 服务器

在此步骤中,您将退出之前的 SSH 会话,并设置另一个虚拟机。在此虚拟机上,您将设置一个秘密发布 Web 服务器。Web 服务器验证收到的证明令牌及其声明。如果验证成功,则将密钥传递给请求者。

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 连接到新虚拟机。

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)即可。创建以下两个文件,用于存储密 Secret 发布网络服务器的源代码。

创建 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)。运行以下命令以构建并运行 Web 服务器。此命令会在端口 :8080 上启动密文发布 Web 服务器。

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 的内容嵌入到以下命令中。这会提取由 Secret Release Web 服务器保存的 Secret!

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 虚拟机实例的内部 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. 启动一次性作业。此作业会在所有工作器节点上设置 vTPM 所有者口令。
kubectl apply -f tpm-tools-task.yaml

2). 创建一个 Kubernetes Secret 来保存 vTPM 所有者口令。

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

3). 创建演示应用容器并将口令传递给该容器。演示应用容器包含用于与 vTPM 交互的 tpm2-tools

  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:显示未封装的 Secret 消息,以确认流程是否成功。

请注意,primary.ctxsealed.priv 文件只能在一个 vTPM 设备上使用。任何有权访问 vTPM 设备和这些文件的人都可以访问密封数据。您可以进一步使用 PCR 值上的政策来密封数据,但这超出了此 Codelab 的范围。

8. 部署工作负载以执行硬件证明:Intel TDX 和 AMD SEV-SNP

cc_device_plugin_arch.png

在此步骤中,您将创建基于 Intel TDX 或 AMD SEV-SNP 的工作负载,并对机密 GKE 节点执行硬件级证明。如上方的架构图所示,我们将使用 guest 设备插件来获取 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)。与这些资源关联的任何工作负载都将能够访问工作器节点上的 guest 设备。

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 工作器节点和非机密 GKE 工作器节点),建议操作员仅将 cc-device-plugin 部署到机密 GKE 工作器节点。

9. 硬件证明:Intel TDX 和 AMD SEV-SNP

在本部分中,您将学习如何通过使用 guest 设备插件获取 Intel TDX 和 AMD SEV-SNP 架构的证明报价,在机密 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 的状态,确保它们已成功调度并正在运行(等待直到它们显示“已完成”或“正在运行”):

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)即可。删除 Web 服务器虚拟机

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 节点