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.

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.

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.

Conforme mostrado na figura acima, a terceira parte deste codelab inclui:
- Como o
cc-device-pluginmapeia 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:
- Opção 1: atestado remoto e lacre do vTPM (AMD SEV): continue diretamente para Configurar nós do CGKE e expor o dispositivo vTPM.
- Opção 2: comprovação de hardware (Intel TDX e AMD SEV-SNP): acesse diretamente Implantação de cargas de trabalho para realizar a comprovação de hardware. Quer conhecer tudo? Basta concluir a opção 1 e continuar sequencialmente para a opção 2.
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).
- Crie o
Dockerfilepara 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
- 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.
- Crie uma conta de serviço do Kubernetes
codelab-ksa.
kubectl create serviceaccount codelab-ksa \
--namespace default
- Crie um papel
Confidential_Computing_Workload_Usere 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
- Crie uma conta de serviço do GCP
codelab-csae vincule-a ao papelConfidential_Computing_Workload_User. Para quecodelab-csatenha 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]"
- Vincule a conta de serviço do Kubernetes
codelab-ksaà conta de serviço do GCPcodelab-csa. Para quecodelab-ksatenha 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 VMcgke-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.
- 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
Dockerfilepara 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
- 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
- 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_LUKScom o comandolsblk -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
- 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.
- 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
- 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.
- 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 arquivoprimary.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
- 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) emsealed.pub. - -r
sealed.priv: armazena a parte privada da chave de criptografia emsealed.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

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.