1. Présentation
Les nœuds Confidential GKE (CGKE) garantissent que les données des charges de travail sont chiffrées en cours d'utilisation. L'exposition du périphérique vTPM aux charges de travail CGKE permet à ces charges de travail d'utiliser les fonctionnalités vTPM. Dans cet atelier de programmation, vous allez découvrir les fonctionnalités de vTPM et apprendre à exploiter l'attestation matérielle Intel TDX et AMD SEV-SNP à l'aide de cc-device-plugin.
- L'attestation à distance vTPM permet à une partie distante de vérifier que les nœuds CGKE hébergeant des charges de travail s'exécutent sur des Confidential VMs (CVM).
- Autorisation et scellement vTPM.
- Attestation matérielle à l'aide des périphériques invités Intel TDX et AMD SEV-SNP exposés.

Comme illustré dans la figure ci-dessus, la première partie de cet atelier de programmation comprend les étapes suivantes :
- Les nœuds CGKE configurent et exposent le périphérique vTPM aux charges de travail sélectionnées.
- Déployez une charge de travail et effectuez une attestation à distance du nœud CGKE qui l'héberge.
- Déployez une charge de travail pour récupérer les citations d'attestation matérielle directement à partir des appareils TDX ou SEV-SNP.
- Configuration du serveur Web de publication secrète.

Comme illustré dans la figure ci-dessus, la deuxième partie de cet atelier de programmation comprend :
- Configuration de l'autorisation vTPM et du scellement vTPM sur les nœuds CGKE.

Comme illustré dans la figure ci-dessus, la troisième partie de cet atelier de programmation comprend :
- Comment
cc-device-pluginmappe de manière sécurisée les appareils matériels lors de la phase de configuration. - Déployez des charges de travail pour récupérer des citations d'attestation matérielle directement à partir d'appareils TDX ou SEV-SNP pendant la phase d'exécution, sans aucun frais généraux.
Points abordés
- Comment exposer l'appareil vTPM aux charges de travail CGKE.
- Comment effectuer une attestation à distance via l'API Informatique confidentielle (service Attestation Verifier) sur les charges de travail CGKE.
- Configurer l'autorisation vTPM et effectuer le scellement vTPM.
- Comment accéder aux appareils SEV-SNP et TDX dans les charges de travail CGKE à l'aide de
cc-device-plugin. - Comment récupérer des citations d'attestation matérielle à l'aide de go-tdx-guest et go-sev-guest.
Prérequis
- Un projet Google Cloud Platform
- Un navigateur tel que Chrome ou Firefox
- Connaissances de base de Google Compute Engine (atelier de programmation), Confidential VM, nœuds Confidential GKE et Artifact Registry
- Pour Intel TDX ou AMD SEV-SNP : version GKE minimale v1.33.5-gke.1697000+ ou v1.34.1-gke.2909000+, et familles de machines appropriées (c3 pour TDX, n2d pour SNP).
2. Préparation
Pour activer les API nécessaires, exécutez la commande suivante dans la console Cloud ou dans votre environnement de développement 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. Configurer le plan de contrôle CGKE partagé
Dans cette étape, vous allez configurer le plan de contrôle GKE partagé et un dépôt Docker Artifact Registry.
1). Configurez les variables d'environnement et créez le cluster GKE partagé :
Remplacez your-project-id par l'ID du projet. Remplacez us-central1-c par la zone souhaitée. (Consultez Régions et zones.)
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). Créez un dépôt Docker Artifact Registry pour stocker les images de conteneurs de charge de travail :
gcloud artifacts repositories create codelab-repo \
--repository-format=docker \
--location=us
Choisir votre chemin
Le plan de contrôle GKE partagé et le dépôt Artifact Registry sont désormais prêts. Choisissez votre parcours :
- Option 1 : Attestation et scellement à distance vTPM (AMD SEV) : passez directement à Configurer les nœuds CGKE et exposer le périphérique vTPM.
- Option 2 : Attestation matérielle (Intel TDX et AMD SEV-SNP) : accédez directement à Déployer des charges de travail pour effectuer l'attestation matérielle. (Vous souhaitez tout explorer ? Il vous suffit de suivre l'option 1, puis de passer à l'option 2.
4. Configurer des nœuds CGKE et exposer le périphérique vTPM à des charges de travail sélectionnées
Dans cette étape, vous allez créer un pool de nœuds vTPM sur CGKE et appliquer un plug-in d'appareil pour exposer l'appareil vTPM CVM aux charges de travail. Accédez à la console Cloud ou à votre environnement de développement local pour exécuter les commandes.
1). Ajouter un pool de nœuds 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). Supprimer le pool de nœuds par défaut (facultatif, mais recommandé pour réduire les coûts/le bruit)
gcloud container node-pools delete default-pool \
--cluster=${CLUSTER_NAME} \
--zone=${ZONE} \
--quiet
3). Démarrez le plug-in de l'appareil pour permettre au cluster CGKE d'exposer l'appareil vTPM aux charges de travail. Nous utilisons un plug-in d'appareil Kubernetes pour créer des ressources (google.com/cc). Toute charge de travail associée à la nouvelle ressource pourra voir les appareils sur le nœud de calcul.
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
La commande suivante vous permet d'afficher le cc-device-plugin déployé.
kubectl get pods -A | grep "cc-device-plugin"
Remarque : Dans le cas d'un cluster GKE en mode mixte (avec des nœuds de calcul Confidential GKE et non Confidential GKE), il est recommandé que l'opérateur ne déploie cc-device-plugin que sur les nœuds de calcul Confidential GKE.
(Facultatif) Appliquez la surveillance Prometheus des pods CGKE. L'activation de la surveillance vous permet d'observer l'état du plug-in de l'appareil.
kubectl apply -f https://raw.githubusercontent.com/google/cc-device-plugin/main/manifests/cc-device-plugin-pod-monitoring.yaml
Accédez à https://console.cloud.google.com/monitoring/metrics-explorer et recherchez les métriques cc-device-plugin ou utilisez PROMQL. Par exemple, la commande PROMQL suivante affiche les secondes de processeur pour chaque processus cc-device-plugin.
rate(process_cpu_seconds_total[${__interval}])
5. Déployer une charge de travail et effectuer une attestation à distance sur la charge de travail (vTPM)
Dans cette étape, vous allez créer et déployer une charge de travail sur le cluster CGKE que vous avez créé à l'étape précédente, puis effectuer une attestation à distance vTPM pour récupérer un jeton d'attestation (jeton OIDC) sur le nœud de calcul.
1). Créez l'image de conteneur d'application et transférez-la vers Artifact Registry. L'image du conteneur d'application contient l'outil go-tpm, qui peut collecter des preuves d'attestation et les envoyer au service Attestation Verifier pour obtenir un jeton d'attestation (jeton OIDC).
- Créez le
Dockerfilepour l'image de conteneur de l'application.
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
- Transférez l'image de conteneur d'application vers 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). Configurez un compte de service Kubernetes pour hériter des autorisations d'un compte de service GCP sur les ressources GCP.
- Créez un compte de service Kubernetes
codelab-ksa.
kubectl create serviceaccount codelab-ksa \
--namespace default
- Créez un rôle
Confidential_Computing_Workload_Useret accordez-lui les autorisations d'accès aux API Confidential Computing.
gcloud iam roles create Confidential_Computing_Workload_User --project=${PROJECT_ID} \
--title="CGKE Workload User" --description="Grants the ability to generate an attestation token in a GKE workload." \
--permissions="confidentialcomputing.challenges.create,confidentialcomputing.challenges.verify,confidentialcomputing.locations.get,confidentialcomputing.locations.list" --stage=GA
- Créez un compte de service GCP
codelab-csaet associez-le au rôleConfidential_Computing_Workload_User. afin quecodelab-csadispose des autorisations nécessaires pour accéder aux API 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]"
- Associez le compte de service Kubernetes
codelab-ksaau compte de service GCPcodelab-csa. afin quecodelab-ksadispose des autorisations nécessaires pour accéder aux API Confidential Computing.
kubectl annotate serviceaccount codelab-ksa \
--namespace default \
iam.gke.io/gcp-service-account=codelab-csa@${PROJECT_ID}.iam.gserviceaccount.com
3). Créez le fichier YAML de déploiement de l'application de démonstration. Attribuez le compte de service Kubernetes codelab-ksa aux charges de travail sélectionnées.
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). Appliquez le déploiement au cluster CGKE.
kubectl apply -f deploy.yaml
5). Connectez-vous à la charge de travail et lancez l'attestation à distance pour récupérer un jeton d'attestation (jeton OIDC).
kubectl exec -it go-tpm-demo -- /bin/bash ./gotpm token --event-log=/run/cc-device-plugin/binary_bios_measurements > attestation_token
6). Imprimez le jeton à l'écran pour pouvoir le copier.
cat attestation_token
Vous pouvez décoder le jeton d'attestation dans jwt.io pour afficher les revendications.
6. Configurer le serveur Web Secret Release
Dans cette étape, vous allez quitter la session SSH précédente et configurer une autre VM. Sur cette VM, vous allez configurer un serveur Web de publication de secrets. Le serveur Web valide le jeton d'attestation reçu et ses revendications. Si les validations réussissent, le secret est transmis au demandeur.
1). Accédez à la console Cloud ou à votre environnement de développement local. Créez une machine virtuelle.
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). Connectez-vous en SSH à votre nouvelle VM.
gcloud compute ssh --zone ${ZONE} cgke-attestation-codelab-web-server
3). Configurez l'environnement 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). Créez les deux fichiers suivants qui stockent le code source du serveur Web de publication secrète.
Créez 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
Créez 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). Exécutez les commandes suivantes pour créer le serveur Web et l'exécuter. Cela démarre le serveur Web de publication des secrets sur le port :8080.
go mod init google.com/codelab go mod tidy go get github.com/golang-jwt/jwt/v4 go build ./codelab
Dépannage : l'avertissement suivant peut s'afficher et peut être ignoré lors de l'exécution de 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). Démarrez un autre onglet de la console cloud ou une autre session d'environnement de développement local, puis exécutez la commande suivante. Vous obtiendrez ainsi le cgke-attestation-codelab-web-server-internal-ip.
gcloud compute instances describe cgke-attestation-codelab-web-server \
--format='get(networkInterfaces[0].networkIP)' \
--zone=${ZONE}
7). Connectez-vous à votre charge de travail CGKE et lancez l'attestation à distance pour récupérer un jeton d'attestation (jeton OIDC). Intégrez ensuite le contenu de attestation-token et cgke-attestation-codelab-web-server-internal-ip dans la commande suivante. Vous obtiendrez ainsi le secret détenu par le serveur Web de publication 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)"
Remplacez les éléments suivants :
cgke-attestation-codelab-web-server-internal-ipest l'adresse IP interne de l'instance de VMcgke-attestation-codelab-web-server.
7. Scellement vTPM sur les nœuds CGKE
Dans cette étape, vous allez configurer l'autorisation du propriétaire vTPM sur les nœuds CGKE et déployer une charge de travail avec le mot de passe du propriétaire vTPM. Vous créez ensuite une clé primaire vTPM pour sceller et desceller les données de la charge de travail à l'aide de la fonctionnalité de scellement vTPM.
1). Configurez l'autorisation du propriétaire du vTPM sur les nœuds CGKE.
- Créez une image de conteneur de job ponctuel. Ce job ponctuel définit le mot de passe du propriétaire pour tous les vTPM. Voici le
Dockerfilepermettant de créer l'image de conteneur.
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
- Créez et transférez l'image de conteneur du job ponctuel vers 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
- Exécutez le job ponctuel via un job Kubernetes. (AVERTISSEMENT : cette tâche efface le vTPM sur chaque CVM. Si votre CVM utilise le vTPM pour chiffrer le disque, cette tâche rendra votre CVM inutilisable après le redémarrage. Vous pouvez vérifier si votre disque possède le FSTYPE
crypto_LUKSavec la commandelsblk -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
Conseil : Si vous devez réexécuter le job tpm-tools-task, veillez à supprimer d'abord le job existant :
kubectl delete job tpm-tools-task
- Lancez le job ponctuel. Cette tâche définit la phrase secrète du propriétaire du module TPM virtuel sur tous les nœuds de calcul.
kubectl apply -f tpm-tools-task.yaml
2). Créez un secret Kubernetes pour stocker la phrase secrète du propriétaire du vTPM.
kubectl create secret generic tpm-secret --from-literal=passphrase='this_is_passphrase'
3). Créez un conteneur d'application de démonstration et transmettez-lui la phrase secrète. Le conteneur de l'application de démonstration contient tpm2-tools pour interagir avec le module TPM virtuel.
- Créez le fichier YAML de déploiement pour le conteneur d'application de démonstration.
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
- Déployez l'application de démonstration.
kubectl apply -f deploy_demo.yaml
4). Effectuez le scellement vTPM dans le conteneur de l'application de démonstration.
- Connectez-vous au conteneur de l'application de démonstration et définissez une clé primaire avec une phrase secrète.
kubectl exec -it tpm-tools-demo -- /bin/bash tpm2_createprimary -C o -c primary.ctx -P $(cat /etc/tpmsecret/passphrase)
tpm2_createprimary interagit avec le vTPM pour générer l'objet principal en fonction de la hiérarchie et du modèle spécifiés.
- -C o : indique que la clé primaire sera créée dans la hiérarchie du propriétaire du module TPM.
- -c
primary.ctx: enregistre le contexte (handle et données associées) de l'objet principal créé dans le fichierprimary.ctx. Ce contexte est essentiel pour les opérations ultérieures.
La charge de travail ne peut pas utiliser la mauvaise phrase secrète du propriétaire pour créer une clé primaire.
tpm2_createprimary -C o -P wrong_passphrase
La commande renvoie les erreurs suivantes :
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
- La clé principale créée peut ensuite être utilisée pour sceller et desceller les données.
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 interagit avec le vTPM pour générer l'objet cryptographique souhaité.
- -C
primary.ctx: utilise le contexte de clé primaire que nous avons créé précédemment. - -u
sealed.pub: stocke la partie publique de la clé de scellement (nécessaire pour le déscellement) danssealed.pub. - -r
sealed.priv: stocke la partie privée de la clé de scellement danssealed.priv. - -i
secret.txt: fichier contenant le secret à sceller.
tpm2_load : charge la clé de scellement dans le TPM à l'aide des parties publiques et privées (sealed.pub, sealed.priv) et enregistre son contexte dans sealed.ctx.
tpm2_unseal : déchiffre (descelle) les données précédemment chiffrées (scellées) à l'aide d'un objet de scellage vTPM.
cat unsealed.txt : affiche le message secret descellé pour confirmer que le processus a réussi.
Notez que les fichiers primary.ctx et sealed.priv ne peuvent être utilisés que sur un seul appareil vTPM. Toute personne ayant accès à l'appareil vTPM et à ces fichiers peut accéder aux données scellées. Vous pouvez également utiliser une règle sur les valeurs PCR pour sceller les données, mais cela ne fait pas partie de cet atelier de programmation.
8. Déployer des charges de travail pour effectuer l'attestation matérielle : Intel TDX et AMD SEV-SNP

Dans cette étape, vous allez créer une charge de travail basée sur Intel TDX ou AMD SEV-SNP, puis effectuer une attestation matérielle sur les nœuds Confidential GKE. Comme illustré dans le diagramme de l'architecture ci-dessus, nous utiliserons le plug-in d'appareil invité pour récupérer les citations d'attestation pour les architectures Intel TDX et AMD SEV-SNP. Accédez à la console Cloud ou à votre environnement de développement local pour exécuter les commandes.
Choisissez l'architecture d'informatique confidentielle (Intel TDX ou AMD SEV-SNP) que vous souhaitez tester.
1). Ajouter un pool de nœuds 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). Ajouter un pool de nœuds 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). Supprimer le pool de nœuds par défaut (facultatif, mais recommandé pour réduire les coûts/le bruit)
gcloud container node-pools delete default-pool \
--cluster=${CLUSTER_NAME} \
--zone=${ZONE} \
--quiet
(Remarque : Si vous avez déjà terminé la section sur le module TPM virtuel, ce pool est déjà supprimé. Vous pouvez donc ignorer les erreurs "Introuvable" qui s'affichent ici.)
4). Démarrez le plug-in de l'appareil pour permettre au cluster CGKE d'exposer les appareils invités d'attestation matérielle aux charges de travail. Nous utilisons un plug-in d'appareil Kubernetes pour créer des ressources (intel.com/tdx et amd.com/sev-snp). Toute charge de travail associée à ces ressources pourra accéder aux appareils invités sur le nœud de calcul.
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
La commande suivante vous permet d'afficher le cc-device-plugin déployé.
kubectl get pods -A | grep "cc-device-plugin"
Remarque : Dans le cas d'un cluster GKE en mode mixte (avec des nœuds de calcul Confidential GKE et non Confidential GKE), il est recommandé que l'opérateur ne déploie cc-device-plugin que sur les nœuds de calcul Confidential GKE.
9. Attestation matérielle : Intel TDX et AMD SEV-SNP
Dans cette section, vous allez apprendre à effectuer une attestation au niveau matériel dans les nœuds Confidential GKE en récupérant des citations d'attestation pour les architectures Intel TDX et AMD SEV-SNP à l'aide des plug-ins de l'appareil invité.
1). Créez une application Go pour récupérer la citation d'attestation matérielle de l'appareil invité concerné.
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). Créez le Dockerfile pour l'outil de test d'attestation matérielle :
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). Créez et transférez l'image vers votre 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). Créez le fichier YAML de déploiement de l'application pour les charges de travail Intel TDX et AMD SEV-SNP afin d'exposer les périphériques matériels.
- Pour les charges de travail Intel TDX, créez le fichier YAML de déploiement suivant :
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
- Pour les charges de travail AMD SEV-SNP, créez le fichier YAML de déploiement suivant :
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). Appliquez les déploiements au cluster CGKE.
kubectl apply -f deploy-tdx.yaml kubectl apply -f deploy-snp.yaml
6). Attendez que les pods se terminent et vérifiez les journaux d'attestation matérielle.
Tout d'abord, vérifiez l'état de vos pods pour vous assurer qu'ils ont bien été planifiés et qu'ils sont en cours d'exécution (attendez qu'ils affichent "Terminé" ou "En cours d'exécution") :
kubectl get pods -w
Une fois les pods prêts, vérifiez les journaux pour vous assurer que les citations d'attestation matérielle ont bien été récupérées :
# View the TDX attestation logs kubectl logs tdx-attestation-pod # View the SEV-SNP attestation logs kubectl logs snp-attestation-pod
(Le temps d'exécution total, le nombre total d'itérations et le nombre de réussites/échecs seront consignés.)
10. Nettoyage
Pour éviter que les ressources créées dans cet atelier de programmation soient facturées en permanence sur votre compte Google Cloud, il est important de les supprimer.
Exécutez les commandes suivantes dans Cloud Shell ou dans votre environnement de développement local.
1). Définissez les variables d'environnement :
export PROJECT_ID="your-project-id"
export ZONE="us-central1-c"
export CLUSTER_NAME="cgke-attestation-codelab"
gcloud config set project ${PROJECT_ID}
2). Supprimer les ressources GKE
gcloud container clusters delete ${CLUSTER_NAME} \
--zone=${ZONE} \
--quiet
3). Supprimer Artifact Registry
gcloud artifacts repositories delete codelab-repo \
--location=us \
--quiet
4). Supprimer la VM du serveur Web
gcloud compute instances delete cgke-attestation-codelab-web-server \
--zone=${ZONE}
5). Supprimer des ressources 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. Étape suivante
En savoir plus sur les nœuds Confidential GKE