Automatizar respuestas a fallos de validación de integridad

Descubre cómo utilizar los activadores de Cloud Run Functions para responder automáticamente a eventos de supervisión de integridad de VM blindadas.

Información general

En el proceso de supervisión de integridad se recopilan mediciones de instancias de máquinas virtuales (VM) blindadas y se muestran en Cloud Logging. Si las mediciones de integridad cambian cada vez que se inicia una instancia de VM blindada, la validación de integridad falla. Este error se captura como un evento registrado y se muestra en Cloud Monitoring.

A veces, las mediciones de integridad de VM blindadas cambian por un motivo legítimo. Por ejemplo, en una actualización del sistema pueden esperarse algunos cambios en el kernel del sistema operativo. Por esta razón, la supervisión de integridad te permite solicitar a una instancia de VM blindada que obtenga un nuevo valor de referencia de la política de integridad en caso de que se prevea que no se superará la validación de integridad.

En este tutorial, primero crearás un sistema automatizado simple que apagará las instancias de VM blindadas que no superen la validación de integridad:

  1. Exporta todos los eventos de la supervisión de integridad a un tema de Pub/Sub.
  2. Crea un activador de funciones de Cloud Run que use los eventos de ese tema para identificar y apagar las instancias de VM blindadas que no superen la validación de integridad.

A continuación, si lo deseas, puedes ampliar el sistema para solicitar a las instancias de VM blindadas que no superen la validación de integridad que obtengan el nuevo valor de referencia si este coincide con una medición correcta o que, en caso contrario, se apaguen:

  1. Crea una base de datos de Firestore para mantener un conjunto de mediciones de valores de referencia de integridad correctas.
  2. Actualiza el activador de Cloud Functions para solicitar a las instancias de VM blindadas que no superen la validación de integridad que obtengan el nuevo valor de referencia si este se encuentra en la base de datos o que, en caso contrario, se apaguen.

Si optas por implementar la solución ampliada, úsala de la siguiente manera:

  1. Cada vez se prevea que una actualización causará un error de validación por un motivo legítimo, ejecuta dicha actualización en una única instancia de VM blindada del grupo de instancias.
  2. Crea un documento en la colección known_good_measurements con el evento de arranque tardío de la instancia de máquina virtual actualizada como fuente para añadir las nuevas mediciones de valores de referencia de la política a la base de datos. Para obtener más información, consulta Crear una base de datos de mediciones de valores de referencia correctas.
  3. Actualiza el resto de las instancias de VM blindadas. El activador solicita al resto de las instancias que obtengan el nuevo valor de referencia, ya que puede verificarse que es correcto. Para obtener más información, consulta Actualizar el activador de Cloud Run Functions para obtener un valor de referencia correcto.

Requisitos previos

  • Utiliza un proyecto que tenga Firestore en modo nativo como servicio de base de datos. Esta selección se realiza al crear el proyecto y no se puede cambiar. Si tu proyecto no utiliza Firestore en modo nativo, aparecerá el mensaje "Este proyecto usa otro servicio de bases de datos" al abrir la consola de Firestore.
  • Ten una instancia de VM blindada de Compute Engine en ese proyecto para utilizarla como origen de las mediciones de valores de referencia de integridad. La instancia de VM blindada debe haberse reiniciado al menos una vez.
  • Instala la herramienta de línea de comandos gcloud.
  • Habilita las APIs de Cloud Logging y Cloud Functions de la siguiente manera:

    1. En la Google Cloud consola, ve a la página APIs & Services (APIs y servicios).

      Ir a APIs y servicios

    2. Comprueba si la API Cloud Functions y la API Stackdriver Logging aparecen en la lista API y servicios habilitados.

    3. Si alguna de las API no aparece, haz clic en Add APIs and Services.

    4. Busca y habilita las API si es necesario.

Exportar entradas de registro de la supervisión de integridad a un tema de Pub/Sub

Usa Logging para exportar todas las entradas de registro de la supervisión de integridad que han generado las instancias de VM blindadas a un tema de Pub/Sub. Este tema se utiliza como fuente de datos de un activador de Cloud Run functions para automatizar las respuestas a eventos de la supervisión de integridad.

Explorador de registros

  1. En la Google Cloud consola, ve a la página Explorador de registros.

    Ir a Cloud Logging

  2. En el Creador de consultas, introduce los siguientes valores.

    resource.type="gce_instance"
    AND logName:  "projects/YOUR_PROJECT_ID/logs/compute.googleapis.com/shielded_vm_integrity"
    

  3. Haz clic en Ejecutar filtro.

  4. Haz clic en Más acciones y, a continuación, selecciona Crear receptor.

  5. En la página Crear sumidero de enrutamiento de registros, haz lo siguiente:

    1. En Sink details (Detalles del receptor), en Sink Name (Nombre del receptor), introduzca integrity-monitoring. A continuación, haga clic en Next (Siguiente).
    2. En Destino del sumidero, despliega Servicio del sumidero y, a continuación, selecciona Cloud Pub/Sub.
    3. Despliega Selecciona un tema de Cloud Pub/Sub y, a continuación, selecciona Crear un tema.
    4. En el cuadro de diálogo Crear un tema, en ID de tema, introduce integrity-monitoring y, a continuación, haz clic en Crear tema.
    5. Haz clic en Siguiente y, a continuación, en Crear receptor.

Explorador de registros

  1. En la Google Cloud consola, ve a la página Explorador de registros.

    Ir a Cloud Logging

  2. Haz clic en Opciones y, a continuación, selecciona Volver al Explorador de registros antiguo.

  3. Expande Filtrar por etiqueta o buscar texto y, a continuación, haz clic en Convertir a filtro avanzado.

  4. Introduce el siguiente filtro avanzado:

    resource.type="gce_instance"
    AND logName:  "projects/YOUR_PROJECT_ID/logs/compute.googleapis.com/shielded_vm_integrity"
    
    Ten en cuenta que hay dos espacios después de logName:.

  5. Haz clic en Enviar filtro.

  6. Haz clic en Crear exportación.

  7. En Nombre del sumidero, introduce integrity-monitoring.

  8. En Servicio del sumidero, selecciona Cloud Pub/Sub.

  9. Despliega Destino del sumidero y, a continuación, haz clic en Crear tema de Cloud Pub/Sub.

  10. En Nombre, introduce integrity-monitoring y haz clic en Crear.

  11. Haz clic en Crear sumidero.

Crear un activador de Cloud Run Functions para responder a errores de integridad

Crea un activador de funciones de Cloud Run que lea los datos del tema de Pub/Sub y que detenga cualquier instancia de VM blindada que no supere la validación de integridad.

  1. El siguiente código define el activador de Cloud Run Functions. Cópialo en un archivo con el nombre main.py.

    import base64
    import json
    import googleapiclient.discovery
    
    def shutdown_vm(data, context):
        """A Cloud Function that shuts down a VM on failed integrity check."""
        log_entry = json.loads(base64.b64decode(data['data']).decode('utf-8'))
        payload = log_entry.get('jsonPayload', {})
        entry_type = payload.get('@type')
        if entry_type != 'type.googleapis.com/cloud_integrity.IntegrityEvent':
          raise TypeError("Unexpected log entry type: %s" % entry_type)
    
        report_event = (payload.get('earlyBootReportEvent')
            or payload.get('lateBootReportEvent'))
    
        if report_event is None:
          # We received a different event type, ignore.
          return
    
        policy_passed = report_event['policyEvaluationPassed']
        if not policy_passed:
          print('Integrity evaluation failed: %s' % report_event)
          print('Shutting down the VM')
    
          instance_id = log_entry['resource']['labels']['instance_id']
          project_id = log_entry['resource']['labels']['project_id']
          zone = log_entry['resource']['labels']['zone']
    
          # Shut down the instance.
          compute = googleapiclient.discovery.build(
              'compute', 'v1', cache_discovery=False)
    
          # Get the instance name from instance id.
          list_result = compute.instances().list(
              project=project_id,
              zone=zone,
                  filter='id eq %s' % instance_id).execute()
          if len(list_result['items']) != 1:
            raise KeyError('unexpected number of items: %d'
                % len(list_result['items']))
          instance_name = list_result['items'][0]['name']
    
          result = compute.instances().stop(project=project_id,
              zone=zone,
              instance=instance_name).execute()
          print('Instance %s in project %s has been scheduled for shut down.'
              % (instance_name, project_id))
  2. En la misma ubicación que main.py, crea un archivo con el nombre requirements.txt y cópialo en las siguientes dependencias:

    google-api-python-client==1.6.6
    google-auth==1.4.1
    google-auth-httplib2==0.0.3
    
  3. Abre una ventana de terminal y ve al directorio que contiene main.py y requirements.txt.

  4. Ejecuta el comando gcloud beta functions deploy para desplegar el activador:

    gcloud beta functions deploy shutdown_vm \
        --project PROJECT_ID \
        --runtime python37 \
        --trigger-resource integrity-monitoring \
        --trigger-event google.pubsub.topic.publish
    

Crear una base de datos de mediciones de valores de referencia correctas

Crea una base de datos de Firestore para proporcionar una fuente de mediciones de valores de referencia de integridad correctas. Para mantener actualizada esta base de datos, debes añadir las mediciones de valores de referencia de forma manual.

  1. En la consola de Google Cloud , ve a la página Instancias de VM.

    Ve a la página Instancias de VM.

  2. Haz clic en el ID de la instancia de VM blindada para abrir la página Detalles de la instancia de VM.

  3. En Registros, haz clic en Stackdriver Logging.

  4. Busca la entrada de registro lateBootReportEvent más reciente.

  5. Despliega la entrada de registro > jsonPayload > lateBootReportEvent > policyMeasurements.

  6. Anota los valores de los elementos que se incluyen en lateBootReportEvent > policyMeasurements.

  7. En la Google Cloud consola, ve a la página Firestore.

    Ve a la consola de Firestore.

  8. Selecciona Iniciar colección.

  9. En ID de colección, introduce known_good_measurements.

  10. En ID de documento, introduce baseline1.

  11. En Nombre del campo, introduce el valor del campo pcrNum del elemento 0 de lateBootReportEvent > policyMeasurements.

  12. En Tipo de campo, selecciona mapa.

  13. Añade tres campos de cadena al campo de mapa con los nombres hashAlgo, pcrNum y value, respectivamente. Asígnales los valores de los campos del elemento 0 en lateBootReportEvent > policyMeasurements.

  14. Crea más campos de mapas, uno para cada elemento adicional de lateBootReportEvent > policyMeasurements. Asígnales los mismos subcampos que al primer campo de mapa. Los valores de esos subcampos deben asignarse a los valores respectivos de cada uno de los elementos adicionales.

    Por ejemplo, si utilizas una máquina virtual de Linux, cuando termines, la colección debería verse como en la siguiente imagen:

    Una base de datos de Firestore que muestra una colección known_good_measurements completada para Linux.

    Si utilizas una máquina virtual de Windows, verás más mediciones, por lo que la colección debería tener un aspecto similar al siguiente:

    Una base de datos de Firestore que muestra una colección known_good_measurements completada para Windows

Actualizar el activador de Cloud Run Functions para obtener un valor de referencia correcto

  1. El siguiente código crea un activador de funciones de Cloud Run que solicita a cualquier instancia de VM blindada que no supere la validación de integridad que obtenga el nuevo valor de referencia si este se encuentra en la base de datos de mediciones correctas o que, en caso contrario, se apague. Copia este código y utilízalo para sobrescribir el código actual en main.py.

    import base64
    import json
    import googleapiclient.discovery
    
    import firebase_admin
    from firebase_admin import credentials
    from firebase_admin import firestore
    
    PROJECT_ID = 'PROJECT_ID'
    
    firebase_admin.initialize_app(credentials.ApplicationDefault(), {
        'projectId': PROJECT_ID,
    })
    
    def pcr_values_to_dict(pcr_values):
      """Converts a list of PCR values to a dict, keyed by PCR num"""
      result = {}
      for value in pcr_values:
        result[value['pcrNum']] = value
      return result
    
    def instance_id_to_instance_name(compute, zone, project_id, instance_id):
      list_result = compute.instances().list(
          project=project_id,
          zone=zone,
          filter='id eq %s' % instance_id).execute()
      if len(list_result['items']) != 1:
        raise KeyError('unexpected number of items: %d'
            % len(list_result['items']))
      return list_result['items'][0]['name']
    
    def relearn_if_known_good(data, context):
        """A Cloud Function that shuts down a VM on failed integrity check.
        """
        log_entry = json.loads(base64.b64decode(data['data']).decode('utf-8'))
        payload = log_entry.get('jsonPayload', {})
        entry_type = payload.get('@type')
        if entry_type != 'type.googleapis.com/cloud_integrity.IntegrityEvent':
          raise TypeError("Unexpected log entry type: %s" % entry_type)
    
        # We only send relearn signal upon receiving late boot report event: if
        # early boot measurements are in a known good database, but late boot
        # measurements aren't, and we send relearn signal upon receiving early boot
        # report event, the VM will also relearn late boot policy baseline, which we
        # don't want, because they aren't known good.
        report_event = payload.get('lateBootReportEvent')
        if report_event is None:
          return
    
        evaluation_passed = report_event['policyEvaluationPassed']
        if evaluation_passed:
          # Policy evaluation passed, nothing to do.
          return
    
        # See if the new measurement is known good, and if it is, relearn.
        measurements = pcr_values_to_dict(report_event['actualMeasurements'])
    
        db = firestore.Client()
        kg_ref = db.collection('known_good_measurements')
    
        # Check current measurements against known good database.
        relearn = False
        for kg in kg_ref.get():
    
          kg_map = kg.to_dict()
    
          # Check PCR values for lateBootReportEvent measurements against the known good
          # measurements stored in the Firestore table
    
          if ('PCR_0' in kg_map and kg_map['PCR_0'] == measurements['PCR_0'] and
              'PCR_4' in kg_map and kg_map['PCR_4'] == measurements['PCR_4'] and
              'PCR_7' in kg_map and kg_map['PCR_7'] == measurements['PCR_7']):
    
            # Linux VM (3 measurements), only need to check above 3 measurements
            if len(kg_map) == 3:
              relearn = True
    
            # Windows VM (6 measurements), need to check 3 additional measurements
            elif len(kg_map) == 6:
              if ('PCR_11' in kg_map and kg_map['PCR_11'] == measurements['PCR_11'] and
                  'PCR_13' in kg_map and kg_map['PCR_13'] == measurements['PCR_13'] and
                  'PCR_14' in kg_map and kg_map['PCR_14'] == measurements['PCR_14']):
                relearn = True
    
        compute = googleapiclient.discovery.build('compute', 'beta',
            cache_discovery=False)
    
        instance_id = log_entry['resource']['labels']['instance_id']
        project_id = log_entry['resource']['labels']['project_id']
        zone = log_entry['resource']['labels']['zone']
    
        instance_name = instance_id_to_instance_name(compute, zone, project_id, instance_id)
    
        if not relearn:
          # Issue shutdown API call.
          print('New measurement is not known good. Shutting down a VM.')
    
          result = compute.instances().stop(project=project_id,
              zone=zone,
              instance=instance_name).execute()
    
          print('Instance %s in project %s has been scheduled for shut down.'
                % (instance_name, project_id))
    
        else:
          # Issue relearn API call.
          print('New measurement is known good. Relearning...')
    
          result = compute.instances().setShieldedInstanceIntegrityPolicy(
              project=project_id,
              zone=zone,
              instance=instance_name,
              body={'updateAutoLearnPolicy':True}).execute()
    
          print('Instance %s in project %s has been scheduled for relearning.'
            % (instance_name, project_id))
  2. Copia las siguientes dependencias y utilízalas para sobrescribir el código actual en requirements.txt:

    google-api-python-client==1.6.6
    google-auth==1.4.1
    google-auth-httplib2==0.0.3
    google-cloud-firestore==0.29.0
    firebase-admin==2.13.0
    
  3. Abre una ventana de terminal y ve al directorio que contiene main.py y requirements.txt.

  4. Ejecuta el comando gcloud beta functions deploy para desplegar el activador:

    gcloud beta functions deploy relearn_if_known_good \
        --project PROJECT_ID \
        --runtime python37 \
        --trigger-resource integrity-monitoring \
        --trigger-event google.pubsub.topic.publish
  5. Elimina manualmente la función shutdown_vm anterior en la consola de funciones de Cloud.

  6. En la Google Cloud consola, ve a la página Cloud Functions.

    Ir a Cloud Functions

  7. Selecciona la función shutdown_vm y haz clic en eliminar.

Verificar las respuestas automatizadas a fallos de validación de integridad

  1. Primero, comprueba si tienes una instancia en ejecución con la opción Arranque seguro activada como opción de VM blindada. Si no es así, puedes crear una instancia con la imagen de VM blindada (Ubuntu 18.04 LTS) y activar la opción Arranque seguro. Es posible que se te cobre unos céntimos por la instancia (este paso se puede completar en una hora).
  2. Ahora, supongamos que, por algún motivo, quieres actualizar el kernel manualmente.
  3. Accede a la instancia mediante SSH y usa el siguiente comando para comprobar el kernel actual.

    uname -sr
    

    Debería ver algo parecido a Linux 4.15.0-1028-gcp.

  4. Descarga un kernel genérico de https://kernel.ubuntu.com/~kernel-ppa/mainline/

  5. Usa el comando para instalarlo.

    sudo dpkg -i *.deb
    
  6. Reinicia la VM.

  7. Deberías observar que la máquina virtual no se inicia (no se puede acceder a la máquina mediante SSH). Es lo que esperamos, ya que la firma del nuevo kernel no está en nuestra lista blanca de arranque seguro. También se muestra cómo Arranque seguro puede evitar que se modifique el kernel sin autorización o de forma maliciosa.

  8. Sin embargo, como sabemos que esta vez la actualización del kernel no es maliciosa y la hemos hecho nosotros, podemos desactivar Arranque seguro para arrancar el nuevo kernel.

  9. Apaga la VM y desmarca la opción Arranque seguro. A continuación, reinicia la VM.

  10. El arranque del equipo debería fallar de nuevo. Sin embargo, esta vez se ha apagado automáticamente mediante la función en la nube que hemos creado, ya que se ha modificado la opción Arranque seguro (también debido a la nueva imagen del kernel), lo que ha provocado que la medición sea diferente a la de referencia. (Podemos comprobarlo en el registro Stackdriver de la función de Cloud).

  11. Como sabemos que no se trata de una modificación malintencionada y conocemos la causa principal, podemos añadir la medición actual en lateBootReportEvent a la tabla de Firebase de mediciones correctas conocidas. (Recuerda que hay dos cosas que se van a cambiar: 1. Arranque seguro opción 2. Imagen del kernel.)

    Sigue el paso anterior Crear una base de datos de mediciones de valores de referencia correctas para añadir un nuevo valor de referencia a la base de datos de Firestore con la medición real del último lateBootReportEvent.

    Una base de datos de Firestore que muestra una nueva colección known_good_measurements completada

  12. Ahora, reinicia el equipo. Cuando consultes el registro de Stackdriver, verás que lateBootReportEvent sigue mostrando el valor false, pero la máquina debería iniciarse correctamente, ya que la función en la nube ha confiado en la nueva medición y la ha vuelto a aprender. Para verificarlo, podemos consultar Stackdriver de la función en la nube.

  13. Con Arranque seguro inhabilitado, ahora podemos arrancar el kernel. Accede a la máquina mediante SSH y vuelve a comprobar el kernel. Verás la nueva versión del kernel.

    uname -sr
    
  14. Por último, vamos a limpiar los recursos y los datos utilizados en este paso.

  15. Apaga la VM si has creado una para este paso para evitar cargos adicionales.

  16. En la consola de Google Cloud , ve a la página Instancias de VM.

    Ve a la página Instancias de VM.

  17. Elimina las mediciones correctas que has añadido en este paso.

  18. En la Google Cloud consola, ve a la página Firestore.

    Ve a la página de Firestore.