Como automatizar respostas para falhas de validação da integridade

Aprenda a usar um gatilho do Cloud Functions para atuar automaticamente nos eventos de monitoramento de integridade da VM protegida.

Visão geral

O monitoramento de integridade coleta medições de instâncias de VM protegidas e as exibe no Cloud Logging. Se as medições de integridade forem alteradas nas inicializações de uma instância de VM protegida, a validação de integridade falhará. Essa falha é captada como um evento registrado e também é gerada no Cloud Monitoring.

Às vezes, as medições de integridade da VM protegida são alteradas por um motivo legítimo. Por exemplo, uma atualização do sistema pode causar alterações esperadas no kernel do sistema operacional. Por isso, o monitoramento de integridade permite que você solicite que uma instância de VM protegida aprenda um novo valor de referência de política de integridade, no caso de uma falha de validação de integridade esperada.

Neste tutorial, primeiro você criará um sistema automatizado simples que encerra as instâncias de VM protegidas que falharem na validação de integridade:

  1. Exporte todos os eventos de monitoramento de integridade para um tópico do Pub/Sub.
  2. Crie um gatilho do Cloud Functions que use os eventos nesse tópico para identificar e encerrar instâncias de VM protegidas que falharem na validação de integridade.

Em seguida, como opção, expanda o sistema caso as instâncias de VM protegidas falhem na validação de integridade. Isso fará com que elas aprendam o novo valor de referência, se ele corresponder a uma medição válida. Caso contrário, elas serão encerradas.

  1. Crie um banco de dados do Firestore para manter um conjunto de medições válidas e conhecidas de valores de referência de integridade.
  2. Atualize o gatilho do Cloud Functions. Isso fará com que as instâncias de VM protegidas que falharem na validação de integridade aprendam o novo valor de referência, se ele estiver no banco de dados. Caso contrário, elas serão encerradas.

Se você optar por implementar a solução expandida, use-a da seguinte maneira:

  1. Sempre que houver uma atualização que possa causar uma falha de validação por um motivo legítimo, execute-a em uma única instância de VM protegida no grupo de instâncias.
  2. Use o evento de inicialização atrasada da instância de VM atualizada como uma origem para adicionar as novas medições de valores de referência da política ao banco de dados por meio da criação de um novo documento na coleção known_good_measurements. Para mais informações, consulte Como criar um banco de dados de medições válidas de valores de referência.
  3. Atualize as demais instâncias de VM protegidas. O acionador solicita que as demais instâncias aprendam o novo valor de referência porque ele pode ser verificado como válido. Consulte Como atualizar o acionador do Cloud Functions para aprender um valor de referência válido para mais informações.

Pré-requisitos

  • Usar um projeto que tenha o Firestore no modo Native selecionado como o serviço de banco de dados. Essa seleção é feita quando você cria o projeto, e não pode ser alterada. Se o projeto não usa o Firestore no modo Native, você verá a mensagem “Este projeto usa outro serviço de banco de dados” ao abrir o console do Firestore.
  • Ter uma instância da VM protegida do Compute Engine nesse projeto para ser a origem das medições de valores de referência de integridade. É preciso que a instância da VM protegida tenha sido reiniciada pelo menos uma vez.
  • Ter a ferramenta de linha de comando gcloud instalada.
  • Ativar as APIs do Cloud Logging e do Cloud Functions seguindo estes passos:

    1. Acesse APIs e serviços.
    2. Veja se a API do Cloud Functions e a API do Stackdriver Logging aparecem na lista APIs e serviços ativados.
    3. Se alguma das APIs não aparecer, clique em Adicionar APIs e serviços.
    4. Pesquise e ative as APIs, conforme necessário.

Como exportar entradas de registro de monitoramento de integridade para um tópico do Pub/Sub

Use o Logging para exportar todas as entradas de registro de monitoramento de integridade geradas por instâncias de VM protegidas para um tópico do Pub/Sub. Use esse tópico como uma fonte de dados para um gatilho do Cloud Functions para automatizar respostas a eventos de monitoramento de integridade.

  1. Acessar o Cloud Logging
  2. Clique na seta suspensa à direita de Filtrar por rótulo ou pesquisa de texto e clique em Converter para filtro avançado.
  3. Digite o seguinte filtro avançado:

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

    substituindo YOUR_PROJECT_ID pelo ID do projeto. Observe que há dois espaços após logName:.

  4. Clique em Enviar filtro.

  5. Clique em Criar exportação.

  6. Para Nome do coletor, digite integrity-monitoring.

  7. Para Serviço de coletor, selecione Cloud Pub/Sub.

  8. Clique na seta suspensa à direita do Destino do coletor e depois clique em Criar novo tópico do Cloud Pub/Sub.

  9. Para Nome, digite integrity-monitoring e depois clique em Criar.

  10. Clique em Criar coletor.

Como criar um gatilho do Cloud Functions para responder a falhas de integridade

Crie um gatilho do Cloud Functions que leia os dados no tópico do Pub/Sub e interrompa qualquer instância de VM protegida com falha na validação de integridade.

  1. O código a seguir define o gatilho do Cloud Functions. Copie-o para um arquivo chamado 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. No mesmo local que main.py, crie um arquivo chamado requirements.txt e copie as seguintes dependências:

    google-api-python-client==1.6.6
    google-auth==1.4.1
    google-auth-httplib2==0.0.3
    
  3. Abra uma janela de terminal e navegue até o diretório que contém main.py e requirements.txt.

  4. Execute o comando gcloud beta functions deploy para implantar o gatilho:

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

    substituindo YOUR_PROJECT_ID pelo ID do projeto.

Como criar um banco de dados de medições válidas de valores de referência

Crie um banco de dados do Firestore para fornecer uma fonte de medições de valores de referência de políticas de integridade válidos. É preciso adicionar manualmente as medições de valores de referência para manter esse banco de dados atualizado.

  1. Acesse a página "Instâncias de VM".
  2. Clique no código da instância da VM protegida para abrir a página Detalhes da instância de VM.
  3. Em Registros, clique em Stackdriver Logging.
  4. Encontre a entrada de registro lateBootReportEvent mais recente.
  5. Expanda a entrada de registro > jsonPayload > lateBootReportEvent > policyMeasurements.
  6. Observe os valores dos elementos contidos em lateBootReportEvent > policyMeasurements.
  7. Acesse o console do Firestore.
  8. Selecione Iniciar coleção.
  9. Para Código da coleção, digite known_good_measurements.
  10. Para o ID do documento, digite baseline1.
  11. Para o Nome do campo, digite o valor do campo pcrNum do elemento 0 em lateBootReportEvent > policyMeasurements.
  12. Para o Tipo de campo, selecione o mapa.
  13. Adicione três campos de string ao campo do mapa, denominados hashAlgo, pcrNum e value, respectivamente. Transforme os valores desses campos nos valores dos campos 0 do elemento em lateBootReportEvent > policyMeasurements.
  14. Crie mais campos de mapa, um para cada novo elemento em lateBootReportEvent > policyMeasurements. Dê a eles os mesmos subcampos que o primeiro campo do mapa. Os valores para esses subcampos devem ser associados aos de cada um dos outros elementos.

    Por exemplo, se você estiver usando uma VM do Linux, a coleção será parecida com a seguinte quando você terminar:

    Um banco de dados do Firestore mostrando uma coleção known_good_measurements completa para Linux.

    Se você estiver usando uma VM do Windows, verá mais medidas. Assim, a coleção será semelhante a esta:

    Um banco de dados do Firestore mostrando uma coleção known_good_muraces completa para Windows.

Como atualizar o gatilho do Cloud Functions para aprender um valor de referência válido

  1. O código a seguir cria um gatilho do Cloud Functions que faz qualquer instância de VM protegida que falhar na validação de integridade aprender o novo valor de referência, se ele estiver no banco de dados de medições válidas. Caso contrário, ela será encerrada. Copie esse código e use-o para substituir o código atual em 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. Copie as seguintes dependências e use-as para substituir o código atual em 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. Abra uma janela de terminal e navegue até o diretório que contém main.py e requirements.txt.

  4. Execute o comando gcloud beta functions deploy para implantar o gatilho:

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

    substituindo YOUR_PROJECT_ID pelo ID do projeto.

  5. Exclua manualmente a função shutdown_vm anterior no console da função do Cloud.

  6. Acesse o Cloud Functions.

  7. Selecione a função shutdown_vm e clique em excluir.

Verificar as respostas automáticas em relação às falhas na validação da integridade

  1. Primeiro, verifique se você tem uma instância em execução com Inicialização segura ativada como uma opção de VM protegida. Caso contrário, crie uma nova instância com imagem de VM protegida (Ubuntu 18.04LTS) e ative a opção Inicialização segura. Alguns centavos podem ser cobrados pela instância (é possível concluir esta etapa em uma hora).
  2. Agora, suponha que, por algum motivo, você queira atualizar o kernel manualmente.
  3. Conecte o SSH à instância e use o seguinte comando para verificar o kernel atual.

    uname -sr
    

    Você verá algo como Linux 4.15.0-1028-gcp.

  4. Faça o download de um kernel genérico em https://kernel.ubuntu.com/~kernel-ppa/mainline/

  5. Use o comando para instalar.

    sudo dpkg -i *.deb
    
  6. Reinicialize a VM.

  7. Você verá que a VM não está inicializando já que o SSH não consegue se conectar à máquina. Isso é o que esperamos porque a assinatura do novo kernel não está na lista de permissões da Inicialização Segura. Isso também demonstra como a Inicialização segura pode impedir uma modificação mal-intencionada ou desautorizada do kernel.

  8. Mas como sabemos que, desta vez, o upgrade do kernel não é malicioso e é feito por nós mesmos, podemos desativar a Inicialização segura para inicializar o novo kernel.

  9. Encerre a VM, desmarque a opção Inicialização Segura e, em seguida, reinicie a VM.

  10. A inicialização da máquina falhará novamente. Mas, dessa vez, ela está sendo encerrada automaticamente porque a função do Cloud criada como opção de Inicialização segura foi mudada (também por causa da nova imagem do kernel), e isso fez com que a medição fosse diferente do valor de referência. Podemos verificar isso no registro do Stackdriver da função do Cloud.

  11. Como sabemos que isso não é uma modificação mal-intencionada e conhecemos a causa, podemos adicionar a medida atual em lateBootReportEvent à tabela de medições válidas do Firebase. Lembre-se de que há duas coisas sendo alteradas: 1. A opção de Inicialização segura 2. A imagem do kernel.

    Siga a etapa anterior Como criar um banco de dados de medições válidas de valores de referência para anexar um novo valor de referência ao banco de dados do Firestore usando a medida real no lateBootReportEvent mais recente.

    Um banco de dados do Firestore mostrando uma nova coleção known_good_mensurements completa.

  12. Agora, reinicialize a máquina. Ao verificar o registro do Stackdriver, você verá que lateBootReportEvent ainda está sendo exibido como "false", mas a máquina iniciará com êxito, porque a função do Cloud confiou na nova medição e a reaprendeu. Isso pode ser verificado no Stackdriver da função do Cloud.

  13. Com a Inicialização segura desativada, agora podemos inicializar no kernel. Conecte o SSH à máquina e verifique o kernel novamente. Você verá a nova versão dele.

    uname -sr
    
  14. Por fim, vamos limpar os recursos e os dados usados nessa etapa.

  15. Se você criou uma VM para essa etapa, encerre-a para evitar mais cobranças.

  16. Acesse a página "Instâncias de VM".

  17. Remova as medições válidas adicionadas nessa etapa.

  18. Acesse o console do Firestore.