Cómo automatizar respuestas para fallas de validación de integridad

Obtén más información sobre cómo utilizar un activador de Cloud Functions para actuar de forma automática ante los eventos de supervisión de la integridad de una VM protegida.

Resumen

La supervisión de integridad recopila mediciones de las instancias de VM protegidas y las muestra en Cloud Logging. Si las mediciones de integridad cambian durante los inicios de una instancia de VM protegida, la validación de integridad fallará. Este error se captura como un evento registrado y también se genera en Cloud Monitoring.

A veces, las mediciones de la integridad de las VM protegidas cambian por una razón legítima. Por ejemplo, una actualización del sistema puede ocasionar cambios esperados en el kernel del sistema operativo. Debido a esto, la supervisión de la integridad permite que una instancia de VM protegida aprenda un modelo de referencia de política de integridad nuevo cuando surja una falla esperada en la validación de integridad.

En este instructivo, primero crearás un sistema automatizado simple para cerrar las instancias de VM protegidas que presenten fallas en la validación de integridad:

  1. Exporta todos los eventos de supervisión de integridad a un tema de Pub/Sub.
  2. Crea un activador de Cloud Functions que utilice los eventos de ese tema para identificar y cerrar las instancias de VM protegidas que presenten fallas en la validación de integridad.

Luego, de manera opcional, puedes expandir el sistema para que las instancias de VM protegidas que fallen en la validación de integridad aprendan el modelo de referencia nuevo en caso de que coincida con una medida buena conocida, o se cierren en caso contrario:

  1. Crea una base de datos de Firestore para mantener un conjunto de medidas buenas conocidas del modelo de referencia de integridad.
  2. Actualiza el activador de Cloud Functions para que las instancias de VM protegidas que fallen en la validación de integridad aprendan el modelo de referencia nuevo si es que existe en la base de datos, o que se cierren en caso contrario.

Si eliges implementar la solución expandida, hazlo de la siguiente manera:

  1. Cada vez que haya una actualización que pueda causar una falla en la validación por una razón legítima, ejecuta esa actualización en una única instancia de VM protegida en el grupo de instancias.
  2. Utiliza el evento de inicio retardado de la instancia de VM actualizada como fuente y agrega las medidas nuevas del modelo de referencia de la política a la base de datos a través de la creación de un documento nuevo en la colección known_good_measurements. Consulta Cómo crear una base de datos con las medidas buenas conocidas del modelo de referencia para obtener más información.
  3. Actualiza las instancias de VM protegida restantes. El activador hace que las instancias restantes aprendan el modelo de referencia nuevo porque puede verificarse como bueno conocido. A fin de obtener más información, consulta Cómo actualizar el activador de Cloud Functions para que aprenda el modelo de referencia bueno conocido.

Requisitos previos

  • Usar un proyecto que tenga seleccionado Firestore en modo nativo como el servicio de base de datos. Debes realizar esta selección cuando creas el proyecto y no podrás cambiarla. Si tu proyecto no usa Firestore en modo nativo, verás el mensaje “Este proyecto usa otro servicio de base de datos” cuando abras la consola de Firestore.
  • Tener una instancia de VM protegida de Compute Engine en ese proyecto, que funcione como la fuente de las medidas del modelo de referencia de integridad. La instancia de VM protegida debe reiniciarse al menos una vez.
  • Tener instalada la herramienta de línea de comandos de gcloud.
  • Para habilitar las API de Cloud Logging y Cloud Functions, sigue estos pasos:

    1. Ir a API y servicios
    2. Verifica si la API de Cloud Functions y la API de Stackdriver Logging figuran en la lista de API y servicios habilitados.
    3. Si alguna de las API no aparece, haz clic en Agregar API y servicios.
    4. Busca y habilita las API según sea necesario.

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

Usa Logging para exportar todas las entradas del registro de supervisión de integridad generadas por las instancias de VM protegida a un tema de Pub/Sub. Este tema se utiliza como una fuente de datos para que el activador de Cloud Functions automatice las respuestas en los eventos de supervisión de la integridad.

  1. Ir a Cloud Logging
  2. Haz clic en la flecha desplegable a la derecha de Filtrar por etiqueta o búsqueda de texto, y luego haz clic en Convertir en filtro avanzado.
  3. Ingresa el siguiente filtro avanzado:

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

    y reemplaza YOUR_PROJECT_ID por el ID de tu proyecto. Ten en cuenta que hay dos espacios después de logName:.

  4. Haz clic en Enviar filtro.

  5. Haz clic en Crear exportación.

  6. En Nombre del receptor, escribe integrity-monitoring.

  7. En Servicio del receptor, selecciona Cloud Pub/Sub.

  8. Haz clic en la flecha desplegable a la derecha de Destino del receptor, y luego haz clic en Crear tema nuevo de Cloud Pub/Sub.

  9. En Nombre, escribe integrity-monitoring, y luego haz clic en Crear.

  10. Haz clic en Crear receptor.

Crea un activador de Cloud Functions que responda a las fallas de integridad

Crea un activador de Cloud Functions que lea los datos del tema de Pub/Sub y que detenga todas las instancias de VM protegida que presenten fallas en la validación de integridad.

  1. Con el siguiente código, se define el activador de Cloud Functions. Cópialo en un archivo llamado 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 llamado 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 la terminal y navega hasta el directorio que contiene main.py y requirements.txt.

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

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

    y reemplaza YOUR_PROJECT_ID por el ID de tu proyecto.

Crea una base de datos con las medidas buenas conocidas del modelo de referencia

Crea una base de datos de Firestore para proporcionar una fuente de medidas buenas conocidas del modelo de referencia de la política de integridad. Para mantener esta base de datos actualizada, debes agregar las medidas del modelo de referencia de forma manual.

  1. Ir a la página Instancias de VM
  2. Haz clic en el ID de la instancia de VM protegida 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. Expande la entrada de registro > jsonPayload > lateBootReportEvent > policyMeasurements.
  6. Anota los valores de los elementos contenidos en lateBootReportEvent > policyMeasurements.
  7. Ir a la consola de Firestore
  8. Selecciona Iniciar colección (Start collection).
  9. En ID de la colección, escribe known_good_measurements.
  10. En ID del documento, escribe baseline1.
  11. En Nombre del campo, escribe el valor del campo pcrNum del elemento 0 en lateBootReportEvent > policyMeasurements.
  12. En Tipo de campo, selecciona mapa.
  13. Agrega tres campos de strings al campo de mapa, llamados hashAlgo, pcrNum y value, respectivamente. Establece los mismos valores que los de los campos del elemento 0 en lateBootReportEvent > policyMeasurements.
  14. Crea más campos de mapa, uno para cada elemento adicional en lateBootReportEvent > policyMeasurements. Crea los mismos subcampos que en el primer campo de mapa. Los valores de esos subcampos deben estar vinculados a los de cada uno de los elementos adicionales.

    Por ejemplo, si utilizas una VM de Linux, la colección debería ser similar a la que se muestra a continuación una vez que hayas terminado:

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

    Si usas una VM de Windows, verás más medidas, por lo que la colección debería ser similar a la que se muestra a continuación:

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

Actualiza el activador de Cloud Functions para que aprenda el modelo de referencia bueno conocido

  1. El siguiente código crea un activador de Cloud Functions que hace que cualquier instancia de VM protegida que falle en la validación de integridad aprenda el modelo de referencia nuevo si se encuentra en la base de datos de medidas buenas conocidas, o que se cierre en caso contrario. Copia este código y úsalo para reemplazar el código existente 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 = 'YOUR_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 úsalas para reemplazar el código existente 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 la terminal y navega hasta el directorio que contiene main.py y requirements.txt.

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

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

    y reemplaza YOUR_PROJECT_ID por el ID de tu proyecto.

  5. Borra de forma manual la función shutdown_vm anterior en la consola de Cloud Function.

  6. Ir a Cloud Functions

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

Verifica las respuestas automáticas para fallas de validación de la integridad

  1. Primero, comprueba si tienes una instancia en ejecución con el Inicio seguro activado como opción de VM protegida. De lo contrario, puedes crear una instancia nueva con imagen de VM protegida (Ubuntu 18.04 LTS) y activar la opción Inicio seguro. Es posible que se te cobren unos centavos por la instancia (este paso puede completarse en una hora).
  2. Ahora, supongamos que deseas actualizar el kernel de forma manual.
  3. Establece una conexión SSH a la instancia y usa el siguiente comando para comprobar el kernel actual.

    uname -sr
    

    Deberías ver algo como Linux 4.15.0-1028-gcp.

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

  5. Usa el comando para la instalación.

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

  7. Deberías notar que la VM no se inicia (no puede establecer una conexión SSH a la máquina). Esto es lo que esperamos, ya que la firma del kernel nuevo no está en la lista blanca de nuestro Inicio seguro. Esto también demuestra cómo el Inicio seguro puede evitar una modificación del kernel no autorizada o maliciosa.

  8. Sin embargo, como sabemos que la actualización del kernel no es maliciosa y que la realizamos nosotros mismos, podemos desactivar el Inicio seguro para iniciar el kernel nuevo.

  9. Apaga la VM, desmarca la opción Inicio seguro y, luego, reinicia la VM.

  10. El inicio de la máquina debería volver a fallar. Aunque esta vez, la función de Cloud Function que creamos la apaga automáticamente, ya que se modificó la opción Inicio seguro (también debido a la imagen nueva del kernel); ambas cuestiones hicieron que la medida sea diferente a la del modelo de referencia. (Podemos verificarlo en el registro de Stackdriver de la función de Cloud Function).

  11. Debido a que sabemos que esta no es una modificación maliciosa y conocemos la causa raíz, podemos agregar la medida actual en lateBootReportEvent a la tabla de Firebase de medidas buenas conocidas. (Recuerda que hay dos cambios en proceso: 1. la opción Inicio seguro 2. la imagen del kernel).

    Sigue el paso anterior Crea una base de datos con las medidas buenas conocidas del modelo de referencia para agregar un modelo de referencia nuevo a la base de datos de Firestore con la medida real del último lateBootReportEvent.

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

  12. Ahora reinicia la máquina. Cuando revises el registro de Stackdriver, verás que lateBootReportEvent aún muestra falso, pero la máquina debería iniciarse de forma correcta, ya que la función de Cloud Function confió y volvió a aprender la medida nueva. Para verificarlo, revisa Stackdriver de la función de Cloud Function.

  13. Si el Inicio seguro está inhabilitado, ahora podemos iniciar en el kernel. Establece una conexión SSH a la máquina y vuelve a comprobar el kernel. Verás la versión nueva del kernel.

    uname -sr
    
  14. Por último, limpiemos los recursos y los datos usados en este paso.

  15. Si creaste una VM para este paso, apágala así evitas cargos adicionales.

  16. Ir a la página Instancias de VM

  17. Quita las medidas buenas conocidas que agregaste en este paso.

  18. Ir a la consola de Firestore