1. Przegląd
Węzły poufnej usługi GKE (CGKE) zapewniają, że dane w zbiorach zadań są szyfrowane podczas używania. Udostępnienie urządzenia vTPM zadaniom CGKE umożliwia im korzystanie z funkcji vTPM. Z tego ćwiczenia dowiesz się, jakie są funkcje vTPM, a także jak korzystać z poświadczeń sprzętowych Intel TDX i AMD SEV-SNP za pomocą cc-device-plugin.
- Poświadczenia zdalne vTPM umożliwiają zdalnej stronie weryfikację, czy węzły CGKE hostujące zbiory zadań działają na poufnych maszynach wirtualnych (CVM).
- autoryzacja vTPM i uszczelnianie vTPM.
- Poświadczanie sprzętowe przy użyciu udostępnionych urządzeń gościa Intel TDX i AMD SEV-SNP.

Jak widać na powyższym rysunku, pierwsza część tego laboratorium obejmuje te kroki:
- Węzły CGKE konfigurują i udostępniają urządzenie vTPM wybranym zbiorom zadań.
- Wdrażanie zbioru zadań i zdalne atestowanie węzła CGKE, w którym jest on hostowany.
- Wdrażanie zbioru zadań w celu pobierania cytatów atestu sprzętu bezpośrednio z urządzeń TDX lub SEV-SNP.
- Konfiguracja serwera WWW do udostępniania obiektu tajnego.

Jak widać na powyższym rysunku, druga część tego laboratorium obejmuje:
- konfigurowanie autoryzacji vTPM i uszczelniania vTPM na węzłach CGKE;

Jak widać na powyższym rysunku, trzecia część tego laboratorium obejmuje:
- Jak
cc-device-pluginbezpiecznie mapuje urządzenia podczas fazy konfiguracji. - Wdrażanie zbiorów zadań w celu pobierania cytatów atestu sprzętu bezpośrednio z urządzeń TDX lub SEV-SNP w fazie działania bez żadnych dodatkowych kosztów.
Czego się nauczysz
- Jak udostępnić urządzenie vTPM zbiorom zadań CGKE.
- Jak przeprowadzić zdalne poświadczenie za pomocą interfejsu Confidential Computing API (usługi weryfikacji poświadczeń) w przypadku zadań CGKE.
- Jak skonfigurować autoryzację vTPM i przeprowadzić uszczelnianie vTPM.
- Jak uzyskać dostęp do urządzeń SEV-SNP i TDX w zbiorach zadań CGKE za pomocą
cc-device-plugin. - Jak pobierać cytaty atestów sprzętowych za pomocą pakietów go-tdx-guest i go-sev-guest.
Czego potrzebujesz
- Projekt Google Cloud Platform
- przeglądarka, np. Chrome lub Firefox;
- Podstawowa wiedza o Google Compute Engine (codelab), poufnych maszynach wirtualnych, poufnych węzłach GKE i Artifact Registry.
- W przypadku technologii Intel TDX lub AMD SEV-SNP: minimalna wersja GKE to v1.33.5-gke.1697000+ lub v1.34.1-gke.2909000+ oraz odpowiednie rodziny maszyn (c3 dla TDX, n2d dla SNP).
2. Konfiguracja i wymagania
Aby włączyć niezbędne interfejsy API, uruchom to polecenie w konsoli Google Cloud lub w lokalnym środowisku programistycznym:
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. Konfigurowanie współdzielonej platformy sterującej CGKE
W tym kroku skonfigurujesz udostępnioną platformę sterującą GKE i repozytorium Dockera w Artifact Registry.
1) Skonfiguruj zmienne środowiskowe i utwórz współdzielony klaster GKE:
Zastąp your-project-id identyfikatorem projektu. Zastąp us-central1-c wybraną strefą. (Patrz Regiony i strefy)
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) Utwórz repozytorium Dockera w Artifact Registry, w którym będziesz przechowywać obrazy kontenerów obciążeń:
gcloud artifacts repositories create codelab-repo \
--repository-format=docker \
--location=us
Wybierz ścieżkę
Udostępniona platforma sterująca GKE i repozytorium Artifact Registry są już gotowe. Wybierz ścieżkę:
- Opcja 1. Poświadczenia zdalne vTPM i uszczelnianie (AMD SEV) – przejdź bezpośrednio do sekcji Konfigurowanie węzłów CGKE i udostępnianie urządzenia vTPM.
- Opcja 2. Atestowanie sprzętu (Intel TDX i AMD SEV-SNP) – przejdź bezpośrednio do sekcji Wdrażanie zbiorów zadań w celu przeprowadzenia atestowania sprzętu. (Chcesz odkryć wszystko? Wystarczy, że wykonasz czynności opisane w Opcji 1 i przejdziesz do Opcji 2).
4. Konfigurowanie węzłów CGKE i udostępnianie urządzenia vTPM wybranym zbiorom zadań
W tym kroku utworzysz pulę węzłów vTPM w CGKE i zastosujesz wtyczkę urządzenia, aby udostępnić urządzenie vTPM CVM obciążeniom. Aby uruchomić polecenia, otwórz konsolę Google Cloud lub lokalne środowisko programistyczne.
1) Dodawanie puli węzłów 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) Usuń domyślną pulę węzłów (opcjonalnie, ale zalecane, aby zaoszczędzić koszty i zmniejszyć szum)
gcloud container node-pools delete default-pool \
--cluster=${CLUSTER_NAME} \
--zone=${ZONE} \
--quiet
3) Uruchom wtyczkę urządzenia, aby umożliwić klastrowi CGKE udostępnianie urządzenia vTPM zbiorom zadań. Do tworzenia nowych zasobów (google.com/cc) używamy wtyczki do urządzenia Kubernetes. Każdy zbiór zadań powiązany z nowym zasobem będzie mógł zobaczyć urządzenia w węźle roboczym.
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
Poniższe polecenie umożliwia wyświetlenie wdrożonego cc-device-plugin.
kubectl get pods -A | grep "cc-device-plugin"
Uwaga: w przypadku klastra GKE w trybie mieszanym (z poufnymi i niepoufnych węzłami roboczymi GKE) zalecamy, aby operator wdrażał cc-device-plugin tylko na poufnych węzłach roboczych GKE.
Opcjonalnie: Zastosuj monitorowanie usługi Prometheus w przypadku poda CGKE. Włączenie monitorowania umożliwia obserwowanie stanu wtyczki urządzenia.
kubectl apply -f https://raw.githubusercontent.com/google/cc-device-plugin/main/manifests/cc-device-plugin-pod-monitoring.yaml
Otwórz https://console.cloud.google.com/monitoring/metrics-explorer i znajdź dane cc-device-plugin lub użyj PROMQL. Na przykład to polecenie PROMQL pokazuje sekundy procesora dla każdego procesu cc-device-plugin:
rate(process_cpu_seconds_total[${__interval}])
5. Wdrażanie zbioru zadań i przeprowadzanie zdalnego atestu zbioru zadań (vTPM)
W tym kroku utworzysz i wdrożysz zbiór zadań w klastrze CGKE utworzonym w poprzednim kroku oraz przeprowadzisz zdalne atestowanie vTPM, aby pobrać token atestu (token OIDC) w węźle roboczym.
1) Utwórz obraz kontenera aplikacji i prześlij go do Artifact Registry. Obraz kontenera aplikacji zawiera narzędzie go-tpm, które może zbierać dowody atestu i wysyłać je do usługi weryfikacji atestu w celu uzyskania tokena atestu (tokena OIDC).
- Utwórz
Dockerfiledla obrazu kontenera aplikacji.
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
- Prześlij obraz kontenera aplikacji do 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) Skonfiguruj konto usługi Kubernetes, aby dziedziczyło uprawnienia konta usługi GCP w zasobach GCP.
- Utwórz konto usługi Kubernetes
codelab-ksa.
kubectl create serviceaccount codelab-ksa \
--namespace default
- Utwórz rolę
Confidential_Computing_Workload_Useri przyznaj jej uprawnienia dostępu do interfejsów 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
- Utwórz konto usługi GCP
codelab-csai powiąż je z roląConfidential_Computing_Workload_User. Dzięki temucodelab-csabędzie mieć uprawnienia dostępu do interfejsów API usługi 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]"
- Powiąż konto usługi Kubernetes
codelab-ksaz kontem usługi GCPcodelab-csa. Dzięki temucodelab-ksabędzie mieć uprawnienia dostępu do interfejsów API usługi Confidential Computing.
kubectl annotate serviceaccount codelab-ksa \
--namespace default \
iam.gke.io/gcp-service-account=codelab-csa@${PROJECT_ID}.iam.gserviceaccount.com
3) Utwórz plik YAML wdrożenia aplikacji demonstracyjnej. Przypisz konto usługi Kubernetes codelab-ksa do wybranych obciążeń.
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) Zastosuj wdrożenie w klastrze CGKE.
kubectl apply -f deploy.yaml
5). Połącz się z zadaniami i uruchom zdalne potwierdzanie, aby pobrać token potwierdzenia (token OIDC).
kubectl exec -it go-tpm-demo -- /bin/bash ./gotpm token --event-log=/run/cc-device-plugin/binary_bios_measurements > attestation_token
6). Wyświetl token na ekranie, aby można go było skopiować.
cat attestation_token
Aby wyświetlić roszczenia, możesz zdekodować token atestu na stronie jwt.io.
6. Konfigurowanie serwera WWW Secret Release
W tym kroku zakończysz poprzednią sesję SSH i skonfigurujesz kolejną maszynę wirtualną. Na tej maszynie wirtualnej skonfigurujesz serwer WWW z tajną wersją. Serwer WWW weryfikuje otrzymany token atestu i jego roszczenia. Jeśli weryfikacja się powiedzie, usługa przekazuje obiekt tajny do osoby wysyłającej żądanie.
1) Otwórz konsolę Google Cloud lub lokalne środowisko programistyczne. Utwórz maszynę wirtualną.
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) Połącz się z nową maszyną wirtualną za pomocą SSH.
gcloud compute ssh --zone ${ZONE} cgke-attestation-codelab-web-server
3) Skonfiguruj środowisko 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) Utwórz te 2 pliki zawierające kod źródłowy serwera WWW z tajną wersją.
Utwórz 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
Utwórz 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). Aby utworzyć serwer WWW i go uruchomić, wykonaj te polecenia. Spowoduje to uruchomienie serwera WWW tajnej wersji na porcie :8080.
go mod init google.com/codelab go mod tidy go get github.com/golang-jwt/jwt/v4 go build ./codelab
Rozwiązywanie problemów: podczas uruchamiania polecenia go mod tidy może pojawić się to ostrzeżenie, które można zignorować:
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). Otwórz kolejną kartę konsoli w chmurze lub sesję lokalnego środowiska programistycznego i uruchom to polecenie. Otrzymasz cgke-attestation-codelab-web-server-internal-ip.
gcloud compute instances describe cgke-attestation-codelab-web-server \
--format='get(networkInterfaces[0].networkIP)' \
--zone=${ZONE}
7). Połącz się z zbiorem zadań CGKE i uruchom zdalne potwierdzenie, aby pobrać token atestu (token OIDC). Następnie w tym poleceniu umieść zawartość plików attestation-token i cgke-attestation-codelab-web-server-internal-ip. Spowoduje to pobranie obiektu tajnego przechowywanego na serwerze WWW udostępniającym obiekty tajne.
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)"
Zastąp te elementy:
cgke-attestation-codelab-web-server-internal-ipto wewnętrzny adres IP instancji maszyny wirtualnejcgke-attestation-codelab-web-server.
7. Uszczelnianie vTPM w węzłach CGKE
W tym kroku skonfigurujesz autoryzację właściciela vTPM na węzłach CGKE i wdrożysz zadanie z hasłem właściciela vTPM. Następnie tworzysz główny klucz vTPM, aby uszczelniać i odszyfrowywać dane w zbiorze zadań za pomocą funkcji uszczelniania vTPM.
1) Skonfiguruj autoryzację właściciela vTPM na węzłach CGKE.
- Utwórz obraz kontenera jednorazowego zadania. Jednorazowe zadanie ustawia hasło właściciela dla wszystkich modułów vTPM. Poniżej znajdziesz
Dockerfile, które umożliwia utworzenie obrazu kontenera.
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
- Utwórz obraz kontenera zadania jednorazowego i przenieś go do 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
- Wykonaj jednorazowe zadanie za pomocą zadania Kubernetes. (OSTRZEŻENIE: to zadanie czyści moduł vTPM na każdej maszynie CVM. Jeśli maszyna CVM używa modułu vTPM do szyfrowania dysku, po ponownym uruchomieniu będzie bezużyteczna. Możesz sprawdzić, czy dysk ma FSTYPE
crypto_LUKSza pomocą polecenialsblk -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
Wskazówka: jeśli musisz ponownie uruchomić zadanie tpm-tools-task, najpierw usuń istniejące zadanie:
kubectl delete job tpm-tools-task
- Uruchom jednorazowe zadanie. To zadanie ustawia hasło właściciela vTPM we wszystkich węzłach roboczych.
kubectl apply -f tpm-tools-task.yaml
2) Utwórz tajny klucz Kubernetes, aby przechowywać hasło właściciela vTPM.
kubectl create secret generic tpm-secret --from-literal=passphrase='this_is_passphrase'
3) Utwórz kontener aplikacji demonstracyjnej i przekaż do niego hasło. Kontener aplikacji demonstracyjnej zawiera tpm2-tools do interakcji z vTPM.
- Utwórz plik YAML wdrożenia dla kontenera aplikacji demonstracyjnej.
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
- Wdróż aplikację demonstracyjną.
kubectl apply -f deploy_demo.yaml
4) Przeprowadź uszczelnianie vTPM w kontenerze aplikacji demonstracyjnej.
- Połącz się z kontenerem aplikacji demonstracyjnej i ustaw klucz podstawowy z hasłem.
kubectl exec -it tpm-tools-demo -- /bin/bash tpm2_createprimary -C o -c primary.ctx -P $(cat /etc/tpmsecret/passphrase)
tpm2_createprimary wchodzi w interakcję z vTPM, aby wygenerować obiekt podstawowy na podstawie określonej hierarchii i szablonu.
- -C o: wskazuje, że klucz podstawowy zostanie utworzony w hierarchii właściciela modułu TPM.
- -c
primary.ctx: zapisuje kontekst (uchwyt i powiązane dane) utworzonego obiektu podstawowego w plikuprimary.ctx. Ten kontekst jest niezbędny w przypadku późniejszych operacji.
Zbiór zadań nie może używać nieprawidłowego hasła właściciela do utworzenia klucza podstawowego.
tpm2_createprimary -C o -P wrong_passphrase
Polecenie zwraca te błędy:
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
- Utworzony klucz podstawowy może być używany do zabezpieczania i odbezpieczania danych.
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 wchodzi w interakcję z vTPM, aby wygenerować żądany obiekt kryptograficzny.
- -C
primary.ctx: używa kontekstu klucza podstawowego utworzonego wcześniej. - -u
sealed.pub: zapisuje publiczną część klucza szyfrującego (potrzebną do odszyfrowania) wsealed.pub. - -r
sealed.priv: przechowuje prywatną część klucza szyfrującego wsealed.priv. - -i
secret.txt: plik zawierający klucz tajny do zaszyfrowania.
tpm2_load: Wczytuje klucz uszczelniający do modułu TPM za pomocą części publicznej i prywatnej (sealed.pub, sealed.priv) i zapisuje jego kontekst w sealed.ctx.
tpm2_unseal: odszyfrowywanie (odpieczętowywanie) danych, które zostały wcześniej zaszyfrowane (zapieczętowane) przy użyciu obiektu pieczętowania vTPM.
cat unsealed.txt: wyświetla odszyfrowaną wiadomość tajną, aby potwierdzić, że proces się powiódł.
Pamiętaj, że pliki primary.ctx i sealed.priv można używać tylko na jednym urządzeniu z modułem vTPM. Każda osoba, która ma dostęp do urządzenia vTPM i tych plików, może uzyskać dostęp do zaszyfrowanych danych. Możesz też użyć zasad dotyczących wartości PCR, aby zabezpieczyć dane, ale wykracza to poza zakres tego laboratorium.
8. Wdrażanie zbiorów zadań w celu przeprowadzenia atestu sprzętu: Intel TDX i AMD SEV-SNP

W tym kroku utworzysz zadanie oparte na Intel TDX lub AMD SEV-SNP i przeprowadzisz atestowanie na poziomie sprzętu w poufnych węzłach GKE. Jak widać na powyższym schemacie architektury, do pobierania atestów dla architektur Intel TDX i AMD SEV-SNP będziemy używać wtyczki urządzenia gościa. Aby uruchomić polecenia, otwórz konsolę Google Cloud lub lokalne środowisko programistyczne.
Wybierz architekturę Confidential Computing, którą chcesz przetestować: Intel TDX lub AMD SEV-SNP.
1) Dodawanie puli węzłów 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) Dodawanie puli węzłów 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) Usuń domyślną pulę węzłów (opcjonalnie, ale zalecane, aby zaoszczędzić koszty i zmniejszyć szum)
gcloud container node-pools delete default-pool \
--cluster=${CLUSTER_NAME} \
--zone=${ZONE} \
--quiet
(Uwaga: jeśli sekcja vTPM została już wcześniej ukończona, pula jest już usunięta i możesz bezpiecznie zignorować wszelkie błędy „Nie znaleziono”).
4) Uruchom wtyczkę urządzenia, aby umożliwić klastrowi CGKE udostępnianie urządzeń gościa z poświadczeniem sprzętowym zadaniom. Do tworzenia nowych zasobów (intel.com/tdx i amd.com/sev-snp) używamy wtyczki do urządzeń Kubernetes. Każde zadanie powiązane z tymi zasobami będzie mogło uzyskać dostęp do urządzeń gościa w węźle roboczym.
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
Poniższe polecenie umożliwia wyświetlenie wdrożonego cc-device-plugin.
kubectl get pods -A | grep "cc-device-plugin"
Uwaga: w przypadku klastra GKE w trybie mieszanym (z poufnymi i niepoufnych węzłami roboczymi GKE) zalecamy, aby operator wdrażał cc-device-plugin tylko w poufnych węzłach roboczych GKE.
9. Atestowanie sprzętu: Intel TDX i AMD SEV-SNP
Z tej sekcji dowiesz się, jak przeprowadzać atestowanie na poziomie sprzętu w poufnych węzłach GKE, pobierając cytaty atestacyjne dla architektur Intel TDX i AMD SEV-SNP za pomocą wtyczek urządzenia gościa.
1) Utwórz aplikację w Go, aby pobrać cytat atestu sprzętowego z odpowiedniego urządzenia gościa.
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) Utwórz Dockerfile dla narzędzia do testowania atestu sprzętowego:
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) Skompiluj obraz i przenieś go do 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) Utwórz plik YAML wdrożenia aplikacji dla zbiorów zadań Intel TDX i AMD SEV-SNP, aby udostępnić urządzenia.
- W przypadku zadań Intel TDX utwórz ten plik YAML wdrożenia:
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
- W przypadku zadań AMD SEV-SNP utwórz ten plik YAML wdrożenia:
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). Zastosuj wdrożenia w klastrze CGKE.
kubectl apply -f deploy-tdx.yaml kubectl apply -f deploy-snp.yaml
6). Poczekaj, aż pody zakończą działanie, i sprawdź dzienniki atestu sprzętowego.
Najpierw sprawdź stan podów, aby upewnić się, że zostały zaplanowane i są uruchomione (poczekaj, aż wyświetli się stan Ukończono lub Uruchomiono):
kubectl get pods -w
Gdy pody będą gotowe, sprawdź logi, aby potwierdzić, że cytaty dotyczące atestu sprzętowego zostały pobrane:
# View the TDX attestation logs kubectl logs tdx-attestation-pod # View the SEV-SNP attestation logs kubectl logs snp-attestation-pod
(Zostaną zarejestrowane łączny czas wykonania, łączna liczba iteracji oraz liczba udanych i nieudanych prób).
10. Czyszczenie
Aby uniknąć obciążania konta Google Cloud bieżącymi opłatami, usuń zasoby utworzone w tym laboratorium.
Uruchom te polecenia w Cloud Shell lub w lokalnym środowisku programistycznym.
1) Ustaw zmienne środowiskowe:
export PROJECT_ID="your-project-id"
export ZONE="us-central1-c"
export CLUSTER_NAME="cgke-attestation-codelab"
gcloud config set project ${PROJECT_ID}
2) Usuwanie zasobów GKE
gcloud container clusters delete ${CLUSTER_NAME} \
--zone=${ZONE} \
--quiet
3) Usuwanie Artifact Registry
gcloud artifacts repositories delete codelab-repo \
--location=us \
--quiet
4) Usuwanie maszyny wirtualnej serwera WWW
gcloud compute instances delete cgke-attestation-codelab-web-server \
--zone=${ZONE}
5). Usuwanie zasobów 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. Co dalej?
Dowiedz się więcej o poufnych węzłach GKE.