vTPM تأیید از راه دور و مهر و موم کردن گره های GKE محرمانه

۱. مرور کلی

گره‌های GKE محرمانه (CGKE) تضمین می‌کنند که داده‌ها در بارهای کاری رمزگذاری شده و در حال استفاده هستند. قرار دادن دستگاه vTPM در معرض بارهای کاری CGKE به بارهای کاری اجازه می‌دهد تا از ویژگی‌های vTPM استفاده کنند. در این آزمایشگاه کد، دو ویژگی vTPM به شما نمایش داده می‌شود.

  • گواهی از راه دور vTPM به یک طرف از راه دور اجازه می‌دهد تا تأیید کند که گره‌های CGKE که بارهای کاری را میزبانی می‌کنند، روی ماشین‌های مجازی محرمانه (CVM) اجرا می‌شوند.
  • مجوز vTPM و مهر و موم vTPM.

683a3b43587ef69f.png

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

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

8f6e80c762a5d911.png

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

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

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

  • چگونه دستگاه vTPM را در معرض بارهای کاری CGKE قرار دهیم.
  • نحوه‌ی گواهی‌دهی از راه دور از طریق API محاسبات محرمانه (سرویس تأیید گواهی) در بارهای کاری CGKE.
  • نحوه تنظیم مجوز vTPM و انجام مهر و موم vTPM.

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

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

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

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

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

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

۱) یک کلاستر CGKE ایجاد کنید، از مخزن هویت بار کاری استفاده کنید تا به بارهای کاری CGKE اجازه دهید از API محاسبات محرمانه GCP استفاده کنند. مخزن هویت بار کاری ضروری است زیرا بارهای کاری CGKE باید به منابع GCP دسترسی داشته باشند. و برای دسترسی به منابع GCP، بارهای کاری 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 شناسه منحصر به فرد پروژه است.

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

(اختیاری). مانیتورینگ 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}])

۴. استقرار یک بار کاری و انجام گواهی از راه دور روی آن بار کاری

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

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

  1. فایل Dockerfile را برای تصویر کانتینر برنامه ایجاد کنید.

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. یک رجیستری مصنوعات ایجاد کنید.
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

۲) یک حساب سرویس 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

موارد زیر را جایگزین کنید:

  • project-id شناسه منحصر به فرد پروژه است.
  1. یک حساب کاربری سرویس GCP codelab-csa ایجاد کنید و آن را به نقش Confidential_Computing_Workload_User. So that 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]"

موارد زیر را جایگزین کنید:

  • project-id شناسه منحصر به فرد پروژه است.
  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

موارد زیر را جایگزین کنید:

  • project-id شناسه منحصر به فرد پروژه است.

۳) 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 شناسه منحصر به فرد پروژه است.

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

kubectl create -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

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

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

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

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

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 شناسه منحصر به فرد پروژه است.

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

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

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

۴) دو فایل زیر را که کد منبع وب سرور انتشار مخفی را ذخیره می‌کنند، ایجاد کنید (کپی پیست با 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)
}

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

۷) به بار کاری 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 ها تنظیم می‌کند. فایل docker برای ایجاد تصویر کانتینر در ادامه آمده است.

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

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

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

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

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

۴) آب‌بندی 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

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) می‌کند.

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

۷. پاکسازی

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

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 شناسه منحصر به فرد پروژه است.

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

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