Удалённая аттестация и запечатывание конфиденциальных узлов GKE (vTPM, SEV-SNP, TDX)

1. Обзор

Узлы Confidential GKE (CGKE) гарантируют, что данные в рабочих нагрузках шифруются во время использования. Предоставление доступа к устройству vTPM рабочим нагрузкам CGKE позволяет рабочим нагрузкам использовать функции vTPM. В этом практическом занятии вы узнаете о возможностях vTPM, а также о том, как использовать аппаратную аттестацию Intel TDX и AMD SEV-SNP с помощью плагина cc-device-plugin .

  • Удалённая аттестация vTPM позволяет удалённой стороне проверить, что узлы CGKE, на которых размещены рабочие нагрузки, работают на конфиденциальных виртуальных машинах (CVM).
  • Авторизация vTPM и запечатывание vTPM.
  • Проверка работоспособности оборудования с использованием открытых гостевых устройств Intel TDX и AMD SEV-SNP.

683a3b43587ef69f.png

Как показано на рисунке выше, первая часть этого практического занятия включает следующие шаги:

  • Узлы CGKE настраивают и предоставляют доступ к устройству vTPM для выбранных рабочих нагрузок.
  • Разверните рабочую нагрузку и проведите удаленную аутентификацию узла CGKE, на котором размещена эта рабочая нагрузка.
  • Разверните рабочую нагрузку для получения аттестационных данных оборудования непосредственно с устройств TDX или SEV-SNP.
  • Настройка веб-сервера Secret Release.

8f6e80c762a5d911.png

Как показано на рисунке выше, вторая часть этой практической работы включает в себя:

  • Настройка авторизации vTPM и блокировка vTPM на узлах CGKE.

cc_device_plugin_concept.png

Как показано на рисунке выше, третья часть этой практической работы включает в себя:

  • Как cc-device-plugin обеспечивает безопасное сопоставление аппаратных устройств на этапе настройки.
  • Развертывание рабочих нагрузок для получения данных аттестации оборудования непосредственно с устройств TDX или SEV-SNP на этапе выполнения без каких-либо накладных расходов.

Что вы узнаете

  • Как сделать устройство vTPM доступным для рабочих нагрузок CGKE.
  • Как выполнить удалённую аттестацию через API конфиденциальных вычислений (сервис проверки аттестации) на рабочих нагрузках CGKE.
  • Как настроить авторизацию vTPM и выполнить блокировку vTPM.
  • Как получить доступ к устройствам SEV-SNP и TDX в рабочих нагрузках CGKE с помощью плагина cc-device-plugin .
  • Как получить аттестационные данные оборудования с помощью go-tdx-guest и go-sev-guest .

Что вам понадобится

2. Настройка и требования

Чтобы активировать необходимые API, выполните следующую команду в консоли облака или в локальной среде разработки:

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. Настройка общей плоскости управления CGKE

На этом этапе вы настраиваете общую плоскость управления GKE и репозиторий Docker для реестра артефактов.

1). Настройте переменные среды и создайте общий кластер GKE:

Замените your-project-id на идентификатор вашего проекта. Замените us-central1-c на желаемую зону. (См. Регионы и зоны )

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). Создайте репозиторий Docker в реестре артефактов для хранения образов контейнеров рабочих нагрузок:

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

Выберите свой путь

Общая плоскость управления GKE и репозиторий реестра артефактов теперь готовы. Выберите свой путь:

4. Настройка узлов CGKE и предоставление доступа к устройству vTPM для выбранных рабочих нагрузок.

На этом шаге вы создадите пул узлов vTPM в CGKE и примените плагин устройства , чтобы предоставить доступ к устройству CVM vTPM для рабочих нагрузок. Перейдите в консоль облака или в локальную среду разработки, чтобы выполнить команды.

1). Добавьте пул узлов 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). Удалите пул узлов по умолчанию (необязательно, но рекомендуется для экономии средств и снижения уровня шума).

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

3). Запустите плагин устройства , чтобы кластер CGKE мог предоставлять доступ к устройству vTPM рабочим нагрузкам. Мы используем плагин устройства Kubernetes для создания новых ресурсов ( google.com/cc ). Любая рабочая нагрузка, связанная с новым ресурсом, сможет видеть устройства на рабочем узле.

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

Следующая команда позволит вам увидеть развернутый плагин cc-device-plugin .

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

Примечание: В случае кластера GKE смешанного режима (с рабочими узлами GKE, содержащими как конфиденциальные, так и неконфиденциальные данные) рекомендуется, чтобы оператор развертывал cc-device-plugin только на рабочих узлах GKE, содержащих конфиденциальные данные.

(Необязательно). Примените мониторинг Prometheus для пода CGKE. Включение мониторинга позволит вам отслеживать состояние подключаемых устройств.

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

Перейдите по ссылке https://console.cloud.google.com/monitoring/metrics-explorer и найдите метрики cc-device-plugin или используйте PROMQL. Например, следующая команда PROMQL покажет количество секунд использования ЦП для каждого процесса cc-device-plugin .

rate(process_cpu_seconds_total[${__interval}])

5. Развертывание рабочей нагрузки и выполнение удаленной аттестации рабочей нагрузки (vTPM)

На этом этапе вы создаете и развертываете рабочую нагрузку в кластере CGKE, созданном на предыдущем шаге, и выполняете удаленную аттестацию vTPM для получения токена аттестации (токена OIDC) на рабочем узле.

1). Создайте образ контейнера приложения и загрузите его в реестр артефактов. Образ контейнера приложения содержит инструмент go-tpm , который может собирать доказательства аттестации и отправлять их в службу проверки аттестации для получения токена аттестации (токена OIDC).

  1. Создайте Dockerfile для образа контейнера приложения.
cat << 'EOF' > Dockerfile
FROM golang:1.26.4 as builder
WORKDIR /
RUN git clone https://github.com/google/go-tpm-tools.git
WORKDIR /go-tpm-tools/cmd/gotpm
RUN CGO_ENABLED=0 GOOS=linux go build -o /gotpm

FROM debian:trixie
WORKDIR /
RUN apt-get update -y
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl
RUN rm -rf /etc/apt/sources.list.d
COPY --from=builder /gotpm /gotpm
CMD ["tail", "-f", "/dev/null"]
EOF
  1. Загрузите образ контейнера приложения в реестр артефактов.
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). Настройте учетную запись службы Kubernetes таким образом, чтобы она наследовала разрешения учетной записи службы GCP на ресурсы GCP.

  1. Создайте учетную запись службы Kubernetes codelab-ksa .
kubectl create serviceaccount codelab-ksa \
    --namespace default
  1. Создайте роль Confidential_Computing_Workload_User и предоставьте ей разрешения на доступ к 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
  1. Создайте учетную запись службы GCP codelab-csa и свяжите ее с ролью Confidential_Computing_Workload_User . Таким образом, у codelab-csa будут разрешения на доступ к 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]"
  1. Свяжите учетную запись службы Kubernetes codelab-ksa с учетной записью службы GCP codelab-csa . Это позволит codelab-ksa получить доступ к API конфиденциальных вычислений.
kubectl annotate serviceaccount codelab-ksa \
    --namespace default \
    iam.gke.io/gcp-service-account=codelab-csa@${PROJECT_ID}.iam.gserviceaccount.com

3). Создайте YAML-файл развертывания приложения для демонстрационного приложения. Назначьте учетную запись службы Kubernetes codelab-ksa выбранным рабочим нагрузкам.

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). Примените развертывание к кластеру CGKE.

kubectl apply -f deploy.yaml

5). Подключитесь к рабочей нагрузке и запустите удаленную аттестацию, чтобы получить токен аттестации (токен OIDC).

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

6). Выведите токен на экран, чтобы вы могли его скопировать.

cat attestation_token

Вы можете расшифровать токен подтверждения на сайте jwt.io, чтобы просмотреть утверждения.

6. Настройка веб-сервера для выпуска секретных файлов

На этом шаге вы выходите из предыдущей SSH-сессии и настраиваете другую виртуальную машину. На этой виртуальной машине вы настраиваете веб-сервер для выпуска секрета. Веб-сервер проверяет полученный токен аттестации и его свойства. Если проверка проходит успешно, он передает секрет запрашивающему лицу.

1). Перейдите в облачную консоль или в локальную среду разработки. Создайте виртуальную машину.

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). Подключитесь к вашей новой виртуальной машине по SSH.

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

3). Настройте среду 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). Создайте два файла, содержащие исходный код веб-сервера для выпуска секретных файлов.

Создайте 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

Создайте 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). Выполните следующие команды, чтобы собрать и запустить веб-сервер. Это запустит веб-сервер для выпуска секретной версии на порту :8080 .

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

Устранение неполадок: при выполнении команды 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). Откройте другую вкладку облачной консоли или локальную сессию среды разработки и выполните следующую команду. Это позволит получить cgke-attestation-codelab-web-server-internal-ip .

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

7). Подключитесь к вашей рабочей нагрузке CGKE и запустите удаленную аттестацию, чтобы получить токен аттестации (токен OIDC). Затем вставьте содержимое attestation-token и cgke-attestation-codelab-web-server-internal-ip в следующую команду. Это позволит вам получить секрет, хранящийся на веб-сервере для выпуска секретов!

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

Замените следующее:

  • cgke-attestation-codelab-web-server-internal-ip — это внутренний IP-адрес экземпляра виртуальной машины cgke-attestation-codelab-web-server .

7. Герметизация vTPM на узлах CGKE

На этом этапе вы настраиваете авторизацию владельца vTPM на узлах CGKE и развертываете рабочую нагрузку с парольной фразой владельца vTPM. Затем вы создаете первичный ключ vTPM для защиты и разблокировки данных в рабочей нагрузке с помощью функции защиты vTPM.

1). Настройте авторизацию владельца vTPM на узлах CGKE.

  1. Создайте образ контейнера для одноразового задания. Это одноразовое задание устанавливает пароль владельца для всех vTPM. Ниже приведен Dockerfile для создания образа контейнера.
cat << 'EOF' > Dockerfile
FROM debian:latest

RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
RUN apt-get update

RUN apt -y install \
  autoconf-archive \
  libcmocka0 \
  libcmocka-dev \
  net-tools \
  build-essential \
  git \
  pkg-config \
  gcc \
  g++ \
  m4 \
  libtool \
  automake \
  libgcrypt20-dev \
  libssl-dev \
  uthash-dev \
  autoconf \
  uuid-dev \
  libcurl4-openssl-dev \
  libjson-c-dev

RUN mkdir /src

WORKDIR /src
RUN git clone https://github.com/tpm2-software/tpm2-tss
WORKDIR /src/tpm2-tss
RUN ./bootstrap
RUN ./configure --prefix=/usr/local
RUN make all install

WORKDIR /src
RUN git clone https://github.com/tpm2-software/tpm2-tools
WORKDIR /src/tpm2-tools
RUN apt-get -y install libcurl4 libcurl4-openssl-dev pandoc man-db
RUN ./bootstrap
RUN ./configure --prefix=/usr/local
RUN make all install

RUN apt-get -y install vim

ENTRYPOINT ["/bin/bash"]
EOF
  1. Создайте и загрузите образ контейнера для одноразового задания в реестр артефактов.
docker build -t us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/tpm-tools:latest .
docker push us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/tpm-tools:latest
  1. Выполните одноразовое задание через задание Kubernetes. (ВНИМАНИЕ: это задание очищает vTPM на каждом CVM. Если ваш CVM использует vTPM для шифрования диска, это задание сделает ваш CVM непригодным для использования после перезагрузки. Вы можете проверить, имеет ли ваш диск тип FSTYPE crypto_LUKS с помощью команды lsblk -f )
cat << EOF > tpm-tools-task.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: tpm-tools-task
spec:
  template:
    spec:
      nodeSelector:
        cloud.google.com/machine-family: "n2d"
        cloud.google.com/gke-confidential-nodes-instance-type: "SEV"
      containers:
      - name: tpm-tools
        image: us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/tpm-tools:latest
        command: ["/bin/sh", "-c"]
        args: ["tpm2_clear; tpm2_changeauth -c owner this_is_passphrase"]
        resources:
          limits:
            google.com/cc: 1
      restartPolicy: Never
EOF

Совет: Если вам нужно повторно запустить задание tpm-tools-task, обязательно сначала удалите существующее задание:

kubectl delete job tpm-tools-task
  1. Запустите одноразовое задание. Это задание устанавливает пароль владельца vTPM на всех рабочих узлах.
kubectl apply -f tpm-tools-task.yaml

2). Создайте секрет Kubernetes для хранения парольной фразы владельца vTPM.

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

3). Создайте контейнер демонстрационного приложения и передайте ему парольную фразу. Контейнер демонстрационного приложения содержит tpm2-tools для взаимодействия с vTPM.

  1. Создайте YAML-файл развертывания для контейнера демонстрационного приложения.
cat << EOF > deploy_demo.yaml
apiVersion: v1
kind: Pod
metadata:
  name: tpm-tools-demo
  labels:
    app.kubernetes.io/name: tpm-tools-demo
spec:
  nodeSelector:
    cloud.google.com/machine-family: "n2d" 
    cloud.google.com/gke-confidential-nodes-instance-type: "SEV"
  containers:
  - name: tpm-tools
    image: us-docker.pkg.dev/${PROJECT_ID}/codelab-repo/tpm-tools:latest
    command: ["tail", "-f", "/dev/null"]
    resources:
      limits:
        google.com/cc: 1
    volumeMounts:
      - name: secret-volume
        mountPath: "/etc/tpmsecret"
        readOnly: true
  volumes:
    - name: secret-volume
      secret:
        secretName: tpm-secret
EOF
  1. Разверните демонстрационное приложение.
kubectl apply -f deploy_demo.yaml

4). Выполните герметизацию vTPM в контейнере демонстрационного приложения.

  1. Подключитесь к контейнеру демонстрационного приложения и установите первичный ключ с парольной фразой.
kubectl exec -it tpm-tools-demo -- /bin/bash
tpm2_createprimary -C o -c primary.ctx -P $(cat /etc/tpmsecret/passphrase)

tpm2_createprimary взаимодействует с vTPM для генерации основного объекта на основе указанной иерархии и шаблона.

  • -C o: Указывает, что первичный ключ будет создан в иерархии владельцев TPM.
  • -c primary.ctx : Сохраняет контекст (дескриптор и связанные данные) созданного основного объекта в файл primary.ctx . Этот контекст необходим для последующих операций.

Рабочая нагрузка не может использовать неверную парольную фразу владельца для создания первичного ключа.

tpm2_createprimary -C o -P wrong_passphrase

Команда выдает следующие ошибки:

WARNING:esys:src/tss2-esys/api/Esys_CreatePrimary.c:401:Esys_CreatePrimary_Finish() Received TPM Error
ERROR:esys:src/tss2-esys/api/Esys_CreatePrimary.c:135:Esys_CreatePrimary() Esys Finish ErrorCode (0x000009a2)
ERROR: Esys_CreatePrimary(0x9A2) - tpm:session(1):authorization failure without DA implications
ERROR: Unable to run tpm2_createprimary
  1. Созданный первичный ключ затем можно использовать для закрытия и открытия доступа к данным.
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 взаимодействует с vTPM для генерации необходимого криптографического объекта.

  • -C primary.ctx : Использует контекст первичного ключа, созданный нами ранее.
  • -u sealed.pub : Сохраняет открытую часть ключа запечатывания (необходимую для распечатывания) в sealed.pub .
  • -r sealed.priv : Сохраняет закрытую часть ключа запечатывания в sealed.priv .
  • -i secret.txt : Файл, содержащий секрет, который необходимо засекретить.

tpm2_load : Загружает ключ запечатывания в TPM, используя открытую и закрытую части ( sealed.pub , sealed.priv ), и сохраняет его контекст в sealed.ctx .

tpm2_unseal : Расшифровать (расшифровать) данные, которые ранее были зашифрованы (запечатаны) с использованием объекта запечатывания vTPM.

cat unsealed.txt : Отображает рассекреченное секретное сообщение, подтверждающее успешное завершение процесса.

Обратите внимание, что файлы primary.ctx и sealed.priv можно использовать только на одном устройстве vTPM. Любой, кто имеет доступ к устройству vTPM и этим файлам, может получить доступ к запечатанным данным. Вы также можете использовать политику на основе значений PCR для запечатывания данных, но это выходит за рамки данного практического занятия.

8. Развертывание рабочих нагрузок для проведения аттестации оборудования: Intel TDX и AMD SEV-SNP

cc_device_plugin_arch.png

На этом шаге вы создадите рабочую нагрузку на основе Intel TDX или AMD SEV-SNP и выполните аттестацию на аппаратном уровне на конфиденциальных узлах GKE. Как показано на приведенной выше архитектурной схеме, мы будем использовать плагин гостевого устройства для получения данных аттестации для архитектур Intel TDX и AMD SEV-SNP. Перейдите в консоль облака или в локальную среду разработки, чтобы выполнить команды.

Выберите архитектуру для конфиденциальных вычислений — Intel TDX или AMD SEV-SNP — которую вы хотите протестировать.

1). Добавьте пул узлов 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). Добавить пул узлов 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). Удалите пул узлов по умолчанию (необязательно, но рекомендуется для экономии средств и снижения уровня шума).

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

(Примечание: Если вы уже выполнили раздел vTPM ранее, этот пул уже удален, и вы можете смело игнорировать любые ошибки "Не найдено" в этом разделе.)

4). Запустите плагин устройства , чтобы кластер CGKE мог предоставлять рабочим нагрузкам доступ к гостевым устройствам аппаратной аттестации. Мы используем плагин устройства Kubernetes для создания новых ресурсов ( intel.com/tdx и amd.com/sev-snp ). Любая рабочая нагрузка, связанная с этими ресурсами, сможет получить доступ к гостевым устройствам на рабочем узле.

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

Следующая команда позволит вам увидеть развернутый плагин cc-device-plugin .

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

Примечание: В случае кластера GKE смешанного режима (с рабочими узлами GKE, содержащими как конфиденциальные, так и неконфиденциальные данные) рекомендуется, чтобы оператор развертывал cc-device-plugin только на рабочих узлах GKE, содержащих конфиденциальные данные.

9. Аппаратная аттестация: Intel TDX и AMD SEV-SNP

В этом разделе вы узнаете, как выполнить аттестацию на аппаратном уровне внутри конфиденциальных узлов GKE, получив аттестационные данные для архитектур Intel TDX и AMD SEV-SNP с помощью плагинов гостевых устройств.

1). Создайте приложение на Go для получения данных об аппаратной аттестации с соответствующего гостевого устройства.

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). Создайте Dockerfile для инструмента тестирования аппаратной аттестации:

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). Создайте и загрузите образ в свой реестр артефактов:

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). Создайте YAML-файл развертывания приложения для рабочих нагрузок Intel TDX и AMD SEV-SNP, чтобы обеспечить доступ к аппаратным устройствам.

  • Для рабочих нагрузок Intel TDX создайте следующий YAML-файл развертывания:
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
  • Для рабочих нагрузок AMD SEV-SNP создайте следующий YAML-файл развертывания:
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). Примените развертывания к кластеру CGKE.

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

6). Дождитесь завершения работы модулей и проверьте журналы аттестации оборудования.

Для начала проверьте статус ваших подов, чтобы убедиться, что они успешно запланированы и запущены (дождитесь, пока они не отобразят статус «Завершено» или «Запущено»):

kubectl get pods -w

После того, как модули будут готовы, проверьте логи, чтобы убедиться, что данные аттестации оборудования были успешно получены:

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

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

(Вы увидите общее время выполнения, общее количество итераций и количество успешных/неудачных операций.)

10. Уборка

Чтобы избежать дальнейших списаний средств с вашего аккаунта Google Cloud, важно удалить ресурсы, созданные в этом практическом задании.

Выполните следующие команды в Cloud Shell или в локальной среде разработки.

1). Установите переменные среды:

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

gcloud config set project ${PROJECT_ID}

2). Удалите ресурсы GKE.

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

3). Удалить реестр артефактов.

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

4). Удалить виртуальную машину веб-сервера.

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

5). Удалите ресурсы 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. Что дальше?

Узнайте больше о конфиденциальных узлах GKE .