אימות וחתימה מרחוק של vTPM בצמתים של GKE סודיים

1. סקירה כללית

צמתים של Confidential GKE‏ (CGKE) מבטיחים שהנתונים בעומסי העבודה מוצפנים בשימוש. חשיפת מכשיר ה-vTPM לעומסי עבודה ב-CGKE מאפשרת לעומסי העבודה להשתמש בתכונות של vTPM. ב-Codelab הזה מוצגות שתי תכונות של vTPM.

  • אימות מרחוק של vTPM מאפשר לצד מרוחק לוודא שצומתי ה-CGKE שמארחים עומסי עבודה פועלים ב-Confidential VMs ‏ (CVM).
  • הרשאה של vTPM ואיטום של vTPM.

683a3b43587ef69f.png

כפי שמוצג באיור שלמעלה, החלק הראשון של ה-codelab כולל את השלבים הבאים:

  • צמתים של CGKE מוגדרים וחושפים את מכשיר ה-vTPM לעומסי עבודה נבחרים.
  • פריסת עומס עבודה ואימות מרחוק של צומת CGKE שמארח את עומס העבודה.
  • הגדרה של שרת אינטרנט לפרסום סודות.

8f6e80c762a5d911.png

כפי שמוצג באיור שלמעלה, החלק השני של ה-codelab הזה כולל:

  • הגדרת הרשאה ל-vTPM ואיטום של vTPM בצמתי CGKE.

מה תלמדו

  • איך חושפים את מכשיר ה-vTPM לעומסי עבודה ב-CGKE.
  • איך לבצע אימות מרחוק באמצעות Confidential Computing API (שירות Attestation Verifier) בעומסי עבודה ב-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 ומחילים תוסף למכשיר כדי לחשוף את מכשיר ה-vTPM של CVM לעומסי עבודה. עוברים אל Cloud Console או אל סביבת הפיתוח המקומית כדי להריץ את הפקודות.

1) יוצרים אשכול CGKE ומשתמשים במאגר זהויות של עומסי עבודה כדי לאפשר לעומסי עבודה של CGKE להשתמש ב-API של Confidential Computing ב-GCP. מאגר זהויות של עומסי עבודה נדרש כי לעומסי העבודה של 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 במצב מעורב (עם צומתי עובד של Confidential GKE וגם כאלה שהם לא Confidential), מומלץ שהאופרטור יפרוס את cc-device-plugin רק בצומתי העובד של Confidential GKE.

(אופציונלי). החלת המעקב של Prometheus על פודים של CGKE. הפעלת המעקב מאפשרת לכם לראות את הסטטוס של תוסף המכשיר.

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.

rate(process_cpu_seconds_total[${__interval}])

4. פריסת עומס עבודה וביצוע אימות מרחוק בעומס העבודה

בשלב הזה, יוצרים פריסת עומס עבודה באשכול CGKE שיצרתם בשלב הקודם, ומבצעים אימות מרחוק של vTPM כדי לאחזר אסימון אימות (אסימון OIDC) בצומת העובד.

1) יוצרים את קובץ האימג' של הקונטיינר של האפליקציה ומעבירים אותו בדחיפה ל-Artifact Registry. קובץ האימג' של הקונטיינר של האפליקציה מכיל את כלי go-tpm, שיכול לאסוף ראיות לאימות ולשלוח אותן לשירות Attestation Verifier כדי לקבל אסימון אימות (OIDC Token).

  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 יהיו הרשאות גישה לממשקי API של Confidential Computing.
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). מתחברים לעומס העבודה ומפעילים אימות (attestation) מרחוק כדי לאחזר אסימון אימות (אסימון 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 ומגדירים מכונה וירטואלית נוספת. במכונה הווירטואלית הזו, תגדירו שרת אינטרנט של הפצה סודית. שרת האינטרנט מאמת את אסימון האישור שהתקבל ואת ההצהרות שלו. אם האימותים מצליחים, הסוד מועבר למבקש.

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.

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). יוצרים את שני הקבצים הבאים שבהם מאוחסן קוד המקור של שרת האינטרנט של הגרסה הסודית (העתקה והדבקה באמצעות 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). מריצים את הפקודות הבאות כדי ליצור את שרת האינטרנט ולהפעיל אותו. הפעולה הזו תפעיל את שרת האינטרנט של פרסום הסוד ביציאה :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). מתחילים עוד כרטיסייה במסוף הענן או סשן בסביבת פיתוח מקומית ומריצים את הפקודה הבאה. כך מקבלים את 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

בשלב הזה מתחיל החלק השני של ה-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. יוצרים קובץ אימג' של קונטיינר למשימה חד-פעמית ומעבירים אותו בדחיפה ל-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. (אזהרה: המשימה הזו מוחקת את 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 tools כדי ליצור אינטראקציה עם ה-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.
  • ‫‎-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: טוען את מפתח האיטום ל-TPM באמצעות החלקים הציבורי והפרטי (sealed.pub, ‏ sealed.priv) ושומר את ההקשר שלו ב-sealed.ctx.

tpm2_unseal: פענוח (ביטול החתימה) של נתונים שהוצפנו (נחתמו) בעבר באמצעות אובייקט חתימה של vTPM.

הערה: קובצי primary.ctx ו-sealed.priv ניתנים לשימוש רק במכשיר אחד עם vTPM. כל מי שיש לו גישה למכשיר vTPM ולקבצים האלה יכול לגשת לנתונים החתומים. אפשר להשתמש במדיניות על ערכי PCR כדי להצפין נתונים, אבל זה לא חלק מההיקף של ה-codelab הזה.

7. הסרת המשאבים

מריצים את הפקודות הבאות במסוף הענן או בסביבת הפיתוח המקומית:

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 Nodes