إجراءات المصادقة عن بُعد والإغلاق على عُقد من Confidential GKE (وحدات النظام الأساسي الافتراضية الموثوقة (vTPM) وSEV-SNP وTDX)

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

683a3b43587ef69f.png

كما هو موضّح في الشكل أعلاه، يتضمّن الجزء الأول من هذا الدرس التطبيقي حول الترميز الخطوات التالية:

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

8f6e80c762a5d911.png

كما هو موضّح في الشكل أعلاه، يتضمّن الجزء الثاني من هذا الدرس العملي ما يلي:

  • إعداد تفويض vTPM وإغلاق vTPM على عُقد CGKE

cc_device_plugin_concept.png

كما هو موضّح في الشكل أعلاه، يتضمّن الجزء الثالث من هذا الدرس العملي ما يلي:

  • كيفية ربط 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

المتطلبات

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 جاهزَين. اختَر مسارك:

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).

  1. أنشئ 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
  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 لاكتساب أذونات حساب خدمة Google Cloud Platform على موارد Google Cloud Platform

  1. أنشئ حساب خدمة Kubernetes codelab-ksa.
kubectl create serviceaccount codelab-ksa \
    --namespace default
  1. أنشئ دورًا 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
  1. أنشئ حساب خدمة على 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]"
  1. اربط حساب خدمة Kubernetes codelab-ksa بحساب خدمة Google Cloud Platform codelab-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

  1. أنشِئ صورة حاوية لوظيفة يتم تنفيذها لمرة واحدة. تضبط المهمة التي يتم تنفيذها لمرة واحدة كلمة مرور المالك لجميع وحدات 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
  1. أنشئ صورة حاوية للوظيفة التي يتم تنفيذها لمرة واحدة وادفعها إلى Artifact Registry.
docker build -t us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/tpm-tools:latest .
docker push us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/tpm-tools:latest
  1. نفِّذ المهمة لمرة واحدة من خلال مهمة 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
  1. ابدأ المهمة التي يتم تنفيذها لمرة واحدة. تضبط هذه المهمة عبارة مرور مالك وحدة 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.

  1. أنشئ ملف 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
  1. انشر التطبيق التجريبي.
kubectl apply -f deploy_demo.yaml

‫4. تنفيذ عملية إغلاق vTPM في حاوية التطبيق التجريبي

  1. اتّصِل بحاوية التطبيق التجريبي واضبط مفتاحًا أساسيًا باستخدام عبارة مرور.
kubectl exec -it tpm-tools-demo -- /bin/bash
tpm2_createprimary -C o -c primary.ctx -P $(cat /etc/tpmsecret/passphrase)

يتفاعل tpm2_createprimary مع vTPM لإنشاء العنصر الأساسي استنادًا إلى التسلسل الهرمي والنموذج المحدّدَين.

  • ‫-C o: يشير إلى أنّه سيتم إنشاء المفتاح الأساسي ضمن التسلسل الهرمي لمالك وحدة TPM.
  • ‫‎-c primary.ctx: يحفظ سياق العنصر الأساسي الذي تم إنشاؤه (المعرّف والبيانات المرتبطة به) في الملف primary.ctx. هذا السياق ضروري للعمليات اللاحقة.

لا يمكن أن تستخدم عبارة المرور الخاصة بالمالك الخاطئ لإنشاء مفتاح أساسي.

tpm2_createprimary -C o -P wrong_passphrase

يعرض الأمر الأخطاء التالية:

WARNING:esys:src/tss2-esys/api/Esys_CreatePrimary.c:401:Esys_CreatePrimary_Finish() Received TPM Error
ERROR:esys:src/tss2-esys/api/Esys_CreatePrimary.c:135:Esys_CreatePrimary() Esys Finish ErrorCode (0x000009a2)
ERROR: Esys_CreatePrimary(0x9A2) - tpm:session(1):authorization failure without DA implications
ERROR: Unable to run tpm2_createprimary
  1. يمكن بعد ذلك استخدام المفتاح الأساسي الذي تم إنشاؤه لإغلاق البيانات وفتحها.
echo "This is my secret message" > secret.txt
tpm2_create -C primary.ctx -u sealed.pub -r sealed.priv -i secret.txt
tpm2_load -C primary.ctx -u sealed.pub -r sealed.priv -c sealed.ctx
tpm2_unseal -c sealed.ctx -o unsealed.txt
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

cc_device_plugin_arch.png

في هذه الخطوة، يمكنك إنشاء عبء عمل يستند إلى 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

11. الخطوات التالية

مزيد من المعلومات حول Confidential GKE Nodes