Zdalna atestacja i zabezpieczanie vTPM w poufnych węzłach GKE

Informacje o tym ćwiczeniu (w Codelabs)
schedule42 minuty
subjectOstatnia aktualizacja: 19 maja 2024
account_circleAutorzy: Ruide Zhang

Poufne węzły GKE (CGKE) zapewniają, że dane w zbiorach zadań są szyfrowane w użyciu. Udostępnienie urządzenia vTPM dla zadań CGKE umożliwia zadaniom korzystanie z funkcji vTPM. W tym ćwiczeniu w Codelabs zobaczysz 2 funkcje vTPM.

  • Zdalna atestacja vTPM umożliwia stronie zdalnej sprawdzanie, czy węzły CGKE hostujące zadania działają w poufnych maszynach wirtualnych (CVM).
  • Autoryzacja vTPM i zabezpieczanie vTPM.

683a3b43587ef69f.png

Jak widać na ilustracji powyżej, pierwsza część tego ćwiczenia z programowania obejmuje te czynności:

  • Konfiguracja węzłów CGKE i udostępnienie urządzenia vTPM wybranym zadaniom.
  • Wdróż zbiór zadań i poświadczaj zdalnie węzeł CGKE hostujący zadanie.
  • Konfiguracja serwera WWW poufnego wydania.

8f6e80c762a5d911.png

Jak widać na powyższej ilustracji, druga część tego ćwiczenia z programowania obejmuje:

  • Konfiguracja autoryzacji vTPM i zamykanie vTPM w węzłach CGKE.

Czego się nauczysz

  • Jak udostępnić urządzenie vTPM dla zadań CGKE.
  • Jak zdalnie poświadczać za pomocą interfejsu Confidential Computing API (usługa atestacji weryfikatora) w zbiorach zadań CGKE.
  • Jak skonfigurować autoryzację vTPM i zabezpieczyć moduł vTPM.

Czego potrzebujesz

2. Konfiguracja i wymagania:

Aby włączyć niezbędne interfejsy API, uruchom to polecenie w konsoli Cloud lub w lokalnym środowisku programistycznym:

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. Konfigurowanie węzłów CGKE i udostępnianie urządzenia vTPM dla wybranych zbiorów zadań

Ten krok rozpoczyna pierwszą część tego ćwiczenia z programowania. W tym kroku uruchomisz klaster CGKE i zastosujesz wtyczkę urządzenia, aby udostępnić urządzenie vTPM CVM dla zadań. Aby go uruchomić, otwórz konsolę Cloud lub lokalne środowisko programistyczne.

1) Utwórz klaster CGKE, użyj puli tożsamości zadań, aby umożliwić zadaniom CGKE korzystanie z interfejsu Confidential Computing w GCP. Pula tożsamości zadań jest niezbędna, ponieważ zadania CGKE muszą mieć dostęp do zasobów GCP. Aby uzyskać dostęp do zasobów GCP, zadania CGKE muszą mieć tożsamość.

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

Zastąp następujące elementy:

  • project-id to unikalny identyfikator projektu.

2) Uruchom wtyczkę urządzenia, aby umożliwić klastrowi CGKE udostępnianie urządzenia vTPM dla zadań. Aby utworzyć nowy zasób – google.com/cc, używamy wtyczki urządzenia Kubernetes. Wszystkie zadania powiązane z nowym zasobem będą miały dostęp do urządzenia vTPM w węźle roboczym.

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

Zastąp następujące elementy:

  • project-id to unikalny identyfikator projektu.

Poniższe polecenie pozwala wyświetlić wdrożoną wtyczkę cc-device-plugin.

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

Uwaga: w przypadku klastra GKE w trybie mieszanym (z poufnymi i niepoufnymi węzłami roboczymi GKE) zaleca się, aby operator wdrażał tylko cc-device-plugin w poufnych węzłach roboczych GKE.

Opcjonalnie: Zastosuj monitorowanie poda Prometheus w 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

Wejdź na https://console.cloud.google.com/monitoring/metrics-explorer i znajdź wskaźniki cc-device-plugin lub użyj PROMQL. np. Poniższe polecenie PROMQL pokazuje liczbę sekund procesora w każdym procesie cc-device-plugin.

rate(process_cpu_seconds_total[${__interval}])

4. Wdrażanie zadania i wykonywanie na nim zdalnego poświadczania

W tym kroku utworzysz i wdrożysz zadanie w klastrze CGKE utworzonym w poprzednim kroku oraz wykonasz zdalny atest vTPM, aby pobrać token atestu (token OIDC) z węzła roboczego.

1) Utwórz obraz kontenera aplikacji i przekaż go do Artifact Registry. Obraz kontenera aplikacji zawiera narzędzie go-tpm, które może gromadzić dowody na poświadczenie i wysyłać je do usługi weryfikatora atestów na potrzeby tokena atestu (tokena OIDC).

  1. Utwórz plik Dockerfile dla obrazu kontenera aplikacji.

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. Utwórz Artifact Registry.
gcloud artifacts repositories create codelab-repo \
    --repository-format=docker \
    --location=us
  1. Przekaż 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 tak, aby dziedziczyło uprawnienia konta usługi GCP dotyczące zasobów GCP.

  1. Utwórz konto usługi Kubernetes codelab-ksa.
kubectl create serviceaccount codelab-ksa \
    --namespace default
  1. Utwórz rolę Confidential_Computing_Workload_User i przyznaj jej uprawnienia dostępu do interfejsów Confidential Computing 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

Zastąp następujące elementy:

  • project-id to unikalny identyfikator projektu.
  1. Utwórz konto usługi GCP codelab-csa i powiąż je z rolą Confidential_Computing_Workload_User. So that codelab-csa, która ma uprawnienia dostępu do interfejsów Confidential Computing 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]"

Zastąp następujące elementy:

  • project-id to unikalny identyfikator projektu.
  1. Powiąż konto usługi Kubernetes codelab-ksa z kontem usługi GCP codelab-csa. codelab-ksa ma uprawnienia dostępu do interfejsów Confidential Computing API.
kubectl annotate serviceaccount codelab-ksa \
    --namespace default \
    iam.gke.io/gcp-service-account=codelab-csa@<project-id>.iam.gserviceaccount.com

Zastąp następujące elementy:

  • project-id to unikalny identyfikator projektu.

3) Utworzenie pliku yaml wdrożenia dla aplikacji demonstracyjnej. Przypisz konto usługi Kubernetes codelab-ksa do wybranych zadań.

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

Zastąp następujące elementy:

  • project-id to unikalny identyfikator projektu.

4) Zastosuj wdrożenie do klastra CGKE.

kubectl create -f deploy.yaml

5). Połącz się z zadaniem i uruchom poświadczanie zdalne, aby pobrać token atestu (token OIDC)

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

Aby wyświetlić deklaracje, możesz zdekodować token atestu w jwt.io.

5. Konfigurowanie serwera WWW z dostępem tajnym

W tym kroku zakończysz poprzednią sesję SSH i skonfigurujesz inną maszynę wirtualną. W tej maszynie wirtualnej konfigurujesz serwer WWW z wersjami tajnymi. Serwer WWW weryfikuje otrzymany token atestu i jego deklaracje. Jeśli weryfikacja się uda, treść zostanie przekazana osobie wysyłającej prośbę.

1) Otwórz konsolę Cloud lub lokalne środowisko programistyczne. utworzyć maszynę wirtualną,

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

Zastąp następujące elementy:

  • project-id to unikalny identyfikator projektu.

2) Połącz się z nową maszyną wirtualną przez SSH.

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

3) Skonfiguruj środowisko 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) Utwórz następujące 2 pliki zawierające kod źródłowy tajnego serwera WWW z wersją (kopia i wklej w 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). Uruchom następujące polecenia, aby utworzyć i uruchomić serwer WWW. Spowoduje to uruchomienie serwera WWW tajnych 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: możesz zobaczyć następujące ostrzeżenie, które możesz zignorować po uruchomieniu funkcji 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). Otwórz inną kartę konsoli Cloud 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=us-central1-c

7). Połącz się ze swoim zadaniem CGKE i uruchom zdalny atest, aby pobrać token atestu (token OIDC). Następnie umieść zawartość attestation-token i cgke-attestation-codelab-web-server-internal-ip w następującym poleceniu. Spowoduje to pobranie obiektu tajnego przechowywanego przez serwer WWW, z którego wyemitowano tajny klucz.

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 następujące elementy:

  • cgke-attestation-codelab-web-server-internal-ip to wewnętrzny adres IP instancji maszyny wirtualnej cgke-attestation-codelab-web-server.

6. Zabezpieczanie vTPM w węzłach CGKE

Ten krok rozpocznie drugą część tego ćwiczenia z programowania. W tym kroku skonfigurujesz autoryzację właściciela vTPM w węzłach CGKE i wdrożysz zadanie z hasłem właściciela vTPM. Później utworzysz klucz podstawowy vTPM do uszczelniania i usuwania danych w zadaniu za pomocą funkcji uszczelniania vTPM.

1) Skonfiguruj autoryzację właściciela vTPM w węzłach CGKE.

  1. Utwórz obraz kontenera zadania jednorazowego. Zadanie jednorazowe ustawia hasło właściciela dla wszystkich modułów vTPM. Poniżej znajduje się plik dockerfile do utworzenia obrazu kontenera.

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. Skompiluj obraz kontenera zadania jednorazowego i wypchnij 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

Zastąp następujące elementy:

  • project-id to unikalny identyfikator projektu.
  1. Wykonaj jednorazowe zadanie za pomocą zadania Kubernetes. (OSTRZEŻENIE: to zadanie usuwa moduł vTPM na każdej maszynie wirtualnej). Jeśli maszyna wirtualna używa vTPM do szyfrowania dysku, to zadanie po ponownym uruchomieniu sprawi, że nie będzie można z niej korzystać. Aby sprawdzić, czy na dysku jest zainstalowany FSTYPE crypto_LUKS, użyj polecenia 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

Zastąp następujące elementy:

  • project-id to unikalny identyfikator projektu.
  1. Uruchom zadanie jednorazowe. To zadanie ustawia hasło właściciela vTPM na wszystkich węzłach roboczych.
kubectl create -f tpm-tools-task.yaml

2) Utwórz obiekt tajny 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ż mu hasło. Kontener aplikacji demonstracyjnej zawiera narzędzia tpm2 do interakcji z modułem vTPM.

  1. Utwórz plik yaml wdrożenia dla kontenera aplikacji demonstracyjnej.

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

Zastąp następujące elementy:

  • project-id to unikalny identyfikator projektu.
  1. Wdróż aplikację demonstracyjną.
kubectl create -f deploy_demo.yaml

4) Wykonaj zamykanie vTPM w kontenerze aplikacji demonstracyjnej.

  1. 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 współdziała z modułem vTPM, aby wygenerować obiekt główny na podstawie określonej hierarchii i szablonu.

  • – O: wskazuje, że klucz podstawowy zostanie utworzony w ramach hierarchii właściciela TPM.
  • -cprimary.ctx: zapisuje kontekst (nick i powiązane dane) utworzonego obiektu głównego w pliku main.ctx. Ten kontekst jest istotny podczas późniejszych operacji.

Zbiór zadań nie może użyć niewłaściwego hasła właściciela do utworzenia klucza podstawowego.

tpm2_createprimary -C o -P błędne_hasło

Polecenie zwraca następujące 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
  1. Utworzony klucz podstawowy można następnie wykorzystać do uszczelniania i otwierania 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

tpm2_create współdziała z modułem vTPM, aby wygenerować żądany obiekt kryptograficzny.

  • -Cprimary.ctx: korzysta z podstawowego kontekstu klucza, który utworzyliśmy wcześniej.
  • -u Sealed.pub: zapisuje publiczną część klucza uszczelniającego (potrzebną do otwarcia) w pliku Sealed.pub.
  • -r Sealed.priv: przechowuje prywatną część klucza uszczelniającego w pliku Sealed.priv.
  • -i secret.txt: plik zawierający obiekt tajny do zapieczętowania.

tpm2_load: wczytuje klucz uszczelniający do TPM z użyciem części publicznych i prywatnych (sealed.pub, Sealed.priv) i zapisuje jego kontekst w pliku Sealed.ctx.

tpm2_unseal: odszyfrowanie (odsłonięcie) danych, które zostały wcześniej zaszyfrowane (zamknięte) przy użyciu obiektu uszczelniającego vTPM.

Pamiętaj, że plików primary.ctx i sealed.priv można używać tylko na 1 urządzeniu vTPM. Każda osoba, która ma dostęp do urządzenia vTPM i do tych plików, również może uzyskać dostęp do zapieczętowanych danych. Możesz w dalszym ciągu użyć zasad dotyczących wartości PCR, aby przykryć dane, ale nie są one objęte tym ćwiczeniem z programowania.

7. Czyszczenie

Uruchom te polecenia w konsoli Cloud lub w lokalnym środowisku programistycznym:

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

Zastąp następujące elementy:

  • project-id to unikalny identyfikator projektu.

8. Co dalej?

Dowiedz się więcej o poufnych węzłach GKE.