Certificación remota y sellado de vTPM en nodos de Confidential GKE

1. Descripción general

Los nodos de Confidential GKE (CGKE) garantizan que los datos de las cargas de trabajo se encripten en uso. Exponer el dispositivo vTPM a las cargas de trabajo de CGKE permite que estas usen las funciones de vTPM. En este codelab, se muestran dos funciones del vTPM.

  • La certificación remota del vTPM permite que una entidad remota verifique que los nodos de CGKE que alojan cargas de trabajo se ejecutan en VMs confidenciales (CVM).
  • Autorización y sellado de vTPM

683a3b43587ef69f.png

Como se muestra en la figura anterior, la primera parte de este codelab incluye los siguientes pasos:

  • Los nodos de CGKE configuran y exponen el dispositivo vTPM a las cargas de trabajo seleccionadas.
  • Implementa una carga de trabajo y realiza la certificación remota del nodo de CGKE que la aloja.
  • Configuración del servidor web de Secret Release.

8f6e80c762a5d911.png

Como se muestra en la figura anterior, la segunda parte de este codelab incluye lo siguiente:

  • Configuración de la autorización del vTPM y sellado del vTPM en los nodos de CGKE

Qué aprenderás

  • Cómo exponer el dispositivo de vTPM a las cargas de trabajo de CGKE
  • Cómo realizar la certificación remota a través de la API de Confidential Computing (servicio de verificador de certificación) en cargas de trabajo de CGKE
  • Cómo configurar la autorización del vTPM y realizar el sellado del vTPM

Requisitos

2. Configuración y requisitos:

Para habilitar las APIs necesarias, ejecuta el siguiente comando en Cloud Console o en tu entorno de desarrollo 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. Configura nodos de CGKE y expón el dispositivo vTPM a cargas de trabajo seleccionadas

Este paso inicia la primera parte de este codelab. En este paso, iniciarás un clúster de CGKE y aplicarás un complemento de dispositivo para exponer el dispositivo vTPM de la CVM a las cargas de trabajo. Ve a Cloud Console o a tu entorno de desarrollo local para ejecutar los comandos.

1). Crea un clúster de CGKE y usa un grupo de identidades para cargas de trabajo para permitir que las cargas de trabajo de CGKE usen la API de Confidential Computing de GCP. El grupo de identidades para cargas de trabajo es necesario porque las cargas de trabajo de CGKE deben acceder a los recursos de GCP. Además, para acceder a los recursos de GCP, las cargas de trabajo de CGKE deben tener una identidad.

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

Reemplaza lo siguiente:

  • project-id es el identificador único del proyecto.

2). Inicia el complemento del dispositivo para permitir que el clúster de CGKE exponga el dispositivo vTPM a las cargas de trabajo. Usamos un complemento de dispositivo de Kubernetes para crear un nuevo recurso: google.com/cc. Cualquier carga de trabajo asociada con el nuevo recurso podrá ver el dispositivo vTPM en el nodo trabajador.

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

Reemplaza lo siguiente:

  • project-id es el identificador único del proyecto.

El siguiente comando te permite ver el cc-device-plugin implementado.

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

Nota: En el caso de un clúster de GKE en modo mixto (con nodos trabajadores de GKE confidenciales y no confidenciales), se recomienda que el operador solo implemente cc-device-plugin en los nodos trabajadores de GKE confidenciales.

(Opcional) Aplica la supervisión de Prometheus del pod de CGKE. Activar la supervisión te permite observar el estado del complemento del dispositivo.

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

Ve a https://console.cloud.google.com/monitoring/metrics-explorer y busca las métricas de cc-device-plugin o usa PROMQL. Por ejemplo, el siguiente comando de PROMQL muestra los segundos de CPU para cada proceso de cc-device-plugin.

rate(process_cpu_seconds_total[${__interval}])

4. Implementar una carga de trabajo y realizar la certificación remota en ella

En este paso, crearás e implementarás una carga de trabajo en el clúster de CGKE que creaste en el paso anterior y realizarás una certificación remota del vTPM para recuperar un token de certificación (token de OIDC) en el nodo de trabajador.

1). Crea la imagen del contenedor de la aplicación y envíala a Artifact Registry. La imagen del contenedor de la aplicación contiene la herramienta go-tpm, que puede recopilar evidencia de certificación y enviarla al servicio de Attestation Verifier para obtener un token de certificación (un token de OIDC).

  1. Crea el Dockerfile para la imagen del contenedor de la aplicación.

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. Crea un registro de artefactos.
gcloud artifacts repositories create codelab-repo \
    --repository-format=docker \
    --location=us
  1. Envía la imagen del contenedor de la aplicación a 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). Configura una cuenta de servicio de Kubernetes para heredar los permisos de una cuenta de servicio de GCP en los recursos de GCP.

  1. Crea una cuenta de servicio de Kubernetes codelab-ksa.
kubectl create serviceaccount codelab-ksa \
    --namespace default
  1. Crea un rol Confidential_Computing_Workload_User y le otorga permisos para acceder a las APIs de 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

Reemplaza lo siguiente:

  • project-id es el identificador único del proyecto.
  1. Crea una cuenta de servicio de GCP codelab-csa y vincúlala con el rol Confidential_Computing_Workload_User. So that codelab-csa que tiene permisos para acceder a las APIs de 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]"

Reemplaza lo siguiente:

  • project-id es el identificador único del proyecto.
  1. Vincula la cuenta de servicio de Kubernetes codelab-ksa con la cuenta de servicio de GCP codelab-csa. Para que codelab-ksa tenga permisos para acceder a las APIs de Confidential Computing
kubectl annotate serviceaccount codelab-ksa \
    --namespace default \
    iam.gke.io/gcp-service-account=codelab-csa@<project-id>.iam.gserviceaccount.com

Reemplaza lo siguiente:

  • project-id es el identificador único del proyecto.

3). Crea el archivo YAML de implementación de la aplicación de demostración. Asigna la cuenta de servicio de Kubernetes codelab-ksa a las cargas de trabajo seleccionadas.

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

Reemplaza lo siguiente:

  • project-id es el identificador único del proyecto.

4). Aplica la implementación al clúster de CGKE.

kubectl create -f deploy.yaml

5). Conéctate a la carga de trabajo y lanza la certificación remota para recuperar un token de certificación (un token de OIDC)

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

Puedes decodificar el token de certificación en jwt.io para ver los reclamos.

5. Cómo configurar el servidor web de Secret Release

En este paso, saldrás de la sesión SSH anterior y configurarás otra VM. En esta VM, configurarás un servidor web de lanzamiento secreto. El servidor web valida el token de certificación recibido y sus declaraciones. Si las validaciones se realizan correctamente, se pasa el secreto al solicitante.

1). Ve a Cloud Console o a tu entorno de desarrollo local. Crea una 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

Reemplaza lo siguiente:

  • project-id es el identificador único del proyecto.

2). Conéctate a tu nueva VM a través de SSH.

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

3). Configura el entorno de 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). Crea los siguientes dos archivos que almacenan el código fuente del servidor web de lanzamiento secreto (copia y pega con 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). Ejecuta los siguientes comandos para compilar el servidor web y ejecutarlo. Esto inicia el servidor web de lanzamiento de secretos en el puerto :8080.

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

Solución de problemas: Es posible que veas la siguiente advertencia, que puedes ignorar cuando ejecutes 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). Inicia otra pestaña de Cloud Console o sesión del entorno de desarrollo local y ejecuta el siguiente comando. Esto te dará el 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). Conéctate a tu carga de trabajo de CGKE y lanza la certificación remota para recuperar un token de certificación (un token de OIDC). Luego, incorpora el contenido de attestation-token y cgke-attestation-codelab-web-server-internal-ip en el siguiente comando. Esto recuperará el secreto que contiene el servidor web de lanzamiento de secretos.

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

Reemplaza lo siguiente:

  • cgke-attestation-codelab-web-server-internal-ip es la IP interna de la instancia de VM cgke-attestation-codelab-web-server.

6. Sellado de vTPM en los nodos de CGKE

Con este paso, comienza la segunda parte de este codelab. En este paso, configurarás la autorización del propietario del vTPM en los nodos de CGKE y, luego, implementarás una carga de trabajo con la frase de contraseña del propietario del vTPM. Luego, crearás una clave principal del vTPM para sellar y desellar datos en la carga de trabajo con la capacidad de sellado del vTPM.

1). Configura la autorización del propietario del vTPM en los nodos de CGKE.

  1. Crea una imagen de contenedor de trabajo único. El trabajo único establece la contraseña del propietario para todos los vTPM. A continuación, se muestra el Dockerfile para crear la imagen del contenedor.

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. Compila y envía la imagen del contenedor de trabajo único al registro de artefactos.
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

Reemplaza lo siguiente:

  • project-id es el identificador único del proyecto.
  1. Ejecuta el trabajo único a través de un trabajo de Kubernetes. (ADVERTENCIA: Este trabajo borra el vTPM en cada CVM. Si tu CVM usa el vTPM para encriptar el disco, este trabajo hará que tu CVM sea inutilizable después de reiniciar. Puedes verificar si tu disco tiene FSTYPE crypto_LUKS con el 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

Reemplaza lo siguiente:

  • project-id es el identificador único del proyecto.
  1. Inicia el trabajo único. Este trabajo establece la frase de contraseña del propietario del vTPM en todos los nodos trabajadores.
kubectl create -f tpm-tools-task.yaml

2). Crea un secreto de Kubernetes para almacenar la frase de contraseña del propietario del vTPM.

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

3). Crea un contenedor de aplicación de demostración y pásale la frase de contraseña. El contenedor de la aplicación de demostración contiene herramientas de tpm2 para interactuar con el vTPM.

  1. Crea el archivo YAML de implementación para el contenedor de la aplicación de demostración.

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

Reemplaza lo siguiente:

  • project-id es el identificador único del proyecto.
  1. Implementa la aplicación de demostración.
kubectl create -f deploy_demo.yaml

4). Realiza el sellado del vTPM en el contenedor de la aplicación de demostración.

  1. Conéctate al contenedor de la aplicación de demostración y establece una clave principal con una frase de contraseña.
kubectl exec -it tpm-tools-demo -- /bin/bash
tpm2_createprimary -C o -c primary.ctx -P $(cat /etc/tpmsecret/passphrase)

tpm2_createprimary interactúa con el vTPM para generar el objeto principal según la jerarquía y la plantilla especificadas.

  • -C o: Indica que la clave principal se creará en la jerarquía del propietario del TPM.
  • -c primary.ctx: Guarda el contexto (controlador y datos asociados) del objeto principal creado en el archivo primary.ctx. Este contexto es esencial para operaciones posteriores.

La carga de trabajo no puede usar la frase de contraseña del propietario incorrecta para crear una clave principal.

tpm2_createprimary -C o -P wrong_passphrase

El comando devuelve los siguientes errores:

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. La clave principal creada se podría usar para sellar y abrir datos.
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 interactúa con el vTPM para generar el objeto criptográfico deseado.

  • -C primary.ctx: Usa el contexto de clave principal que creamos anteriormente.
  • -u sealed.pub: Almacena la parte pública de la clave de sellado (necesaria para el desellado) en sealed.pub.
  • -r sealed.priv: Almacena la parte privada de la clave de sellado en sealed.priv.
  • -i secret.txt: Es el archivo que contiene el secreto que se sellará.

tpm2_load: Carga la clave de sellado en el TPM con las partes pública y privada (sealed.pub, sealed.priv) y guarda su contexto en sealed.ctx.

tpm2_unseal: Desencripta (desella) los datos que se encriptaron (sellaron) anteriormente con un objeto de sellado de vTPM.

Ten en cuenta que los archivos primary.ctx y sealed.priv solo se pueden usar en un dispositivo vTPM. Además, cualquier persona que tenga acceso al dispositivo vTPM y a estos archivos podrá acceder a los datos sellados. También puedes usar la política sobre los valores del PCR para sellar los datos, pero está fuera del alcance de este codelab.

7. Limpieza

Ejecuta los siguientes comandos en la consola de Cloud o en tu entorno de desarrollo 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

Reemplaza lo siguiente:

  • project-id es el identificador único del proyecto.

8. ¿Qué sigue?

Obtén más información sobre los Confidential GKE Nodes.