گواهی و مهر و موم از راه دور روی گره‌های GKE محرمانه (vTPM، SEV-SNP، TDX)

۱. مرور کلی

گره‌های 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 مستقر کنید.
  • راه‌اندازی سرور وب با انتشار مخفی.

8f6e80c762a5d911.png

همانطور که در شکل بالا نشان داده شده است، بخش دوم این آزمایشگاه کد شامل موارد زیر است:

  • تنظیم مجوز vTPM و آب‌بندی vTPM روی گره‌های CGKE.

cc_device_plugin_concept.png

همانطور که در شکل بالا نشان داده شده است، بخش سوم این آزمایشگاه کد شامل موارد زیر است:

  • چگونه افزونه cc-device-plugin به طور ایمن دستگاه‌های سخت‌افزاری را در طول مرحله راه‌اندازی نگاشت می‌کند.
  • استقرار بارهای کاری برای دریافت گواهی سخت‌افزار، مستقیماً از دستگاه‌های TDX یا SEV-SNP در طول فاز زمان اجرا با سربار صفر.

آنچه یاد خواهید گرفت

  • چگونه دستگاه vTPM را در معرض بارهای کاری CGKE قرار دهیم.
  • نحوه‌ی گواهی‌دهی از راه دور از طریق API محاسبات محرمانه (سرویس تأیید گواهی) در بارهای کاری CGKE.
  • نحوه تنظیم مجوز vTPM و انجام مهر و موم vTPM.
  • نحوه دسترسی به دستگاه‌های SEV-SNP و TDX در بارهای کاری CGKE با استفاده از cc-device-plugin .
  • نحوه دریافت پیش‌فاکتورهای گواهی سخت‌افزار با استفاده از go-tdx-guest و go-sev-guest .

آنچه نیاز دارید

۲. تنظیمات و الزامات

برای فعال کردن API های لازم، دستور زیر را در کنسول ابری یا محیط توسعه محلی خود اجرا کنید:

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

۳. راه‌اندازی صفحه کنترل مشترک CGKE

در این مرحله، صفحه کنترل مشترک GKE و مخزن Docker مربوط به Artifact Registry را راه‌اندازی می‌کنید.

۱) متغیرهای محیطی را تنظیم کنید و خوشه 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}

۲) یک مخزن Docker مربوط به Artifact Registry ایجاد کنید تا ایمیج‌های کانتینر بار کاری را در آن ذخیره کنید:

gcloud artifacts repositories create codelab-repo \
    --repository-format=docker \
    --location=us

مسیر خود را انتخاب کنید

صفحه کنترل مشترک GKE و مخزن Artifact Registry اکنون آماده هستند. مسیر خود را انتخاب کنید:

۴. راه‌اندازی گره‌های CGKE و قرار دادن دستگاه vTPM در معرض بارهای کاری انتخاب‌شده

در این مرحله، شما یک vTPM node pool روی CGKE ایجاد می‌کنید و یک افزونه دستگاه را برای قرار دادن دستگاه CVM vTPM در معرض بارهای کاری اعمال می‌کنید. برای اجرای دستورات به کنسول ابری یا محیط توسعه محلی خود بروید.

۱) یک استخر گره 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

۲) حذف مجموعه گره‌های پیش‌فرض (اختیاری است اما برای صرفه‌جویی در هزینه/نویز توصیه می‌شود)

gcloud container node-pools delete default-pool \
    --cluster=${CLUSTER_NAME} \
    --zone=${ZONE} \
    --quiet

۳). افزونه دستگاه را اجرا کنید تا به خوشه 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 محرمانه مستقر کند.

(اختیاری). مانیتورینگ CGKE pod Prometheus را اعمال کنید. روشن کردن مانیتورینگ به شما امکان می‌دهد وضعیت افزونه دستگاه را مشاهده کنید.

kubectl apply -f https://raw.githubusercontent.com/google/cc-device-plugin/main/manifests/cc-device-plugin-pod-monitoring.yaml

به https://console.cloud.google.com/monitoring/metrics-explorer بروید و معیارهای cc-device-plugin را پیدا کنید یا از PROMQL استفاده کنید. به عنوان مثال، دستور PROMQL زیر، میزان CPU را برای هر فرآیند cc-device-plugin به ثانیه نشان می‌دهد.

rate(process_cpu_seconds_total[${__interval}])

۵. استقرار یک بار کاری و انجام گواهی از راه دور روی بار کاری (vTPM)

در این مرحله، شما یک بار کاری را در کلاستر CGKE که در مرحله قبل ایجاد کرده‌اید، ایجاد و مستقر می‌کنید و یک گواهی از راه دور vTPM را برای بازیابی یک توکن گواهی (OIDC Token) در گره کارگر انجام می‌دهید.

۱) تصویر کانتینر برنامه را ایجاد کرده و آن را به رجیستری مصنوعات (Artifact Registry) ارسال کنید. تصویر کانتینر برنامه شامل ابزار go-tpm است که می‌تواند شواهد گواهی را جمع‌آوری کرده و آن را برای دریافت توکن گواهی (یک توکن OIDC) به سرویس تأییدکننده گواهی ارسال کند.

  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

۲) یک حساب سرویس Kubernetes تنظیم کنید تا مجوزهای یک حساب سرویس GCP را روی منابع GCP به ارث ببرد.

  1. یک حساب کاربری سرویس Kubernetes codelab-ksa ایجاد کنید.
kubectl create serviceaccount codelab-ksa \
    --namespace default
  1. یک نقش Confidential_Computing_Workload_User ایجاد کنید و مجوزهای دسترسی به APIهای Confidential Computing را به این نقش اعطا کنید.
gcloud iam roles create Confidential_Computing_Workload_User --project=${PROJECT_ID} \
    --title="CGKE Workload User" --description="Grants the ability to generate an attestation token in a GKE workload." \
    --permissions="confidentialcomputing.challenges.create,confidentialcomputing.challenges.verify,confidentialcomputing.locations.get,confidentialcomputing.locations.list" --stage=GA
  1. یک حساب کاربری سرویس 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]"
  1. حساب سرویس Kubernetes codelab-ksa به حساب سرویس GCP codelab-csa متصل کنید. به این ترتیب codelab-ksa مجوزهای دسترسی به APIهای محاسبات محرمانه را خواهد داشت.
kubectl annotate serviceaccount codelab-ksa \
    --namespace default \
    iam.gke.io/gcp-service-account=codelab-csa@${PROJECT_ID}.iam.gserviceaccount.com

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

۴) استقرار را به خوشه CGKE اعمال کنید.

kubectl apply -f deploy.yaml

۵) به بار کاری متصل شوید و گواهی از راه دور را برای دریافت یک توکن گواهی (یک توکن OIDC) راه‌اندازی کنید.

kubectl exec -it go-tpm-demo -- /bin/bash
./gotpm token --event-log=/run/cc-device-plugin/binary_bios_measurements > attestation_token

۶) توکن را روی صفحه چاپ کنید تا بتوانید آن را کپی کنید.

cat attestation_token

شما می‌توانید توکن گواهی را در jwt.io رمزگشایی کنید تا ادعاها را مشاهده کنید.

۶. راه‌اندازی وب سرور انتشار مخفی

در این مرحله، از جلسه SSH قبلی خارج می‌شوید و یک ماشین مجازی دیگر راه‌اندازی می‌کنید. روی این ماشین مجازی، یک وب سرور انتشار محرمانه راه‌اندازی می‌کنید. وب سرور، توکن گواهی دریافتی و ادعاهای آن را اعتبارسنجی می‌کند. اگر اعتبارسنجی‌ها موفقیت‌آمیز باشند، آنگاه رمز را به درخواست‌کننده ارسال می‌کند.

۱) به کنسول ابری یا محیط توسعه محلی خود بروید. یک ماشین مجازی ایجاد کنید.

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

۲) به ماشین مجازی جدید خود SSH کنید.

gcloud compute ssh --zone ${ZONE} cgke-attestation-codelab-web-server

۳) محیط 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

۴) دو فایل زیر را ایجاد کنید که کد منبع وب سرور انتشار مخفی را ذخیره می‌کنند.

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

۵) دستورات زیر را برای ساخت و اجرای وب سرور اجرا کنید. این کار وب سرور انتشار مخفی را در پورت :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

۶) یک تب کنسول ابری یا جلسه محیط توسعه محلی دیگر را باز کنید و دستور زیر را اجرا کنید. این دستور cgke-attestation-codelab-web-server-internal-ip برای شما دریافت می‌کند.

gcloud compute instances describe cgke-attestation-codelab-web-server \
    --format='get(networkInterfaces[0].networkIP)' \
    --zone=${ZONE}

۷) به بار کاری 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 است.

۷. آب‌بندی vTPM روی گره‌های CGKE

در این مرحله، شما مجوز مالک vTPM را روی گره‌های CGKE تنظیم می‌کنید و یک بار کاری را با عبارت عبور مالک vTPM مستقر می‌کنید. پس از آن، یک کلید اصلی vTPM ایجاد می‌کنید تا داده‌ها را در بار کاری با قابلیت مهر و موم کردن vTPM مهر و موم کرده و از حالت مهر و موم خارج کنید.

۱) مجوز مالک 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. ایمیج کانتینر کار یکبار مصرف را بسازید و به رجیستری مصنوعات ارسال کنید.
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 شما را پس از راه‌اندازی مجدد غیرقابل استفاده می‌کند. می‌توانید با دستور lsblk -f بررسی کنید که آیا دیسک شما دارای FSTYPE crypto_LUKS است یا خیر)
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

نکته: اگر نیاز به اجرای مجدد job tpm-tools-task دارید، ابتدا مطمئن شوید که job موجود را حذف کرده‌اید:

kubectl delete job tpm-tools-task
  1. کار یک‌باره را اجرا کنید. این کار رمز عبور مالک vTPM را روی تمام گره‌های کارگر تنظیم می‌کند.
kubectl apply -f tpm-tools-task.yaml

۲) یک Kubernetes secret ایجاد کنید تا عبارت عبور مالک vTPM را در خود نگه دارد.

kubectl create secret generic tpm-secret --from-literal=passphrase='this_is_passphrase'

۳) یک کانتینر برنامه آزمایشی ایجاد کنید و عبارت عبور را به آن بدهید. کانتینر برنامه آزمایشی شامل ابزارهای tpm2 برای تعامل با 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

۴) آب‌بندی 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 عبارت عبور اشتباه

دستور خطاهای زیر را برمی‌گرداند:

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 : کلید مهر و موم را با استفاده از بخش‌های عمومی و خصوصی ( sealed.pub ، sealed.priv ) در TPM بارگذاری می‌کند و زمینه آن را در sealed.ctx ذخیره می‌کند.

tpm2_unseal : داده‌هایی را که قبلاً با استفاده از یک شیء مهر و موم vTPM رمزگذاری (مهر و موم شده) بودند، رمزگشایی (unseal) می‌کند.

cat unsealed.txt : پیام مخفی unsealed را برای تأیید موفقیت‌آمیز بودن فرآیند نمایش می‌دهد.

توجه داشته باشید که فایل‌های primary.ctx و sealed.priv فقط در یک دستگاه vTPM قابل استفاده هستند. و هر کسی که به دستگاه vTPM و این فایل‌ها دسترسی داشته باشد، می‌تواند به داده‌های مهر و موم شده دسترسی پیدا کند. می‌توانید از سیاست روی مقادیر PCR برای مهر و موم کردن داده‌ها استفاده کنید، اما این کار خارج از محدوده این آزمایشگاه کد است.

۸. استقرار بارهای کاری برای انجام گواهی سخت‌افزار: Intel TDX و AMD SEV-SNP

افزونه_دستگاه_cc_arch.png

در این مرحله، شما یک بار کاری مبتنی بر Intel TDX یا AMD SEV-SNP ایجاد می‌کنید و یک گواهی سطح سخت‌افزاری را روی گره‌های محرمانه GKE انجام می‌دهید. همانطور که در نمودار معماری بالا نشان داده شده است، ما از افزونه دستگاه مهمان برای دریافت قیمت‌های گواهی برای معماری‌های Intel TDX و AMD SEV-SNP استفاده خواهیم کرد. برای اجرای دستورات به کنسول ابری یا محیط توسعه محلی خود بروید.

معماری محاسبات محرمانه - Intel TDX یا AMD SEV-SNP - را که می‌خواهید آزمایش کنید، انتخاب کنید.

۱) یک مجموعه گره 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

۲) یک مجموعه گره 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"

۳) حذف مجموعه گره‌های پیش‌فرض (اختیاری است اما برای صرفه‌جویی در هزینه/نویز توصیه می‌شود)

gcloud container node-pools delete default-pool \
    --cluster=${CLUSTER_NAME} \
    --zone=${ZONE} \
    --quiet

(توجه: اگر قبلاً بخش vTPM را تکمیل کرده‌اید، این pool از قبل حذف شده است و می‌توانید با خیال راحت هرگونه خطای "یافت نشد" را در اینجا نادیده بگیرید.)

۴). افزونه دستگاه را اجرا کنید تا به خوشه 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 محرمانه و غیرمحرمانه)، توصیه می‌شود که اپراتور فقط cc-device-plugin را روی گره‌های کارگر GKE محرمانه مستقر کند.

۹. گواهی سخت‌افزار: Intel TDX و AMD SEV-SNP

در این بخش، شما یاد خواهید گرفت که چگونه با دریافت قیمت‌های گواهی برای معماری‌های Intel TDX و AMD SEV-SNP با استفاده از افزونه‌های دستگاه مهمان، گواهی سطح سخت‌افزار را درون گره‌های Confidential GKE انجام دهید.

۱) یک برنامه 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

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

۳) ایمیج را بسازید و به رجیستری مصنوعات خود منتقل کنید:

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

۴) برای هر دو بار کاری Intel TDX و AMD SEV-SNP، YAML مربوط به استقرار برنامه را ایجاد کنید تا دستگاه‌های سخت‌افزاری در معرض دید قرار گیرند.

  • برای بارهای کاری 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

۶) منتظر بمانید تا پادها لاگ‌های مربوط به تأیید سخت‌افزار را تکمیل و تأیید کنند.

ابتدا، وضعیت پادها را بررسی کنید تا مطمئن شوید که با موفقیت زمان‌بندی و اجرا می‌شوند (صبر کنید تا گزینه‌های Completed یا Running نمایش داده شوند):

kubectl get pods -w

پس از آماده شدن پادها، لاگ‌ها را بررسی کنید تا تأیید کنید که نقل قول‌های مربوط به گواهی سخت‌افزار با موفقیت دریافت شده‌اند:

# View the TDX attestation logs
kubectl logs tdx-attestation-pod

# View the SEV-SNP attestation logs
kubectl logs snp-attestation-pod

(شما کل زمان اجرا، کل تکرارها و تعداد موفقیت/شکست ثبت شده را مشاهده خواهید کرد.)

۱۰. پاکسازی

برای جلوگیری از هزینه‌های مداوم برای حساب Google Cloud خود، حذف منابع ایجاد شده در این codelab مهم است.

دستورات زیر را در Cloud Shell یا محیط توسعه محلی خود اجرا کنید.

۱) تنظیم متغیرهای محیطی:

export PROJECT_ID="your-project-id"
export ZONE="us-central1-c"
export CLUSTER_NAME="cgke-attestation-codelab"

gcloud config set project ${PROJECT_ID}

۲) منابع GKE را حذف کنید

gcloud container clusters delete ${CLUSTER_NAME} \
    --zone=${ZONE} \
    --quiet

3) رجیستری مصنوعات را حذف کنید

gcloud artifacts repositories delete codelab-repo \
    --location=us \
    --quiet

۴) ماشین مجازی وب سرور را حذف کنید

gcloud compute instances delete cgke-attestation-codelab-web-server \
    --zone=${ZONE}

۵) منابع 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

۱۱. قدم بعدی چیست؟

درباره گره‌های GKE محرمانه بیشتر بدانید.