مصادقة vTPM عن بُعد والمصادقة على عُقد GKE السرية

لمحة عن هذا الدرس التطبيقي حول الترميز
schedule42 دقيقة
subjectتاريخ التعديل الأخير: 19 مايو 2024
account_circleتأليف: Ruide Zhang

تضمن عُقد GKE (CGKE) السرية أن تكون البيانات في أحمال العمل مشفّرة أثناء الاستخدام. عند عرض جهاز vTPM على أحمال عمل CGKE، تتيح لأعباء العمل استخدام ميزات vTPM. في هذا الدرس التطبيقي حول الترميز، يمكنكم التعرّف على ميزتَين في وحدة vTPM.

  • تسمح مصادقة vTPM عن بُعد للطرف البعيد بالتحقق من أن عُقد CGKE التي تستضيف أحمال العمل تعمل على أجهزة افتراضية سرية (CVM).
  • تفويض vTPM وختم vTPM.

683a3b43587ef69f.png

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

  • إعداد عُقد CGKE وعرض جهاز vTPM على أحمال عمل محدّدة
  • نشر عبء عمل وإجراء المصادقة عن بُعد على عقدة CGKE التي تستضيف عبء العمل
  • إعداد خادم الويب ذي الإصدار السري.

8f6e80c762a5d911.png

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

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

المعلومات التي ستطّلع عليها

المتطلبات

2. الإعداد والمتطلبات:

لتفعيل واجهات برمجة التطبيقات اللازمة، شغِّل الأمر التالي في Cloud Console أو بيئة التطوير على الجهاز:

gcloud auth login

gcloud services enable \
    cloudapis.googleapis.com \
    cloudshell.googleapis.com \
    container.googleapis.com \
    containerregistry.googleapis.com \
    confidentialcomputing.googleapis.com \
    iamcredentials.googleapis.com \
    compute.googleapis.com

3- إعداد عُقد CGKE وتعريض جهاز vTPM إلى أحمال عمل محدّدة

تبدأ هذه الخطوة الجزء الأول من هذا الدرس التطبيقي حول الترميز. في هذه الخطوة، تبدأ مجموعة CGKE في تطبيق مكوّن إضافي للجهاز لتعرّض جهاز CVM vTPM إلى أحمال العمل. لتشغيل الأوامر، انتقِل إلى Cloud Console أو بيئة التطوير المحلية.

‫1. يمكنك إنشاء مجموعة CGKE، واستخدام مجمّع المعلومات عن أحمال العمل للسماح لأعباء العمل في CGKE باستخدام واجهة برمجة تطبيقات الحوسبة السرية في Google Cloud Platform. إنّ تجميع المعلومات التعريفية في Workload Identity ضروري لأنّ أحمال العمل في CGKE تحتاج إلى الوصول إلى موارد Google Cloud Platform. للوصول إلى موارد Google Cloud Platform، يجب أن تتوفّر هوية في مهام عمل CGKE.

gcloud container clusters create cgke-attestation-codelab \
    --machine-type=n2d-standard-2        \
    --enable-confidential-nodes \
--zone us-central1-c \
--workload-pool=${PROJECT_ID}.svc.id.goog \
--workload-metadata=GKE_METADATA

استبدِل ما يلي:

  • project-id هو المعرّف الفريد للمشروع.

‫2. ابدأ تشغيل المكوّن الإضافي للجهاز للسماح لمجموعة CGKE بعرض جهاز vTPM في أحمال العمل. نستخدم المكوّن الإضافي لجهاز kubernetes لإنشاء مورد جديد - google.com/cc. سيكون بإمكان أي أعباء عمل مرتبط بالمورد الجديد عرض جهاز vTPM على عقدة العامل.

gcloud container clusters get-credentials cgke-attestation-codelab --zone us-central1-c --project ${PROJECT_ID}

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

استبدِل ما يلي:

  • project-id هو المعرّف الفريد للمشروع.

يتيح لك الأمر التالي الاطّلاع على cc-device-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 التالي ثوانٍ وحدة المعالجة المركزية (CPU) لكل عملية cc-device-plugin.

rate(process_cpu_seconds_total[${__interval}])

4. نشر أعباء عمل وإجراء المصادقة عن بُعد على أعباء العمل

في هذه الخطوة، يمكنك إنشاء ونشر عمل في مجموعة CGKE التي أنشأتها في الخطوة السابقة وإجراء مصادقة عن بُعد لوحدة vTPM لاسترداد رمز المصادقة المميّز (رمز OIDC المميّز) على عقدة العامل.

‫1. أنشِئ صورة حاوية التطبيق وانشرها إلى Artifact Registry. تحتوي صورة حاوية التطبيق على أداة go-tpm التي يمكنها جمع أدلة المصادقة وإرسالها إلى خدمة Attestation Verifier للحصول على الرمز المميّز للمصادقة (رمز OIDC المميز).

  1. أنشئ الملف الشامل لصورة حاوية التطبيق.

Dockerfile

FROM golang:1.21.0 as builder
WORKDIR /
RUN git clone https://github.com/google/go-tpm-tools.git
WORKDIR /go-tpm-tools/cmd/gotpm
RUN CGO_ENABLED=0 GOOS=linux go build -o /gotpm

FROM debian:trixie
WORKDIR /
RUN apt-get update -y
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates
RUN rm -rf /etc/apt/sources.list.d
COPY --from=builder /gotpm /gotpm
CMD ["tail", "-f", "/dev/null"]
  1. قم بإنشاء Artifact Registry.
gcloud artifacts repositories create codelab-repo \
    --repository-format=docker \
    --location=us
  1. أرسِل صورة حاوية التطبيق إلى Artifact Registry.
docker build -t us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/go-tpm:latest .
docker push us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/go-tpm:latest

‫2. يمكنك إعداد حساب خدمة Kubernetes لاكتساب أذونات حساب خدمة Google Cloud Platform (GCP) على موارد 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

استبدِل ما يلي:

  • project-id هو المعرّف الفريد للمشروع.
  1. يمكنك إنشاء حساب خدمة Google Cloud Platform codelab-csa وربطه بالدور Confidential_Computing_Workload_User. So that 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]"

استبدِل ما يلي:

  • project-id هو المعرّف الفريد للمشروع.
  1. اربط حساب خدمة Kubernetes codelab-ksa بحساب خدمة GCP codelab-csa. وبالتالي، يحصل codelab-ksa على أذونات الوصول إلى واجهات برمجة تطبيقات الحوسبة السرية.
kubectl annotate serviceaccount codelab-ksa \
    --namespace default \
    iam.gke.io/gcp-service-account=codelab-csa@<project-id>.iam.gserviceaccount.com

استبدِل ما يلي:

  • project-id هو المعرّف الفريد للمشروع.

‫3. إنشاء ملف yaml لنشر التطبيق للتطبيق التجريبي. يمكنك تخصيص حساب خدمة Kubernetes codelab-ksa لأحمال العمل المحدّدة.

deploy.yaml

apiVersion: v1
kind: Pod
metadata:
  name: go-tpm-demo
  labels:
    app.kubernetes.io/name: go-tpm-demo
spec:
  serviceAccountName: codelab-ksa
  nodeSelector:
    iam.gke.io/gke-metadata-server-enabled: "true"
  containers:
  - name: go-tpm
    image: us-docker.pkg.dev/<project-id>/codelab-repo/go-tpm:latest
    resources:
      limits:
        google.com/cc: 1

استبدِل ما يلي:

  • project-id هو المعرّف الفريد للمشروع.

4. طبِّق عملية النشر على مجموعة CGKE.

kubectl create -f deploy.yaml

5. ربط أعباء العمل وبدء المصادقة عن بُعد لاسترجاع الرمز المميّز للمصادقة (رمز OIDC المميّز)

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

يمكنك فك ترميز الرمز المميّز للمصادقة في jwt.io لعرض المطالبات.

5- إعداد خادم الويب ذي الإصدار السري

في هذه الخطوة، يمكنك الخروج من جلسة بروتوكول النقل الآمن السابقة وإعداد جهاز افتراضي آخر. على هذا الجهاز الافتراضي، يمكنك إعداد خادم ويب للإصدار السري. يتحقّق خادم الويب من الرمز المميّز للتأكيد الذي تم استلامه ومطالباته. إذا نجحت عمليات التحقق من الصحة، يتم تمرير مفتاح سري إلى مقدِّم الطلب.

‫1. انتقِل إلى Cloud Console أو بيئة التطوير المحلي. إنشاء جهاز افتراضي

gcloud config set project <project-id>

gcloud compute instances create cgke-attestation-codelab-web-server \
    --machine-type=n2d-standard-2 \
    --zone=us-central1-c \
    --image=ubuntu-2204-jammy-v20240228 \
    --image-project=ubuntu-os-cloud

استبدِل ما يلي:

  • project-id هو المعرّف الفريد للمشروع.

‫2. بروتوكول النقل الآمن (SSH) إلى جهازك الافتراضي الجديد.

gcloud compute ssh --zone us-central1-c cgke-attestation-codelab-web-server

‫3. إعداد بيئة Go.

wget https://go.dev/dl/go1.22.0.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.22.0.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin

4. أنشئ الملفين التاليَين لتخزين رمز المصدر لخادم ويب الإصدار السري (النسخ واللصق باستخدام nano).

main.go

package main

import (
        "fmt"
        "net/http"
        "strings"
        "time"
        "log"

        "github.com/golang-jwt/jwt/v4"
)

const (
        theSecret = "This is the super secret information!"
)

func homePage(w http.ResponseWriter, r *http.Request) {
        tokenString := r.Header.Get("Authorization")
        if tokenString != "" {
                tokenString, err := extractToken(tokenString)
                if err != nil {
                        http.Error(w, err.Error(), http.StatusUnauthorized)
                }

                tokenBytes := []byte(tokenString)
                // A method to return a public key from the well-known endpoint
                keyFunc := getRSAPublicKeyFromJWKsFile

                token, err := decodeAndValidateToken(tokenBytes, keyFunc)
                if err != nil {
                        http.Error(w, "Invalid JWT Token", http.StatusUnauthorized)
                }

                if ok, err := isValid(token.Claims.(jwt.MapClaims)); ok {
                        fmt.Fprintln(w, theSecret)
                } else {
                        if err != nil {
                                http.Error(w, "Error validating JWT claims: "+err.Error(), http.StatusUnauthorized)
                        } else {
                                http.Error(w, "Invalid JWT token Claims", http.StatusUnauthorized)
                        }
                }
        } else {
                http.Error(w, "Authorization token required", http.StatusUnauthorized)
        }
}

func extractToken(tokenString string) (string, error) {
        if strings.HasPrefix(tokenString, "Bearer ") {
                return strings.TrimPrefix(tokenString, "Bearer "), nil
        }
        return "", fmt.Errorf("invalid token format")
}

func isValid(claims jwt.MapClaims) (bool, error) {
        // 1. Evaluating Standard Claims:
        subject, ok := claims["sub"].(string)
        if !ok {
                return false, fmt.Errorf("missing or invalid 'sub' claim")
        }
        fmt.Println("Subject:", subject)
        // e.g. "sub":"https://www.googleapis.com/compute/v1/projects/<project_id>/zones/<project_zone>/instances/<instance_name>"

        issuedAt, ok := claims["iat"].(float64)
        if !ok {
                return false, fmt.Errorf("missing or invalid 'iat' claim")
        }
        fmt.Println("Issued At:", time.Unix(int64(issuedAt), 0))

        // 2. Evaluating Remote Attestation Claims:
        hwModel, ok := claims["hwmodel"].(string)
        if !ok || hwModel != "GCP_AMD_SEV" {
                return false, fmt.Errorf("missing or invalid 'hwModel'")
        }
        fmt.Println("hwmodel:", hwModel)

        swName, ok := claims["swname"].(string)
        if !ok || swName != "GCE" {
                return false, fmt.Errorf("missing or invalid 'hwModel'")
        }
        fmt.Println("swname:", swName)

        return true, nil
}

func main() {
        http.HandleFunc("/", homePage)
        fmt.Println("Server listening on :8080")
        err := http.ListenAndServe(":8080", nil)
        if err != nil {
                log.Fatalf("Server failed to start: %v", err)
        }
}

helper.go

package main

import (
        "crypto/rsa"
        "encoding/base64"
        "encoding/json"
        "errors"
        "fmt"
        "io"
        "math/big"
        "net/http"

        "github.com/golang-jwt/jwt/v4"
)

const (
        socketPath     = "/run/container_launcher/teeserver.sock"
        expectedIssuer = "https://confidentialcomputing.googleapis.com"
        wellKnownPath  = "/.well-known/openid-configuration"
)

type jwksFile struct {
        Keys []jwk `json:"keys"`
}

type jwk struct {
        N   string `json:"n"`   // "nMMTBwJ7H6Id8zUCZd-L7uoNyz9b7lvoyse9izD9l2rtOhWLWbiG-7pKeYJyHeEpilHP4KdQMfUo8JCwhd-OMW0be_XtEu3jXEFjuq2YnPSPFk326eTfENtUc6qJohyMnfKkcOcY_kTE11jM81-fsqtBKjO_KiSkcmAO4wJJb8pHOjue3JCP09ZANL1uN4TuxbM2ibcyf25ODt3WQn54SRQTV0wn098Y5VDU-dzyeKYBNfL14iP0LiXBRfHd4YtEaGV9SBUuVhXdhx1eF0efztCNNz0GSLS2AEPLQduVuFoUImP4s51YdO9TPeeQ3hI8aGpOdC0syxmZ7LsL0rHE1Q",
        E   string `json:"e"`   // "AQAB" or 65537 as an int
        Kid string `json:"kid"` // "1f12fa916c3a0ef585894b4b420ad17dc9d6cdf5",

        // Unused fields:
        // Alg string `json:"alg"` // "RS256",
        // Kty string `json:"kty"` // "RSA",
        // Use string `json:"use"` // "sig",
}

type wellKnown struct {
        JwksURI string `json:"jwks_uri"` // "https://www.googleapis.com/service_accounts/v1/metadata/jwk/signer@confidentialspace-sign.iam.gserviceaccount.com"

        // Unused fields:
        // Iss                                   string `json:"issuer"`                                // "https://confidentialcomputing.googleapis.com"
        // Subject_types_supported               string `json:"subject_types_supported"`               // [ "public" ]
        // Response_types_supported              string `json:"response_types_supported"`              // [ "id_token" ]
        // Claims_supported                      string `json:"claims_supported"`                      // [ "sub", "aud", "exp", "iat", "iss", "jti", "nbf", "dbgstat", "eat_nonce", "google_service_accounts", "hwmodel", "oemid", "secboot", "submods", "swname", "swversion" ]
        // Id_token_signing_alg_values_supported string `json:"id_token_signing_alg_values_supported"` // [ "RS256" ]
        // Scopes_supported                      string `json:"scopes_supported"`                      // [ "openid" ]
}

func getWellKnownFile() (wellKnown, error) {
        httpClient := http.Client{}
        resp, err := httpClient.Get(expectedIssuer + wellKnownPath)
        if err != nil {
                return wellKnown{}, fmt.Errorf("failed to get raw .well-known response: %w", err)
        }

        wellKnownJSON, err := io.ReadAll(resp.Body)
        if err != nil {
                return wellKnown{}, fmt.Errorf("failed to read .well-known response: %w", err)
        }

        wk := wellKnown{}
        json.Unmarshal(wellKnownJSON, &wk)
        return wk, nil
}

func getJWKFile() (jwksFile, error) {
        wk, err := getWellKnownFile()
        if err != nil {
                return jwksFile{}, fmt.Errorf("failed to get .well-known json: %w", err)
        }

        // Get JWK URI from .wellknown
        uri := wk.JwksURI
        fmt.Printf("jwks URI: %v\n", uri)

        httpClient := http.Client{}
        resp, err := httpClient.Get(uri)
        if err != nil {
                return jwksFile{}, fmt.Errorf("failed to get raw JWK response: %w", err)
        }

        jwkbytes, err := io.ReadAll(resp.Body)
        if err != nil {
                return jwksFile{}, fmt.Errorf("failed to read JWK body: %w", err)
        }

        file := jwksFile{}
        err = json.Unmarshal(jwkbytes, &file)
        if err != nil {
                return jwksFile{}, fmt.Errorf("failed to unmarshall JWK content: %w", err)
        }

        return file, nil
}

// N and E are 'base64urlUInt' encoded: https://www.rfc-editor.org/rfc/rfc7518#section-6.3
func base64urlUIntDecode(s string) (*big.Int, error) {
        b, err := base64.RawURLEncoding.DecodeString(s)
        if err != nil {
                return nil, err
        }
        z := new(big.Int)
        z.SetBytes(b)
        return z, nil
}

func getRSAPublicKeyFromJWKsFile(t *jwt.Token) (any, error) {
        keysfile, err := getJWKFile()
        if err != nil {
                return nil, fmt.Errorf("failed to fetch the JWK file: %w", err)
        }

        // Multiple keys are present in this endpoint to allow for key rotation.
        // This method finds the key that was used for signing to pass to the validator.
        kid := t.Header["kid"]
        for _, key := range keysfile.Keys {
                if key.Kid != kid {
                        continue // Select the key used for signing
                }

                n, err := base64urlUIntDecode(key.N)
                if err != nil {
                        return nil, fmt.Errorf("failed to decode key.N %w", err)
                }
                e, err := base64urlUIntDecode(key.E)
                if err != nil {
                        return nil, fmt.Errorf("failed to decode key.E %w", err)
                }

                // The parser expects an rsa.PublicKey: https://github.com/golang-jwt/jwt/blob/main/rsa.go#L53
                // or an array of keys. We chose to show passing a single key in this example as its possible
                // not all validators accept multiple keys for validation.
                return &rsa.PublicKey{
                        N: n,
                        E: int(e.Int64()),
                }, nil
        }

        return nil, fmt.Errorf("failed to find key with kid '%v' from well-known endpoint", kid)
}

func decodeAndValidateToken(tokenBytes []byte, keyFunc func(t *jwt.Token) (any, error)) (*jwt.Token, error) {
        var err error
        fmt.Println("Unmarshalling token and checking its validity...")
        token, err := jwt.NewParser().Parse(string(tokenBytes), keyFunc)

        fmt.Printf("Token valid: %v\n", token.Valid)
        if token.Valid {
                return token, nil
        }
        if ve, ok := err.(*jwt.ValidationError); ok {
                if ve.Errors&jwt.ValidationErrorMalformed != 0 {
                        return nil, fmt.Errorf("token format invalid. Please contact the Confidential Space team for assistance")
                }
                if ve.Errors&(jwt.ValidationErrorNotValidYet) != 0 {
                        // If device time is not synchronized with the Attestation Service you may need to account for that here.
                        return nil, errors.New("token is not active yet")
                }
                if ve.Errors&(jwt.ValidationErrorExpired) != 0 {
                        return nil, fmt.Errorf("token is expired")
                }
                return nil, fmt.Errorf("unknown validation error: %v", err)
        }

        return nil, fmt.Errorf("couldn't handle this token or couldn't read a validation error: %v", err)
}

5. شغِّل الأوامر التالية لإنشاء خادم الويب وتشغيله. يؤدي هذا إلى بدء خادم الإصدار السري في منفذ :8080.

go mod init google.com/codelab
go mod tidy
go get github.com/golang-jwt/jwt/v4
go build
./codelab

تحديد المشاكل وحلّها: قد يظهر لك التحذير التالي الذي يمكن تجاهله عند تشغيل "go mod tidy:".

go: finding module for package github.com/golang-jwt/jwt/v4
go: downloading github.com/golang-jwt/jwt v3.2.2+incompatible
go: downloading github.com/golang-jwt/jwt/v4 v4.5.0
go: found github.com/golang-jwt/jwt/v4 in github.com/golang-jwt/jwt/v4 v4.5.0
go: google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible: import path "google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible" should not have @version
go: google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible/cmd/jwt: import path "google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible/cmd/jwt" should not have @version
go: google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible/request: import path "google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible/request" should not have @version
go: google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible/test: import path "google.com/codelab/go/pkg/mod/github.com/golang-jwt/jwt@v3.2.2+incompatible/test" should not have @version

6. ابدأ علامة تبويب أخرى في Cloud Console أو جلسة في بيئة تطوير محلية ونفِّذ الأمر التالي. سيؤدي هذا الإجراء إلى الحصول على cgke-attestation-codelab-web-server-internal-ip.

​​gcloud compute instances describe cgke-attestation-codelab-web-server     --format='get(networkInterfaces[0].networkIP)' --zone=us-central1-c

7) الربط بأعباء عمل CGKE وبدء المصادقة عن بُعد لاسترجاع الرمز المميّز للمصادقة (رمز 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 الداخلي لمثيل الجهاز الافتراضي (VM) من cgke-attestation-codelab-web-server.

6. إغلاق vTPM على عُقد CGKE

تبدأ هذه الخطوة الجزء الثاني من هذا الدرس التطبيقي حول الترميز. في هذه الخطوة، يمكنك إعداد تفويض مالك vTPM على عُقد CGKE ونشر عبء عمل باستخدام عبارة مرور مالك vTPM. بعد ذلك، يمكنك إنشاء مفتاح vTPM أساسي لإغلاق البيانات وكشف ختمها في أعباء العمل باستخدام إمكانية إغلاق vTPM.

‫1. إعداد تفويض مالك vTPM على عُقد CGKE

  1. أنشئ صورة حاوية وظائف لمرّة واحدة. تحدّد المهمة التي تُستخدم لمرة واحدة كلمة مرور المالك لجميع وحدات vTPM. فيما يلي ملف Dockerfile لإنشاء صورة الحاوية.

Dockerfile

FROM debian:latest

RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
RUN apt-get update

RUN apt -y install \
  autoconf-archive \
  libcmocka0 \
  libcmocka-dev \
  net-tools \
  build-essential \
  git \
  pkg-config \
  gcc \
  g++ \
  m4 \
  libtool \
  automake \
  libgcrypt20-dev \
  libssl-dev \
  uthash-dev \
  autoconf \
  uuid-dev \
  libcurl4-openssl-dev \
  libjson-c-dev

RUN mkdir /src

WORKDIR /src
RUN git clone https://github.com/tpm2-software/tpm2-tss
WORKDIR /src/tpm2-tss
RUN ./bootstrap
RUN ./configure --prefix=/usr/local
RUN make all install

WORKDIR /src
RUN git clone https://github.com/tpm2-software/tpm2-tools
WORKDIR /src/tpm2-tools
RUN apt-get -y install libcurl4 libcurl4-openssl-dev pandoc man-db
RUN ./bootstrap
RUN ./configure --prefix=/usr/local
RUN make all install

RUN apt-get -y install vim

ENTRYPOINT ["/bin/bash"]
  1. قم بإنشاء صورة حاوية الوظيفة لمرة واحدة وإرسالها إلى سجل العناصر.
docker build -t us-docker.pkg.dev/<project-id>/codelab-repo/tpm-tools:latest .
docker push us-docker.pkg.dev/<project-id>/codelab-repo/tpm-tools:latest

استبدِل ما يلي:

  • project-id هو المعرّف الفريد للمشروع.
  1. تنفيذ الوظيفة لمرة واحدة من خلال وظيفة Kubernetes. (تحذير: تزيل هذه المهمة vTPM على كل CVM، إذا كان CVM يستخدم vTPM لتشفير القرص، فستكون هذه المهمة غير قابلة للاستخدام بعد إعادة التشغيل. يمكنك التحقّق مما إذا كان القرص يحتوي على crypto_LUKS FSTYPE باستخدام الأمر lsblk -f).

tpm-tools-task.yaml

apiVersion: batch/v1
kind: Job
metadata:
  name: tpm-tools-task
spec:
  template:
    spec:
      containers:
      - name: tpm-tools
        image: us-docker.pkg.dev/<project-id>/codelab-repo/tpm-tools:latest
        command: ["/bin/sh", "-c"]
        args: ["tpm2_clear; tpm2_changeauth -c owner this_is_passphrase"]
        resources:
          limits:
            google.com/cc: 1
      restartPolicy: Never

استبدِل ما يلي:

  • project-id هو المعرّف الفريد للمشروع.
  1. إطلاق الوظيفة لمرة واحدة تضبط هذه المهمة عبارة مرور مالك vTPM على جميع عُقد العامل.
kubectl create -f tpm-tools-task.yaml

‫2. أنشِئ مفتاح Kubernetes للاحتفاظ بعبارة مرور مالك vTPM.

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

‫3. أنشِئ حاوية تطبيق تجريبي وأرسِل عبارة المرور إليها. تحتوي حاوية تطبيق العرض التوضيحي على أدوات tpm2 للتفاعل مع vTPM.

  1. أنشئ ملف yaml للنشر لحاوية التطبيق التجريبي.

deploy_demo.yaml

apiVersion: v1
kind: Pod
metadata:
  name: tpm-tools-demo
  labels:
    app.kubernetes.io/name: tpm-tools-demo
spec:
  containers:
  - name: tpm-tools
    image: us-docker.pkg.dev/<project-id>/codelab-repo/tpm-tools:latest
    command: ["tail", "-f", "/dev/null"]
    resources:
      limits:
        google.com/cc: 1
    volumeMounts:
      - name: secret-volume
        mountPath: "/etc/tpmsecret"
        readOnly: true
  volumes:
    - name: secret-volume
      secret:
        secretName: tpm-secret

استبدِل ما يلي:

  • project-id هو المعرّف الفريد للمشروع.
  1. انشر التطبيق التجريبي.
kubectl create -f deploy_demo.yaml

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

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

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

  • -C o: يشير إلى أنه سيتم إنشاء المفتاح الأساسي ضمن التدرّج الهرمي لمالك الوحدة النمطية للنظام الأساسي الموثوق به.
  • -cprimary.%: يحفظ السياق (الاسم المعرِّف والبيانات المرتبطة به) في الكائن الأساسي الذي تم إنشاؤه في الملف basic.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

يتفاعل tpm2_create مع vTPM لإنشاء كائن التشفير المطلوب.

  • -Cprimary.%: يستخدم سياق المفتاح الأساسي الذي أنشأناه سابقًا.
  • -u Sealed.pub: يخزن الجزء العام من مفتاح الإغلاق (المطلوب لإلغاء الختم) في seeled.pub.
  • -r Sealed.priv: يُخزن الجزء الخاص من مفتاح الإغلاق في Seled.priv.
  • -i secret.txt: الملف الذي يحتوي على السر المطلوب إغلاقه.

tpm2_load: لتحميل مفتاح الإغلاق في الوحدة النمطية للنظام الأساسي الموثوق به باستخدام الأجزاء العامة والخاصة (sealed.pub وseled.priv) ويتم حفظ السياق على willled.ctx.

tpm2_unseal: فك تشفير (إلغاء الختم) البيانات التي تم تشفيرها سابقًا (إحكامًا) باستخدام عنصر إغلاق vTPM

ملاحظة: يمكن استخدام ملفَّي primary.ctx وsealed.priv على جهاز vTPM واحد فقط. ويمكن لأي شخص لديه إذن الوصول إلى جهاز vTPM وهذه الملفات الوصول إلى البيانات المغلقة. يمكنك أيضًا استخدام سياسة خاصة بقيم PCR لختم البيانات، ولكنّ هذا ليس موضوع هذا الدرس التطبيقي حول الترميز.

7. تنظيف

شغِّل الأوامر التالية في Cloud Console أو بيئة التطوير المحلية:

gcloud config set project <project-id>

# Delete the CGKE cluster
gcloud container clusters delete cgke-attestation-codelab --zone us-central1-c

# Delete the Artifact Registry
gcloud artifacts repositories delete codelab-repo --location=us

# Delete the web server VM instance
gcloud compute instances delete cgke-attestation-codelab-web-server --zone=us-central1-c

# Delete the GCP service account
gcloud iam service-accounts delete codelab-csa@<project-id>.iam.gserviceaccount.com

# Delete the role
gcloud iam roles delete Confidential_Computing_Workload_User

استبدِل ما يلي:

  • project-id هو المعرّف الفريد للمشروع.

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

يمكنك الاطّلاع على مزيد من المعلومات حول عُقد GKE السرية.