Certificación y sellado remotos en nodos de Confidential GKE (vTPM, SEV-SNP, TDX)

1. Descripción general

Los nodos de Confidential GKE (CGKE) garantizan que los datos de las cargas de trabajo se encripten en uso. Exponer el dispositivo vTPM a las cargas de trabajo de CGKE permite que estas usen las funciones de vTPM. En este codelab, aprenderás sobre las funciones del vTPM y cómo aprovechar la certificación de hardware de Intel TDX y AMD SEV-SNP con cc-device-plugin.

  • La certificación remota de vTPM permite que una entidad externa verifique que los nodos de CGKE que alojan cargas de trabajo se ejecutan en Confidential VMs (CVM).
  • Autorización y sellado de vTPM
  • Certificación de hardware con dispositivos invitados Intel TDX y AMD SEV-SNP expuestos.

683a3b43587ef69f.png

Como se muestra en la figura anterior, la primera parte de este codelab incluye los siguientes pasos:

  • Los nodos de CGKE configuran y exponen el dispositivo vTPM a las cargas de trabajo seleccionadas.
  • Implementa una carga de trabajo y realiza la certificación remota del nodo de CGKE que la aloja.
  • Implementa una carga de trabajo para recuperar citas de certificación de hardware directamente desde dispositivos TDX o SEV-SNP.
  • Configuración del servidor web de Secret Release.

8f6e80c762a5d911.png

Como se muestra en la figura anterior, la segunda parte de este codelab incluye lo siguiente:

  • Configuración de la autorización de vTPM y sellado de vTPM en los nodos de CGKE

cc_device_plugin_concept.png

Como se muestra en la figura anterior, la tercera parte de este codelab incluye lo siguiente:

  • Cómo cc-device-plugin asigna de forma segura los dispositivos de hardware durante la fase de configuración
  • Implementa cargas de trabajo para recuperar citas de certificación de hardware directamente desde dispositivos TDX o SEV-SNP durante la fase de tiempo de ejecución sin sobrecarga.

Qué aprenderás

  • Cómo exponer el dispositivo de vTPM a las cargas de trabajo de CGKE
  • Cómo realizar la certificación remota a través de la API de Confidential Computing (servicio de verificador de certificación) en cargas de trabajo de CGKE
  • Cómo configurar la autorización de vTPM y realizar el sellado de vTPM
  • Cómo acceder a dispositivos SEV-SNP y TDX en cargas de trabajo de CGKE con cc-device-plugin
  • Cómo recuperar citas de certificación de hardware con go-tdx-guest y go-sev-guest

Requisitos

  • Un proyecto de Google Cloud Platform
  • Un navegador, como Chrome o Firefox
  • Conocimientos básicos de Google Compute Engine (codelab), Confidential VMs, Confidential GKE nodes y Artifact Registry
  • Para Intel TDX o AMD SEV-SNP: Versión mínima de GKE v1.33.5-gke.1697000+ o v1.34.1-gke.2909000+ y familias de máquinas adecuadas (c3 para TDX, n2d para SNP).

2. Configuración y requisitos

Para habilitar las APIs necesarias, ejecuta el siguiente comando en Cloud Console o en tu entorno de desarrollo local:

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. Configura el plano de control de CGKE compartido

En este paso, configurarás el plano de control compartido de GKE y un repositorio de Docker de Artifact Registry.

1). Configura las variables de entorno y crea el clúster de GKE compartido:

Reemplaza your-project-id con el ID del proyecto. Reemplaza us-central1-c por la zona deseada. (Consulta Regiones y zonas).

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). Crea un repositorio de Docker en Artifact Registry para almacenar imágenes de contenedores de cargas de trabajo:

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

Elija su ruta

El plano de control de GKE compartido y el repositorio de Artifact Registry ya están listos. Elige tu camino:

4. Configura nodos de CGKE y expón el dispositivo vTPM a cargas de trabajo seleccionadas

En este paso, crearás un grupo de nodos de vTPM en CGKE y aplicarás un complemento de dispositivo para exponer el dispositivo de vTPM de CVM a las cargas de trabajo. Ve a Cloud Console o a tu entorno de desarrollo local para ejecutar los comandos.

1). Agrega un grupo de nodos de 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). Borra el grupo de nodos predeterminado (opcional, pero se recomienda para ahorrar costos y reducir el ruido).

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

3). Inicia el complemento del dispositivo para permitir que el clúster de CGKE exponga el dispositivo vTPM a las cargas de trabajo. Usamos un complemento de dispositivo de Kubernetes para crear recursos nuevos (google.com/cc). Cualquier carga de trabajo asociada con el recurso nuevo podrá ver los dispositivos en el nodo trabajador.

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

El siguiente comando te permite ver el cc-device-plugin implementado.

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

Nota: En el caso de un clúster de GKE en modo mixto (con nodos trabajadores de GKE confidenciales y no confidenciales), se recomienda que el operador solo implemente cc-device-plugin en los nodos trabajadores de GKE confidenciales.

(Opcional) Aplica la supervisión de Prometheus del pod de CGKE. Activar la supervisión te permite observar el estado del complemento del dispositivo.

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

Ve a https://console.cloud.google.com/monitoring/metrics-explorer y busca las métricas de cc-device-plugin o usa PROMQL. Por ejemplo, el siguiente comando de PROMQL muestra los segundos de CPU para cada proceso de cc-device-plugin.

rate(process_cpu_seconds_total[${__interval}])

5. Implementar una carga de trabajo y realizar la certificación remota en la carga de trabajo (vTPM)

En este paso, crearás e implementarás una carga de trabajo en el clúster de CGKE que creaste en el paso anterior y realizarás una certificación remota del vTPM para recuperar un token de certificación (token de OIDC) en el nodo de trabajador.

1). Crea la imagen del contenedor de la aplicación y envíala a Artifact Registry. La imagen del contenedor de la aplicación contiene la herramienta go-tpm, que puede recopilar evidencia de certificación y enviarla al servicio de Attestation Verifier para obtener un token de certificación (un token de OIDC).

  1. Crea el Dockerfile para la imagen de contenedor de la aplicación.
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. Envía la imagen del contenedor de la aplicación a 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). Configura una cuenta de servicio de Kubernetes para heredar los permisos de una cuenta de servicio de GCP en los recursos de GCP.

  1. Crea una cuenta de servicio de Kubernetes codelab-ksa.
kubectl create serviceaccount codelab-ksa \
    --namespace default
  1. Crea un rol Confidential_Computing_Workload_User y otórgale permisos para acceder a las APIs de 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. Crea una cuenta de servicio de GCP codelab-csa y vincúlala con el rol Confidential_Computing_Workload_User. Para que codelab-csa tenga permisos para acceder a las APIs de 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. Vincula la cuenta de servicio de Kubernetes codelab-ksa con la cuenta de servicio de GCP codelab-csa. Para que codelab-ksa tenga permisos para acceder a las APIs de Confidential Computing
kubectl annotate serviceaccount codelab-ksa \
    --namespace default \
    iam.gke.io/gcp-service-account=codelab-csa@${PROJECT_ID}.iam.gserviceaccount.com

3). Crea el archivo YAML de implementación de la aplicación de demostración. Asigna la cuenta de servicio de Kubernetes codelab-ksa a las cargas de trabajo seleccionadas.

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). Aplica la implementación al clúster de CGKE.

kubectl apply -f deploy.yaml

5). Conéctate a la carga de trabajo y lanza la certificación remota para recuperar un token de certificación (un token de OIDC).

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

6). Imprime el token en la pantalla para que puedas copiarlo.

cat attestation_token

Puedes decodificar el token de certificación en jwt.io para ver los reclamos.

6. Cómo configurar el servidor web de Secret Release

En este paso, saldrás de la sesión SSH anterior y configurarás otra VM. En esta VM, configurarás un servidor web de lanzamiento secreto. El servidor web valida el token de certificación recibido y sus declaraciones. Si las validaciones se realizan correctamente, se pasa el secreto al solicitante.

1). Ve a Cloud Console o a tu entorno de desarrollo local. Crea una máquina virtual.

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). Conéctate a tu nueva VM a través de SSH.

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

3). Configura el entorno de 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). Crea los siguientes dos archivos que almacenan el código fuente del servidor web de lanzamiento secreto.

Crea 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

Crea 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). Ejecuta los siguientes comandos para compilar el servidor web y ejecutarlo. Esto inicia el servidor web de lanzamiento de secretos en el puerto :8080.

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

Solución de problemas: Es posible que veas la siguiente advertencia, que puedes ignorar cuando ejecutes go mod tidy:

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

6). Inicia otra pestaña de la consola de Cloud o sesión del entorno de desarrollo local y ejecuta el siguiente comando. Esto te dará el cgke-attestation-codelab-web-server-internal-ip.

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

7). Conéctate a tu carga de trabajo de CGKE y lanza la certificación remota para recuperar un token de certificación (un token de OIDC). Luego, incorpora el contenido de attestation-token y cgke-attestation-codelab-web-server-internal-ip en el siguiente comando. Esto recuperará el secreto que contiene el servidor web de lanzamiento de secretos.

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

Reemplaza lo siguiente:

  • cgke-attestation-codelab-web-server-internal-ip es la IP interna de la instancia de VM cgke-attestation-codelab-web-server.

7. Sellado de vTPM en los nodos de CGKE

En este paso, configurarás la autorización del propietario del vTPM en los nodos de CGKE y, luego, implementarás una carga de trabajo con la frase de contraseña del propietario del vTPM. Luego, crearás una clave principal de vTPM para sellar y desellar datos en la carga de trabajo con la capacidad de sellado de vTPM.

1). Configura la autorización del propietario del vTPM en los nodos de CGKE.

  1. Crea una imagen de contenedor de trabajo único. El trabajo único establece la contraseña del propietario para todos los vTPM. A continuación, se muestra el Dockerfile para crear la imagen del contenedor.
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. Compila y envía la imagen del contenedor del trabajo único a 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. Ejecuta el trabajo único a través de un trabajo de Kubernetes. (ADVERTENCIA: Este trabajo borra el vTPM en cada CVM. Si tu CVM usa el vTPM para encriptar el disco, este trabajo hará que tu CVM sea inutilizable después de reiniciar. Puedes verificar si tu disco tiene FSTYPE crypto_LUKS con el comando 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

Sugerencia: Si necesitas volver a ejecutar el trabajo tpm-tools-task, asegúrate de borrar el trabajo existente primero:

kubectl delete job tpm-tools-task
  1. Inicia el trabajo único. Este trabajo establece la frase de contraseña del propietario del vTPM en todos los nodos trabajadores.
kubectl apply -f tpm-tools-task.yaml

2). Crea un secreto de Kubernetes para guardar la frase de contraseña del propietario del vTPM.

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

3). Crea un contenedor de aplicación de demostración y pásale la frase de contraseña. El contenedor de la aplicación de demostración contiene tpm2-tools para interactuar con el vTPM.

  1. Crea el archivo YAML de implementación para el contenedor de la aplicación de demostración.
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. Implementa la aplicación de demostración.
kubectl apply -f deploy_demo.yaml

4). Realiza el sellado del vTPM en el contenedor de la aplicación de demostración.

  1. Conéctate al contenedor de la aplicación de demostración y establece una clave principal con una frase de contraseña.
kubectl exec -it tpm-tools-demo -- /bin/bash
tpm2_createprimary -C o -c primary.ctx -P $(cat /etc/tpmsecret/passphrase)

tpm2_createprimary interactúa con el vTPM para generar el objeto principal según la jerarquía y la plantilla especificadas.

  • -C o: Indica que la clave principal se creará en la jerarquía del propietario del TPM.
  • -c primary.ctx: Guarda el contexto (controlador y datos asociados) del objeto principal creado en el archivo primary.ctx. Este contexto es esencial para las operaciones posteriores.

La carga de trabajo no puede usar la frase de contraseña del propietario incorrecta para crear una clave principal.

tpm2_createprimary -C o -P contraseña_incorrecta

El comando devuelve los siguientes errores:

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. La clave principal creada se podría usar para sellar y abrir datos.
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 interactúa con el vTPM para generar el objeto criptográfico deseado.

  • -C primary.ctx: Usa el contexto de clave principal que creamos antes.
  • -u sealed.pub: Almacena la parte pública de la clave de sellado (necesaria para el desellado) en sealed.pub.
  • -r sealed.priv: Almacena la parte privada de la clave de sellado en sealed.priv.
  • -i secret.txt: Es el archivo que contiene el secreto que se sellará.

tpm2_load: Carga la clave de sellado en el TPM con las partes pública y privada (sealed.pub, sealed.priv) y guarda su contexto en sealed.ctx.

tpm2_unseal: Desencripta (desella) los datos que se encriptaron (sellaron) previamente con un objeto de sellado de vTPM.

cat unsealed.txt: Muestra el mensaje secreto sin sellar para confirmar que el proceso se realizó correctamente.

Ten en cuenta que los archivos primary.ctx y sealed.priv solo se pueden usar en un dispositivo con vTPM. Además, cualquier persona que tenga acceso al dispositivo vTPM y a estos archivos puede acceder a los datos sellados. También puedes usar la política sobre los valores del PCR para sellar los datos, pero está fuera del alcance de este codelab.

8. Implementa cargas de trabajo para realizar la certificación de hardware: Intel TDX y AMD SEV-SNP

cc_device_plugin_arch.png

En este paso, crearás una carga de trabajo basada en Intel TDX o AMD SEV-SNP y realizarás una certificación a nivel del hardware en Confidential GKE Nodes. Como se ilustra en el diagrama de arquitectura anterior, usaremos el complemento del dispositivo invitado para recuperar citas de certificación para las arquitecturas Intel TDX y AMD SEV-SNP. Ve a Cloud Console o a tu entorno de desarrollo local para ejecutar los comandos.

Elige la arquitectura de Confidential Computing (Intel TDX o AMD SEV-SNP) que deseas probar.

1). Agrega un grupo de nodos 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). Agrega un grupo de nodos de 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). Borra el grupo de nodos predeterminado (opcional, pero se recomienda para ahorrar costos y reducir el ruido).

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

(Nota: Si ya completaste la sección del vTPM, este grupo ya se borró y puedes ignorar con seguridad los errores de "No se encontró" que aparezcan aquí).

4). Inicia el complemento del dispositivo para permitir que el clúster de CGKE exponga dispositivos invitados de certificación de hardware a las cargas de trabajo. Usamos un complemento de dispositivo de Kubernetes para crear recursos nuevos (intel.com/tdx y amd.com/sev-snp). Cualquier carga de trabajo asociada con estos recursos podrá acceder a los dispositivos invitados en el nodo trabajador.

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

El siguiente comando te permite ver el cc-device-plugin implementado.

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

Nota: En el caso de un clúster de GKE en modo mixto (con nodos trabajadores de GKE confidenciales y no confidenciales), se recomienda que el operador solo implemente cc-device-plugin en los nodos trabajadores de GKE confidenciales.

9. Certificación de hardware: Intel TDX y AMD SEV-SNP

En esta sección, aprenderás a realizar la certificación a nivel del hardware dentro de los nodos de GKE confidenciales recuperando citas de certificación para las arquitecturas Intel TDX y AMD SEV-SNP con los complementos del dispositivo invitado.

1). Crea una aplicación en Go para recuperar la cita de certificación de hardware del dispositivo invitado correspondiente.

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). Crea el Dockerfile para la herramienta de prueba de certificación de hardware:

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). Compila y envía la imagen a tu 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). Crea el archivo YAML de implementación de la aplicación para las cargas de trabajo de Intel TDX y AMD SEV-SNP para exponer los dispositivos de hardware.

  • Para las cargas de trabajo de Intel TDX, crea el siguiente archivo YAML de implementación:
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
  • Para las cargas de trabajo de AMD SEV-SNP, crea el siguiente archivo YAML de implementación:
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). Aplica las implementaciones al clúster de CGKE.

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

6). Espera a que se completen los pods y verifica los registros de certificación de hardware.

Primero, verifica el estado de tus Pods para asegurarte de que se hayan programado y estén en ejecución correctamente (espera hasta que muestren Completado o En ejecución):

kubectl get pods -w

Una vez que los Pods estén listos, revisa los registros para verificar que las citas de certificación de hardware se hayan recuperado correctamente:

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

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

(Verás el tiempo total de ejecución, las iteraciones totales y los recuentos de éxitos y errores registrados).

10. Limpieza

Para evitar que se apliquen cargos a tu cuenta de Google Cloud, es importante que borres los recursos que creaste en este codelab.

Ejecuta los siguientes comandos en Cloud Shell o en tu entorno de desarrollo local.

1). Establece las variables de entorno:

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

gcloud config set project ${PROJECT_ID}

2). Borra recursos de GE

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

3). Borra Artifact Registry

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

4). Borra la VM del servidor web

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

5). Borra recursos de 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. ¿Qué sigue?

Obtén más información sobre los Confidential GKE Nodes.