การรับรองและปิดผนึกระยะไกล vTPM บนโหนด GKE ที่เป็นความลับ

เกี่ยวกับ Codelab นี้
schedule42 นาที
subjectอัปเดตล่าสุดเมื่อ 19 พฤษภาคม 2567
account_circleเขียนโดย Ruide Zhang

โหนด Confidential GKE (CGKE) ช่วยให้มั่นใจว่าข้อมูลในภาระงานได้รับการเข้ารหัสขณะใช้งาน การเปิดเผยอุปกรณ์ vTPM ต่อภาระงาน CGKE จะช่วยให้ภาระงานใช้ฟีเจอร์ vTPM ได้ ใน Codelab นี้ คุณจะได้พบกับ 2 ฟีเจอร์ของ vTPM

  • เอกสารรับรองระยะไกลของ vTPM ช่วยให้ฝ่ายระยะไกลตรวจสอบได้ว่าโหนด CGKE ที่โฮสต์ภาระงานกำลังทำงานอยู่บน Confidential VM (CVM) หรือไม่
  • การให้สิทธิ์ vTPM และการปิดผนึก vTPM

683a3b43587ef69f.png

ตามที่แสดงให้เห็นในรูปข้างต้น ส่วนแรกของ Codelab นี้มีขั้นตอนต่อไปนี้

  • ตั้งค่าโหนด CGKE และแสดงอุปกรณ์ vTPM แก่ภาระงานที่เลือก
  • ทำให้ภาระงานและการรับรองระยะไกลของโหนด CGKE ที่โฮสต์ภาระงานใช้งานได้
  • การตั้งค่าเว็บเซิร์ฟเวอร์ Secret Release

8f6e80c762a5d911.png

ดังที่แสดงในรูปด้านบน ส่วนที่ 2 ของ Codelab นี้ประกอบด้วย

  • การตั้งค่าการให้สิทธิ์ vTPM และการปิดผนึก vTPM บนโหนด CGKE

สิ่งที่คุณจะได้เรียนรู้

  • วิธีเปิดเผยอุปกรณ์ vTPM ต่อภาระงาน CGKE
  • วิธียืนยันจากระยะไกลผ่าน Confidential Computing API (บริการ Attestation Verifier) ในภาระงาน CGKE
  • วิธีตั้งค่าการให้สิทธิ์ vTPM และดำเนินการปิดผนึก vTPM

สิ่งที่คุณต้องมี

2. การตั้งค่าและข้อกำหนด

หากต้องการเปิดใช้ API ที่จำเป็น ให้เรียกใช้คำสั่งต่อไปนี้ใน Cloud Console หรือสภาพแวดล้อมในการพัฒนาซอฟต์แวร์ภายในของคุณ

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 Console หรือสภาพแวดล้อมการพัฒนาในเครื่องเพื่อเรียกใช้คำสั่ง

1). สร้างคลัสเตอร์ CGKE โดยใช้ Workload Identity Pool เพื่ออนุญาตให้ภาระงาน CGKE ใช้ Confidential Computing API จำเป็นต้องมี Workload Identity Pool เนื่องจากภาระงาน 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 ลับเท่านั้น

(ไม่บังคับ) ใช้การตรวจสอบ 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 ต่อไปนี้แสดงวินาทีของ CPU สำหรับกระบวนการของปลั๊กอินอุปกรณ์สำเนาแต่ละรายการ

rate(process_cpu_seconds_total[${__interval}])

4. การทำให้ภาระงานใช้งานได้และดำเนินการรับรองระยะไกลกับภาระงาน

ในขั้นตอนนี้ คุณจะสร้างและทำให้ภาระงานใช้งานได้กับคลัสเตอร์ CGKE ที่คุณสร้างในขั้นตอนก่อนหน้า และดำเนินการรับรองระยะไกลของ vTPM เพื่อเรียกโทเค็นเอกสารรับรอง (โทเค็น OIDC) ในโหนดของผู้ปฏิบัติงาน

1). สร้างอิมเมจคอนเทนเนอร์ของแอปพลิเคชันและพุชไปยัง Artifact Registry อิมเมจคอนเทนเนอร์ของแอปพลิเคชันมีเครื่องมือ go-tpm ซึ่งสามารถรวบรวมหลักฐานของเอกสารรับรองและส่งไปยังบริการ Attestation Verifier สำหรับโทเค็นของเอกสารรับรอง (โทเค็น 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 และให้สิทธิ์แก่บทบาทในการเข้าถึง Confidential Computing API
gcloud iam roles create Confidential_Computing_Workload_User --project=<project-id> \
    --title="CGKE Workload User" --description="Grants the ability to generate an attestation token in a GKE workload." \
 --permissions="confidentialcomputing.challenges.create,confidentialcomputing.challenges.verify,confidentialcomputing.locations.get,confidentialcomputing.locations.list" --stage=GA

แทนที่ค่าต่อไปนี้

  • project-id คือตัวระบุที่ไม่ซ้ำกันของโปรเจ็กต์
  1. สร้างบัญชีบริการ GCP codelab-csa และเชื่อมโยงกับบทบาท Confidential_Computing_Workload_User. So that codelab-csa มีสิทธิ์เข้าถึง Confidential Computing API
gcloud iam service-accounts create codelab-csa \
    --project=<project-id>

gcloud projects add-iam-policy-binding <project-id> \
    --member "serviceAccount:codelab-csa@<project-id>.iam.gserviceaccount.com" \
    --role "projects/<project-id>/roles/Confidential_Computing_Workload_User"

gcloud iam service-accounts add-iam-policy-binding codelab-csa@<project-id>.iam.gserviceaccount.com \
    --role roles/iam.workloadIdentityUser \
    --member "serviceAccount:<project-id>.svc.id.goog[default/codelab-ksa]"

แทนที่ค่าต่อไปนี้

  • project-id คือตัวระบุที่ไม่ซ้ำกันของโปรเจ็กต์
  1. เชื่อมโยงบัญชีบริการ Kubernetes codelab-ksa กับบัญชีบริการ GCP codelab-csa เพื่อให้ codelab-ksa มีสิทธิ์เข้าถึง Confidential Computing API
kubectl annotate serviceaccount codelab-ksa \
    --namespace default \
    iam.gke.io/gcp-service-account=codelab-csa@<project-id>.iam.gserviceaccount.com

แทนที่ค่าต่อไปนี้

  • 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. การตั้งค่าเว็บเซิร์ฟเวอร์การเผยแพร่ข้อมูลลับ

ในขั้นตอนนี้ คุณจะออกจากเซสชัน SSH ก่อนหน้าและตั้งค่า VM อื่น คุณสร้างเว็บเซิร์ฟเวอร์การเผยแพร่ข้อมูลลับใน VM นี้ เว็บเซิร์ฟเวอร์จะตรวจสอบโทเค็นเอกสารรับรองที่ได้รับและการอ้างสิทธิ์ หากการตรวจสอบประสบความสำเร็จ ระบบจะส่งผ่านข้อมูลลับไปยังผู้ขอ

1). ไปที่ Cloud Console หรือสภาพแวดล้อมในการพัฒนาซอฟต์แวร์ในเครื่อง สร้างเครื่องเสมือน

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 ไปยัง VM ใหม่

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) สร้างไฟล์ 2 ไฟล์ต่อไปนี้ที่จัดเก็บซอร์สโค้ดของเว็บเซิร์ฟเวอร์ที่เผยแพร่ลับ (คัดลอกและวางด้วยนาโน)

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) เรียกใช้คำสั่งต่อไปนี้เพื่อสร้างเว็บเซิร์ฟเวอร์และเรียกใช้ การดำเนินการนี้จะเริ่มต้นเว็บเซิร์ฟเวอร์เผยแพร่ข้อมูลลับที่พอร์ต :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 Console หรือเซสชันสภาพแวดล้อมการพัฒนาซอฟต์แวร์ภายในแล้วเรียกใช้คำสั่งต่อไปนี้ ซึ่งจะทำให้คุณได้รับ 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-token และ cgke-attestation-codelab-web-server-internal-ip ในคำสั่งต่อไปนี้ การดำเนินการนี้จะดึงข้อมูลลับที่เก็บไว้โดยเว็บเซิร์ฟเวอร์การเผยแพร่ลับ

kubectl exec -it go-tpm-demo -- /bin/bash
./gotpm token --event-log=/run/cc-device-plugin/binary_bios_measurements > attestation_token
curl http://<cgke-attestation-codelab-web-server-internal-ip>:8080 -H "Authorization: Bearer $(cat ./attestation_token)"

แทนที่ค่าต่อไปนี้

  • cgke-attestation-codelab-web-server-internal-ip คือ IP ภายในของอินสแตนซ์ VM cgke-attestation-codelab-web-server

6. การปิดผนึก vTPM บนโหนด CGKE

ขั้นตอนนี้จะเริ่มต้นส่วนที่ 2 ของ Codelab นี้ ในขั้นตอนนี้ คุณจะตั้งค่าการให้สิทธิ์เจ้าของ vTPM บนโหนด CGKE และทำให้ภาระงานด้วยรหัสผ่านของเจ้าของ vTPM ใช้งานได้ หลังจากนั้น คุณจะต้องสร้างคีย์หลัก vTPM เพื่อปิดและเปิดเผยข้อมูลในภาระงานด้วยความสามารถในการปิดผนึก vTPM

1). ตั้งค่าการให้สิทธิ์เจ้าของ vTPM บนโหนด CGKE

  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. สร้างและพุชอิมเมจคอนเทนเนอร์งานแบบครั้งเดียวไปยังรีจิสทรีอาร์ติแฟกต์
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 (คำเตือน: งานนี้จะล้าง vTPM ใน CVM แต่ละรายการ หาก CVM ใช้ vTPM ในการเข้ารหัสดิสก์ งานนี้จะทำให้ CVM ของคุณใช้งานไม่ได้หลังจากการรีบูต คุณตรวจสอบได้ว่าดิสก์มี FSTYPE crypto_LUKS ด้วยคำสั่ง lsblk -f หรือไม่)

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 เพื่อเก็บรักษารหัสผ่านของเจ้าของ vTPM

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

3). สร้างคอนเทนเนอร์แอปพลิเคชันเดโม และส่งรหัสผ่านไปยังคอนเทนเนอร์ดังกล่าว คอนเทนเนอร์ของแอปพลิเคชันเดโมมีเครื่องมือ tpm2 เพื่อโต้ตอบกับ vTPM

  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
  • -cprimary.ctx: บันทึกบริบท (แฮนเดิลและข้อมูลที่เกี่ยวข้อง) ของออบเจ็กต์หลักที่สร้างขึ้นไปยังไฟล์ main.ctx บริบทนี้มีความสำคัญต่อการดำเนินการในภายหลัง

ภาระงานต้องไม่ใช้รหัสผ่านที่ไม่ถูกต้องของเจ้าของเพื่อสร้างคีย์หลัก

tpm2_createprimary -C o -P invalid_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 เพื่อสร้างออบเจ็กต์การเข้ารหัสที่ต้องการ

  • -Cprimary.ctx: ใช้บริบทคีย์หลักที่เราสร้างขึ้นก่อนหน้านี้
  • -u Sealed.pub: จัดเก็บส่วนสาธารณะของคีย์ปิดผนึก (ใช้ในการเปิดปิด) ในซีล.pub
  • -r Seled.priv: จัดเก็บส่วนที่เป็นส่วนตัวของคีย์ปิดผนึกใน Sealed.priv
  • -iSecret.txt: ไฟล์ที่มีข้อมูลลับที่จะปิดผนึก

tpm2_load: โหลดคีย์ปิดผนึกลงใน TPM โดยใช้ส่วนสาธารณะและส่วนส่วนตัว (sealed.pub, caseled.priv) และบันทึกบริบทลงในซีล.ctx

tpm2_unseal: ถอดรหัส (ปิดผนึก) ข้อมูลที่เข้ารหัสไว้ก่อนหน้านี้ (ซีล) โดยใช้ออบเจ็กต์ปิดผนึก vTPM

โปรดทราบว่าไฟล์ primary.ctx, sealed.priv ใช้ได้ในอุปกรณ์ vTPM เครื่องเดียวเท่านั้น และทุกคนที่มีสิทธิ์เข้าถึงอุปกรณ์ vTPM และไฟล์เหล่านี้จะสามารถเข้าถึงข้อมูลที่ปิดผนึกได้ คุณยังใช้นโยบายกับค่า PCR เพื่อปิดข้อมูลได้ แต่อยู่นอกขอบเขตของ Codelab นี้

7. ล้างข้อมูล

เรียกใช้คำสั่งต่อไปนี้ใน Cloud Console หรือสภาพแวดล้อมในการพัฒนาซอฟต์แวร์ภายในของคุณ

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. ขั้นตอนถัดไป

ดูข้อมูลเพิ่มเติมเกี่ยวกับ Confidential GKE Node