1. نظرة عامة
تضمن عقد Confidential GKE (CGKE) تشفير البيانات المستخدَمة في أحجام العمل. يتيح عرض جهاز vTPM لأحمال عمل CGKE استخدام ميزات vTPM. في هذا الدرس التطبيقي حول الترميز، ستتعرّف على ميزات vTPM وكيفية الاستفادة من مصادقة أجهزة Intel TDX وAMD SEV-SNP باستخدام cc-device-plugin.
- تتيح عملية المصادقة عن بُعد لجهاز vTPM لجهة بعيدة التحقّق من أنّ عُقد CGKE التي تستضيف أحجام العمل تعمل على الأجهزة الافتراضية السرية (CVM).
- تفويض vTPM وإغلاق vTPM
- المصادقة على الأجهزة باستخدام أجهزة الضيوف التي تستخدم Intel TDX وAMD SEV-SNP

كما هو موضّح في الشكل أعلاه، يتضمّن الجزء الأول من هذا الدرس التطبيقي حول الترميز الخطوات التالية:
- تُعدّ عقد CGKE وتتيح جهاز vTPM لأحجام العمل المحدّدة.
- نشر حمل عمل وإثبات صحة العقدة التي تستضيف حمل العمل عن بُعد في CGKE
- يمكنك نشر عبء عمل لجلب "اقتباسات المصادقة" للأجهزة مباشرةً من أجهزة TDX أو SEV-SNP.
- إعداد خادم الويب لإصدار Secret

كما هو موضّح في الشكل أعلاه، يتضمّن الجزء الثاني من هذا الدرس العملي ما يلي:
- إعداد تفويض vTPM وإغلاق vTPM على عُقد CGKE

كما هو موضّح في الشكل أعلاه، يتضمّن الجزء الثالث من هذا الدرس العملي ما يلي:
- كيفية ربط
cc-device-pluginبشكل آمن بالأجهزة خلال مرحلة الإعداد - نشر أحمال العمل لجلب "اقتباسات شهادة الأجهزة" مباشرةً من أجهزة TDX أو SEV-SNP خلال "مرحلة وقت التشغيل" بدون أي تكلفة إضافية
ما ستتعلمه
- كيفية عرض جهاز vTPM لأحجام عمل CGKE
- كيفية إجراء المصادقة عن بُعد من خلال Confidential Computing API (خدمة Attestation Verifier) على أحجام عمل CGKE
- كيفية إعداد تفويض vTPM وإجراء عملية إغلاق vTPM
- كيفية الوصول إلى أجهزة SEV-SNP وTDX في أحمال عمل CGKE باستخدام
cc-device-plugin - كيفية الحصول على "اقتباسات شهادة المصادقة" للأجهزة باستخدام go-tdx-guest وgo-sev-guest
المتطلبات
- مشروع على Google Cloud Platform
- متصفّح، مثل Chrome أو Firefox
- معرفة أساسية بخدمة Google Compute Engine (درس تطبيقي حول الترميز) وConfidential VM وعُقد Confidential GKE وArtifact Registry
- بالنسبة إلى Intel TDX أو AMD SEV-SNP: الحد الأدنى لإصدار GKE هو v1.33.5-gke.1697000+ أو v1.34.1-gke.2909000+ وسلاسل الآلات المناسبة (c3 لـ TDX وn2d لـ SNP).
2. الإعداد والمتطلبات
لتفعيل واجهات برمجة التطبيقات اللازمة، نفِّذ الأمر التالي في "وحدة تحكّم Google 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: المصادقة عن بُعد والإغلاق لجهاز vTPM (AMD SEV): انتقِل مباشرةً إلى إعداد عُقد CGKE وعرض جهاز vTPM.
- الخيار 2: إثبات صحة الأجهزة (Intel TDX وAMD SEV-SNP): انتقِل مباشرةً إلى نشر أحمال العمل لإجراء عملية إثبات صحة الأجهزة. (هل تريد استكشاف كل شيء؟ ما عليك سوى إكمال الخيار 1 والمتابعة بالتسلسل إلى الخيار 2)
4. إعداد عُقد CGKE وإتاحة جهاز vTPM لأحجام العمل المحدّدة
في هذه الخطوة، يمكنك إنشاء مجموعة أجهزة ذات التخصيص نفسه لـ vTPM على CGKE وتطبيق مكوّن إضافي للجهاز لعرض جهاز CVM vTPM على أحمال العمل. انتقِل إلى 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 في الوضع المختلط (مع عقد عاملة في GKE في الوضع السرّي وغير السرّي)، يُنصح المشغّل بنشر cc-device-plugin على عقد عاملة في 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- نشر عبء عمل وإجراء مصادقة عن بُعد على عبء العمل (جهاز vTPM)
في هذه الخطوة، يمكنك إنشاء عبء عمل ونشره في مجموعة CGKE التي أنشأتها في الخطوة السابقة، وإجراء عملية إثبات صحة عن بُعد باستخدام vTPM لاسترداد رمز إثبات الصحة (رمز OIDC) على عقدة العامل.
1. أنشئ صورة حاوية التطبيق وادفعها إلى Artifact Registry. تحتوي صورة حاوية التطبيق على أداة go-tpm، التي يمكنها جمع أدلة التصديق وإرسالها إلى خدمة Attestation Verifier للحصول على رمز مميّز للتصديق (رمز مميّز لاتصال OpenID).
- أنشئ
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 لاكتساب أذونات حساب خدمة Google Cloud Platform على موارد Google Cloud Platform
- أنشئ حساب خدمة Kubernetes
codelab-ksa.
kubectl create serviceaccount codelab-ksa \
--namespace default
- أنشئ دورًا
Confidential_Computing_Workload_Userوامنح الدور أذونات الوصول إلى واجهات برمجة التطبيقات في الحوسبة السرية.
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
- أنشئ حساب خدمة على Google Cloud Platform
codelab-csaواربطه بالدورConfidential_Computing_Workload_User. لكي يحصلcodelab-csaعلى أذونات للوصول إلى واجهات برمجة التطبيقات الخاصة بـ "الحوسبة السرية".
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بحساب خدمة Google Cloud Platformcodelab-csa. لكي يحصلcodelab-ksaعلى أذونات للوصول إلى واجهات برمجة التطبيقات الخاصة بـ "الحوسبة السرية".
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
في هذه الخطوة، ستخرج من جلسة SSH السابقة وتُعدّ جهازًا افتراضيًا آخر. على هذا الجهاز الظاهري، يمكنك إعداد خادم ويب لإصدار سري. يتحقّق خادم الويب من صحة رمز الإثبات والبيانات التي يتضمّنها. في حال نجاح عمليات التحقّق، يتم تمرير كلمة المرور إلى مقدّم الطلب.
1. انتقِل إلى وحدة تحكّم السحابة الإلكترونية أو بيئة التطوير المحلية. أنشئ جهازًا افتراضيًا.
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. ابدأ جلسة أخرى في علامة تبويب Cloud Console أو بيئة تطوير محلية ونفِّذ الأمر التالي. سيؤدي ذلك إلى الحصول على 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 الداخلي لمثيل الجهاز الافتراضيcgke-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
- ابدأ المهمة التي يتم تنفيذها لمرة واحدة. تضبط هذه المهمة عبارة مرور مالك وحدة TPM الافتراضية على جميع عُقد العامل.
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 لتشفير البيانات، ولكنّ ذلك خارج نطاق هذا الدرس العملي.
8. نشر أحمال العمل لإجراء شهادة صحة الأجهزة: Intel TDX وAMD SEV-SNP

في هذه الخطوة، يمكنك إنشاء عبء عمل يستند إلى Intel TDX أو AMD SEV-SNP وإجراء عملية إثبات صحة على مستوى الأجهزة على عقد GKE السرية. كما هو موضّح في مخطط البنية أعلاه، سنستخدم إضافة الجهاز الخاصة بالضيف لجلب عروض الشهادات لبنيتَي Intel TDX وAMD SEV-SNP. انتقِل إلى Cloud Console أو بيئة التطوير المحلية لتنفيذ الأوامر.
اختَر بنية الحوسبة السرية التي تريد اختبارها، وهي 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 في الوضع المختلط (مع عقد عاملة في GKE Confidential وغير Confidential)، يُنصح المشغّل بنشر cc-device-plugin على عقد عاملة في GKE Confidential فقط.
9. شهادة صحة الجهاز: Intel TDX وAMD SEV-SNP
في هذا القسم، ستتعرّف على كيفية إجراء عملية إثبات صحة على مستوى الأجهزة داخل عُقد Confidential 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. انتظِر حتى تكتمل وحدات Pod وتحقَّق من سجلّات إثبات صحة الأجهزة.
في البداية، تحقَّق من حالة الحلقات للتأكّد من أنّه تمّت جدولتها وتشغيلها بنجاح (انتظِر إلى أن تظهر الحالة "مكتملة" أو "قيد التشغيل"):
kubectl get pods -w
بعد أن تصبح وحدات Pod جاهزة، راجِع السجلات للتأكّد من أنّه تم جلب عروض الأسعار الخاصة بإثبات صحة الجهاز بنجاح:
# View the TDX attestation logs kubectl logs tdx-attestation-pod # View the SEV-SNP attestation logs kubectl logs snp-attestation-pod
(سيظهر لك إجمالي وقت التنفيذ وإجمالي عدد التكرارات وعدد حالات النجاح/التعذّر التي تم تسجيلها).
10. تنظيف
لتجنُّب فرض رسوم مستمرة على حسابك على Google Cloud، من المهم حذف الموارد التي تم إنشاؤها في هذا الدرس العملي.
نفِّذ الأوامر التالية في 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