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

מידע על Codelab זה
schedule42 דקות
subjectהעדכון האחרון: 19 במאי 2024
account_circleנכתב על ידי Ruide Zhang

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

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

683a3b43587ef69f.png

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

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

8f6e80c762a5d911.png

כפי שמתואר באיור שלמעלה, החלק השני של ה-Codelab הזה כולל:

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

מה תלמדו

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

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

הערה: במקרה של אשכול GKE במצב מעורב (עם צמתים של עובדים סודיים וגם לא סודיים ב-GKE), מומלץ שהאופרטור יפרוס cc-device-Plugin רק בצמתים של עובדים ב-Confidential GKE.

(אופציונלי). החלת מעקב אחר Pod 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 הבאה מציגה שניות מעבד (CPU) לכל תהליך cc-device-plugin.

rate(process_cpu_seconds_total[${__interval}])

4. פריסת עומס עבודה וביצוע אימות (attestation) מרחוק של עומס העבודה

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

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

  1. יוצרים את קובץ ה-Docker לקובץ האימג' של הקונטיינר של האפליקציה.

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 של Confidential Computing.
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 של Confidential Computing.
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 לפריסת האפליקציה בשביל אפליקציית ההדגמה. מקצים את חשבון השירות codelab-ksa ב-Kubernetes לעומסי עבודה שנבחרו.

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

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

6. סגירה של vTPM בצמתים של CGKE

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

1) הגדרה של הרשאת בעלים של vTPM בצמתים של CGKE.

  1. יצירת קובץ אימג' של קונטיינר של משימה חד-פעמית. המשימה החד-פעמית מגדירה את סיסמת הבעלים לכל ה-vTPM. בהמשך מופיע קובץ ה-Docker ליצירת קובץ האימג' בקונטיינר.

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).
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 בכל צמתים של Worker.
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.
  • -c primary.proxy: שומר את ההקשר (הכינוי ונתונים משויכים) של האובייקט הראשי שנוצר בקובץ בכתובת בכתובת המשתמש המשתמש המשתמש המשתמש בכתובת המשתמש המשתמש המשתמש בעליו בסוג הכתובת מול העסק. ההקשר הזה חיוני לפעולות מאוחרות יותר.

עומס העבודה לא יכול להשתמש בביטוי סיסמה שגוי של בעלים כדי ליצור מפתח ראשי.

tpm2_createprimary -C o -P negative_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.admob: משתמש בהקשר של המפתח הראשי שיצרנו קודם.
  • -u setated.pub: מאחסן את החלק הציבורי של מפתח החתימה (יש צורך בביטול נעילה) ב-sald.pub.
  • -r למקסם.priv: אחסון החלק הפרטי של מפתח הסגירה ב-salt.priv.
  • -iSecret.txt: הקובץ שמכיל את הסוד לסגירה.

tpm2_load: טוענים את מפתח החתימה ל-TPM באמצעות החלקים הציבוריים והפרטיים (sealed.pub, tablet.priv) ושומרים את ההקשר ל-sated.ctx.

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

הערה: אפשר להשתמש בקובצי primary.ctx ו-sealed.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. המאמרים הבאים

מידע נוסף על Confidential GKE Node.