Gizli GKE düğümlerinde uzaktan doğrulama ve mühürleme (vTPM, SEV-SNP, TDX)

1. Genel Bakış

Gizli GKE (CGKE) düğümleri, iş yüklerindeki verilerin kullanım sırasında şifrelenmesini sağlar. vTPM cihazının CGKE iş yüklerine sunulması, iş yüklerinin vTPM özelliklerini kullanmasına olanak tanır. Bu codelab'de, vTPM'nin özelliklerini ve cc-device-plugin kullanarak Intel TDX ve AMD SEV-SNP donanım onayından nasıl yararlanacağınızı öğreneceksiniz.

  • vTPM uzaktan doğrulama, uzaktaki bir tarafın iş yüklerini barındıran CGKE düğümlerinin Gizli Sanal Makineler (CVM) üzerinde çalıştığını doğrulamasını sağlar.
  • vTPM yetkilendirmesi ve vTPM mühürleme.
  • Açığa çıkarılmış Intel TDX ve AMD SEV-SNP konuk cihazlarını kullanarak donanım onayı.

683a3b43587ef69f.png

Yukarıdaki şekilde gösterildiği gibi, bu codelab'in ilk bölümünde aşağıdaki adımlar yer alır:

  • CGKE düğümleri, vTPM cihazını kurar ve seçili iş yüklerine sunar.
  • Bir iş yükü dağıtın ve iş yükünü barındıran CGKE düğümünü uzaktan onaylayın.
  • Donanım onay alıntılarını doğrudan TDX veya SEV-SNP cihazlarından getirmek için bir iş yükü dağıtın.
  • Secret Release Web Server kurulumu.

8f6e80c762a5d911.png

Yukarıdaki şekilde gösterildiği gibi, bu codelab'in ikinci bölümünde şunlar yer almaktadır:

  • CGKE düğümlerinde vTPM yetkilendirme kurulumu ve vTPM mühürleme.

cc_device_plugin_concept.png

Yukarıdaki şekilde gösterildiği gibi, bu codelab'in üçüncü bölümünde şunlar yer almaktadır:

  • cc-device-plugin, kurulum aşamasında donanım cihazlarını güvenli bir şekilde nasıl eşler?
  • Donanım tasdik alıntılarını doğrudan TDX veya SEV-SNP cihazlarından çalışma zamanı aşamasında sıfır ek yükle getirmek için iş yüklerini dağıtma.

Neler öğreneceksiniz?

  • vTPM cihazı CGKE iş yüklerine nasıl sunulur?
  • CGKE iş yüklerinde Gizli Bilişim API (Attestation Verifier hizmeti) aracılığıyla uzaktan doğrulama yapma
  • vTPM yetkilendirmesi nasıl ayarlanır ve vTPM mühürleme nasıl yapılır?
  • cc-device-plugin kullanarak CGKE iş yüklerinde SEV-SNP ve TDX cihazlarına erişme
  • go-tdx-guest ve go-sev-guest kullanarak donanım tasdik alıntıları nasıl alınır?

Gerekenler

  • Google Cloud Platform projesi
  • Chrome veya Firefox gibi bir tarayıcı
  • Google Compute Engine (codelab), Gizli Sanal Makine, Gizli GKE düğümleri ve Artifact Registry hakkında temel bilgiler
  • Intel TDX veya AMD SEV-SNP için: Minimum GKE sürümü v1.33.5-gke.1697000+ veya v1.34.1-gke.2909000+ ve uygun makine aileleri (TDX için c3, SNP için n2d).

2. Kurulum ve Gereksinimler

Gerekli API'leri etkinleştirmek için bulut konsolunda veya yerel geliştirme ortamınızda aşağıdaki komutu çalıştırın:

gcloud auth login

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

3. Paylaşılan CGKE kontrol düzlemini ayarlama

Bu adımda, paylaşılan GKE kontrol düzlemini ve bir Artifact Registry Docker deposunu ayarlarsınız.

1). Ortam değişkenlerini ayarlayın ve paylaşılan GKE kümesini oluşturun:

your-project-id yerine proje kimliğinizi yazın. us-central1-c yerine istediğiniz bölgeyi yazın. (Bkz. Bölgeler ve alt bölgeler)

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

gcloud config set project ${PROJECT_ID}

gcloud container clusters create ${CLUSTER_NAME} \
    --zone=${ZONE} \
    --num-nodes=1 \
    --machine-type=e2-medium \
    --workload-pool=${PROJECT_ID}.svc.id.goog \
    --workload-metadata=GKE_METADATA

gcloud container clusters get-credentials ${CLUSTER_NAME} --zone=${ZONE}

2). İş yükü container görüntülerini depolamak için bir Artifact Registry Docker deposu oluşturun:

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

Yolunuzu seçin

Paylaşılan GKE kontrol düzlemi ve Artifact Registry deposu artık hazır. Yolunuzu seçin:

4. CGKE düğümlerini ayarlama ve vTPM cihazını belirli iş yüklerine sunma

Bu adımda, CGKE'de bir vTPM düğüm havuzu oluşturur ve CVM vTPM cihazını iş yüklerine sunmak için bir cihaz eklentisi uygularsınız. Komutları çalıştırmak için Cloud Console'a veya yerel geliştirme ortamınıza gidin.

1). AMD SEV düğüm havuzu ekleme

gcloud container node-pools create sev-pool \
    --cluster=${CLUSTER_NAME} \
    --zone=${ZONE} \
    --machine-type=n2d-standard-2 \
    --num-nodes=1 \
    --enable-confidential-nodes \
    --confidential-node-type=sev

2). Varsayılan düğüm havuzunu silme (isteğe bağlıdır ancak maliyetten/gürültüden tasarruf etmek için önerilir)

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

3). CGKE kümesinin vTPM cihazını iş yüklerine sunmasına izin vermek için cihaz eklentisini başlatın. Yeni kaynaklar (google.com/cc) oluşturmak için kubernetes cihaz eklentisi kullanırız. Yeni kaynakla ilişkili tüm iş yükleri, çalışan düğümündeki cihazları görebilir.

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

Aşağıdaki komut, dağıtılan cc-device-plugin öğesini görmenizi sağlar.

kubectl get pods -A | grep "cc-device-plugin"

Not: Karma modlu bir GKE kümesi (hem Confidential hem de Confidential olmayan GKE çalışma düğümleriyle) söz konusu olduğunda, operatörün yalnızca Confidential GKE çalışma düğümlerine cc-device-plugin dağıtması önerilir.

(İsteğe bağlı). CGKE pod'u Prometheus izlemesini uygulayın. İzlemeyi etkinleştirdiğinizde cihaz eklentisinin durumunu gözlemleyebilirsiniz.

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 adresine gidin ve cc-device-plugin metriklerini bulun veya PROMQL'yi kullanın. Örneğin, aşağıdaki PROMQL komutu her cc-device-plugin işlemi için CPU saniyelerini gösterir.

rate(process_cpu_seconds_total[${__interval}])

5. İş yükü dağıtma ve iş yükünde uzaktan doğrulama gerçekleştirme (vTPM)

Bu adımda, önceki adımda oluşturduğunuz CGKE kümesinde bir iş yükü oluşturup dağıtacak ve çalışma düğümünde bir onay jetonu (OIDC jetonu) almak için vTPM uzaktan onaylama işlemi gerçekleştireceksiniz.

1). Uygulama container görüntüsünü oluşturun ve Artifact Registry'ye aktarın. Uygulama kapsayıcı görüntüsü, onay kanıtı toplayıp Onay Jetonu (bir OIDC jetonu) için Onay Doğrulayıcı hizmetine gönderebilen go-tpm aracını içerir.

  1. Uygulama kapsayıcı görüntüsü için Dockerfile oluşturun.
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. Uygulama container görüntüsünü Artifact Registry'ye aktarın.
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). GCP kaynaklarında bir GCP hizmet hesabının izinlerini devralmak için bir Kubernetes hizmet hesabı oluşturun.

  1. Kubernetes hizmet hesabı oluşturun codelab-ksa.
kubectl create serviceaccount codelab-ksa \
    --namespace default
  1. Bir rol oluşturun Confidential_Computing_Workload_User ve role, Confidential Computing API'lerine erişim izni verin.
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. Bir GCP hizmet hesabı oluşturun codelab-csa ve bu hesabı rol ile bağlayın Confidential_Computing_Workload_User. codelab-csa, Gizli Bilgi İşlem API'lerine erişmek için gerekli izinlere sahip olmalıdır.
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 hizmet hesabını codelab-ksa GCP hizmet hesabıyla codelab-csa bağlayın. codelab-ksa, Gizli Bilgi İşlem API'lerine erişmek için gerekli izinlere sahip olmalıdır.
kubectl annotate serviceaccount codelab-ksa \
    --namespace default \
    iam.gke.io/gcp-service-account=codelab-csa@${PROJECT_ID}.iam.gserviceaccount.com

3). Demo uygulaması için uygulama dağıtımı YAML'sini oluşturun. Kubernetes hizmet hesabını codelab-ksa seçili iş yüklerine atayın.

cat << EOF > deploy.yaml
apiVersion: v1
kind: Pod
metadata:
  name: go-tpm-demo
  labels:
    app.kubernetes.io/name: go-tpm-demo
spec:
  serviceAccountName: codelab-ksa
  nodeSelector:
    iam.gke.io/gke-metadata-server-enabled: "true"
    cloud.google.com/machine-family: "n2d" 
    cloud.google.com/gke-confidential-nodes-instance-type: "SEV"
  containers:
  - name: go-tpm
    image: us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/go-tpm:latest
    resources:
      limits:
        google.com/cc: 1
EOF

4). Dağıtımı CGKE kümesine uygulayın.

kubectl apply -f deploy.yaml

5). İş yüküne bağlanın ve bir doğrulama jetonu (OIDC jetonu) almak için uzaktan doğrulamayı başlatın.

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

6). Jetonu ekrana yazdırın, böylece kopyalayabilirsiniz.

cat attestation_token

Hak taleplerini görüntülemek için jwt.io adresinde onay jetonunun kodunu çözebilirsiniz.

6. Secret Release web sunucusunu ayarlama

Bu adımda, önceki SSH oturumundan çıkıp başka bir sanal makine oluşturacaksınız. Bu sanal makinede, gizli sürüm web sunucusu ayarlarsınız. Web sunucusu, alınan onay jetonunu ve jetonun taleplerini doğrular. Doğrulamalar başarılı olursa gizli anahtar, istekte bulunan kullanıcıya iletilir.

1). Cloud Console'a veya yerel geliştirme ortamınıza gidin. Sanal makine oluşturun.

gcloud config set project ${PROJECT_ID}

gcloud compute instances create cgke-attestation-codelab-web-server \
    --machine-type=n2d-standard-2 \
    --zone=${ZONE} \
    --image-family=ubuntu-2204-lts \
    --image-project=ubuntu-os-cloud

2). Yeni sanal makinenize SSH ile bağlanın.

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

3). Go ortamını ayarlayın.

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

4). Gizli sürüm web sunucusunun kaynak kodunu depolayan aşağıdaki iki dosyayı oluşturun.

Oluşturma 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

Oluşturma helper.go:

cat << 'EOF' > helper.go
package main

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	return file, nil
}

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

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

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

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

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

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

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

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

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

5). Web sunucusunu oluşturup çalıştırmak için aşağıdaki komutları çalıştırın. Bu işlem, :8080 bağlantı noktasında gizli yayın web sunucusunu başlatır.

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

Sorun giderme: go mod tidy çalıştırılırken aşağıdaki uyarıyı görebilirsiniz. Bu uyarıyı göz ardı edebilirsiniz:

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). Başka bir Cloud Console sekmesi veya yerel geliştirme ortamı oturumu başlatın ve aşağıdaki komutu çalıştırın. Bu işlem, cgke-attestation-codelab-web-server-internal-ip almanızı sağlar.

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

7). CGKE iş yükünüze bağlanın ve bir doğrulama jetonu (OIDC jetonu) getirmek için uzaktan doğrulamayı başlatın. Ardından, attestation-token ve cgke-attestation-codelab-web-server-internal-ip içeriğini aşağıdaki komuta yerleştirin. Bu işlem, gizli sürüm web sunucusunda tutulan gizli anahtarı getirir.

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

Aşağıdakini değiştirin:

  • cgke-attestation-codelab-web-server-internal-ip, cgke-attestation-codelab-web-server sanal makine örneğinin dahili IP'sidir.

7. CGKE düğümlerinde vTPM mühürleme

Bu adımda, CGKE düğümlerinde vTPM sahibi yetkilendirmesini ayarlayacak ve vTPM sahibi parola ifadesiyle bir iş yükü dağıtacaksınız. Ardından, iş yükündeki verileri vTPM mühürleme özelliğiyle mühürlemek ve mühürlerini açmak için bir vTPM birincil anahtarı oluşturursunuz.

1). CGKE düğümlerinde vTPM sahibi yetkilendirmesini ayarlayın.

  1. Tek seferlik iş kapsayıcı görüntüsü oluşturun. Tek seferlik iş, tüm vTPM'ler için sahip şifresini ayarlar. Aşağıda, kapsayıcı görüntüsü oluşturmak için kullanılan Dockerfile verilmiştir.
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. Tek seferlik iş container görüntüsünü oluşturup Artifact Registry'ye aktarın.
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. Tek seferlik işi bir Kubernetes işi aracılığıyla yürütün. (UYARI: Bu iş, her CVM'de vTPM'yi temizler. CVM'niz diski şifrelemek için vTPM kullanıyorsa bu iş, yeniden başlatma işleminden sonra CVM'nizi kullanılamaz hale getirir. Diskinizde FSTYPE crypto_LUKS olup olmadığını lsblk -f komutuyla kontrol edebilirsiniz.
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

İpucu: tpm-tools-task işini yeniden çalıştırmanız gerekirse önce mevcut işi sildiğinizden emin olun:

kubectl delete job tpm-tools-task
  1. Tek seferlik işi başlatın. Bu iş, tüm çalışan düğümlerinde vTPM sahibi geçiş ifadesini ayarlar.
kubectl apply -f tpm-tools-task.yaml

2). vTPM sahibi parolasını tutmak için bir Kubernetes gizli dizisi oluşturun.

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

3). Bir demo uygulama kapsayıcısı oluşturun ve parolayı bu kapsayıcıya iletin. Demo uygulama kapsayıcısı, vTPM ile etkileşim kurmak için tpm2-tools'u içerir.

  1. Demo uygulama container'ı için dağıtım YAML dosyasını oluşturun.
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. Demo uygulamasını dağıtın.
kubectl apply -f deploy_demo.yaml

4). Demo uygulama container'ında vTPM mühürleme işlemini gerçekleştirin.

  1. Demo uygulama kapsayıcısına bağlanın ve parola ile birincil anahtar ayarlayın.
kubectl exec -it tpm-tools-demo -- /bin/bash
tpm2_createprimary -C o -c primary.ctx -P $(cat /etc/tpmsecret/passphrase)

tpm2_createprimary, belirtilen hiyerarşiye ve şablona göre birincil nesneyi oluşturmak için vTPM ile etkileşime girer.

  • -C o: Birincil anahtarın, TPM'nin sahip hiyerarşisi altında oluşturulacağını gösterir.
  • -c primary.ctx: Oluşturulan birincil nesnenin bağlamını (işleyici ve ilişkili veriler) primary.ctx dosyasına kaydeder. Bu bağlam, sonraki işlemler için gereklidir.

İş yükü, birincil anahtar oluşturmak için yanlış sahip parolasını kullanamaz.

tpm2_createprimary -C o -P wrong_passphrase

Komut aşağıdaki hataları döndürüyor:

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. Oluşturulan birincil anahtar, verileri mühürlemek ve mühürlerini açmak için kullanılabilir.
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, istenen şifreleme nesnesini oluşturmak için vTPM ile etkileşime girer.

  • -C primary.ctx: Daha önce oluşturduğumuz birincil anahtar bağlamını kullanır.
  • -u sealed.pub: Mühürleme anahtarının herkese açık bölümünü (mühürün açılması için gereklidir) sealed.pub içinde saklar.
  • -r sealed.priv: Mühürleme anahtarının özel kısmını sealed.priv içinde saklar.
  • -i secret.txt: Mühürlenecek gizli bilgiyi içeren dosya.

tpm2_load: Mühürleme anahtarını, genel ve özel kısımları (sealed.pub, sealed.priv) kullanarak TPM'ye yükler ve bağlamını sealed.ctx konumuna kaydeder.

tpm2_unseal: Daha önce vTPM mühürleme nesnesi kullanılarak şifrelenmiş (mühürlenmiş) verilerin şifresini çözün (mühürünü açın).

cat unsealed.txt: İşlemin başarılı olduğunu onaylamak için mühürlenmemiş gizli mesajı gösterir.

primary.ctx ve sealed.priv dosyalarının yalnızca bir vTPM cihazda kullanılabileceğini unutmayın. vTPM cihazına ve bu dosyalara erişimi olan herkes de mühürlenmiş verilere erişebilir. Verileri mühürlemek için PCR değerleriyle ilgili politikayı da kullanabilirsiniz ancak bu codelab'in kapsamı dışındadır.

8. Donanım doğrulama gerçekleştirmek için iş yüklerini dağıtma: Intel TDX ve AMD SEV-SNP

cc_device_plugin_arch.png

Bu adımda, Intel TDX veya AMD SEV-SNP tabanlı bir iş yükü oluşturup Confidential GKE düğümlerinde donanım düzeyinde bir onaylama işlemi gerçekleştirirsiniz. Yukarıdaki mimari diyagramında gösterildiği gibi, Intel TDX ve AMD SEV-SNP mimarileri için onay alıntılarını getirmek üzere konuk cihaz eklentisini kullanacağız. Komutları çalıştırmak için Cloud Console'a veya yerel geliştirme ortamınıza gidin.

Test etmek istediğiniz Gizli Bilişim mimarisini (Intel TDX veya AMD SEV-SNP) seçin.

1). Intel TDX düğüm havuzu ekleme

gcloud container node-pools create tdx-pool \
    --cluster=${CLUSTER_NAME} \
    --zone=${ZONE} \
    --machine-type=c3-standard-4 \
    --num-nodes=1 \
    --enable-confidential-nodes \
    --confidential-node-type=tdx

2). AMD SEV-SNP düğüm havuzu ekleme

gcloud container node-pools create snp-pool \
    --cluster=${CLUSTER_NAME} \
    --zone=${ZONE} \
    --machine-type=n2d-standard-2 \
    --num-nodes=1 \
    --enable-confidential-nodes \
    --confidential-node-type=sev_snp \
    --min-cpu-platform="AMD Milan"

3). Varsayılan düğüm havuzunu silme (isteğe bağlıdır ancak maliyetten/gürültüden tasarruf etmek için önerilir)

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

(Not: vTPM bölümünü daha önce tamamladıysanız bu havuz zaten silinmiştir ve buradaki "Bulunamadı" hatalarını güvenle yoksayabilirsiniz.)

4). CGKE kümesinin, donanım onaylı misafir cihazları iş yüklerine sunmasına izin vermek için cihaz eklentisini başlatın. Yeni kaynaklar (intel.com/tdx ve amd.com/sev-snp) oluşturmak için kubernetes cihaz eklentisi kullanırız. Bu kaynaklarla ilişkili tüm iş yükleri, çalışan düğümündeki konuk cihazlara erişebilir.

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

Aşağıdaki komut, dağıtılan cc-device-plugin öğesini görmenizi sağlar.

kubectl get pods -A | grep "cc-device-plugin"

Not: Karma mod GKE kümesi (hem Confidential hem de Confidential olmayan GKE çalışma düğümleriyle) durumunda, operatörün yalnızca Confidential GKE çalışma düğümlerine cc-device-plugin dağıtması önerilir.

9. Donanım Onayı: Intel TDX ve AMD SEV-SNP

Bu bölümde, konuk cihaz eklentilerini kullanarak Intel TDX ve AMD SEV-SNP mimarileri için onay alıntıları getirerek gizli GKE düğümlerinde donanım düzeyinde onay işlemini nasıl gerçekleştireceğinizi öğreneceksiniz.

1). Donanım onayı alıntısını ilgili konuk cihazdan getirmek için bir Go uygulaması oluşturun.

cat << 'EOF' > main.go
package main

import (
	"crypto/rand"
	"log"
	"os"
	"strconv"
	"time"
	"unsafe"

	sevclient "github.com/google/go-sev-guest/client"
	"golang.org/x/sys/unix"
)

// TDX Linux ABI Definitions
const (
	iocTdxGetReport = 0xc4405401
)

type tdxReportReq struct {
	ReportData [64]byte
	TdReport   [1024]byte
}

func main() {
	ccType := os.Getenv("CC_TYPE") // Expected "tdx" or "snp"

	iterationsStr := os.Getenv("ITERATIONS")
	iterations := 100
	if val, err := strconv.Atoi(iterationsStr); err == nil && val > 0 {
		iterations = val
	}

	successCount := 0
	failureCount := 0
	startTime := time.Now()
	
	log.Printf("Starting Attestation test for CC_TYPE: %s (iterations: %d)", ccType, iterations)

	if ccType == "tdx" {
		devicePath := "/dev/tdx_guest"
		file, err := os.OpenFile(devicePath, os.O_RDWR|os.O_SYNC, 0)
		if err != nil {
			log.Fatalf("FATAL: Failed to open %s: %v", devicePath, err)
		}
		defer file.Close()

		for i := 1; i <= iterations; i++ {
			var reportData [64]byte
			rand.Read(reportData[:])
			req := tdxReportReq{ReportData: reportData}
			
			_, _, errno := unix.Syscall(unix.SYS_IOCTL, file.Fd(), uintptr(iocTdxGetReport), uintptr(unsafe.Pointer(&req)))
			if errno != 0 || (req.TdReport[0] == 0 && req.TdReport[10] == 0) {
				failureCount++
			} else {
				successCount++
			}
		}
	} else if ccType == "snp" {
		device, err := sevclient.OpenDevice()
		if err != nil {
			log.Fatalf("FATAL: Failed to open SEV-SNP device: %v", err)
		}
		defer device.Close()

		for i := 1; i <= iterations; i++ {
			var reportData [64]byte
			rand.Read(reportData[:])
			report, err := sevclient.GetRawReport(device, reportData)
			if err != nil || len(report) == 0 {
				failureCount++
			} else {
				successCount++
			}
		}
	}

	elapsed := time.Since(startTime)
	log.Printf("=== Test Completed ===")
	log.Printf("Total Iterations: %d", iterations)
	log.Printf("Success: %d, Failures: %d", successCount, failureCount)
	log.Printf("Total Time: %s", elapsed)
}
EOF

2). Donanım tasdik testi aracı için Dockerfile oluşturun:

cat << 'EOF' > Dockerfile
FROM golang:1.26.4 AS builder
WORKDIR /app
COPY main.go .
RUN go mod init cc-attestation
RUN go mod tidy
RUN CGO_ENABLED=0 GOOS=linux go build -o /cc-attestation-check main.go

FROM ubuntu:22.04
COPY --from=builder /cc-attestation-check /usr/local/bin/
CMD ["/usr/local/bin/cc-attestation-check"]
EOF

3). Görüntüyü oluşturup Artifact Registry'nize aktarın:

docker build -t us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/cc-attestation-check:latest .
docker push us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/cc-attestation-check:latest

4). Donanım cihazlarını kullanıma sunmak için hem Intel TDX hem de AMD SEV-SNP iş yükleri için uygulama dağıtımı YAML'si oluşturun.

  • Intel TDX iş yükleri için aşağıdaki dağıtım YAML'sini oluşturun:
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 iş yükleri için aşağıdaki dağıtım YAML'sini oluşturun:
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). Dağıtımları CGKE kümesine uygulayın.

kubectl apply -f deploy-tdx.yaml
kubectl apply -f deploy-snp.yaml

6). Pod'ların tamamlanmasını bekleyin ve donanım onay günlüklerini doğrulayın.

Öncelikle, podlarınızın durumunu kontrol ederek başarıyla planlandığından ve çalıştığından emin olun (Tamamlandı veya Çalışıyor olarak gösterilene kadar bekleyin):

kubectl get pods -w

Pod'lar hazır olduğunda, donanım tasdik alıntılarının başarıyla getirildiğini doğrulamak için günlükleri kontrol edin:

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

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

(Toplam yürütme süresi, toplam yineleme sayısı ve başarı/başarısızlık sayıları günlüğe kaydedilir.)

10. Temizleme

Google Cloud hesabınızın sürekli olarak ücretlendirilmesini önlemek için bu codelab'de oluşturulan kaynakları silmeniz önemlidir.

Cloud Shell'de veya yerel geliştirme ortamınızda aşağıdaki komutları çalıştırın.

1). Ortam değişkenlerini ayarlayın:

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

gcloud config set project ${PROJECT_ID}

2). GKE kaynaklarını silme

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

3). Artifact Registry'yi silme

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

4). Web sunucusu VM'sini silme

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

5). IAM kaynaklarını silme

# Delete the GCP service account
gcloud iam service-accounts delete codelab-csa@${PROJECT_ID}.iam.gserviceaccount.com \
    --quiet

# Delete the custom IAM role
gcloud iam roles delete Confidential_Computing_Workload_User \
    --project=${PROJECT_ID} \
    --quiet

11. Sırada ne var?

Gizli GKE düğümleri hakkında daha fazla bilgi edinin.