Remote Attestation und Versiegelung auf Confidential GKE Nodes (vTPM, SEV-SNP, TDX)

1. Übersicht

Confidential GKE-Knoten (CGKE) sorgen dafür, dass Daten in Arbeitslasten während der Nutzung verschlüsselt werden. Wenn Sie das vTPM-Gerät für CGKE-Arbeitslasten verfügbar machen, können diese vTPM-Funktionen nutzen. In diesem Codelab erfahren Sie mehr über die Funktionen von vTPM und wie Sie die Hardware-Attestierung von Intel TDX und AMD SEV-SNP mit dem cc-device-plugin nutzen können.

  • Mit der vTPM-Remote-Attestation kann ein Remote-Partner überprüfen, ob die CGKE-Knoten, auf denen Arbeitslasten gehostet werden, auf Confidential VMs (CVMs) ausgeführt werden.
  • vTPM-Autorisierung und vTPM-Versiegelung.
  • Hardware-Attestierung mit bereitgestellten Intel TDX- und AMD SEV-SNP-Gastgeräten.

683a3b43587ef69f.png

Wie in der Abbildung oben dargestellt, umfasst der erste Teil dieses Codelabs die folgenden Schritte:

  • CGKE-Knoten richten das vTPM-Gerät ein und stellen es für ausgewählte Arbeitslasten bereit.
  • Stellen Sie eine Arbeitslast bereit und führen Sie eine Remote-Attestierung des CGKE-Knotens durch, auf dem die Arbeitslast gehostet wird.
  • Stellen Sie eine Arbeitslast bereit, um Hardware-Attestierungsangebote direkt von TDX- oder SEV-SNP-Geräten abzurufen.
  • Webserver für die Secret-Freigabe einrichten.

8f6e80c762a5d911.png

Wie in der Abbildung oben dargestellt, umfasst der zweite Teil dieses Codelabs Folgendes:

  • Einrichtung der vTPM-Autorisierung und vTPM-Versiegelung auf den CGKE-Knoten.

cc_device_plugin_concept.png

Wie in der Abbildung oben dargestellt, umfasst der dritte Teil dieses Codelabs Folgendes:

  • Wie cc-device-plugin Hardwaregeräte während der Einrichtungsphase sicher zuordnet.
  • Arbeitslasten bereitstellen, um Hardware-Attestierungsangebote während der Laufzeitphase direkt von TDX- oder SEV-SNP-Geräten abzurufen – ohne zusätzlichen Aufwand.

Lerninhalte

  • So stellen Sie das vTPM-Gerät für CGKE-Arbeitslasten bereit.
  • So führen Sie die Remote-Attestierung über die Confidential Computing API (Attestation Verifier-Dienst) für CGKE-Arbeitslasten durch.
  • vTPM-Autorisierung einrichten und vTPM-Versiegelung durchführen
  • So greifen Sie mit dem cc-device-plugin auf SEV-SNP- und TDX-Geräte in CGKE-Arbeitslasten zu.
  • So rufen Sie Hardware-Attestierungsangebote mit go-tdx-guest und go-sev-guest ab.

Voraussetzungen

  • Google Cloud Platform-Projekt
  • Ein Browser, z. B. Chrome oder Firefox
  • Grundkenntnisse in Google Compute Engine (Codelab), Confidential VMs, Confidential GKE-Knoten und Artifact Registry
  • Für Intel TDX oder AMD SEV-SNP: Mindestversion von GKE v1.33.5-gke.1697000+ oder v1.34.1-gke.2909000+ und entsprechende Maschinenfamilien (c3 für TDX, n2d für SNP).

2. Einrichtung und Anforderungen

Führen Sie den folgenden Befehl in der Cloud Console oder in Ihrer lokalen Entwicklungsumgebung aus, um die erforderlichen APIs zu aktivieren:

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. Gemeinsame CGKE-Steuerungsebene einrichten

In diesem Schritt richten Sie die freigegebene GKE-Steuerungsebene und ein Artifact Registry-Docker-Repository ein.

1). Umgebungsvariablen einrichten und den freigegebenen GKE-Cluster erstellen:

Ersetzen Sie your-project-id durch Ihre Projekt-ID. Ersetzen Sie us-central1-c durch die gewünschte Zone. Weitere Informationen finden Sie unter Regionen und Zonen.

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). Erstellen Sie ein Artifact Registry-Docker-Repository zum Speichern von Container-Images für Arbeitslasten:

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

Lernpfad auswählen

Die gemeinsame GKE-Steuerungsebene und das Artifact Registry-Repository sind jetzt bereit. Wählen Sie Ihren Weg:

4. CGKE-Knoten einrichten und das vTPM-Gerät für ausgewählte Arbeitslasten verfügbar machen

In diesem Schritt erstellen Sie einen vTPM-Knotenpool in CGKE und wenden ein Geräte-Plug-in an, um das CVM-vTPM-Gerät für Arbeitslasten verfügbar zu machen. Rufen Sie die Cloud Console oder Ihre lokale Entwicklungsumgebung auf, um die Befehle auszuführen.

1). AMD SEV-Knotenpool hinzufügen

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). Standardknotenpool löschen (optional, aber empfohlen, um Kosten und Rauschen zu reduzieren)

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

3). Starten Sie das Geräte-Plug-in, damit der CGKE-Cluster das vTPM-Gerät für Arbeitslasten verfügbar machen kann. Wir verwenden ein Kubernetes-Geräte-Plug-in, um neue Ressourcen (google.com/cc) zu erstellen. Jede Arbeitslast, die mit der neuen Ressource verknüpft ist, kann die Geräte auf dem Worker-Knoten sehen.

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

Mit dem folgenden Befehl können Sie die bereitgestellte cc-device-plugin aufrufen.

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

Hinweis: Bei einem GKE-Cluster im gemischten Modus (mit vertraulichen und nicht vertraulichen GKE-Worker-Knoten) wird empfohlen, dass der Operator cc-device-plugin nur auf den vertraulichen GKE-Worker-Knoten bereitstellt.

Optional: Wenden Sie das Prometheus-Monitoring für CGKE-Pods an. Wenn Sie das Monitoring aktivieren, können Sie den Status des Geräte-Plug-ins beobachten.

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

Rufen Sie https://console.cloud.google.com/monitoring/metrics-explorer auf und suchen Sie nach den cc-device-plugin-Messwerten oder verwenden Sie PROMQL. Mit dem folgenden PROMQL-Befehl werden beispielsweise die CPU-Sekunden für jeden cc-device-plugin-Prozess angezeigt.

rate(process_cpu_seconds_total[${__interval}])

5. Bereitstellung einer Arbeitslast und Durchführung der Remote-Attestierung für die Arbeitslast (vTPM)

In diesem Schritt erstellen und stellen Sie eine Arbeitslast für den CGKE-Cluster bereit, den Sie im vorherigen Schritt erstellt haben. Außerdem führen Sie eine vTPM-Remote-Attestierung durch, um ein Attestierungstoken (OIDC-Token) auf dem Worker-Knoten abzurufen.

1). Erstellen Sie das Container-Image der Anwendung und übertragen Sie es per Push an Artifact Registry. Das Anwendungscontainer-Image enthält das go-tpm-Tool, mit dem Attestierungsnachweise erfasst und an den Attestierungsprüfdienst gesendet werden können, um ein Attestierungstoken (ein OIDC-Token) zu erhalten.

  1. Erstellen Sie die Dockerfile für das Anwendungscontainer-Image.
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. Übertragen Sie das Anwendungscontainer-Image per Push an 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). Richten Sie ein Kubernetes-Dienstkonto ein, damit die Berechtigungen eines GCP-Dienstkontos für GCP-Ressourcen übernommen werden.

  1. Erstellen Sie ein Kubernetes-Dienstkonto codelab-ksa.
kubectl create serviceaccount codelab-ksa \
    --namespace default
  1. Erstellen Sie eine Rolle Confidential_Computing_Workload_User und gewähren Sie der Rolle Berechtigungen für den Zugriff auf Confidential Computing APIs.
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. Erstellen Sie ein GCP-Dienstkonto codelab-csa und binden Sie es an die Rolle Confidential_Computing_Workload_User. Damit codelab-csa Berechtigungen für den Zugriff auf Confidential Computing-APIs hat.
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. Binden Sie das Kubernetes-Dienstkonto codelab-ksa an das GCP-Dienstkonto codelab-csa. Damit codelab-ksa Berechtigungen für den Zugriff auf Confidential Computing-APIs hat.
kubectl annotate serviceaccount codelab-ksa \
    --namespace default \
    iam.gke.io/gcp-service-account=codelab-csa@${PROJECT_ID}.iam.gserviceaccount.com

3). Erstellen Sie die YAML-Datei für die Anwendungsbereitstellung für die Demoanwendung. Weisen Sie das Kubernetes-Dienstkonto codelab-ksa ausgewählten Arbeitslasten zu.

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). Wenden Sie das Deployment auf den CGKE-Cluster an.

kubectl apply -f deploy.yaml

5). Stellen Sie eine Verbindung zur Arbeitslast her und starten Sie die Remote-Attestierung, um ein Attestierungstoken (ein OIDC-Token) abzurufen.

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

6). Lassen Sie das Token auf dem Bildschirm ausgeben, damit Sie es kopieren können.

cat attestation_token

Sie können das Attestierungstoken unter jwt.io decodieren, um die Ansprüche aufzurufen.

6. Secret Release Web Server einrichten

In diesem Schritt beenden Sie die vorherige SSH-Sitzung und richten eine weitere VM ein. Auf dieser VM richten Sie einen Webserver für die Secret-Freigabe ein. Der Webserver validiert das empfangene Attestation Token und seine Ansprüche. Wenn die Validierungen erfolgreich sind, wird das Secret an den Anfragenden übergeben.

1). Rufen Sie die Cloud Console oder Ihre lokale Entwicklungsumgebung auf. Erstellen Sie eine virtuelle Maschine.

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). Stellen Sie eine SSH-Verbindung zu Ihrer neuen VM her.

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

3). Richten Sie die Go-Umgebung ein.

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). Erstellen Sie die folgenden zwei Dateien, in denen der Quellcode des Webservers für die geheime Veröffentlichung gespeichert wird.

main.go erstellen:

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

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

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

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

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

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

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

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

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

func isValid(claims jwt.MapClaims) (bool, error) {
	// 1. Evaluating Standard Claims:
	subject, ok := claims["sub"].(string)
	if !ok {
		return false, fmt.Errorf("missing or invalid 'sub' claim")
	}
	fmt.Println("Subject:", subject)

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

	// 2. Evaluating Remote Attestation Claims:
	hwModel, ok := claims["hwmodel"].(string)
	// Support attestation of both AMD SEV and Intel TDX
	if !ok || (hwModel != "GCP_AMD_SEV" && hwModel != "GCP_INTEL_TDX") {
		return false, fmt.Errorf("missing or invalid 'hwModel': %v", hwModel)
	}
	fmt.Println("hwmodel:", hwModel)

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

	return true, nil
}


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

helper.go erstellen:

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). Führen Sie die folgenden Befehle aus, um den Webserver zu erstellen und auszuführen. Dadurch wird der Webserver für die Secret-Freigabe an Port :8080 gestartet.

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

Fehlerbehebung: Beim Ausführen von go mod tidy wird möglicherweise die folgende Warnung angezeigt, die Sie ignorieren können:

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). Starten Sie einen weiteren Cloud Console-Tab oder eine weitere Sitzung der lokalen Entwicklungsumgebung und führen Sie den folgenden Befehl aus. Dadurch erhalten Sie die cgke-attestation-codelab-web-server-internal-ip.

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

7). Stellen Sie eine Verbindung zu Ihrer CGKE-Arbeitslast her und starten Sie die Remote-Attestierung, um ein Attestierungs-Token (ein OIDC-Token) abzurufen. Bette dann den Inhalt von attestation-token und cgke-attestation-codelab-web-server-internal-ip in den folgenden Befehl ein. Dadurch wird das Secret abgerufen, das vom Webserver für die Secret-Freigabe gespeichert wird.

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

Ersetzen Sie Folgendes:

  • cgke-attestation-codelab-web-server-internal-ip ist die interne IP-Adresse der cgke-attestation-codelab-web-server-VM-Instanz.

7. vTPM-Versiegelung auf CGKE-Knoten

In diesem Schritt richten Sie die vTPM-Inhaberautorisierung auf den CGKE-Knoten ein und stellen eine Workload mit der vTPM-Inhaber-Passphrase bereit. Anschließend erstellen Sie einen primären vTPM-Schlüssel, um Daten in der Arbeitslast mit der vTPM-Versiegelungsfunktion zu versiegeln und zu entsiegeln.

1). vTPM-Eigentümerautorisierung auf den CGKE-Knoten einrichten

  1. Container-Image für einmaligen Job erstellen Mit dem Einmaljob wird das Inhaberpasswort für alle vTPMs festgelegt. Im Folgenden finden Sie das Dockerfile zum Erstellen des Container-Images.
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. Erstellen Sie das Container-Image für den Einmaljob und übertragen Sie es per Push in die 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. Führen Sie den einmaligen Job über einen Kubernetes-Job aus. ACHTUNG: Bei diesem Job wird vTPM auf jeder CVM gelöscht. Wenn Ihre CVM vTPM zum Verschlüsseln von Festplatten verwendet, ist sie nach dem Neustart nicht mehr nutzbar. Mit dem Befehl lsblk -f können Sie prüfen, ob Ihr Laufwerk den FSTYPE crypto_LUKS hat.
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

Tipp: Wenn Sie den Job „tpm-tools-task“ noch einmal ausführen müssen, löschen Sie zuerst den vorhandenen Job:

kubectl delete job tpm-tools-task
  1. Starten Sie den einmaligen Job. Mit diesem Job wird die vTPM-Inhaber-Passphrase auf allen Worker-Knoten festgelegt.
kubectl apply -f tpm-tools-task.yaml

2). Erstellen Sie ein Kubernetes-Secret, das die vTPM-Inhaber-Passphrase enthält.

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

3). Erstellen Sie einen Democontainer für die Anwendung und übergeben Sie die Passphrase an ihn. Der Demokarten-Anwendungscontainer enthält tpm2-tools für die Interaktion mit dem vTPM.

  1. Erstellen Sie die YAML-Datei für die Bereitstellung des Container der Demoanwendung.
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. Demoanwendung bereitstellen
kubectl apply -f deploy_demo.yaml

4). Führen Sie das vTPM-Sealing im Democontainer der Anwendung aus.

  1. Stellen Sie eine Verbindung zum Demanwendungscontainer her und legen Sie einen Primärschlüssel mit Passphrase fest.
kubectl exec -it tpm-tools-demo -- /bin/bash
tpm2_createprimary -C o -c primary.ctx -P $(cat /etc/tpmsecret/passphrase)

tpm2_createprimary interagiert mit dem vTPM, um das primäre Objekt basierend auf der angegebenen Hierarchie und Vorlage zu generieren.

  • -C o: Gibt an, dass der Primärschlüssel in der Hierarchie des TPM-Inhabers erstellt wird.
  • -c primary.ctx: Speichert den Kontext (Handle und zugehörige Daten) des erstellten primären Objekts in der Datei primary.ctx. Dieser Kontext ist für spätere Vorgänge unerlässlich.

Die Arbeitslast kann nicht die falsche Inhaber-Passphrase verwenden, um einen Primärschlüssel zu erstellen.

tpm2_createprimary -C o -P wrong_passphrase

Der Befehl gibt die folgenden Fehler zurück:

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. Der erstellte primäre Schlüssel kann dann zum Versiegeln und Entsiegeln von Daten verwendet werden.
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 interagiert mit dem vTPM, um das gewünschte kryptografische Objekt zu generieren.

  • -C primary.ctx: Verwendet den zuvor erstellten Kontext für den Primärschlüssel.
  • -u sealed.pub: Speichert den öffentlichen Teil des Versiegelungsschlüssels (der zum Entsiegeln benötigt wird) in sealed.pub.
  • -r sealed.priv: Speichert den privaten Teil des Versiegelungsschlüssels in sealed.priv.
  • -i secret.txt: Die Datei mit dem zu versiegelnden Secret.

tpm2_load: Lädt den Versiegelungsschlüssel mithilfe der öffentlichen und privaten Teile (sealed.pub, sealed.priv) in das TPM und speichert den Kontext in sealed.ctx.

tpm2_unseal: Entschlüsselt (entsiegelt) Daten, die zuvor mit einem vTPM-Siegelungsobjekt verschlüsselt (versiegelt) wurden.

cat unsealed.txt: Zeigt die nicht versiegelte geheime Nachricht an, um zu bestätigen, dass der Vorgang erfolgreich war.

Die Dateien primary.ctx und sealed.priv können nur auf einem vTPM-Gerät verwendet werden. Jeder, der Zugriff auf das vTPM-Gerät und diese Dateien hat, kann auf die versiegelten Daten zugreifen. Sie könnten die Richtlinie für PCR-Werte auch verwenden, um Daten zu versiegeln. Dies ist jedoch nicht Gegenstand dieses Codelabs.

8. Arbeitslasten zur Durchführung der Hardware-Attestierung bereitstellen: Intel TDX und AMD SEV-SNP

cc_device_plugin_arch.png

In diesem Schritt erstellen Sie eine auf Intel TDX oder AMD SEV-SNP basierende Arbeitslast und führen eine Attestierung auf Hardwareebene für Confidential GKE Nodes durch. Wie im Architekturdiagramm oben dargestellt, verwenden wir das Geräte-Plug-in des Gastbetriebssystems, um Attestierungsangebote für Intel TDX- und AMD SEV-SNP-Architekturen abzurufen. Rufen Sie die Cloud Console oder Ihre lokale Entwicklungsumgebung auf, um die Befehle auszuführen.

Wählen Sie die Confidential Computing-Architektur aus, die Sie testen möchten: Intel TDX oder AMD SEV-SNP.

1). Intel TDX-Knotenpool hinzufügen

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-Knotenpool hinzufügen

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). Standardknotenpool löschen (optional, aber empfohlen, um Kosten und Rauschen zu reduzieren)

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

Hinweis: Wenn Sie den vTPM-Abschnitt bereits abgeschlossen haben, wurde dieser Pool bereits gelöscht. Sie können alle „Nicht gefunden“-Fehler hier ignorieren.

4). Starten Sie das Geräte-Plug-in, damit CGKE-Cluster Hardware-Attestierungs-Gastgeräte für Arbeitslasten bereitstellen können. Wir verwenden ein Kubernetes-Geräte-Plug-in, um neue Ressourcen (intel.com/tdx und amd.com/sev-snp) zu erstellen. Jede Arbeitslast, die diesen Ressourcen zugeordnet ist, kann auf die Gastgeräte auf dem Worker-Knoten zugreifen.

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

Mit dem folgenden Befehl können Sie die bereitgestellte cc-device-plugin aufrufen.

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

Hinweis: Bei einem GKE-Cluster im gemischten Modus (mit vertraulichen und nicht vertraulichen GKE-Worker-Knoten) wird empfohlen, dass der Operator cc-device-plugin nur auf den vertraulichen GKE-Worker-Knoten bereitstellt.

9. Hardware-Attestierung: Intel TDX und AMD SEV-SNP

In diesem Abschnitt erfahren Sie, wie Sie die Hardware-Attestierung in vertraulichen GKE-Knoten durchführen, indem Sie Attestierungsangebote für Intel TDX- und AMD SEV-SNP-Architekturen mit den Gastgeräte-Plug-ins abrufen.

1). Erstellen Sie eine Go-Anwendung, um das Hardware-Attestierungsangebot vom jeweiligen Gastgerät abzurufen.

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). Erstellen Sie die Dockerfile für das Hardware-Attestierungstest-Tool:

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). Erstellen Sie das Image und übertragen Sie es per Push in Ihre 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). Erstellen Sie die YAML-Datei für die Anwendungsbereitstellung für Intel TDX- und AMD SEV-SNP-Arbeitslasten, um die Hardwaregeräte verfügbar zu machen.

  • Für Intel TDX-Arbeitslasten erstellen Sie die folgende YAML-Datei für die Bereitstellung:
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
  • Für AMD SEV-SNP-Arbeitslasten erstellen Sie die folgende Bereitstellungs-YAML-Datei:
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). Wenden Sie die Deployments auf den CGKE-Cluster an.

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

6). Warten Sie, bis die Pods abgeschlossen sind, und prüfen Sie die Hardware-Attestierungsprotokolle.

Prüfen Sie zuerst den Status Ihrer Pods, um sicherzustellen, dass sie erfolgreich geplant wurden und ausgeführt werden. Warten Sie, bis „Abgeschlossen“ oder „Wird ausgeführt“ angezeigt wird:

kubectl get pods -w

Sobald die Pods bereit sind, prüfen Sie in den Logs, ob die Hardware-Attestierungsangebote erfolgreich abgerufen wurden:

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

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

Die Gesamtausführungszeit, die Gesamtzahl der Iterationen und die Anzahl der erfolgreichen/fehlgeschlagenen Ausführungen werden protokolliert.

10. Bereinigen

Damit Ihrem Google Cloud-Konto keine laufenden Gebühren berechnet werden, ist es wichtig, die in diesem Codelab erstellten Ressourcen zu löschen.

Führen Sie die folgenden Befehle in Cloud Shell oder in Ihrer lokalen Entwicklungsumgebung aus.

1). Legen Sie Umgebungsvariablen fest:

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-Ressourcen löschen

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

3). Artifact Registry löschen

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

4). Webserver-VM löschen

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

5). IAM-Ressourcen löschen

# 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. Nächste Schritte

Weitere Informationen zu Confidential GKE Nodes