Удаленная аттестация и запечатывание vTPM на конфиденциальных узлах GKE

О практической работе
schedule42 минуты
subjectПоследнее обновление: 19 мая 2024 г.
account_circleАвторы: Ruide Zhang

Конфиденциальные узлы GKE (CGKE) обеспечивают шифрование данных в рабочих нагрузках во время использования. Предоставление устройства vTPM рабочим нагрузкам CGKE позволяет рабочим нагрузкам использовать функции vTPM. В этой лаборатории кода вам будут продемонстрированы две функции vTPM.

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

683a3b43587ef69f.png

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

  • Настройка узлов CGKE и предоставление устройства vTPM выбранным рабочим нагрузкам.
  • Разверните рабочую нагрузку и удаленно подтвердите узел CGKE, на котором размещена рабочая нагрузка.
  • Настройка веб-сервера секретного релиза.

8f6e80c762a5d911.png

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

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

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

  • Как предоставить устройству vTPM рабочие нагрузки CGKE.
  • Как удаленно подтвердить рабочие нагрузки CGKE с помощью API конфиденциальных вычислений (служба проверки аттестации).
  • Как настроить авторизацию vTPM и выполнить запечатывание vTPM.

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

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

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

gcloud auth login

gcloud services enable \
    cloudapis.googleapis.com \
    cloudshell.googleapis.com \
    container.googleapis.com \
    containerregistry.googleapis.com \
    confidentialcomputing.googleapis.com \
    iamcredentials.googleapis.com \
    compute.googleapis.com

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

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

1). Создайте кластер CGKE, используйте пул идентификаторов рабочей нагрузки , чтобы рабочие нагрузки CGKE могли использовать API конфиденциальных вычислений GCP. Пул идентификаторов рабочей нагрузки необходим, поскольку рабочим нагрузкам CGKE необходим доступ к ресурсам GCP. А для доступа к ресурсам GCP рабочие нагрузки CGKE должны иметь идентификатор.

gcloud container clusters create cgke-attestation-codelab \
    --machine-type=n2d-standard-2        \
    --enable-confidential-nodes \
--zone us-central1-c \
--workload-pool=${PROJECT_ID}.svc.id.goog \
--workload-metadata=GKE_METADATA

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

  • project-id — уникальный идентификатор проекта.

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

gcloud container clusters get-credentials cgke-attestation-codelab --zone us-central1-c --project ${PROJECT_ID}

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

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

  • project-id — уникальный идентификатор проекта.

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

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

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

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

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}])

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

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

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

  1. Создайте Dockerfile для образа контейнера приложения.

Dockerfile

FROM golang:1.21.0 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
RUN rm -rf /etc/apt/sources.list.d
COPY --from=builder /gotpm /gotpm
CMD ["tail", "-f", "/dev/null"]
  1. Создайте реестр артефактов.
gcloud artifacts repositories create codelab-repo \
    --repository-format=docker \
    --location=us
  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 конфиденциальных вычислений.
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

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

  • project-id — уникальный идентификатор проекта.
  1. Создайте учетную запись службы GCP codelab-csa и привяжите ее к роли Confidential_Computing_Workload_User. So that codelab-csa есть разрешения на доступ к API конфиденциальных вычислений.
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]"

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

  • project-id — уникальный идентификатор проекта.
  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

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

  • project-id — уникальный идентификатор проекта.

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

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"
  containers:
  - name: go-tpm
    image: us-docker.pkg.dev/<project-id>/codelab-repo/go-tpm:latest
    resources:
      limits:
        google.com/cc: 1

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

  • project-id — уникальный идентификатор проекта.

4). Примените развертывание к кластеру CGKE.

kubectl create -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

Вы можете декодировать токен аттестации в jwt.io , чтобы просмотреть претензии!

5. Настройка веб-сервера секретного выпуска

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

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

gcloud config set project <project-id>

gcloud compute instances create cgke-attestation-codelab-web-server \
    --machine-type=n2d-standard-2 \
    --zone=us-central1-c \
    --image=ubuntu-2204-jammy-v20240228 \
    --image-project=ubuntu-os-cloud

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

  • project-id — уникальный идентификатор проекта.

2). SSH к вашей новой виртуальной машине.

gcloud compute ssh --zone us-central1-c cgke-attestation-codelab-web-server

3). Настройте среду Go.

wget https://go.dev/dl/go1.22.0.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.22.0.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin

4). Создайте следующие два файла, хранящие исходный код веб-сервера секретного выпуска (скопируйте и вставьте с помощью nano).

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)
        // e.g. "sub":"https://www.googleapis.com/compute/v1/projects/<project_id>/zones/<project_zone>/instances/<instance_name>"

        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)
        if !ok || hwModel != "GCP_AMD_SEV" {
                return false, fmt.Errorf("missing or invalid 'hwModel'")
        }
        fmt.Println("hwmodel:", hwModel)

        swName, ok := claims["swname"].(string)
        if !ok || swName != "GCE" {
                return false, fmt.Errorf("missing or invalid 'hwModel'")
        }
        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)
        }
}

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

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=us-central1-c

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 .

6. Опломбирование vTPM на узлах CGKE

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

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

  1. Создайте образ контейнера одноразового задания. Одноразовое задание устанавливает пароль владельца для всех vTPM. Ниже приведен файл docker для создания образа контейнера.

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

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

  • project-id — уникальный идентификатор проекта.
  1. Выполните одноразовое задание с помощью задания Kubernetes. (ВНИМАНИЕ: это задание очищает vTPM на каждой CVM. Если ваша CVM использует vTPM для шифрования диска, это задание сделает вашу CVM непригодной для использования после перезагрузки. Вы можете проверить, имеет ли ваш диск FSTYPE crypto_LUKS с помощью команды lsblk -f )

tpm-tools-task.yaml

apiVersion: batch/v1
kind: Job
metadata:
  name: tpm-tools-task
spec:
  template:
    spec:
      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

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

  • project-id — уникальный идентификатор проекта.
  1. Запустите одноразовое задание. Это задание устанавливает парольную фразу владельца vTPM на всех рабочих узлах.
kubectl create -f tpm-tools-task.yaml

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

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

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

  1. Создайте файл yaml развертывания для контейнера демонстрационного приложения.

deploy_demo.yaml

apiVersion: v1
kind: Pod
metadata:
  name: tpm-tools-demo
  labels:
    app.kubernetes.io/name: tpm-tools-demo
spec:
  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

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

  • project-id — уникальный идентификатор проекта.
  1. Разверните демонстрационное приложение.
kubectl create -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: указывает, что первичный ключ будет создан в иерархии владельцев доверенного платформенного модуля.
  • -c Primary.ctx: сохраняет контекст (дескриптор и связанные данные) созданного первичного объекта в файле Primary.ctx. Этот контекст важен для последующих операций.

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

tpm2_createprimary -C o -P неправильная_парольная фраза

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

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

tpm2_create взаимодействует с vTPM для создания желаемого криптографического объекта.

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

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

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

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

7. Очистка

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

gcloud config set project <project-id>

# Delete the CGKE cluster
gcloud container clusters delete cgke-attestation-codelab --zone us-central1-c

# Delete the Artifact Registry
gcloud artifacts repositories delete codelab-repo --location=us

# Delete the web server VM instance
gcloud compute instances delete cgke-attestation-codelab-web-server --zone=us-central1-c

# Delete the GCP service account
gcloud iam service-accounts delete codelab-csa@<project-id>.iam.gserviceaccount.com

# Delete the role
gcloud iam roles delete Confidential_Computing_Workload_User

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

  • project-id — уникальный идентификатор проекта.

8. Что дальше

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