机密 GKE 节点上的 vTPM 远程证明和密封

关于此 Codelab
schedule42 分钟
subject上次更新时间:2024年5月19日
account_circleRuide Zhang 编写

机密 GKE (CGKE) 节点可确保工作负载中的数据在使用过程中加密。将 vTPM 设备公开给 CGKE 工作负载可让工作负载使用 vTPM 功能。在此 Codelab 中,我们将向您介绍 vTPM 的两项功能。

  • 通过 vTPM 远程证明,远程方可以验证托管工作负载的 CGKE 节点是否在机密虚拟机 (CVM) 上运行。
  • vTPM 授权和 vTPM 密封。

683a3b43587ef69f

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

  • CGKE 节点会设置 vTPM 设备并将其公开给选定的工作负载。
  • 部署工作负载并远程证明托管工作负载的 CGKE 节点。
  • Secret 发布 Web 服务器设置。

“8f6e80c762a5d911”

如上图所示,此 Codelab 的第二部分包括:

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

学习内容

  • 如何向 CGKE 工作负载公开 vTPM 设备。
  • 如何通过 Confidential Computing API(证明验证程序服务)对 CGKE 工作负载进行远程证明。
  • 如何设置 vTPM 授权并执行 vTPM 密封。

所需条件

2. 设置和要求:

如需启用必要的 API,请在 Cloud 控制台或本地开发环境中运行以下命令:

gcloud auth login

gcloud services enable \
    cloudapis.googleapis.com \
    cloudshell.googleapis.com \
    container.googleapis.com \
    containerregistry.googleapis.com \
    confidentialcomputing.googleapis.com \
    iamcredentials.googleapis.com \
    compute.googleapis.com

3. 设置 CGKE 节点并向所选工作负载公开 vTPM 设备

此步骤是此 Codelab 的第一部分。在此步骤中,您将启动 CGKE 集群并应用设备插件,以将 CVM vTPM 设备公开给工作负载。转到 Cloud 控制台或本地开发环境以运行命令。

1)。创建 CGKE 集群,使用工作负载身份池以允许 CGKE 工作负载使用 GCP 机密计算 API。工作负载身份池是必需的,因为 CGKE 工作负载需要访问 GCP 资源。为访问 GCP 资源,CGKE 工作负载需要拥有身份。

gcloud container clusters create cgke-attestation-codelab \
    --machine-type=n2d-standard-2        \
    --enable-confidential-nodes \
--zone us-central1-c \
--workload-pool=${PROJECT_ID}.svc.id.goog \
--workload-metadata=GKE_METADATA

替换以下内容:

  • project-id 是项目的唯一标识符。

2)。启动设备插件,以允许 CGKE 集群向工作负载公开 vTPM 设备。我们使用 kubernetes 设备插件来创建新资源 google.com/cc。与新资源关联的任何工作负载都将能够看到工作器节点上的 vTPM 设备。

gcloud container clusters get-credentials cgke-attestation-codelab --zone us-central1-c --project ${PROJECT_ID}

kubectl create -f https://raw.githubusercontent.com/google/cc-device-plugin/main/manifests/cc-device-plugin.yaml

替换以下内容:

  • project-id 是项目的唯一标识符。

您可以使用以下命令查看已部署的 cc-device-plugin。

kubectl get pods -A | grep "cc-device-plugin"

注意:如果是混合模式 GKE 集群(同时包含机密和非机密 GKE 工作器节点),建议运营商仅在机密 GKE 工作器节点上部署 cc-device-plugin。

(可选)应用 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}])

4. 部署工作负载并对工作负载执行远程证明

在此步骤中,您将创建工作负载并将其部署到在上一步中创建的 CGKE 集群,并执行 vTPM 远程证明以检索工作器节点上的认证令牌(OIDC 令牌)。

1)。创建应用容器映像并将其推送到 Artifact Registry。应用容器映像包含 go-tpm 工具,该工具可以收集证明证据并将其发送到证明验证程序服务以获取证明令牌(OIDC 令牌)。

  1. 为应用容器映像创建 Dockerfile。

Dockerfile

FROM golang:1.21.0 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
RUN rm -rf /etc/apt/sources.list.d
COPY --from=builder /gotpm /gotpm
CMD ["tail", "-f", "/dev/null"]
  1. 创建 Artifact Registry。
gcloud artifacts repositories create codelab-repo \
    --repository-format=docker \
    --location=us
  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

替换以下内容:

  • project-id 是项目的唯一标识符。
  1. 创建 GCP 服务账号 codelab-csa,并将其与 Confidential_Computing_Workload_User. So that 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]"

替换以下内容:

  • project-id 是项目的唯一标识符。
  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

替换以下内容:

  • project-id 是项目的唯一标识符。

3)。为演示应用创建应用部署 yaml。将 Kubernetes 服务账号 codelab-ksa 分配给选定的工作负载。

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"
  containers:
  - name: go-tpm
    image: us-docker.pkg.dev/<project-id>/codelab-repo/go-tpm:latest
    resources:
      limits:
        google.com/cc: 1

替换以下内容:

  • project-id 是项目的唯一标识符。

4)即可。将部署应用于 CGKE 集群。

kubectl create -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

您可以在 jwt.io 中解码认证令牌以查看声明!

5. 设置 Secret 发布 Web 服务器

在此步骤中,您将退出之前的 SSH 会话并设置另一个虚拟机。在此虚拟机上,您将设置一个 Secret 发布 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=us-central1-c \
    --image=ubuntu-2204-jammy-v20240228 \
    --image-project=ubuntu-os-cloud

替换以下内容:

  • project-id 是项目的唯一标识符。

2)。通过 SSH 连接到新虚拟机。

gcloud compute ssh --zone us-central1-c cgke-attestation-codelab-web-server

3)。设置 Go 环境。

wget https://go.dev/dl/go1.22.0.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.22.0.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin

4)即可。创建以下两个文件,这些文件用于存储 Secret 发布 Web 服务器的源代码(使用 nano 复制粘贴)。

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)
        // e.g. "sub":"https://www.googleapis.com/compute/v1/projects/<project_id>/zones/<project_zone>/instances/<instance_name>"

        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)
        if !ok || hwModel != "GCP_AMD_SEV" {
                return false, fmt.Errorf("missing or invalid 'hwModel'")
        }
        fmt.Println("hwmodel:", hwModel)

        swName, ok := claims["swname"].(string)
        if !ok || swName != "GCE" {
                return false, fmt.Errorf("missing or invalid 'hwModel'")
        }
        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)
        }
}

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)
}

5)。运行以下命令来构建并运行 Web 服务器。这将在 :8080 端口启动 Secret 发布 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=us-central1-c

7)。连接到您的 CGKE 工作负载并启动远程证明,以提取证明令牌(OIDC 令牌)。然后在以下命令中嵌入 attestation-tokencgke-attestation-codelab-web-server-internal-ip 的内容。这将为您获取 Secret 发布 Web 服务器持有的密钥!

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。

6. CGKE 节点上的 vTPM 密封

此步骤将开始此 Codelab 的第二部分。在此步骤中,您将在 CGKE 节点上设置 vTPM 所有者授权,并使用 vTPM 所有者密码部署工作负载。之后,您将创建 vTPM 主键,以使用 vTPM 密封功能密封和解封工作负载中的数据。

1)。在 CGKE 节点上设置 vTPM 所有者授权。

  1. 创建一次性作业容器映像。该一次性作业会为所有 vTPM 设置所有者密码。以下是用于创建容器映像的 dockerfile。

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"]
  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

替换以下内容:

  • project-id 是项目的唯一标识符。
  1. 通过 Kubernetes 作业执行一次性作业。(警告:此作业会清除每个 CVM 上的 vTPM;如果您的 CVM 使用 vTPM 加密磁盘,则此作业会导致您的 CVM 在重新启动后无法使用。您可以使用 lsblk -f 命令检查您的磁盘是否具有 FSTYPE crypto_LUKS

tpm-tools-task.yaml

apiVersion: batch/v1
kind: Job
metadata:
  name: tpm-tools-task
spec:
  template:
    spec:
      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

替换以下内容:

  • project-id 是项目的唯一标识符。
  1. 启动一次性作业。此作业会在所有工作器节点上设置 vTPM 所有者口令。
kubectl create -f tpm-tools-task.yaml

2)。创建 Kubernetes Secret 以保存 vTPM 所有者口令。

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

3)。创建一个演示应用容器并将密码传递给它。演示应用容器包含用于与 vTPM 交互的 tpm2 工具

  1. 为演示应用容器创建部署 yaml 文件。

deploy_demo.yaml

apiVersion: v1
kind: Pod
metadata:
  name: tpm-tools-demo
  labels:
    app.kubernetes.io/name: tpm-tools-demo
spec:
  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

替换以下内容:

  • project-id 是项目的唯一标识符。
  1. 部署演示应用。
kubectl create -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

tpm2_create 会与 vTPM 交互,以生成所需的加密对象。

  • -C primary.ctx:使用我们之前创建的主键上下文。
  • -u Sealed.pub:将密封密钥的公开部分(解封所需的)存储在 Sealed.pub 中。
  • -r Sealed.priv:将密封密钥的私有部分存储在 Sealed.priv 中。
  • -i secret.txt:包含要密封的密钥的文件。

tpm2_load:使用公钥和私有部分(sealed.pub、seled.priv)将密封密钥加载到 TPM 中,并将其上下文保存到 Sealed.ctx。

tpm2_unseal:解密(解封)之前使用 vTPM 密封对象加密(密封)的数据。

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

7. 清理

在 Cloud 控制台或本地开发环境中运行以下命令:

gcloud config set project <project-id>

# Delete the CGKE cluster
gcloud container clusters delete cgke-attestation-codelab --zone us-central1-c

# Delete the Artifact Registry
gcloud artifacts repositories delete codelab-repo --location=us

# Delete the web server VM instance
gcloud compute instances delete cgke-attestation-codelab-web-server --zone=us-central1-c

# Delete the GCP service account
gcloud iam service-accounts delete codelab-csa@<project-id>.iam.gserviceaccount.com

# Delete the role
gcloud iam roles delete Confidential_Computing_Workload_User

替换以下内容:

  • project-id 是项目的唯一标识符。

8. 后续步骤

详细了解机密 GKE 节点