Atestado remoto e lacre em nós do GKE Confidencial (vTPM, SEV-SNP, TDX)

1. Visão geral

Os nós do GKE confidencial (CGKE) garantem que os dados nas cargas de trabalho sejam criptografados em uso. Ao expor o dispositivo vTPM às cargas de trabalho do CGKE, elas podem usar os recursos do vTPM. Neste codelab, você vai aprender sobre os recursos do vTPM e como aproveitar a atestação de hardware Intel TDX e AMD SEV-SNP usando o cc-device-plugin.

  • O atestado remoto do vTPM permite que uma parte remota verifique se os nós do CGKE que hospedam cargas de trabalho estão sendo executados em VMs confidenciais (CVMs).
  • Autorização e lacre do vTPM.
  • Atestado de hardware usando dispositivos convidados Intel TDX e AMD SEV-SNP expostos.

683a3b43587ef69f.png

Conforme mostrado na figura acima, a primeira parte deste codelab inclui as seguintes etapas:

  • Os nós do CGKE configuram e expõem o dispositivo vTPM a cargas de trabalho selecionadas.
  • Implante uma carga de trabalho e faça o atestado remoto do nó do CGKE que a hospeda.
  • Implante uma carga de trabalho para buscar declarações de atestado de hardware diretamente de dispositivos TDX ou SEV-SNP.
  • Configuração do servidor da Web de lançamento secreto.

8f6e80c762a5d911.png

Conforme mostrado na figura acima, a segunda parte deste codelab inclui:

  • Configuração de autorização e lacre do vTPM nos nós do CGKE.

cc_device_plugin_concept.png

Conforme mostrado na figura acima, a terceira parte deste codelab inclui:

  • Como o cc-device-plugin mapeia com segurança dispositivos de hardware durante a fase de configuração.
  • Implantação de cargas de trabalho para buscar declarações de atestado de hardware diretamente de dispositivos TDX ou SEV-SNP durante a fase de execução com sobrecarga zero.

O que você vai aprender

  • Como expor o dispositivo vTPM a cargas de trabalho do CGKE.
  • Como fazer a atestação remota usando a API Computação confidencial (serviço de verificador de atestado) em cargas de trabalho do CGKE.
  • Como configurar a autorização do vTPM e realizar o lacre do vTPM.
  • Como acessar dispositivos SEV-SNP e TDX em cargas de trabalho do CGKE usando o cc-device-plugin.
  • Como buscar declarações de atestado de hardware usando go-tdx-guest e go-sev-guest.

O que é necessário

  • Um projeto do Google Cloud Platform
  • Um navegador, como o Chrome ou o Firefox
  • Conhecimento básico do Google Compute Engine (codelab), VM confidencial, nós confidenciais do GKE e Artifact Registry
  • Para Intel TDX ou AMD SEV-SNP: versão mínima do GKE v1.33.5-gke.1697000+ ou v1.34.1-gke.2909000+ e famílias de máquinas adequadas (c3 para TDX, n2d para SNP).

2. Configuração e requisitos

Para ativar as APIs necessárias, execute o seguinte comando no console do Cloud ou no ambiente de desenvolvimento 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. Como configurar o plano de controle compartilhado do CGKE

Nesta etapa, você vai configurar o plano de controle compartilhado do GKE e um repositório do Docker do Artifact Registry.

1) Configure as variáveis de ambiente e crie o cluster compartilhado do GKE:

Substitua your-project-id pela ID do seu projeto. Substitua us-central1-c pela zona desejada. Consulte Regiões e 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) Crie um repositório Docker do Artifact Registry para armazenar imagens de contêiner de carga de trabalho:

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

Escolha seu caminho

O plano de controle compartilhado do GKE e o repositório do Artifact Registry estão prontos. Escolha seu caminho:

4. Configurar nós do CGKE e expor o dispositivo vTPM a cargas de trabalho selecionadas

Nesta etapa, você vai criar um pool de nós vTPM no CGKE e aplicar um plug-in de dispositivo para expor o dispositivo vTPM da CVM às cargas de trabalho. Acesse o console do Cloud ou o ambiente de desenvolvimento local para executar os comandos.

1) Adicionar um pool de nós do 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) Exclua o pool de nós padrão (opcional, mas recomendado para economizar custos/ruídos).

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

3) Inicie o plug-in de dispositivo para permitir que o cluster do CGKE exponha o dispositivo vTPM às cargas de trabalho. Usamos um plug-in de dispositivo do Kubernetes para criar novos recursos (google.com/cc). Qualquer carga de trabalho associada ao novo recurso poderá ver os dispositivos no nó de trabalho.

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

O comando a seguir permite ver o cc-device-plugin implantado.

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

Observação: em caso de um cluster do GKE de modo misto (com nós de trabalho do GKE Confidencial e não confidenciais), recomendamos que o operador implante cc-device-plugin apenas nos nós de trabalho do GKE Confidencial.

(Opcional). Aplique o monitoramento do Prometheus do pod do CGKE. Ao ativar o monitoramento, você pode observar o status do plug-in do dispositivo.

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

Acesse https://console.cloud.google.com/monitoring/metrics-explorer e encontre as métricas cc-device-plugin ou use o PROMQL. Por exemplo, o comando PROMQL a seguir mostra os segundos de CPU de cada processo cc-device-plugin.

rate(process_cpu_seconds_total[${__interval}])

5. Implantar uma carga de trabalho e realizar o atestado remoto nela (vTPM)

Nesta etapa, você vai criar e implantar uma carga de trabalho no cluster do CGKE criado na etapa anterior e realizar uma atestação remota do vTPM para recuperar um token de atestação (token OIDC) no nó de trabalho.

1) Crie a imagem do contêiner do aplicativo e envie-a para o Artifact Registry. A imagem do contêiner do aplicativo contém a ferramenta go-tpm, que pode coletar evidências de atestado e enviá-las ao serviço Attestation Verifier para um token de atestado (um token OIDC).

  1. Crie o Dockerfile para a imagem do contêiner do aplicativo.
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. Envie a imagem do contêiner do aplicativo para o 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) Configure uma conta de serviço do Kubernetes para herdar as permissões de uma conta de serviço do GCP nos recursos do GCP.

  1. Crie uma conta de serviço do Kubernetes codelab-ksa.
kubectl create serviceaccount codelab-ksa \
    --namespace default
  1. Crie um papel Confidential_Computing_Workload_User e conceda as permissões dele para acessar as APIs do 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. Crie uma conta de serviço do GCP codelab-csa e vincule-a ao papel Confidential_Computing_Workload_User. Para que codelab-csa tenha permissões de acesso às APIs da Computação confidencial.
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. Vincule a conta de serviço do Kubernetes codelab-ksa à conta de serviço do GCP codelab-csa. Para que codelab-ksa tenha permissões de acesso às APIs da Computação confidencial.
kubectl annotate serviceaccount codelab-ksa \
    --namespace default \
    iam.gke.io/gcp-service-account=codelab-csa@${PROJECT_ID}.iam.gserviceaccount.com

3) Crie o YAML de implantação do aplicativo de demonstração. Atribua a conta de serviço do Kubernetes codelab-ksa às cargas de trabalho selecionadas.

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). Aplique a implantação ao cluster do CGKE.

kubectl apply -f deploy.yaml

5). Conecte-se à carga de trabalho e inicie o atestado remoto para buscar um token de atestado (um token OIDC).

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

6). Imprima o token na tela para poder copiá-lo.

cat attestation_token

É possível decodificar o token de atestação em jwt.io para ver as declarações.

6. Como configurar o servidor da Web de lançamento secreto

Nesta etapa, você vai sair da sessão SSH anterior e configurar outra VM. Nessa VM, você vai configurar um servidor da Web de lançamento secreto. O servidor da Web valida o token de atestação recebido e as declarações dele. Se as validações forem bem-sucedidas, o segredo será transmitido ao solicitante.

1) Acesse o console do Cloud ou seu ambiente de desenvolvimento local. Crie uma 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) Conecte-se por SSH à nova VM.

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

3) Configure o ambiente do 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). Crie os dois arquivos a seguir para armazenar o código-fonte do servidor da Web de lançamento secreto.

Crie 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

Crie 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). Execute os comandos a seguir para criar e executar o servidor da Web. Isso inicia o servidor da Web de lançamento de secrets na porta :8080.

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

Solução de problemas: talvez você veja o seguinte aviso, que pode ser ignorado ao executar 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). Inicie outra guia do console do Cloud ou sessão do ambiente de desenvolvimento local e execute o seguinte comando. Isso vai dar a você o cgke-attestation-codelab-web-server-internal-ip.

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

7). Conecte-se à sua carga de trabalho do CGKE e inicie o atestado remoto para buscar um token de atestado (um token OIDC). Em seguida, incorpore o conteúdo de attestation-token e cgke-attestation-codelab-web-server-internal-ip no seguinte comando. Isso vai buscar o secret mantido pelo servidor da Web de lançamento de secrets.

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

Substitua:

  • cgke-attestation-codelab-web-server-internal-ip é o IP interno da instância de VM cgke-attestation-codelab-web-server.

7. Lacração do vTPM nos nós do CGKE

Nesta etapa, você configura a autorização do proprietário do vTPM nos nós do CGKE e implanta uma carga de trabalho com a senha longa do proprietário do vTPM. Depois, você cria uma chave primária do vTPM para lacrar e remover o lacre dos dados na carga de trabalho com a capacidade de lacre do vTPM.

1) Configure a autorização do proprietário do vTPM nos nós do CGKE.

  1. Crie uma imagem do contêiner de job único. O job único define a senha do proprietário para todos os vTPMs. Confira a seguir o Dockerfile para criar a imagem do contêiner.
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. Crie e envie a imagem do contêiner do job único para o 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. Execute o job único usando um job do Kubernetes. AVISO: esse job limpa o vTPM em cada CVM. Se a CVM usa o vTPM para criptografar o disco, esse job torna a CVM inutilizável após a reinicialização. Verifique se o disco tem FSTYPE crypto_LUKS com o 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

Dica: se você precisar executar o job tpm-tools-task novamente, exclua o job atual primeiro:

kubectl delete job tpm-tools-task
  1. Inicie o job único. Esse job define a senha do proprietário do vTPM em todos os nós de worker.
kubectl apply -f tpm-tools-task.yaml

2) Crie um Secret do Kubernetes para armazenar a senha longa do proprietário do vTPM.

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

3) Crie um contêiner de aplicativo de demonstração e transmita a senha longa a ele. O contêiner do aplicativo de demonstração contém tpm2-tools para interagir com o vTPM.

  1. Crie o arquivo YAML de implantação para o contêiner do aplicativo de demonstração.
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. Implante o aplicativo de demonstração.
kubectl apply -f deploy_demo.yaml

4). Realize o lacre do vTPM no contêiner do aplicativo de demonstração.

  1. Conecte-se ao contêiner do aplicativo de demonstração e defina uma chave primária com senha.
kubectl exec -it tpm-tools-demo -- /bin/bash
tpm2_createprimary -C o -c primary.ctx -P $(cat /etc/tpmsecret/passphrase)

O tpm2_createprimary interage com o vTPM para gerar o objeto principal com base na hierarquia e no modelo especificados.

  • -C o: indica que a chave primária será criada na hierarquia do proprietário do TPM.
  • -c primary.ctx: salva o contexto (handle e dados associados) do objeto principal criado no arquivo primary.ctx. Esse contexto é essencial para operações posteriores.

A carga de trabalho não pode usar a senha longa do proprietário errada para criar uma chave primária.

tpm2_createprimary -C o -P wrong_passphrase

O comando retorna os seguintes erros:

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. A chave principal criada pode ser usada para lacrar e abrir dados.
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

O tpm2_create interage com o vTPM para gerar o objeto criptográfico desejado.

  • -C primary.ctx: usa o contexto de chave primária que criamos antes.
  • -u sealed.pub: armazena a parte pública da chave de lacre (necessária para remover o lacre) em sealed.pub.
  • -r sealed.priv: armazena a parte privada da chave de criptografia em sealed.priv.
  • -i secret.txt: o arquivo que contém o secret a ser lacrado.

tpm2_load: carrega a chave de lacre no TPM usando as partes pública e privada (sealed.pub, sealed.priv) e salva o contexto em sealed.ctx.

tpm2_unseal: descriptografa (desprotege) dados que foram criptografados (protegidos) anteriormente usando um objeto de proteção vTPM.

cat unsealed.txt: mostra a mensagem secreta não lacrada para confirmar que o processo foi concluído.

Os arquivos primary.ctx e sealed.priv só podem ser usados em um dispositivo vTPM. Qualquer pessoa com acesso ao dispositivo vTPM e a esses arquivos pode acessar os dados criptografados. Você também pode usar a política em valores de PCR para proteger os dados, mas isso está fora do escopo deste codelab.

8. Implantação de cargas de trabalho para realizar atestado de hardware: Intel TDX e AMD SEV-SNP

cc_device_plugin_arch.png

Nesta etapa, você cria uma carga de trabalho baseada em Intel TDX ou AMD SEV-SNP e realiza um atestado no nível do hardware em Confidential GKE Nodes. Conforme ilustrado no diagrama de arquitetura acima, vamos usar o plug-in de dispositivo convidado para buscar declarações de atestado para arquiteturas Intel TDX e AMD SEV-SNP. Acesse o console do Cloud ou o ambiente de desenvolvimento local para executar os comandos.

Escolha a arquitetura de Computação Confidencial (Intel TDX ou AMD SEV-SNP) que você quer testar.

1) Adicionar um pool de nós do 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) Adicionar um pool de nós 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) Exclua o pool de nós padrão (opcional, mas recomendado para economizar custos/ruídos).

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

Observação: se você já concluiu a seção vTPM, esse pool já foi excluído. Ignore os erros "Não encontrado" que aparecerem.

4). Inicie o plug-in de dispositivo para permitir que o cluster do CGKE exponha dispositivos convidados de atestação de hardware a cargas de trabalho. Usamos um plug-in de dispositivo do Kubernetes para criar novos recursos (intel.com/tdx e amd.com/sev-snp). Qualquer carga de trabalho associada a esses recursos poderá acessar os dispositivos convidados no nó de trabalho.

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

O comando a seguir permite ver o cc-device-plugin implantado.

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

Observação: em caso de um cluster do GKE de modo misto (com nós de trabalho do GKE Confidencial e não confidenciais), é recomendável que o operador implante apenas cc-device-plugin nos nós de trabalho do GKE Confidencial.

9. Atestado de hardware: Intel TDX e AMD SEV-SNP

Nesta seção, você vai aprender a realizar o atestado no nível do hardware em nós do GKE Confidencial buscando cotas de atestado para arquiteturas Intel TDX e AMD SEV-SNP usando os plug-ins de dispositivo convidado.

1) Crie um aplicativo Go para buscar a cotação de atestado de hardware do dispositivo convidado respectivo.

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) Crie o Dockerfile para a ferramenta de teste de atestado 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) Crie e envie a imagem para o 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). Crie o YAML de implantação do aplicativo para cargas de trabalho do Intel TDX e do AMD SEV-SNP para expor os dispositivos de hardware.

  • Para cargas de trabalho do Intel TDX, crie o seguinte YAML de implantação:
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 cargas de trabalho do AMD SEV-SNP, crie o seguinte YAML de implantação:
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). Aplique as implantações ao cluster do CGKE.

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

6). Aguarde a conclusão dos pods e verifique os registros de atestado de hardware.

Primeiro, verifique o status dos seus pods para garantir que eles foram programados e estão em execução (aguarde até que mostrem "Concluído" ou "Em execução"):

kubectl get pods -w

Quando os pods estiverem prontos, verifique os registros para confirmar se as declarações de atestado de hardware foram buscadas:

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

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

O tempo total de execução, o total de iterações e as contagens de sucesso/falha serão registrados.

10. Limpeza

Para evitar cobranças contínuas na sua conta do Google Cloud, exclua os recursos criados neste codelab.

Execute os comandos a seguir no Cloud Shell ou no ambiente de desenvolvimento local.

1) Defina as variáveis de ambiente:

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

gcloud config set project ${PROJECT_ID}

2) Excluir recursos do GKE

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

3) Excluir o Artifact Registry

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

4). Excluir VM do servidor da Web

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

5). Excluir recursos do 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. A seguir

Saiba mais sobre os Confidential GKE Nodes.