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 客户机设备进行硬件证明。

如上图所示,本 Codelab 的第一部分包含以下步骤:
- CGKE 节点设置并向所选工作负载公开 vTPM 设备。
- 部署工作负载并远程证明托管该工作负载的 CGKE 节点。
- 部署工作负载以直接从 TDX 或 SEV-SNP 设备获取硬件证明引用。
- Secret Release Web 服务器设置。

如上图所示,此 Codelab 的第二部分包含以下内容:
- CGKE 节点上的 vTPM 授权设置和 vTPM 密封。

如上图所示,此 Codelab 的第三部分包含:
cc-device-plugin在设置阶段如何安全地映射硬件设备。- 在运行时阶段部署工作负载,以直接从 TDX 或 SEV-SNP 设备获取硬件证明引用,且零开销。
学习内容
- 如何向 CGKE 工作负载公开 vTPM 设备。
- 如何在 CGKE 工作负载上通过机密计算 API(证明验证器服务)进行远程证明。
- 如何设置 vTPM 授权并执行 vTPM 密封。
- 如何使用
cc-device-plugin在 CGKE 工作负载中访问 SEV-SNP 和 TDX 设备。 - 如何使用 go-tdx-guest 和 go-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 代码库现已准备就绪。选择适合自己的学习方向:
- 选项 1:vTPM 远程证明和密封 (AMD SEV) - 直接继续执行设置 CGKE 节点并公开 vTPM 设备。
- 方案 2:硬件证明(Intel 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 集群(同时包含机密 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 令牌)。
- 为应用容器映像创建
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。
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 资源的权限。
- 创建 Kubernetes 服务账号
codelab-ksa。
kubectl create serviceaccount codelab-ksa \
--namespace default
- 创建角色
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
- 创建 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]"
- 将 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-token 和 cgke-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-ip是cgke-attestation-codelab-web-server虚拟机实例的内部 IP。
7. CGKE 节点上的 vTPM 密封
在此步骤中,您将在 CGKE 节点上设置 vTPM 所有者授权,并部署具有 vTPM 所有者口令的工作负载。之后,您将创建一个 vTPM 主密钥,以使用 vTPM 密封功能密封和解封工作负载中的数据。
1)。在 CGKE 节点上设置 vTPM 所有者授权。
- 创建一次性作业容器映像。一次性作业会为所有 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
- 构建一次性作业容器映像并将其推送到 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
- 通过 Kubernetes 作业执行一次性作业。(警告:此作业会清除每个 CVM 上的 vTPM。如果您的 CVM 使用 vTPM 加密磁盘,此作业会导致您的 CVM 在重启后无法使用。您可以使用
lsblk -f命令检查磁盘是否具有 FSTYPEcrypto_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
- 启动一次性作业。此作业会在所有工作器节点上设置 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。
- 为演示应用容器创建部署 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:显示未封装的 Secret 消息,以确认流程是否成功。
请注意,primary.ctx、sealed.priv 文件只能在一个 vTPM 设备上使用。任何有权访问 vTPM 设备和这些文件的人都可以访问密封数据。您可以进一步使用 PCR 值上的政策来密封数据,但这超出了此 Codelab 的范围。
8. 部署工作负载以执行硬件证明:Intel TDX 和 AMD SEV-SNP

在此步骤中,您将创建基于 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/tdx 和 amd.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 节点。