Automazione delle risposte a errori di convalida dell'integrità

Scopri come utilizzare un trigger di Cloud Run Functions per agire automaticamente sugli eventi di monitoraggio dell'integrità delle Shielded VM.

Panoramica

Il monitoraggio dell'integrità raccoglie le misurazioni dalle istanze Shielded VM e le mostra in Cloud Logging. Se le misurazioni di integrità cambiano tra gli avvii di un'istanza Shielded VM, la convalida dell'integrità non viene superata. Questo errore viene acquisito come evento registrato e viene anche segnalato in Cloud Monitoring.

A volte, le misurazioni di integrità delle Shielded VM vengono modificate per un motivo legittimo. Ad esempio, un aggiornamento di sistema potrebbe causare modifiche previste al kernel del sistema operativo. Per questo motivo, il monitoraggio dell'integrità consente di richiedere all'istanza Shielded VM di apprendere una nuova baseline delle policy di integrità nel caso di un errore di convalida dell'integrità previsto.

In questo tutorial, creerai innanzitutto un semplice sistema automatico che arresta le istanze Shielded VM che non superano la convalida dell'integrità:

  1. Esporta tutti gli eventi di monitoraggio dell'integrità in un argomento Cloud Pub/Sub.
  2. Crea un trigger di Cloud Run Functions che utilizza gli eventi in quell'argomento per identificare e arrestare le istanze Shielded VM che non superano la convalida dell'integrità.

Quindi, facoltativamente, puoi espandere il sistema in modo che richieda alle istanze Shielded VM che non superano la convalida dell'integrità di apprendere la nuova baseline se questa corrisponde a una misurazione nota, o altrimenti di arrestarsi:

  1. Crea un database Firestore per mantenere un insieme di misurazioni di baseline di integrità note.
  2. Aggiorna il trigger di Cloud Run Functions in modo che richieda alle istanze Shielded VM che non superano la convalida dell'integrità di apprendere la nuova baseline se questa si trova nel database, oppure di arrestarsi.

Se scegli di implementare la soluzione espansa, utilizzala nel modo seguente:

  1. Ogni volta che è previsto un aggiornamento che causa un errore di convalida per un motivo legittimo, esegui l'aggiornamento su una singola istanza Shielded VM nel gruppo di istanze.
  2. Utilizzando l'evento della fase finale di avvio dall'istanza VM aggiornata come origine, aggiungi le nuove misurazioni di baseline dei criteri nel database creando un nuovo documento nella raccolta known_good_measurements. Consulta Creazione di un database con misurazioni di baseline note per ulteriori informazioni.
  3. Aggiorna le rimanenti istanze Shielded VM. Il trigger richiede alle istanze rimanenti di apprendere la nuova baseline, perché può essere verificata come nota. Per saperne di più, consulta Aggiornamento del trigger di Cloud Run Functions per apprendere la baseline nota.

Prerequisiti

  • Utilizza un progetto con Firestore in modalità nativa selezionato come servizio di database. Puoi effettuare questa selezione al momento della creazione del progetto e non può essere modificata. Se il progetto non utilizza Firestore in modalità nativa, visualizzerai il messaggio "Questo progetto utilizza un altro servizio di database" quando apri la console di Firestore.
  • Disponi di un'istanza Shielded VM di Compute Engine nel progetto, che sarà la fonte delle misurazioni di baseline dell'integrità. Devi aver riavviato l'istanza Shielded VM almeno una volta.
  • Lo strumento a riga di comando gcloud è installato.
  • Abilita le API Cloud Logging e Cloud Run Functions seguendo questi passaggi:

    1. Nella console Trusted Cloud , vai alla pagina API e servizi.

      Vai su API e servizi

    2. Verifica se l'API Cloud Functions e l'API Stackdriver Logging sono visualizzate nell'elenco API e servizi abilitati.

    3. Se nessuna delle due API viene visualizzata, fai clic su Aggiungi API e servizi.

    4. Cerca e abilita le API, se necessario.

Esportazione delle voci di log del monitoraggio di integrità in un argomento Pub/Sub

Utilizza Logging per esportare tutte le voci di log del monitoraggio di integrità generate dalle istanze Shielded VM in un argomento Cloud Pub/Sub. Utilizza questo argomento come origine dati per un trigger di Cloud Run Functions al fine di automatizzare le risposte agli eventi di monitoraggio dell'integrità.

Esplora log

  1. Nella console Trusted Cloud , vai alla pagina Esplora log.

    Vai a Cloud Logging

  2. In Query Builder, inserisci i seguenti valori.

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

  3. Fai clic su Esegui query.

  4. Fai clic su Altre azioni, quindi seleziona Crea un sink.

  5. Nella pagina Crea sink di routing dei log:

    1. In Dettagli sink, in Nome sink, inserisci integrity-monitoring e fai clic su Avanti.
    2. In Destinazione sink, espandi Seleziona il servizio sink, quindi seleziona Cloud Pub/Sub.
    3. Espandi Seleziona un argomento Cloud Pub/Sub, quindi seleziona Crea un argomento.
    4. Nella finestra di dialogo Crea un argomento, in ID argomento, inserisci integrity-monitoring, quindi fai clic su Crea argomento.
    5. Fai clic su Avanti e poi su Crea sink.

Esplora log

  1. Nella console Trusted Cloud , vai alla pagina Esplora log.

    Vai a Cloud Logging

  2. Fai clic su Opzioni e seleziona Torna a Esplora log legacy.

  3. Espandi Filtra per etichetta o testo, quindi fai clic su Converti in filtro avanzato.

  4. Inserisci il seguente filtro avanzato:

    resource.type="gce_instance"
    AND logName:  "projects/YOUR_PROJECT_ID/logs/compute.googleapis.com/shielded_vm_integrity"
    
    Tieni presente che dopo logName: ci sono due spazi.

  5. Fai clic su Invia filtro.

  6. Fai clic su Crea esportazione.

  7. In Nome sink, inserisci integrity-monitoring.

  8. Per il Servizio sink, seleziona Cloud Pub/Sub.

  9. Espandi Destinazione sink, quindi fai clic su Crea nuovo argomento Cloud Pub/Sub.

  10. In Nome, inserisci integrity-monitoring e fai clic su Crea.

  11. Fai clic su Crea sink.

Creazione di un trigger di Cloud Run Functions per rispondere agli errori di integrità

Crea un trigger di Cloud Run Functions che legge i dati nell'argomento Cloud Pub/Sub e arresta qualsiasi istanza Shielded VM che non supera la convalida dell'integrità.

  1. Il codice seguente definisce il trigger di Cloud Run Functions. Copialo in un file denominato 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. Nella stessa posizione di main.py, crea un file denominato requirements.txt e copia le seguenti dipendenze:

    google-api-python-client==1.6.6
    google-auth==1.4.1
    google-auth-httplib2==0.0.3
    
  3. Apri una finestra del terminale e vai alla directory contenente main.py e requirements.txt.

  4. Utilizza il comando gcloud beta functions deploy per eseguire il deployment del trigger:

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

Creazione di un database con misurazioni di baseline note

Crea un database Firestore per fornire una fonte di misurazioni di baseline delle policy di integrità note. Devi aggiungere manualmente le misurazioni di baseline per mantenere aggiornato questo database.

  1. Nella console Trusted Cloud , vai alla pagina Istanze VM.

    Vai alla pagina Istanze VM

  2. Fai clic sull'ID dell'istanza Shielded VM per aprire la pagina dei dettagli dell'istanza VM.

  3. Sotto Log, fai clic su Stackdriver Logging.

  4. Individua la voce di log lateBootReportEvent più recente.

  5. Espandi la voce di log > jsonPayload > lateBootReportEvent > policyMeasurements.

  6. Prendi nota dei valori per gli elementi contenuti in lateBootReportEvent > policyMeasurements.

  7. Nella console Trusted Cloud , vai alla pagina Firestore.

    Vai alla console Firestore

  8. Scegli Avvia raccolta.

  9. Per ID raccolta, digita known_good_measurements.

  10. Per ID documento, digita baseline1.

  11. Per Nome campo, digita il valore del campo pcrNum dall'elemento 0 in lateBootReportEvent > policyMeasurements.

  12. Per Tipo di campo, seleziona mappa.

  13. Aggiungi tre campi stringa al campo mappa, rispettivamente denominati hashAlgo, pcrNum e value. Usa i rispettivi valori come valori dei campi elemento 0 in lateBootReportEvent > policyMeasurements.

  14. Crea più campi mappa, uno per ogni elemento aggiuntivo in lateBootReportEvent > policyMeasurements. Fornisci gli stessi sottocampi del primo campo mappa. I valori per questi sottocampi dovrebbero essere mappati in quelli di ciascuno degli elementi aggiuntivi.

    Ad esempio, se stai utilizzando una VM Linux, la raccolta dovrebbe apparire simile a quanto segue quando hai terminato:

    Un database Firestore che mostra una raccolta known_good_measurements completata per Linux.

    Se utilizzi una VM Windows, vedrai più misurazioni, quindi la raccolta dovrebbe essere simile alla seguente:

    Un database Firestore che mostra una raccolta known_good_measurements completata per Windows.

Aggiornamento del trigger di Cloud Run Functions per apprendere la baseline nota

  1. Il codice seguente crea un trigger di Cloud Run Functions che indica a un'eventuale istanza Shielded VM che non supera la convalida dell'integrità di apprendere la nuova baseline, se questa si trova nel database delle misurazioni note, oppure di arrestarsi. Copia questo codice e utilizzalo per sovrascrivere il codice esistente in 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 le seguenti dipendenze e usale per sovrascrivere il codice esistente in 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. Apri una finestra del terminale e vai alla directory contenente main.py e requirements.txt.

  4. Utilizza il comando gcloud beta functions deploy per eseguire il deployment del trigger:

    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 funzione shutdown_vm precedente nella console della funzione Cloud Functions.

  6. Nella console Trusted Cloud , vai alla pagina Cloud Functions.

    Vai a Cloud Functions

  7. Seleziona la funzione shutdown_vm e fai clic su Elimina.

Verifica le risposte automatizzate agli errori di convalida dell'integrità

  1. Innanzitutto, controlla se hai un'istanza in esecuzione con Avvio protetto attivato come opzione Shielded VM. In caso contrario, puoi creare una nuova istanza con l'immagine Shielded VM (Ubuntu 18.04 LTS) e attivare l'opzione Avvio protetto. Potresti ricevere un addebito di pochi centesimi per l'istanza (puoi completare questo passaggio entro un'ora).
  2. Ora, supponiamo che per qualche motivo tu voglia fare l'upgrade manuale del kernel.
  3. Connettiti all'istanza tramite SSH e utilizza il seguente comando per controllare il kernel attuale.

    uname -sr
    

    Dovresti vedere qualcosa di simile: Linux 4.15.0-1028-gcp.

  4. Scarica un kernel generico da https://kernel.ubuntu.com/~kernel-ppa/mainline/

  5. Utilizza il comando per l'installazione.

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

  7. Dovresti notare che la VM non si avvia (non riesci ad accedere alla macchina tramite SSH). È quello che ci aspettavamo, perché la firma del nuovo kernel non è presente nella nostra lista consentita di Avvio protetto. Questo dimostra anche come Avvio protetto possa impedire una modifica del kernel non autorizzata/dannosa.

  8. Tuttavia, poiché sappiamo che questa volta l'upgrade del kernel non è dannoso ed è stato effettivamente eseguito da noi, possiamo disattivare Avvio protetto per avviare il nuovo kernel.

  9. Arresta la VM e deseleziona l'opzione Avvio protetto, quindi riavviala.

  10. Anche in questo caso, la macchina non dovrebbe avviarsi. Tuttavia, questa volta viene arrestata automaticamente dalla funzione Cloud Functions che abbiamo creato perché l'opzione Avvio protetto è stata modificata (anche a causa della nuova immagine del kernel) e questo ha causato una misurazione diversa rispetto alla baseline. Possiamo verificarlo nel log Stackdriver della funzione Cloud Functions.

  11. Poiché sappiamo che non si tratta di una modifica dannosa e conosciamo la causa principale, possiamo aggiungere la misurazione corrente in lateBootReportEvent alla tabella Firebase delle misurazioni note come valide. (Ricorda che vengono modificati due elementi: 1. L'opzione Avvio protetto. 2. L'immagine del kernel.)

    Segui il passaggio precedente, Creazione di un database con misurazioni di baseline note, per aggiungere una nuova baseline al database Firestore utilizzando la misurazione effettiva nel lateBootReportEvent più recente.

    Un database Firestore che mostra una nuova raccolta known_good_measurements completata.

  12. Ora riavvia la macchina. Quando controlli il log di Stackdriver, vedrai che lateBootReportEvent continua a mostrare false, ma la macchina ora dovrebbe avviarsi correttamente perché la funzione Cloud Functions ha considerato attendibile e riappreso la nuova misurazione. Possiamo verificarlo controllando Stackdriver della funzione Cloud Functions.

  13. Ora che l'Avvio protetto è disattivato, possiamo avviare il kernel. Accedi tramite SSH alla macchina e controlla di nuovo il kernel, vedrai la nuova versione del kernel.

    uname -sr
    
  14. Infine, ripuliamo le risorse e i dati utilizzati in questo passaggio.

  15. Arresta la VM se ne hai creata una per questo passaggio per evitare addebiti aggiuntivi.

  16. Nella console Trusted Cloud , vai alla pagina Istanze VM.

    Vai alla pagina Istanze VM

  17. Rimuovi le misurazioni note come valide aggiunte in questo passaggio.

  18. Nella console Trusted Cloud , vai alla pagina Firestore.

    Vai alla pagina Firestore