Poświadczenia zdalne i uszczelnianie poufnych węzłów GKE (vTPM, SEV-SNP, TDX)

1. Przegląd

Węzły poufnej usługi GKE (CGKE) zapewniają, że dane w zbiorach zadań są szyfrowane podczas używania. Udostępnienie urządzenia vTPM zadaniom CGKE umożliwia im korzystanie z funkcji vTPM. Z tego ćwiczenia dowiesz się, jakie są funkcje vTPM, a także jak korzystać z poświadczeń sprzętowych Intel TDX i AMD SEV-SNP za pomocą cc-device-plugin.

  • Poświadczenia zdalne vTPM umożliwiają zdalnej stronie weryfikację, czy węzły CGKE hostujące zbiory zadań działają na poufnych maszynach wirtualnych (CVM).
  • autoryzacja vTPM i uszczelnianie vTPM.
  • Poświadczanie sprzętowe przy użyciu udostępnionych urządzeń gościa Intel TDX i AMD SEV-SNP.

683a3b43587ef69f.png

Jak widać na powyższym rysunku, pierwsza część tego laboratorium obejmuje te kroki:

  • Węzły CGKE konfigurują i udostępniają urządzenie vTPM wybranym zbiorom zadań.
  • Wdrażanie zbioru zadań i zdalne atestowanie węzła CGKE, w którym jest on hostowany.
  • Wdrażanie zbioru zadań w celu pobierania cytatów atestu sprzętu bezpośrednio z urządzeń TDX lub SEV-SNP.
  • Konfiguracja serwera WWW do udostępniania obiektu tajnego.

8f6e80c762a5d911.png

Jak widać na powyższym rysunku, druga część tego laboratorium obejmuje:

  • konfigurowanie autoryzacji vTPM i uszczelniania vTPM na węzłach CGKE;

cc_device_plugin_concept.png

Jak widać na powyższym rysunku, trzecia część tego laboratorium obejmuje:

  • Jak cc-device-plugin bezpiecznie mapuje urządzenia podczas fazy konfiguracji.
  • Wdrażanie zbiorów zadań w celu pobierania cytatów atestu sprzętu bezpośrednio z urządzeń TDX lub SEV-SNP w fazie działania bez żadnych dodatkowych kosztów.

Czego się nauczysz

  • Jak udostępnić urządzenie vTPM zbiorom zadań CGKE.
  • Jak przeprowadzić zdalne poświadczenie za pomocą interfejsu Confidential Computing API (usługi weryfikacji poświadczeń) w przypadku zadań CGKE.
  • Jak skonfigurować autoryzację vTPM i przeprowadzić uszczelnianie vTPM.
  • Jak uzyskać dostęp do urządzeń SEV-SNP i TDX w zbiorach zadań CGKE za pomocą cc-device-plugin.
  • Jak pobierać cytaty atestów sprzętowych za pomocą pakietów go-tdx-guestgo-sev-guest.

Czego potrzebujesz

2. Konfiguracja i wymagania

Aby włączyć niezbędne interfejsy API, uruchom to polecenie w konsoli Google Cloud lub w lokalnym środowisku programistycznym:

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. Konfigurowanie współdzielonej platformy sterującej CGKE

W tym kroku skonfigurujesz udostępnioną platformę sterującą GKE i repozytorium Dockera w Artifact Registry.

1) Skonfiguruj zmienne środowiskowe i utwórz współdzielony klaster GKE:

Zastąp your-project-id identyfikatorem projektu. Zastąp us-central1-c wybraną strefą. (Patrz Regiony i strefy)

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) Utwórz repozytorium Dockera w Artifact Registry, w którym będziesz przechowywać obrazy kontenerów obciążeń:

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

Wybierz ścieżkę

Udostępniona platforma sterująca GKE i repozytorium Artifact Registry są już gotowe. Wybierz ścieżkę:

4. Konfigurowanie węzłów CGKE i udostępnianie urządzenia vTPM wybranym zbiorom zadań

W tym kroku utworzysz pulę węzłów vTPM w CGKE i zastosujesz wtyczkę urządzenia, aby udostępnić urządzenie vTPM CVM obciążeniom. Aby uruchomić polecenia, otwórz konsolę Google Cloud lub lokalne środowisko programistyczne.

1) Dodawanie puli węzłów AMD SEV

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

2) Usuń domyślną pulę węzłów (opcjonalnie, ale zalecane, aby zaoszczędzić koszty i zmniejszyć szum)

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

3) Uruchom wtyczkę urządzenia, aby umożliwić klastrowi CGKE udostępnianie urządzenia vTPM zbiorom zadań. Do tworzenia nowych zasobów (google.com/cc) używamy wtyczki do urządzenia Kubernetes. Każdy zbiór zadań powiązany z nowym zasobem będzie mógł zobaczyć urządzenia w węźle roboczym.

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

Poniższe polecenie umożliwia wyświetlenie wdrożonego cc-device-plugin.

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

Uwaga: w przypadku klastra GKE w trybie mieszanym (z poufnymi i niepoufnych węzłami roboczymi GKE) zalecamy, aby operator wdrażał cc-device-plugin tylko na poufnych węzłach roboczych GKE.

Opcjonalnie: Zastosuj monitorowanie usługi Prometheus w przypadku poda CGKE. Włączenie monitorowania umożliwia obserwowanie stanu wtyczki urządzenia.

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

Otwórz https://console.cloud.google.com/monitoring/metrics-explorer i znajdź dane cc-device-plugin lub użyj PROMQL. Na przykład to polecenie PROMQL pokazuje sekundy procesora dla każdego procesu cc-device-plugin:

rate(process_cpu_seconds_total[${__interval}])

5. Wdrażanie zbioru zadań i przeprowadzanie zdalnego atestu zbioru zadań (vTPM)

W tym kroku utworzysz i wdrożysz zbiór zadań w klastrze CGKE utworzonym w poprzednim kroku oraz przeprowadzisz zdalne atestowanie vTPM, aby pobrać token atestu (token OIDC) w węźle roboczym.

1) Utwórz obraz kontenera aplikacji i prześlij go do Artifact Registry. Obraz kontenera aplikacji zawiera narzędzie go-tpm, które może zbierać dowody atestu i wysyłać je do usługi weryfikacji atestu w celu uzyskania tokena atestu (tokena OIDC).

  1. Utwórz Dockerfile dla obrazu kontenera aplikacji.
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. Prześlij obraz kontenera aplikacji do 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) Skonfiguruj konto usługi Kubernetes, aby dziedziczyło uprawnienia konta usługi GCP w zasobach GCP.

  1. Utwórz konto usługi Kubernetes codelab-ksa.
kubectl create serviceaccount codelab-ksa \
    --namespace default
  1. Utwórz rolę Confidential_Computing_Workload_User i przyznaj jej uprawnienia dostępu do interfejsów API Confidential Computing.
gcloud iam roles create Confidential_Computing_Workload_User --project=${PROJECT_ID} \
    --title="CGKE Workload User" --description="Grants the ability to generate an attestation token in a GKE workload." \
    --permissions="confidentialcomputing.challenges.create,confidentialcomputing.challenges.verify,confidentialcomputing.locations.get,confidentialcomputing.locations.list" --stage=GA
  1. Utwórz konto usługi GCP codelab-csa i powiąż je z rolą Confidential_Computing_Workload_User. Dzięki temu codelab-csa będzie mieć uprawnienia dostępu do interfejsów API usługi Confidential Computing.
gcloud iam service-accounts create codelab-csa \
    --project=${PROJECT_ID}

gcloud projects add-iam-policy-binding ${PROJECT_ID} \
    --member "serviceAccount:codelab-csa@${PROJECT_ID}.iam.gserviceaccount.com" \
    --role "projects/${PROJECT_ID}/roles/Confidential_Computing_Workload_User"

gcloud iam service-accounts add-iam-policy-binding codelab-csa@${PROJECT_ID}.iam.gserviceaccount.com \
    --role roles/iam.workloadIdentityUser \
    --member "serviceAccount:${PROJECT_ID}.svc.id.goog[default/codelab-ksa]"
  1. Powiąż konto usługi Kubernetes codelab-ksa z kontem usługi GCP codelab-csa. Dzięki temu codelab-ksa będzie mieć uprawnienia dostępu do interfejsów API usługi Confidential Computing.
kubectl annotate serviceaccount codelab-ksa \
    --namespace default \
    iam.gke.io/gcp-service-account=codelab-csa@${PROJECT_ID}.iam.gserviceaccount.com

3) Utwórz plik YAML wdrożenia aplikacji demonstracyjnej. Przypisz konto usługi Kubernetes codelab-ksa do wybranych obciążeń.

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) Zastosuj wdrożenie w klastrze CGKE.

kubectl apply -f deploy.yaml

5). Połącz się z zadaniami i uruchom zdalne potwierdzanie, aby pobrać token potwierdzenia (token OIDC).

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

6). Wyświetl token na ekranie, aby można go było skopiować.

cat attestation_token

Aby wyświetlić roszczenia, możesz zdekodować token atestu na stronie jwt.io.

6. Konfigurowanie serwera WWW Secret Release

W tym kroku zakończysz poprzednią sesję SSH i skonfigurujesz kolejną maszynę wirtualną. Na tej maszynie wirtualnej skonfigurujesz serwer WWW z tajną wersją. Serwer WWW weryfikuje otrzymany token atestu i jego roszczenia. Jeśli weryfikacja się powiedzie, usługa przekazuje obiekt tajny do osoby wysyłającej żądanie.

1) Otwórz konsolę Google Cloud lub lokalne środowisko programistyczne. Utwórz maszynę wirtualną.

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) Połącz się z nową maszyną wirtualną za pomocą SSH.

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

3) Skonfiguruj środowisko Go.

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

4) Utwórz te 2 pliki zawierające kod źródłowy serwera WWW z tajną wersją.

Utwórz 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

Utwórz 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). Aby utworzyć serwer WWW i go uruchomić, wykonaj te polecenia. Spowoduje to uruchomienie serwera WWW tajnej wersji na porcie :8080.

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

Rozwiązywanie problemów: podczas uruchamiania polecenia go mod tidy może pojawić się to ostrzeżenie, które można zignorować:

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). Otwórz kolejną kartę konsoli w chmurze lub sesję lokalnego środowiska programistycznego i uruchom to polecenie. Otrzymasz cgke-attestation-codelab-web-server-internal-ip.

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

7). Połącz się z zbiorem zadań CGKE i uruchom zdalne potwierdzenie, aby pobrać token atestu (token OIDC). Następnie w tym poleceniu umieść zawartość plików attestation-tokencgke-attestation-codelab-web-server-internal-ip. Spowoduje to pobranie obiektu tajnego przechowywanego na serwerze WWW udostępniającym obiekty tajne.

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

Zastąp te elementy:

  • cgke-attestation-codelab-web-server-internal-ip to wewnętrzny adres IP instancji maszyny wirtualnej cgke-attestation-codelab-web-server.

7. Uszczelnianie vTPM w węzłach CGKE

W tym kroku skonfigurujesz autoryzację właściciela vTPM na węzłach CGKE i wdrożysz zadanie z hasłem właściciela vTPM. Następnie tworzysz główny klucz vTPM, aby uszczelniać i odszyfrowywać dane w zbiorze zadań za pomocą funkcji uszczelniania vTPM.

1) Skonfiguruj autoryzację właściciela vTPM na węzłach CGKE.

  1. Utwórz obraz kontenera jednorazowego zadania. Jednorazowe zadanie ustawia hasło właściciela dla wszystkich modułów vTPM. Poniżej znajdziesz Dockerfile, które umożliwia utworzenie obrazu kontenera.
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. Utwórz obraz kontenera zadania jednorazowego i przenieś go do Artifact Registry.
docker build -t us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/tpm-tools:latest .
docker push us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/tpm-tools:latest
  1. Wykonaj jednorazowe zadanie za pomocą zadania Kubernetes. (OSTRZEŻENIE: to zadanie czyści moduł vTPM na każdej maszynie CVM. Jeśli maszyna CVM używa modułu vTPM do szyfrowania dysku, po ponownym uruchomieniu będzie bezużyteczna. Możesz sprawdzić, czy dysk ma FSTYPE crypto_LUKS za pomocą polecenia lsblk -f.
cat << EOF > tpm-tools-task.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: tpm-tools-task
spec:
  template:
    spec:
      nodeSelector:
        cloud.google.com/machine-family: "n2d"
        cloud.google.com/gke-confidential-nodes-instance-type: "SEV"
      containers:
      - name: tpm-tools
        image: us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/tpm-tools:latest
        command: ["/bin/sh", "-c"]
        args: ["tpm2_clear; tpm2_changeauth -c owner this_is_passphrase"]
        resources:
          limits:
            google.com/cc: 1
      restartPolicy: Never
EOF

Wskazówka: jeśli musisz ponownie uruchomić zadanie tpm-tools-task, najpierw usuń istniejące zadanie:

kubectl delete job tpm-tools-task
  1. Uruchom jednorazowe zadanie. To zadanie ustawia hasło właściciela vTPM we wszystkich węzłach roboczych.
kubectl apply -f tpm-tools-task.yaml

2) Utwórz tajny klucz Kubernetes, aby przechowywać hasło właściciela vTPM.

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

3) Utwórz kontener aplikacji demonstracyjnej i przekaż do niego hasło. Kontener aplikacji demonstracyjnej zawiera tpm2-tools do interakcji z vTPM.

  1. Utwórz plik YAML wdrożenia dla kontenera aplikacji demonstracyjnej.
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. Wdróż aplikację demonstracyjną.
kubectl apply -f deploy_demo.yaml

4) Przeprowadź uszczelnianie vTPM w kontenerze aplikacji demonstracyjnej.

  1. Połącz się z kontenerem aplikacji demonstracyjnej i ustaw klucz podstawowy z hasłem.
kubectl exec -it tpm-tools-demo -- /bin/bash
tpm2_createprimary -C o -c primary.ctx -P $(cat /etc/tpmsecret/passphrase)

tpm2_createprimary wchodzi w interakcję z vTPM, aby wygenerować obiekt podstawowy na podstawie określonej hierarchii i szablonu.

  • -C o: wskazuje, że klucz podstawowy zostanie utworzony w hierarchii właściciela modułu TPM.
  • -c primary.ctx: zapisuje kontekst (uchwyt i powiązane dane) utworzonego obiektu podstawowego w pliku primary.ctx. Ten kontekst jest niezbędny w przypadku późniejszych operacji.

Zbiór zadań nie może używać nieprawidłowego hasła właściciela do utworzenia klucza podstawowego.

tpm2_createprimary -C o -P wrong_passphrase

Polecenie zwraca te błędy:

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. Utworzony klucz podstawowy może być używany do zabezpieczania i odbezpieczania danych.
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 wchodzi w interakcję z vTPM, aby wygenerować żądany obiekt kryptograficzny.

  • -C primary.ctx: używa kontekstu klucza podstawowego utworzonego wcześniej.
  • -u sealed.pub: zapisuje publiczną część klucza szyfrującego (potrzebną do odszyfrowania) w sealed.pub.
  • -r sealed.priv: przechowuje prywatną część klucza szyfrującego w sealed.priv.
  • -i secret.txt: plik zawierający klucz tajny do zaszyfrowania.

tpm2_load: Wczytuje klucz uszczelniający do modułu TPM za pomocą części publicznej i prywatnej (sealed.pub, sealed.priv) i zapisuje jego kontekst w sealed.ctx.

tpm2_unseal: odszyfrowywanie (odpieczętowywanie) danych, które zostały wcześniej zaszyfrowane (zapieczętowane) przy użyciu obiektu pieczętowania vTPM.

cat unsealed.txt: wyświetla odszyfrowaną wiadomość tajną, aby potwierdzić, że proces się powiódł.

Pamiętaj, że pliki primary.ctxsealed.priv można używać tylko na jednym urządzeniu z modułem vTPM. Każda osoba, która ma dostęp do urządzenia vTPM i tych plików, może uzyskać dostęp do zaszyfrowanych danych. Możesz też użyć zasad dotyczących wartości PCR, aby zabezpieczyć dane, ale wykracza to poza zakres tego laboratorium.

8. Wdrażanie zbiorów zadań w celu przeprowadzenia atestu sprzętu: Intel TDX i AMD SEV-SNP

cc_device_plugin_arch.png

W tym kroku utworzysz zadanie oparte na Intel TDX lub AMD SEV-SNP i przeprowadzisz atestowanie na poziomie sprzętu w poufnych węzłach GKE. Jak widać na powyższym schemacie architektury, do pobierania atestów dla architektur Intel TDX i AMD SEV-SNP będziemy używać wtyczki urządzenia gościa. Aby uruchomić polecenia, otwórz konsolę Google Cloud lub lokalne środowisko programistyczne.

Wybierz architekturę Confidential Computing, którą chcesz przetestować: Intel TDX lub AMD SEV-SNP.

1) Dodawanie puli węzłów Intel TDX

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

2) Dodawanie puli węzłów AMD SEV-SNP

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

3) Usuń domyślną pulę węzłów (opcjonalnie, ale zalecane, aby zaoszczędzić koszty i zmniejszyć szum)

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

(Uwaga: jeśli sekcja vTPM została już wcześniej ukończona, pula jest już usunięta i możesz bezpiecznie zignorować wszelkie błędy „Nie znaleziono”).

4) Uruchom wtyczkę urządzenia, aby umożliwić klastrowi CGKE udostępnianie urządzeń gościa z poświadczeniem sprzętowym zadaniom. Do tworzenia nowych zasobów (intel.com/tdxamd.com/sev-snp) używamy wtyczki do urządzeń Kubernetes. Każde zadanie powiązane z tymi zasobami będzie mogło uzyskać dostęp do urządzeń gościa w węźle roboczym.

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

Poniższe polecenie umożliwia wyświetlenie wdrożonego cc-device-plugin.

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

Uwaga: w przypadku klastra GKE w trybie mieszanym (z poufnymi i niepoufnych węzłami roboczymi GKE) zalecamy, aby operator wdrażał cc-device-plugin tylko w poufnych węzłach roboczych GKE.

9. Atestowanie sprzętu: Intel TDX i AMD SEV-SNP

Z tej sekcji dowiesz się, jak przeprowadzać atestowanie na poziomie sprzętu w poufnych węzłach GKE, pobierając cytaty atestacyjne dla architektur Intel TDX i AMD SEV-SNP za pomocą wtyczek urządzenia gościa.

1) Utwórz aplikację w Go, aby pobrać cytat atestu sprzętowego z odpowiedniego urządzenia gościa.

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) Utwórz Dockerfile dla narzędzia do testowania atestu sprzętowego:

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) Skompiluj obraz i przenieś go do Artifact Registry:

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

4) Utwórz plik YAML wdrożenia aplikacji dla zbiorów zadań Intel TDX i AMD SEV-SNP, aby udostępnić urządzenia.

  • W przypadku zadań Intel TDX utwórz ten plik YAML wdrożenia:
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
  • W przypadku zadań AMD SEV-SNP utwórz ten plik YAML wdrożenia:
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). Zastosuj wdrożenia w klastrze CGKE.

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

6). Poczekaj, aż pody zakończą działanie, i sprawdź dzienniki atestu sprzętowego.

Najpierw sprawdź stan podów, aby upewnić się, że zostały zaplanowane i są uruchomione (poczekaj, aż wyświetli się stan Ukończono lub Uruchomiono):

kubectl get pods -w

Gdy pody będą gotowe, sprawdź logi, aby potwierdzić, że cytaty dotyczące atestu sprzętowego zostały pobrane:

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

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

(Zostaną zarejestrowane łączny czas wykonania, łączna liczba iteracji oraz liczba udanych i nieudanych prób).

10. Czyszczenie

Aby uniknąć obciążania konta Google Cloud bieżącymi opłatami, usuń zasoby utworzone w tym laboratorium.

Uruchom te polecenia w Cloud Shell lub w lokalnym środowisku programistycznym.

1) Ustaw zmienne środowiskowe:

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

gcloud config set project ${PROJECT_ID}

2) Usuwanie zasobów GKE

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

3) Usuwanie Artifact Registry

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

4) Usuwanie maszyny wirtualnej serwera WWW

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

5). Usuwanie zasobów IAM

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

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

11. Co dalej?

Dowiedz się więcej o poufnych węzłach GKE.