Atestado remoto e isolamento de vTPM em nós confidenciais do GKE

Sobre este codelab
schedule42 minutos
subjectÚltimo 19 de maio de 2024 atualizado
account_circleEscrito por Ruide Zhang

Os nós do Confidential GKE (CGKE) garantem que os dados nas cargas de trabalho sejam criptografados em uso. A exposição do dispositivo vTPM para cargas de trabalho do CGKE permite que as cargas de trabalho usem recursos do vTPM. Neste codelab, você conhecerá dois recursos do vTPM.

  • O atestado remoto do vTPM permite que uma parte remota verifique se os nós do CGKE que hospedam cargas de trabalho estão em execução em VMs confidenciais (CVM).
  • Autorização de vTPM e isolamento de vTPM.

683a3b43587ef69f.png

Conforme mostrado na figura acima, a primeira parte deste codelab inclui as seguintes etapas:

  • Os nós do CGKE configuram e expõem o dispositivo vTPM para cargas de trabalho selecionadas.
  • Implantar uma carga de trabalho e atestar remotamente o nó do CGKE que hospeda a carga de trabalho.
  • Configuração do servidor da Web de versão secreta.

8f6e80c762a5d911.png

Conforme mostrado na figura acima, a segunda parte deste codelab inclui:

  • Configuração de autorização do vTPM e isolamento de vTPM nos nós do CGKE.

O que você vai aprender

  • Como expor o dispositivo vTPM para cargas de trabalho do CGKE.
  • Como fazer atestados remotos pela API Confidential Computing (serviço Verificador de atestados) em cargas de trabalho do CGKE.
  • Como configurar a autorização do vTPM e realizar a isolamento do vTPM.

O que é necessário

2. Configuração e requisitos:

Para ativar as APIs necessárias, execute o seguinte comando no console do Cloud ou no ambiente de desenvolvimento local:

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. Como configurar nós do CGKE e expor o dispositivo vTPM para cargas de trabalho selecionadas

Esta etapa inicia a primeira parte deste codelab. Nesta etapa, você inicia um cluster do CGKE e aplica um plug-in de dispositivo para expor o dispositivo vTPM da CVM às cargas de trabalho. Acesse o console do Cloud ou o ambiente de desenvolvimento local para executar os comandos.

1) Criar um cluster do CGKE usando o pool de identidade da carga de trabalho para que as cargas de trabalho do CGKE usem a API de computação confidencial do GCP. O pool de identidade da carga de trabalho é necessário porque as cargas de trabalho do CGKE precisam acessar os recursos do GCP. E, para acessar os recursos do GCP, as cargas de trabalho do CGKE precisam ter uma identidade.

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

Substitua:

  • project-id é o identificador exclusivo do projeto.

2) Inicie o plug-in do dispositivo para permitir que o cluster do CGKE exponha o dispositivo vTPM às cargas de trabalho. Usamos um plug-in de dispositivo do Kubernetes para criar um novo recurso, google.com/cc. Qualquer carga de trabalho associada ao novo recurso poderá ver o dispositivo vTPM no nó de trabalho.

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

Substitua:

  • project-id é o identificador exclusivo do projeto.

O comando a seguir permite que você veja o cc-device-plugin implantado.

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

Observação: no caso de um cluster do GKE de modo misto (com nós de trabalho confidenciais e não confidenciais do GKE), é recomendável que o operador implante cc-device-plugin apenas nos nós de trabalho confidenciais do GKE.

(Opcional). Aplique o monitoramento do CGKE pod Prometheus. Ativar o monitoramento permite observar o status do plug-in do dispositivo.

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

Acesse https://console.cloud.google.com/monitoring/metrics-explorer e encontre as métricas "cc-device-plugin" ou use o PROMQL. Por exemplo: O comando PROMQL a seguir mostra os segundos de CPU para cada processo cc-device-plugin.

rate(process_cpu_seconds_total[${__interval}])

4. Implantar uma carga de trabalho e executar um atestado remoto nela

Nesta etapa, você cria e implanta uma carga de trabalho no cluster do CGKE criado na etapa anterior e executa um atestado remoto do vTPM para recuperar um token de atestado (token OIDC) no nó de trabalho.

1) Crie a imagem do contêiner do aplicativo e envie-a para o Artifact Registry. A imagem do contêiner do aplicativo contém a ferramenta go-tpm, que pode coletar evidências de atestados e enviá-las ao serviço do Verificador de atestados para um token de atestado (um token OIDC).

  1. Crie o Dockerfile da imagem do contêiner do aplicativo.

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. Crie um Artifact Registry.
gcloud artifacts repositories create codelab-repo \
    --repository-format=docker \
    --location=us
  1. Envie a imagem do contêiner do aplicativo para o 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) Configurar uma conta de serviço do Kubernetes para herdar as permissões dela nos recursos do GCP.

  1. Criar uma conta de serviço do Kubernetes codelab-ksa.
kubectl create serviceaccount codelab-ksa \
    --namespace default
  1. Crie um papel Confidential_Computing_Workload_User e conceda as permissões para acessar as APIs de Computação confidencial.
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

Substitua:

  • project-id é o identificador exclusivo do projeto.
  1. Crie uma conta de serviço do GCP codelab-csa e vincule-a ao papel Confidential_Computing_Workload_User. So that codelab-csa tem permissões para acessar as APIs de Computação confidencial.
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]"

Substitua:

  • project-id é o identificador exclusivo do projeto.
  1. Vincule a conta de serviço do Kubernetes codelab-ksa à conta de serviço do GCP codelab-csa. Para que codelab-ksa tenha permissões para acessar APIs de Computação confidencial.
kubectl annotate serviceaccount codelab-ksa \
    --namespace default \
    iam.gke.io/gcp-service-account=codelab-csa@<project-id>.iam.gserviceaccount.com

Substitua:

  • project-id é o identificador exclusivo do projeto.

3) Crie o yaml de implantação do aplicativo de demonstração. Atribua a conta de serviço do Kubernetes codelab-ksa às cargas de trabalho selecionadas.

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

Substitua:

  • project-id é o identificador exclusivo do projeto.

4). Aplique a implantação ao cluster do CGKE.

kubectl create -f deploy.yaml

5). Conecte-se à carga de trabalho e inicie o atestado remoto para buscar um token de atestado (um token OIDC)

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

Você pode decodificar o token de atestado em jwt.io para ver as declarações.

5. Como configurar o servidor da Web de versão secreta

Nesta etapa, você sai da sessão SSH anterior e configura outra VM. Nesta VM, você configura um servidor da Web de versão secreta. O servidor da Web valida o token de atestado recebido e as declarações dele. Se as validações forem bem-sucedidas, ele vai transmitir o secret ao solicitante.

1) Acesse o console do Cloud ou o ambiente de desenvolvimento local. Criar uma máquina virtual.

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

Substitua:

  • project-id é o identificador exclusivo do projeto.

2) Conecte-se via SSH à nova VM.

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

3) Configurar o ambiente 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). Crie os dois arquivos a seguir que armazenam o código-fonte do servidor da Web de lançamento secreto (copie e cole com o 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). Execute os comandos a seguir para criar e executar o servidor da Web. Isso inicia o servidor da Web de lançamento do secret na porta :8080.

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

Solução de problemas: talvez você veja o aviso a seguir, que pode ser ignorado ao executar 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) Inicie outra guia do console do Cloud ou uma sessão do ambiente de desenvolvimento local e execute o comando a seguir. Isso vai dar a você o 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). Conecte-se à carga de trabalho do CGKE e inicie o atestado remoto para buscar um token de atestado (um token OIDC). Em seguida, incorpore o conteúdo de attestation-token e cgke-attestation-codelab-web-server-internal-ip no comando a seguir. Isso buscará o segredo mantido pelo servidor da Web de lançamento secreto.

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

Substitua:

  • cgke-attestation-codelab-web-server-internal-ip é o IP interno da instância de VM cgke-attestation-codelab-web-server.

6. Isolamento de vTPM nos nós do CGKE

Esta etapa inicia a segunda parte deste codelab. Nesta etapa, você configura a autorização de proprietário do vTPM nos nós do CGKE e implanta uma carga de trabalho com a senha longa de proprietário do vTPM. Em seguida, você criará uma chave primária do vTPM para bloquear e desbloquear os dados na carga de trabalho com o recurso de isolamento do vTPM.

1) Configurar a autorização de proprietário do vTPM nos nós do CGKE.

  1. Crie uma imagem de contêiner do job uma única vez. O job único define a senha do proprietário para todos os vTPM. Veja a seguir o dockerfile para criar a imagem do contêiner.

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. Crie e envie a imagem do contêiner do job uma única vez para o 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

Substitua:

  • project-id é o identificador exclusivo do projeto.
  1. Execute o job único usando um job do Kubernetes. (AVISO: este job limpa o vTPM em cada CVM. Se a CVM usa o vTPM para criptografar o disco, o job torna a CVM inutilizável após a reinicialização. É possível verificar se o disco tem FSTYPE crypto_LUKS com o comando 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

Substitua:

  • project-id é o identificador exclusivo do projeto.
  1. Iniciar o job único. Esse job define a senha longa do proprietário do vTPM em todos os nós de trabalho.
kubectl create -f tpm-tools-task.yaml

2) Crie uma chave secreta do Kubernetes para armazenar a senha longa do proprietário do vTPM.

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

3) Crie um contêiner de aplicativo de demonstração e transmita a senha longa a ele. O contêiner do aplicativo de demonstração contém ferramentas tpm2 para interagir com o vTPM.

  1. Crie o arquivo yaml de implantação para o contêiner do aplicativo de demonstração.

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

Substitua:

  • project-id é o identificador exclusivo do projeto.
  1. Implante o aplicativo de demonstração.
kubectl create -f deploy_demo.yaml

4). Executar o isolamento de vTPM no contêiner do aplicativo de demonstração.

  1. Conecte-se ao contêiner do aplicativo de demonstração e defina uma chave primária com uma senha longa.
kubectl exec -it tpm-tools-demo -- /bin/bash
tpm2_createprimary -C o -c primary.ctx -P $(cat /etc/tpmsecret/passphrase)

O tpm2_createprimary interage com o vTPM para gerar o objeto principal com base na hierarquia e no modelo especificados.

  • -C o: indica que a chave primária será criada na hierarquia de proprietários do TPM.
  • -c primary.ctx: salva o contexto (identificador e dados associados) do objeto principal criado no arquivo primary.ctx. Esse contexto é essencial para operações posteriores.

A carga de trabalho não pode usar a senha longa do proprietário errada para criar uma chave primária.

tpm2_createprimary -C o -P resposta_senha incorreta

O comando retorna os seguintes erros:

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. A chave primária criada pode ser usada para selar e desbloquear os dados.
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

O tpm2_create interage com o vTPM para gerar o objeto criptográfico desejado.

  • -C primary.ctx: usa o contexto de chave primária que criamos anteriormente.
  • -u sealed.pub: armazena a parte pública da chave de vedação (necessária para desbloqueio) em sealed.pub.
  • -r sealed.priv: armazena a parte privada da chave de selagem em sealed.priv.
  • -i secret.txt: o arquivo que contém o secret a ser selado.

tpm2_load: carrega a chave de isolamento no TPM usando as partes pública e privada (sealed.pub, sealed.priv) e salva o contexto em sealed.ctx.

tpm2_unseal: descriptografa (descriptografar) dados que foram criptografados anteriormente (selados) usando um objeto de isolamento de vTPM.

Observe que os arquivos primary.ctx e sealed.priv só podem ser usados em um dispositivo vTPM. E qualquer pessoa com acesso ao dispositivo vTPM e a esses arquivos pode acessar os dados selados. Você pode usar a política sobre valores de PCR para isolar os dados, mas isso está fora do escopo deste codelab.

7. Limpeza

Execute os comandos a seguir no console do Cloud ou no ambiente de desenvolvimento local:

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

Substitua:

  • project-id é o identificador exclusivo do projeto.

8. A seguir

Saiba mais sobre os Confidential GKE Nodes.