1. סקירה כללית
צמתים של Confidential GKE (CGKE) מבטיחים שהנתונים בעומסי העבודה מוצפנים בשימוש. חשיפת מכשיר ה-vTPM לעומסי עבודה ב-CGKE מאפשרת לעומסי העבודה להשתמש בתכונות של vTPM. בשיעור Codelab הזה נסביר על התכונות של vTPM ואיך להשתמש באימות חומרה של Intel TDX ו-AMD SEV-SNP באמצעות cc-device-plugin.
- אימות (attestation) מרחוק של vTPM מאפשר לצד מרוחק לוודא שהצמתים של CGKE שמארחים עומסי עבודה פועלים במכונות וירטואליות סודיות (CVM).
- הרשאה ואיטום של vTPM.
- אימות חומרה באמצעות מכשירי אורח חשופים של Intel TDX ו-AMD SEV-SNP.

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

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

כפי שמוצג באיור שלמעלה, החלק השלישי של ה-codelab הזה כולל:
- איך
cc-device-pluginממפה בצורה מאובטחת מכשירי חומרה במהלך שלב ההגדרה. - פריסת עומסי עבודה כדי לאחזר ישירות ציטוטים של אימות חומרה ממכשירי TDX או SEV-SNP במהלך שלב זמן הריצה ללא תקורה.
מה תלמדו
- איך חושפים את מכשיר ה-vTPM לעומסי עבודה ב-CGKE.
- איך לבצע אימות (attestation) מרחוק באמצעות Confidential Computing API (שירות Attestation Verifier) בעומסי עבודה של CGKE.
- איך מגדירים הרשאה ואיטום של vTPM.
- איך לגשת למכשירי SEV-SNP ו-TDX בעומסי עבודה של CGKE באמצעות
cc-device-plugin. - איך מאחזרים הצעות מחיר של אימות חומרה באמצעות go-tdx-guest ו-go-sev-guest.
מה תצטרכו
- פרויקט ב-Google Cloud Platform
- דפדפן, כמו Chrome או Firefox
- ידע בסיסי ב-Google Compute Engine (codelab), ב-Confidential VM, ב-Confidential GKE nodes וב-Artifact Registry
- Intel TDX או AMD SEV-SNP: גרסת GKE מינימלית של v1.33.5-gke.1697000+ או v1.34.1-gke.2909000+ וסוגי מכונות מתאימים (c3 ל-TDX, n2d ל-SNP).
2. הגדרה ודרישות
כדי להפעיל את ממשקי ה-API הנדרשים, מריצים את הפקודה הבאה במסוף Cloud או בסביבת הפיתוח המקומית:
gcloud auth login
gcloud services enable \
cloudapis.googleapis.com \
cloudshell.googleapis.com \
container.googleapis.com \
artifactregistry.googleapis.com \
confidentialcomputing.googleapis.com \
iamcredentials.googleapis.com \
compute.googleapis.com
3. הגדרת מישור בקרה משותף של CGKE
בשלב הזה מגדירים את מישור הבקרה המשותף של GKE ואת מאגר Docker של Artifact Registry.
1) מגדירים משתני סביבה ויוצרים את אשכול GKE המשותף:
מחליפים את your-project-id במזהה הפרויקט. מחליפים את us-central1-c באזור הרצוי. (ראו אזורים ותחומים)
export PROJECT_ID="your-project-id"
export ZONE="us-central1-c"
export CLUSTER_NAME="cgke-attestation-codelab"
gcloud config set project ${PROJECT_ID}
gcloud container clusters create ${CLUSTER_NAME} \
--zone=${ZONE} \
--num-nodes=1 \
--machine-type=e2-medium \
--workload-pool=${PROJECT_ID}.svc.id.goog \
--workload-metadata=GKE_METADATA
gcloud container clusters get-credentials ${CLUSTER_NAME} --zone=${ZONE}
2). יוצרים מאגר Docker ב-Artifact Registry כדי לאחסן קובצי אימג' של קונטיינרים של עומסי עבודה:
gcloud artifacts repositories create codelab-repo \
--repository-format=docker \
--location=us
בחירת הנתיב
מישור הבקרה המשותף של GKE ומאגר Artifact Registry מוכנים עכשיו. בוחרים את הנתיב:
- אפשרות 1: אימות (attestation) מרחוק ואיטום של vTPM (AMD SEV) – אפשר להמשיך ישירות אל הגדרת צמתים של CGKE וחשיפת מכשיר ה-vTPM.
- אפשרות 2: אימות חומרה (Intel TDX ו-AMD SEV-SNP) – אפשר לדלג ישירות אל פריסת עומסי עבודה לביצוע אימות חומרה. (רוצים לראות את כל האפשרויות? פשוט משלימים את אפשרות 1 וממשיכים לאפשרות 2)
4. הגדרת צמתים של CGKE וחשיפת מכשיר ה-vTPM לעומסי עבודה נבחרים
בשלב הזה, יוצרים מאגר צמתים של vTPM ב-CGKE ומחילים תוסף למכשיר כדי לחשוף את מכשיר ה-vTPM של ה-CVM לעומסי עבודה. כדי להריץ את הפקודות, עוברים אל Cloud Console או אל סביבת הפיתוח המקומית.
1) הוספת מאגר צמתים של AMD SEV
gcloud container node-pools create sev-pool \
--cluster=${CLUSTER_NAME} \
--zone=${ZONE} \
--machine-type=n2d-standard-2 \
--num-nodes=1 \
--enable-confidential-nodes \
--confidential-node-type=sev
2). מחיקת מאגר הצמתים שמוגדר כברירת מחדל (אופציונלי, אבל מומלץ כדי לחסוך בעלויות ולמנוע הפרעות)
gcloud container node-pools delete default-pool \
--cluster=${CLUSTER_NAME} \
--zone=${ZONE} \
--quiet
3). מפעילים את תוסף המכשיר כדי לאפשר לאשכול CGKE לחשוף מכשיר vTPM לעומסי עבודה. אנחנו משתמשים בתוסף למכשיר Kubernetes כדי ליצור משאבים חדשים (google.com/cc). כל עומס עבודה שמשויך למשאב החדש יוכל לראות את המכשירים בצומת העובד.
gcloud container clusters get-credentials ${CLUSTER_NAME} --zone ${ZONE} --project ${PROJECT_ID}
kubectl apply -f https://raw.githubusercontent.com/google/cc-device-plugin/main/manifests/cc-device-plugin.yaml
הפקודה הבאה מאפשרת לראות את האפליקציה cc-device-plugin שנפרסה.
kubectl get pods -A | grep "cc-device-plugin"
הערה: במקרה של אשכול GKE במצב מעורב (עם צמתים של עובדי 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}])
5. פריסת עומס עבודה וביצוע אימות (attestation) מרחוק בעומס העבודה (vTPM)
בשלב הזה, יוצרים פריסת עומס עבודה באשכול CGKE שיצרתם בשלב הקודם, ומבצעים אימות מרחוק של vTPM כדי לאחזר אסימון אימות (אסימון OIDC) בצומת העובד.
1) יוצרים את קובץ האימג' של הקונטיינר של האפליקציה ומעבירים אותו בדחיפה ל-Artifact Registry. קובץ אימג' של קונטיינר האפליקציות מכיל את הכלי go-tpm, שיכול לאסוף ראיות לאימות ולשלוח אותן לשירות Attestation Verifier כדי לקבל אסימון אימות (OIDC Token).
- יוצרים את
Dockerfileעבור קובץ האימג' של קונטיינר האפליקציה.
cat << 'EOF' > Dockerfile FROM golang:1.26.4 as builder WORKDIR / RUN git clone https://github.com/google/go-tpm-tools.git WORKDIR /go-tpm-tools/cmd/gotpm RUN CGO_ENABLED=0 GOOS=linux go build -o /gotpm FROM debian:trixie WORKDIR / RUN apt-get update -y RUN DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl RUN rm -rf /etc/apt/sources.list.d COPY --from=builder /gotpm /gotpm CMD ["tail", "-f", "/dev/null"] EOF
- מעבירים בדחיפה את קובץ האימג' של הקונטיינר של האפליקציה אל Artifact Registry.
docker build -t us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/go-tpm:latest .
docker push us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/go-tpm:latest
2). הגדרת חשבון שירות של Kubernetes כדי לרשת את ההרשאות של חשבון שירות ב-GCP במשאבי GCP.
- יוצרים חשבון שירות ב-Kubernetes
codelab-ksa.
kubectl create serviceaccount codelab-ksa \
--namespace default
- יוצרים תפקיד
Confidential_Computing_Workload_Userומעניקים לתפקיד הרשאות גישה לממשקי API של 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
- יוצרים חשבון שירות ב-GCP
codelab-csaומקשרים אותו לתפקידConfidential_Computing_Workload_User. כדי של-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]"
- מקשרים את חשבון השירות של Kubernetes
codelab-ksaלחשבון השירות של GCPcodelab-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
3). יוצרים את קובץ ה-YAML לפריסת האפליקציה עבור אפליקציית ההדגמה. מקצים את חשבון השירות של Kubernetes codelab-ksa לעומסי עבודה נבחרים.
cat << EOF > deploy.yaml
apiVersion: v1
kind: Pod
metadata:
name: go-tpm-demo
labels:
app.kubernetes.io/name: go-tpm-demo
spec:
serviceAccountName: codelab-ksa
nodeSelector:
iam.gke.io/gke-metadata-server-enabled: "true"
cloud.google.com/machine-family: "n2d"
cloud.google.com/gke-confidential-nodes-instance-type: "SEV"
containers:
- name: go-tpm
image: us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/go-tpm:latest
resources:
limits:
google.com/cc: 1
EOF
4). מחילים את הפריסה על אשכול CGKE.
kubectl apply -f deploy.yaml
5). מתחברים לעומס העבודה ומפעילים אימות מרחוק כדי לאחזר אסימון אימות (אסימון OIDC).
kubectl exec -it go-tpm-demo -- /bin/bash ./gotpm token --event-log=/run/cc-device-plugin/binary_bios_measurements > attestation_token
6). מדפיסים את האסימון למסך כדי שתוכלו להעתיק אותו.
cat attestation_token
אפשר לפענח את אסימון האימות בכתובת jwt.io כדי לראות את ההצהרות.
6. הגדרת שרת אינטרנט של Secret Release
בשלב הזה יוצאים מהסשן הקודם של 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=${ZONE} \
--image-family=ubuntu-2204-lts \
--image-project=ubuntu-os-cloud
2). מתחברים למכונה הווירטואלית החדשה באמצעות SSH.
gcloud compute ssh --zone ${ZONE} cgke-attestation-codelab-web-server
3). מגדירים את סביבת Go.
wget https://go.dev/dl/go1.26.4.linux-amd64.tar.gz sudo tar -C /usr/local -xzf go1.26.4.linux-amd64.tar.gz export PATH=$PATH:/usr/local/go/bin
4). יוצרים את שני הקבצים הבאים שבהם מאוחסן קוד המקור של שרת האינטרנט של הגרסה הסודית.
יצירה main.go:
cat << 'EOF' > main.go
package main
import (
"fmt"
"net/http"
"strings"
"time"
"log"
"github.com/golang-jwt/jwt/v4"
)
const (
theSecret = "This is the super secret information!"
)
func homePage(w http.ResponseWriter, r *http.Request) {
tokenString := r.Header.Get("Authorization")
if tokenString != "" {
tokenString, err := extractToken(tokenString)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
}
tokenBytes := []byte(tokenString)
// A method to return a public key from the well-known endpoint
keyFunc := getRSAPublicKeyFromJWKsFile
token, err := decodeAndValidateToken(tokenBytes, keyFunc)
if err != nil {
http.Error(w, "Invalid JWT Token", http.StatusUnauthorized)
}
if ok, err := isValid(token.Claims.(jwt.MapClaims)); ok {
fmt.Fprintln(w, theSecret)
} else {
if err != nil {
http.Error(w, "Error validating JWT claims: "+err.Error(), http.StatusUnauthorized)
} else {
http.Error(w, "Invalid JWT token Claims", http.StatusUnauthorized)
}
}
} else {
http.Error(w, "Authorization token required", http.StatusUnauthorized)
}
}
func extractToken(tokenString string) (string, error) {
if strings.HasPrefix(tokenString, "Bearer ") {
return strings.TrimPrefix(tokenString, "Bearer "), nil
}
return "", fmt.Errorf("invalid token format")
}
func isValid(claims jwt.MapClaims) (bool, error) {
// 1. Evaluating Standard Claims:
subject, ok := claims["sub"].(string)
if !ok {
return false, fmt.Errorf("missing or invalid 'sub' claim")
}
fmt.Println("Subject:", subject)
issuedAt, ok := claims["iat"].(float64)
if !ok {
return false, fmt.Errorf("missing or invalid 'iat' claim")
}
fmt.Println("Issued At:", time.Unix(int64(issuedAt), 0))
// 2. Evaluating Remote Attestation Claims:
hwModel, ok := claims["hwmodel"].(string)
// Support attestation of both AMD SEV and Intel TDX
if !ok || (hwModel != "GCP_AMD_SEV" && hwModel != "GCP_INTEL_TDX") {
return false, fmt.Errorf("missing or invalid 'hwModel': %v", hwModel)
}
fmt.Println("hwmodel:", hwModel)
swName, ok := claims["swname"].(string)
if !ok || swName != "GCE" {
return false, fmt.Errorf("missing or invalid 'swName'")
}
fmt.Println("swname:", swName)
return true, nil
}
func main() {
http.HandleFunc("/", homePage)
fmt.Println("Server listening on :8080")
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatalf("Server failed to start: %v", err)
}
}
EOF
יצירה helper.go:
cat << 'EOF' > helper.go
package main
import (
"crypto/rsa"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"math/big"
"net/http"
"github.com/golang-jwt/jwt/v4"
)
const (
socketPath = "/run/container_launcher/teeserver.sock"
expectedIssuer = "https://confidentialcomputing.googleapis.com"
wellKnownPath = "/.well-known/openid-configuration"
)
type jwksFile struct {
Keys []jwk `json:"keys"`
}
type jwk struct {
N string `json:"n"` // "nMMTBwJ7H6Id8zUCZd-L7uoNyz9b7lvoyse9izD9l2rtOhWLWbiG-7pKeYJyHeEpilHP4KdQMfUo8JCwhd-OMW0be_XtEu3jXEFjuq2YnPSPFk326eTfENtUc6qJohyMnfKkcOcY_kTE11jM81-fsqtBKjO_KiSkcmAO4wJJb8pHOjue3JCP09ZANL1uN4TuxbM2ibcyf25ODt3WQn54SRQTV0wn098Y5VDU-dzyeKYBNfL14iP0LiXBRfHd4YtEaGV9SBUuVhXdhx1eF0efztCNNz0GSLS2AEPLQduVuFoUImP4s51YdO9TPeeQ3hI8aGpOdC0syxmZ7LsL0rHE1Q",
E string `json:"e"` // "AQAB" or 65537 as an int
Kid string `json:"kid"` // "1f12fa916c3a0ef585894b4b420ad17dc9d6cdf5",
// Unused fields:
// Alg string `json:"alg"` // "RS256",
// Kty string `json:"kty"` // "RSA",
// Use string `json:"use"` // "sig",
}
type wellKnown struct {
JwksURI string `json:"jwks_uri"` // "https://www.googleapis.com/service_accounts/v1/metadata/jwk/signer@confidentialspace-sign.iam.gserviceaccount.com"
// Unused fields:
// Iss string `json:"issuer"` // "https://confidentialcomputing.googleapis.com"
// Subject_types_supported string `json:"subject_types_supported"` // [ "public" ]
// Response_types_supported string `json:"response_types_supported"` // [ "id_token" ]
// Claims_supported string `json:"claims_supported"` // [ "sub", "aud", "exp", "iat", "iss", "jti", "nbf", "dbgstat", "eat_nonce", "google_service_accounts", "hwmodel", "oemid", "secboot", "submods", "swname", "swversion" ]
// Id_token_signing_alg_values_supported string `json:"id_token_signing_alg_values_supported"` // [ "RS256" ]
// Scopes_supported string `json:"scopes_supported"` // [ "openid" ]
}
func getWellKnownFile() (wellKnown, error) {
httpClient := http.Client{}
resp, err := httpClient.Get(expectedIssuer + wellKnownPath)
if err != nil {
return wellKnown{}, fmt.Errorf("failed to get raw .well-known response: %w", err)
}
wellKnownJSON, err := io.ReadAll(resp.Body)
if err != nil {
return wellKnown{}, fmt.Errorf("failed to read .well-known response: %w", err)
}
wk := wellKnown{}
json.Unmarshal(wellKnownJSON, &wk)
return wk, nil
}
func getJWKFile() (jwksFile, error) {
wk, err := getWellKnownFile()
if err != nil {
return jwksFile{}, fmt.Errorf("failed to get .well-known json: %w", err)
}
// Get JWK URI from .wellknown
uri := wk.JwksURI
fmt.Printf("jwks URI: %v\n", uri)
httpClient := http.Client{}
resp, err := httpClient.Get(uri)
if err != nil {
return jwksFile{}, fmt.Errorf("failed to get raw JWK response: %w", err)
}
jwkbytes, err := io.ReadAll(resp.Body)
if err != nil {
return jwksFile{}, fmt.Errorf("failed to read JWK body: %w", err)
}
file := jwksFile{}
err = json.Unmarshal(jwkbytes, &file)
if err != nil {
return jwksFile{}, fmt.Errorf("failed to unmarshall JWK content: %w", err)
}
return file, nil
}
// N and E are 'base64urlUInt' encoded: https://www.rfc-editor.org/rfc/rfc7518#section-6.3
func base64urlUIntDecode(s string) (*big.Int, error) {
b, err := base64.RawURLEncoding.DecodeString(s)
if err != nil {
return nil, err
}
z := new(big.Int)
z.SetBytes(b)
return z, nil
}
func getRSAPublicKeyFromJWKsFile(t *jwt.Token) (any, error) {
keysfile, err := getJWKFile()
if err != nil {
return nil, fmt.Errorf("failed to fetch the JWK file: %w", err)
}
// Multiple keys are present in this endpoint to allow for key rotation.
// This method finds the key that was used for signing to pass to the validator.
kid := t.Header["kid"]
for _, key := range keysfile.Keys {
if key.Kid != kid {
continue // Select the key used for signing
}
n, err := base64urlUIntDecode(key.N)
if err != nil {
return nil, fmt.Errorf("failed to decode key.N %w", err)
}
e, err := base64urlUIntDecode(key.E)
if err != nil {
return nil, fmt.Errorf("failed to decode key.E %w", err)
}
// The parser expects an rsa.PublicKey: https://github.com/golang-jwt/jwt/blob/main/rsa.go#L53
// or an array of keys. We chose to show passing a single key in this example as its possible
// not all validators accept multiple keys for validation.
return &rsa.PublicKey{
N: n,
E: int(e.Int64()),
}, nil
}
return nil, fmt.Errorf("failed to find key with kid '%v' from well-known endpoint", kid)
}
func decodeAndValidateToken(tokenBytes []byte, keyFunc func(t *jwt.Token) (any, error)) (*jwt.Token, error) {
var err error
fmt.Println("Unmarshalling token and checking its validity...")
token, err := jwt.NewParser().Parse(string(tokenBytes), keyFunc)
fmt.Printf("Token valid: %v\n", token.Valid)
if token.Valid {
return token, nil
}
if ve, ok := err.(*jwt.ValidationError); ok {
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
return nil, fmt.Errorf("token format invalid. Please contact the Confidential Space team for assistance")
}
if ve.Errors&(jwt.ValidationErrorNotValidYet) != 0 {
// If device time is not synchronized with the Attestation Service you may need to account for that here.
return nil, errors.New("token is not active yet")
}
if ve.Errors&(jwt.ValidationErrorExpired) != 0 {
return nil, fmt.Errorf("token is expired")
}
return nil, fmt.Errorf("unknown validation error: %v", err)
}
return nil, fmt.Errorf("couldn't handle this token or couldn't read a validation error: %v", err)
}
EOF
5). מריצים את הפקודות הבאות כדי ליצור את שרת האינטרנט ולהפעיל אותו. הפעולה הזו תפעיל את שרת האינטרנט של שחרור הסוד ביציאה :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=${ZONE}
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 הפנימית של מכונת ה-VMcgke-attestation-codelab-web-server.
7. איטום של vTPM בצמתים של CGKE
בשלב הזה מגדירים הרשאת בעלים של vTPM בצמתי CGKE ומפריסים עומס עבודה עם ביטוי הסיסמה של הבעלים של vTPM. לאחר מכן, יוצרים מפתח ראשי של vTPM כדי לאטום ולבטל את האיטום של נתונים בעומס העבודה באמצעות יכולת האיטום של vTPM.
1) הגדרת הרשאת בעלים של vTPM בצמתי CGKE.
- יוצרים קובץ אימג' של קונטיינר של משימה חד-פעמית. העבודה החד-פעמית מגדירה את סיסמת הבעלים לכל מודולי ה-vTPM. בהמשך מופיעה הפקודה
Dockerfileליצירת קובץ האימג' של הקונטיינר.
cat << 'EOF' > Dockerfile FROM debian:latest RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections RUN apt-get update RUN apt -y install \ autoconf-archive \ libcmocka0 \ libcmocka-dev \ net-tools \ build-essential \ git \ pkg-config \ gcc \ g++ \ m4 \ libtool \ automake \ libgcrypt20-dev \ libssl-dev \ uthash-dev \ autoconf \ uuid-dev \ libcurl4-openssl-dev \ libjson-c-dev RUN mkdir /src WORKDIR /src RUN git clone https://github.com/tpm2-software/tpm2-tss WORKDIR /src/tpm2-tss RUN ./bootstrap RUN ./configure --prefix=/usr/local RUN make all install WORKDIR /src RUN git clone https://github.com/tpm2-software/tpm2-tools WORKDIR /src/tpm2-tools RUN apt-get -y install libcurl4 libcurl4-openssl-dev pandoc man-db RUN ./bootstrap RUN ./configure --prefix=/usr/local RUN make all install RUN apt-get -y install vim ENTRYPOINT ["/bin/bash"] EOF
- יוצרים קובץ אימג' של קונטיינר למשימה חד-פעמית ומעבירים אותו בדחיפה ל-Artifact Registry.
docker build -t us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/tpm-tools:latest .
docker push us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/tpm-tools:latest
- מריצים את המשימה החד-פעמית באמצעות משימת Kubernetes. (אזהרה: המשימה הזו מוחקת את vTPM בכל CVM. אם ה-CVM שלכם משתמש ב-vTPM כדי להצפין את הדיסק, המשימה הזו תגרום לכך שלא תוכלו להשתמש ב-CVM אחרי הפעלה מחדש. אפשר לבדוק אם בדיסק יש FSTYPE
crypto_LUKSבאמצעות הפקודהlsblk -f)
cat << EOF > tpm-tools-task.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: tpm-tools-task
spec:
template:
spec:
nodeSelector:
cloud.google.com/machine-family: "n2d"
cloud.google.com/gke-confidential-nodes-instance-type: "SEV"
containers:
- name: tpm-tools
image: us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/tpm-tools:latest
command: ["/bin/sh", "-c"]
args: ["tpm2_clear; tpm2_changeauth -c owner this_is_passphrase"]
resources:
limits:
google.com/cc: 1
restartPolicy: Never
EOF
טיפ: אם אתם צריכים להריץ מחדש את המשימה tpm-tools-task, הקפידו למחוק קודם את המשימה הקיימת:
kubectl delete job tpm-tools-task
- מפעילים את העבודה החד-פעמית. הג'וב הזה מגדיר את ביטוי הגישה של הבעלים של vTPM בכל צמתי העובדים.
kubectl apply -f tpm-tools-task.yaml
2). יוצרים סוד של Kubernetes כדי לשמור את ביטוי הסיסמה של הבעלים של vTPM.
kubectl create secret generic tpm-secret --from-literal=passphrase='this_is_passphrase'
3). יוצרים מאגר תגים של אפליקציית הדגמה ומעבירים אליו את ביטוי הסיסמה. קונטיינר האפליקציה להדגמה מכיל את tpm2-tools כדי ליצור אינטראקציה עם ה-vTPM.
- יוצרים את קובץ ה-YAML של הפריסה עבור קונטיינר האפליקציה לדוגמה.
cat << EOF > deploy_demo.yaml
apiVersion: v1
kind: Pod
metadata:
name: tpm-tools-demo
labels:
app.kubernetes.io/name: tpm-tools-demo
spec:
nodeSelector:
cloud.google.com/machine-family: "n2d"
cloud.google.com/gke-confidential-nodes-instance-type: "SEV"
containers:
- name: tpm-tools
image: us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/tpm-tools:latest
command: ["tail", "-f", "/dev/null"]
resources:
limits:
google.com/cc: 1
volumeMounts:
- name: secret-volume
mountPath: "/etc/tpmsecret"
readOnly: true
volumes:
- name: secret-volume
secret:
secretName: tpm-secret
EOF
- פורסים את אפליקציית ההדגמה.
kubectl apply -f deploy_demo.yaml
4). מבצעים איטום של vTPM במאגר של אפליקציית ההדגמה.
- מתחברים למאגר של אפליקציית ההדגמה ומגדירים מפתח ראשי עם סיסמה.
kubectl exec -it tpm-tools-demo -- /bin/bash tpm2_createprimary -C o -c primary.ctx -P $(cat /etc/tpmsecret/passphrase)
tpm2_createprimary מתקשר עם ה-vTPM כדי ליצור את האובייקט הראשי על סמך ההיררכיה והתבנית שצוינו.
- -C o: מציין שהמפתח הראשי ייווצר בהיררכיית הבעלים של ה-TPM.
- -c
primary.ctx: שומר את ההקשר (הטיפול והנתונים המשויכים) של האובייקט הראשי שנוצר בקובץprimary.ctx. ההקשר הזה חיוני לפעולות שיתבצעו בהמשך.
עומס העבודה לא יכול להשתמש בסיסמה שגויה של הבעלים כדי ליצור מפתח ראשי.
tpm2_createprimary -C o -P wrong_passphrase
הפקודה מחזירה את השגיאות הבאות:
WARNING:esys:src/tss2-esys/api/Esys_CreatePrimary.c:401:Esys_CreatePrimary_Finish() Received TPM Error ERROR:esys:src/tss2-esys/api/Esys_CreatePrimary.c:135:Esys_CreatePrimary() Esys Finish ErrorCode (0x000009a2) ERROR: Esys_CreatePrimary(0x9A2) - tpm:session(1):authorization failure without DA implications ERROR: Unable to run tpm2_createprimary
- אחר כך אפשר להשתמש במפתח הראשי שנוצר כדי לאטום ולפתוח נתונים.
echo "This is my secret message" > secret.txt tpm2_create -C primary.ctx -u sealed.pub -r sealed.priv -i secret.txt tpm2_load -C primary.ctx -u sealed.pub -r sealed.priv -c sealed.ctx tpm2_unseal -c sealed.ctx -o unsealed.txt cat unsealed.txt
tpm2_create מתקשר עם ה-vTPM כדי ליצור את האובייקט הקריפטוגרפי הרצוי.
- -C
primary.ctx: משתמש בהקשר של המפתח הראשי שיצרנו קודם. - -u
sealed.pub: מאחסן את החלק הציבורי של מפתח האיטום (שנדרש לביטול האיטום) ב-sealed.pub. - -r
sealed.priv: מאחסן את החלק הפרטי של מפתח החתימה ב-sealed.priv. - -i
secret.txt: הקובץ שמכיל את הסוד שצריך לאטום.
tpm2_load: טוען את מפתח החתימה ל-TPM באמצעות החלקים הציבורי והפרטי (sealed.pub, sealed.priv) ושומר את ההקשר שלו ב-sealed.ctx.
tpm2_unseal: פענוח (ביטול האיטום) של נתונים שהוצפנו (נאטמו) בעבר באמצעות אובייקט איטום של vTPM.
cat unsealed.txt: מוצגת ההודעה הסודית שלא הוצפנה כדי לאשר שהתהליך הצליח.
שימו לב שאי אפשר להשתמש בקובצי primary.ctx ו-sealed.priv ביותר ממכשיר אחד עם vTPM. כל מי שיש לו גישה למכשיר vTPM ולקבצים האלה יכול לגשת לנתונים המוצפנים. אפשר להשתמש במדיניות לגבי ערכי PCR כדי להצפין נתונים, אבל זה לא חלק מההיקף של ה-codelab הזה.
8. פריסת עומסי עבודה (workloads) לביצוע אימות חומרה: Intel TDX ו-AMD SEV-SNP

בשלב הזה, יוצרים עומס עבודה שמבוסס על Intel TDX או על AMD SEV-SNP ומבצעים אימות ברמת החומרה בצמתים של GKE Confidential. כפי שמוצג בתרשים הארכיטקטורה שלמעלה, נשתמש בתוסף למכשיר האורח כדי לאחזר הצעות מחיר לאימות עבור ארכיטקטורות Intel TDX ו-AMD SEV-SNP. כדי להריץ את הפקודות, עוברים אל Cloud Console או אל סביבת הפיתוח המקומית.
בוחרים את ארכיטקטורת ה-Confidential Computing – Intel TDX או AMD SEV-SNP – שרוצים לבדוק.
1) הוספת מאגר צמתים של Intel TDX
gcloud container node-pools create tdx-pool \
--cluster=${CLUSTER_NAME} \
--zone=${ZONE} \
--machine-type=c3-standard-4 \
--num-nodes=1 \
--enable-confidential-nodes \
--confidential-node-type=tdx
2). הוספת מאגר צמתים של AMD SEV-SNP
gcloud container node-pools create snp-pool \
--cluster=${CLUSTER_NAME} \
--zone=${ZONE} \
--machine-type=n2d-standard-2 \
--num-nodes=1 \
--enable-confidential-nodes \
--confidential-node-type=sev_snp \
--min-cpu-platform="AMD Milan"
3). מחיקת מאגר הצמתים שמוגדר כברירת מחדל (אופציונלי, אבל מומלץ כדי לחסוך בעלויות ולמנוע הפרעות)
gcloud container node-pools delete default-pool \
--cluster=${CLUSTER_NAME} \
--zone=${ZONE} \
--quiet
(הערה: אם כבר השלמתם את הקטע בנושא vTPM קודם לכן, המאגר הזה כבר נמחק ואפשר להתעלם בבטחה מכל שגיאה מסוג 'לא נמצא' שמופיעה כאן).
4). מפעילים את תוסף המכשיר כדי לאפשר לאשכול CGKE לחשוף עומסי עבודה במכשירים אורחים עם אימות חומרה. אנחנו משתמשים בתוסף למכשיר Kubernetes כדי ליצור משאבים חדשים (intel.com/tdx ו-amd.com/sev-snp). כל עומס עבודה שמשויך למשאבים האלה יוכל לגשת למכשירי האורח בצומת העובד.
gcloud container clusters get-credentials ${CLUSTER_NAME} --zone ${ZONE} --project ${PROJECT_ID}
kubectl apply -f https://raw.githubusercontent.com/google/cc-device-plugin/main/manifests/cc-device-plugin.yaml
הפקודה הבאה מאפשרת לראות את האפליקציה cc-device-plugin שנפרסה.
kubectl get pods -A | grep "cc-device-plugin"
הערה: במקרה של אשכול GKE במצב מעורב (עם צמתי עובד של Confidential GKE וגם כאלה שלא), מומלץ שהאופרטור יפרוס את cc-device-plugin רק בצמתי העובד של Confidential GKE.
9. אימות חומרה: Intel TDX ו-AMD SEV-SNP
בקטע הזה נסביר איך לבצע אימות ברמת החומרה בתוך צומתי GKE סודיים על ידי אחזור ציטוטי אימות לארכיטקטורות Intel TDX ו-AMD SEV-SNP באמצעות התוספים של מכשיר האורח.
1) יוצרים אפליקציית Go כדי לאחזר את הצעת המחיר של אימות החומרה ממכשיר האורח הרלוונטי.
cat << 'EOF' > main.go
package main
import (
"crypto/rand"
"log"
"os"
"strconv"
"time"
"unsafe"
sevclient "github.com/google/go-sev-guest/client"
"golang.org/x/sys/unix"
)
// TDX Linux ABI Definitions
const (
iocTdxGetReport = 0xc4405401
)
type tdxReportReq struct {
ReportData [64]byte
TdReport [1024]byte
}
func main() {
ccType := os.Getenv("CC_TYPE") // Expected "tdx" or "snp"
iterationsStr := os.Getenv("ITERATIONS")
iterations := 100
if val, err := strconv.Atoi(iterationsStr); err == nil && val > 0 {
iterations = val
}
successCount := 0
failureCount := 0
startTime := time.Now()
log.Printf("Starting Attestation test for CC_TYPE: %s (iterations: %d)", ccType, iterations)
if ccType == "tdx" {
devicePath := "/dev/tdx_guest"
file, err := os.OpenFile(devicePath, os.O_RDWR|os.O_SYNC, 0)
if err != nil {
log.Fatalf("FATAL: Failed to open %s: %v", devicePath, err)
}
defer file.Close()
for i := 1; i <= iterations; i++ {
var reportData [64]byte
rand.Read(reportData[:])
req := tdxReportReq{ReportData: reportData}
_, _, errno := unix.Syscall(unix.SYS_IOCTL, file.Fd(), uintptr(iocTdxGetReport), uintptr(unsafe.Pointer(&req)))
if errno != 0 || (req.TdReport[0] == 0 && req.TdReport[10] == 0) {
failureCount++
} else {
successCount++
}
}
} else if ccType == "snp" {
device, err := sevclient.OpenDevice()
if err != nil {
log.Fatalf("FATAL: Failed to open SEV-SNP device: %v", err)
}
defer device.Close()
for i := 1; i <= iterations; i++ {
var reportData [64]byte
rand.Read(reportData[:])
report, err := sevclient.GetRawReport(device, reportData)
if err != nil || len(report) == 0 {
failureCount++
} else {
successCount++
}
}
}
elapsed := time.Since(startTime)
log.Printf("=== Test Completed ===")
log.Printf("Total Iterations: %d", iterations)
log.Printf("Success: %d, Failures: %d", successCount, failureCount)
log.Printf("Total Time: %s", elapsed)
}
EOF
2). יוצרים את Dockerfile עבור כלי הבדיקה של אימות החומרה:
cat << 'EOF' > Dockerfile FROM golang:1.26.4 AS builder WORKDIR /app COPY main.go . RUN go mod init cc-attestation RUN go mod tidy RUN CGO_ENABLED=0 GOOS=linux go build -o /cc-attestation-check main.go FROM ubuntu:22.04 COPY --from=builder /cc-attestation-check /usr/local/bin/ CMD ["/usr/local/bin/cc-attestation-check"] EOF
3). יוצרים את קובץ האימג' ומעבירים אותו בדחיפה ל-Artifact Registry:
docker build -t us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/cc-attestation-check:latest .
docker push us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/cc-attestation-check:latest
4). יוצרים את קובץ ה-YAML לפריסת האפליקציה עבור עומסי עבודה של Intel TDX ו-AMD SEV-SNP כדי לחשוף את מכשירי החומרה.
- עבור עומסי עבודה של Intel TDX, יוצרים את קובץ ה-YAML הבא של הפריסה:
cat << EOF > deploy-tdx.yaml
apiVersion: v1
kind: Pod
metadata:
name: tdx-attestation-pod
spec:
restartPolicy: Never
containers:
- name: tdx-container
image: us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/cc-attestation-check:latest
env:
- name: CC_TYPE
value: "tdx"
- name: ITERATIONS
value: "1000"
resources:
limits:
intel.com/tdx: 1
nodeSelector:
cloud.google.com/gke-confidential-nodes-instance-type: "TDX"
cloud.google.com/machine-family: "c3"
EOF
- עבור עומסי עבודה של AMD SEV-SNP, יוצרים את קובץ ה-YAML הבא של הפריסה:
cat << EOF > deploy-snp.yaml
apiVersion: v1
kind: Pod
metadata:
name: snp-attestation-pod
spec:
restartPolicy: Never
containers:
- name: snp-container
image: us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/cc-attestation-check:latest
env:
- name: CC_TYPE
value: "snp"
- name: ITERATIONS
value: "100"
resources:
limits:
amd.com/sev-snp: 1
nodeSelector:
cloud.google.com/gke-confidential-nodes-instance-type: "SEV_SNP"
cloud.google.com/machine-family: "n2d"
EOF
5). החלת הפריסות על אשכול CGKE.
kubectl apply -f deploy-tdx.yaml kubectl apply -f deploy-snp.yaml
6). מחכים לסיום הפעולה של ה-pods ומאמתים את היומנים של אימות החומרה.
קודם כל, בודקים את הסטטוס של ה-pods כדי לוודא שהם מתוזמנים ופועלים בהצלחה (צריך להמתין עד שהסטטוס שלהם יהיה Completed או Running):
kubectl get pods -w
אחרי שה-pods מוכנים, בודקים את היומנים כדי לוודא שהציטוטים של אימות החומרה אוחזרו בהצלחה:
# View the TDX attestation logs kubectl logs tdx-attestation-pod # View the SEV-SNP attestation logs kubectl logs snp-attestation-pod
(בקטע היומן יופיעו זמן הביצוע הכולל, מספר האיטרציות הכולל ומספר ההצלחות והכישלונות).
10. הסרת המשאבים
כדי להימנע מחיובים שוטפים בחשבון Google Cloud, חשוב למחוק את המשאבים שנוצרו ב-codelab הזה.
מריצים את הפקודות הבאות ב-Cloud Shell או בסביבת הפיתוח המקומית.
1) הגדרת משתני סביבה:
export PROJECT_ID="your-project-id"
export ZONE="us-central1-c"
export CLUSTER_NAME="cgke-attestation-codelab"
gcloud config set project ${PROJECT_ID}
2). מחיקת משאבי GKE
gcloud container clusters delete ${CLUSTER_NAME} \
--zone=${ZONE} \
--quiet
3). מחיקת Artifact Registry
gcloud artifacts repositories delete codelab-repo \
--location=us \
--quiet
4). מחיקה של מכונה וירטואלית של שרת אינטרנט
gcloud compute instances delete cgke-attestation-codelab-web-server \
--zone=${ZONE}
5). מחיקת משאבי IAM
# Delete the GCP service account
gcloud iam service-accounts delete codelab-csa@${PROJECT_ID}.iam.gserviceaccount.com \
--quiet
# Delete the custom IAM role
gcloud iam roles delete Confidential_Computing_Workload_User \
--project=${PROJECT_ID} \
--quiet