Automatiser les réponses aux échecs de validation d'intégrité

Apprenez à utiliser un déclencheur Cloud Functions pour agir automatiquement sur les événements de surveillance de l'intégrité des VM protégées.

Aperçu

La surveillance de l'intégrité collecte des mesures à partir d'instances de VM protégées et les transmet à Cloud Logging. Si les mesures d'intégrité changent entre deux démarrages d'une instance de VM protégée, la validation d'intégrité échoue. Cet échec est enregistré en tant qu'événement consigné et est également remonté dans Cloud Monitoring.

Les mesures d'intégrité d'une VM protégée changent parfois pour un motif légitime. Par exemple, une mise à jour système peut entraîner des modifications attendues au niveau du noyau du système d'exploitation. C'est pourquoi la surveillance de l'intégrité vous permet de demander à une instance de VM protégée d'enregistrer une nouvelle règle d'intégrité de référence dans le cas d'un échec anticipé de validation d'intégrité.

Dans ce tutoriel, vous allez tout d'abord créer un système simple automatisé qui arrête les instances de VM protégées dont la validation d'intégrité échoue :

  1. Exportez tous les événements de surveillance de l'intégrité vers un sujet Pub/Sub.
  2. Créez un déclencheur Cloud Functions qui exploite les événements de ce sujet pour identifier et arrêter les instances de VM protégées dont la validation d'intégrité échoue.

Si vous le souhaitez, vous pouvez ensuite étendre le système pour qu'il invite les instances de VM protégées dont la validation d'intégrité échoue à apprendre la nouvelle règle de référence, si celle-ci correspond à une bonne mesure connue, ou qu'il les arrête si ce n'est pas le cas :

  1. Créez une base de données Firestore pour conserver un ensemble de bonnes mesures connues pour la règle d'intégrité de référence.
  2. Mettez à jour le déclencheur Cloud Functions pour qu'il invite les instances de VM protégées dont la validation d'intégrité échoue à apprendre la nouvelle règle de référence, si elle se trouve dans la base de données, ou qu'il les arrête dans le cas contraire.

Si vous choisissez de mettre en œuvre la solution étendue, suivez la démarche suivante :

  1. Chaque fois qu'une mise à jour est susceptible d'entraîner un échec de validation pour un motif légitime, exécutez-la sur une seule instance de VM protégée du groupe d'instances.
  2. En vous appuyant sur l'événement de démarrage tardif de l'instance de VM mise à jour, ajoutez les mesures de la nouvelle règle de référence à la base de données en créant un document dans la collection known_good_measurements. Pour en savoir plus, consultez la section Créer une base de données de bonnes mesures connues pour la règle de référence.
  3. Mettez à jour les instances de VM protégées restantes. Le déclencheur les invite à apprendre la nouvelle règle de référence, car elle peut être validée en tant que bonne règle connue. Pour plus d'informations, consultez la section Mettre à jour le déclencheur Cloud Functions pour l'apprentissage de bonnes mesures connues pour la règle de référence.

Prérequis

  • Vous devez disposer d'un projet qui utilise Firestore en mode natif en tant que service de base de données. Vous pouvez choisir ce mode lors de la création du projet. Notez que ce paramètre n'est pas modifiable. Si votre projet n'utilise pas Firestore en mode natif, le message "Ce projet utilise un autre service de base de données" s'affiche lorsque vous ouvrez la console Firestore.
  • Vous devez disposer d'une instance de VM protégée Compute Engine dans ce projet. Elle fournira une source de mesures à la règle d'intégrité de référence. L'instance de VM protégée doit avoir été redémarrée au moins une fois.
  • Installez l'outil de ligne de commande gcloud.
  • Vous devez activer les API Cloud Logging et Cloud Functions en procédant comme suit :

    1. Accéder aux API et services
    2. Vérifiez si les API Cloud Functions et Stackdriver Logging apparaissent dans la liste des API et services activés.
    3. Si elles n'apparaissent pas, cliquez sur Ajouter des API et des services.
    4. Recherchez les API et activez-les, si nécessaire.

Exporter des entrées du journal de surveillance de l'intégrité vers un sujet Pub/Sub

À l'aide de Stackdriver Logging, exportez toutes les entrées du journal de surveillance de l'intégrité générées par vos instances de VM protégées vers un sujet Pub/Sub. Ce sujet servira de source de données à un déclencheur Cloud Functions afin d'automatiser les réponses aux événements de surveillance de l'intégrité.

  1. Accéder à CloudLogging
  2. Cliquez sur la flèche du menu déroulant à droite de Filtrer par libellé ou texte recherché, puis sur Convertir en filtre avancé.
  3. Saisissez le filtre avancé suivant :

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

    en remplaçant YOUR_PROJECT_ID par l'ID de votre projet. Veuillez noter qu'il existe deux espaces après logName:.

  4. Cliquez sur Envoyer le filtre.

  5. Cliquez sur Créer une exportation.

  6. Dans le champ Nom du récepteur, saisissez integrity-monitoring.

  7. Dans la section Service du récepteur, sélectionnez Cloud Pub/Sub.

  8. Cliquez sur la flèche du menu déroulant à droite de Destination du récepteur, puis sur Créer un sujet Cloud Pub/Sub.

  9. Dans le champ Nom, saisissez integrity-monitoring, puis cliquez sur Créer.

  10. Cliquez sur Créer un récepteur.

Créer un déclencheur Cloud Functions afin de traiter les échecs de validation d'intégrité

Créez un déclencheur Cloud Functions qui lit les données du sujet Pub/Sub et qui arrête toute instance de VM protégée dont la validation d'intégrité échoue.

  1. Le code suivant définit le déclencheur Cloud Functions. Copiez-le dans un fichier nommé 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. Créez un fichier nommé requirements.txt au même emplacement que main.py et copiez-le dans les dépendances suivantes :

    google-api-python-client==1.6.6
    google-auth==1.4.1
    google-auth-httplib2==0.0.3
    
  3. Ouvrez une fenêtre de terminal et accédez au répertoire contenant main.py et requirements.txt.

  4. Exécutez la commande gcloud beta functions deploy pour déployer le déclencheur :

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

    en remplaçant YOUR_PROJECT_ID par l'ID de votre projet.

Créer une base de données de bonnes mesures connues pour la règle de référence

Créez une base de données Firestore afin de fournir une source de bonnes mesures connues à la règle d'intégrité de référence. Vous devez ajouter manuellement des mesures à la règle de référence afin de maintenir cette base de données à jour.

  1. Accéder à la page Instances de VM
  2. Cliquez sur l'ID d'instance de VM protégée pour ouvrir la page Informations sur l'instance de VM.
  3. Sous Journaux, cliquez sur Stackdriver Logging.
  4. Recherchez l'entrée de journal lateBootReportEvent la plus récente.
  5. Développez l'entrée de journal > jsonPayload > lateBootReportEvent > policyMeasurements.
  6. Notez les valeurs des éléments contenus dans lateBootReportEvent > policyMeasurements.
  7. Accéder à la console Firestore
  8. Sélectionnez Start Collection (Commencer une collection).
  9. Dans le champ ID de collection, saisissez known_good_measurements.
  10. Dans le champ ID du document, saisissez baseline1.
  11. Dans Nom du champ, saisissez la valeur de champ associée à pcrNum à partir de l'élément 0 dans lateBootReportEvent > policyMeasurements.
  12. Dans la section Type de champ, sélectionnez mappage.
  13. Ajoutez trois champs de type chaîne au champ de mappage et appelez-les respectivement hashAlgo, pcrNum et value. Attribuez-leur les valeurs des champs de l'élément 0 dans lateBootReportEvent > policyMeasurements.
  14. Créez d'autres champs de mappage, un pour chaque élément supplémentaire dans lateBootReportEvent > policyMeasurements. Attribuez-leur les mêmes sous-champs que le premier champ de mappage. Les valeurs de ces sous-champs doivent correspondre à celles de chacun des éléments supplémentaires.

    Par exemple, si vous utilisez une VM Linux, la collection doit ressembler à l'exemple ci-dessous une fois l'opération terminée :

    Une base de données Firestore affichant une collection known_good_measurements terminée pour Linux.

    Si vous utilisez une VM Windows, vous verrez davantage de mesures. Par conséquent, la collection doit ressembler à ce qui suit :

    Une base de données Firestore affichant une collection known_good_measurements terminée pour Windows.

Mettre à jour le déclencheur Cloud Functions pour l'apprentissage de bonnes mesures connues pour la règle de référence

  1. Le code suivant crée un déclencheur Cloud Functions qui permet aux instances de VM protégées dont la validation d'intégrité échoue d'apprendre la nouvelle règle de référence, si elle se trouve dans la base de données contenant les bonnes mesures connues, ou qui les arrête si ce n'est pas le cas. Copiez ce code et utilisez-le pour remplacer le code existant dans 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. Copiez les dépendances suivantes et utilisez-les pour remplacer le code existant dans 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. Ouvrez une fenêtre de terminal et accédez au répertoire contenant main.py et requirements.txt.

  4. Exécutez la commande gcloud beta functions deploy pour déployer le déclencheur :

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

    en remplaçant YOUR_PROJECT_ID par l'ID de votre projet.

  5. Supprimez manuellement la fonction shutdown_vm précédente dans la console des fonctions cloud.

  6. Accéder à Cloud Functions

  7. Sélectionnez la fonction shutdown_vm, puis cliquez sur Supprimer.

Vérifier les réponses automatiques aux échecs de validation d'intégrité

  1. Tout d'abord, vérifiez si vous disposez d'une instance en cours d'exécution avec le paramètre Démarrage sécurisé activé en tant qu'option de VM protégée. Si ce n'est pas le cas, vous pouvez créer une instance avec une image de VM protégée (Ubuntu 18.04LTS) et activer l'option Démarrage sécurisé. Quelques centimes peuvent vous être facturés pour l'instance. Cette étape peut durer une heure.
  2. Supposons maintenant que vous souhaitiez mettre à jour le noyau manuellement.
  3. Connectez-vous à l'instance en SSH et utilisez la commande suivante pour vérifier le noyau actuel.

    uname -sr
    

    Vous devriez voir quelque chose semblable à Linux 4.15.0-1028-gcp.

  4. Téléchargez un noyau générique à l'adresse https://kernel.ubuntu.com/~kernel-ppa/mainline/

  5. Utilisez la commande pour installer.

    sudo dpkg -i *.deb
    
  6. Redémarrez la VM.

  7. Vous remarquerez que la VM ne démarre pas (impossible de se connecter en SSH à la machine). C'est ce que nous attendons, car la signature du nouveau noyau ne figure pas dans notre liste blanche Démarrage sécurisé. Cela montre également comment le Démarrage sécurisé peut empêcher une modification non autorisée/malveillante du noyau.

  8. Comme nous savons que la mise à jour du noyau n'est pas malveillante et que vous en êtes l'auteur, nous pouvons désactiver le Démarrage sécurisé pour démarrer le nouveau noyau.

  9. Arrêtez la VM et décochez la case Démarrage sécurisé, puis redémarrez la VM.

  10. Le démarrage de la machine devrait à nouveau échouer. Cette fois, la machine est automatiquement arrêtée par la fonction cloud créée, car l'option Démarrage sécurisé a été modifiée (également à cause de l'image du nouveau noyau). Cela a provoqué une mesure différente de la valeur de référence. (Nous pouvons vérifier cela dans le journal Stackdriver de la fonction cloud).

  11. Comme nous savons qu'il ne s'agit pas d'une modification malveillante et que nous connaissons la cause racine, nous pouvons ajouter la mesure actuelle dans lateBootReportEvent au tableau Firebase de bonnes mesures connues. (N'oubliez pas que deux éléments sont en cours de modification : 1. l'option Démarrage sécurisé 2. l'image du noyau.)

    Suivez l'étape précédente Créer une base de données de bonnes mesures connues pour la règle de référence pour ajouter une nouvelle référence à la base de données Firestore en utilisant la mesure réelle dans le dernier lateBootReportEvent.

    Base de données Firestore affichant une nouvelle collection known_good_measurements terminée.

  12. Redémarrez maintenant la machine. Lorsque vous vérifiez le journal Stackdriver, vous voyez que lateBootReportEvent indique toujours une erreur, mais la machine devrait à présent démarrer correctement, car la fonction cloud a fait confiance et a réappris la nouvelle mesure. Nous pouvons le vérifier en examinant le journal Stackdriver de la fonction cloud.

  13. Le Démarrage sécurisé étant désactivé, nous pouvons maintenant démarrer dans le noyau. Connectez-vous à la machine en SSH et vérifiez à nouveau le noyau. La nouvelle version du noyau s'affiche.

    uname -sr
    
  14. Enfin, nettoyons les ressources et les données utilisées lors de cette étape.

  15. Arrêtez la VM si vous en avez créé une pour éviter des frais supplémentaires.

  16. Accéder à la page Instances de VM

  17. Supprimez les bonnes mesures connues ajoutées à cette étape.

  18. Accéder à la console Firestore