整合性検証の失敗に対するレスポンスの自動化

Cloud Functions のトリガーを使用して Shielded VM 整合性モニタリング イベントに自動的に対処する方法について説明します。

概要

整合性モニタリングは Shielded VM インスタンスから測定値を収集し、それらを Stackdriver Logging で明らかにします。整合性測定値が Shielded VM インスタンスのブート内で変わると、整合性検証は失敗します。この失敗は記録されたイベントとして取り込まれます。これは Stackdriver Monitoring でも発生します。

正当な理由で Shielded VM の整合性の測定値が変わることがあります。たとえば、システムの更新によって、オペレーティング システムのカーネルに予想される変化が生じた場合などです。この変化のため、整合性検証が予想どおりに失敗した場合、整合性モニタリングによって Shielded VM インスタンスが新しい整合性ポリシーのベースラインを習得する必要があります。

このチュートリアルでは、まず、次の手順で、整合性検証に失敗した Shielded VM インスタンスをシャットダウンする単純な自動システムを作成します。

  1. すべての整合性モニタリング イベントを Pub/Sub トピックにエクスポートします。
  2. 整合性検証に失敗した Shielded VM インスタンスを識別してシャットダウンするため、そのトピックのイベントを使用する Cloud Functions のトリガーを作成します。

次に、必要に応じてシステムを拡張し、整合性検証に失敗した Shielded VM インスタンスが既知の正常な測定値と一致する場合は新しいベースラインを習得するか、そうでない場合はシャットダウンするように指示します。

  1. Firestore データベースを作成して、既存の正常な整合性ベースライン測定値を維持します。
  2. Cloud Functions のトリガーを更新して、整合性検証に失敗した Shielded VM インスタンスがデータベース内にある場合は、インスタンスに新しいベースラインを習得するように指示し、それ以外の場合はシャットダウンするように指示します。

拡張されたソリューションを実装する場合は、次の方法で行ってください。

  1. 正当な理由で検証が失敗すると予想される更新があるたびに、インスタンス グループ内の単一の Shielded VM インスタンスでその更新を実行します。
  2. 更新された VM インスタンスからの後期ブートイベントをソースとして使用し、known_good_measurements コレクションに新しいドキュメントを作成して、新しいポリシー ベースライン測定値をデータベースに追加します。詳しくは、既存の正常なベースライン測定値のデータベースの作成をご覧ください。
  3. 残りの Shielded VM インスタンスを更新します。トリガーによって、残りのインスタンスに新しいベースラインを習得することを求められます。既存の正常なベースラインとして検証される可能性があるためです。詳細は、Cloud Functions トリガーを更新して既知の適切なベースラインを習得するをご覧ください。

前提条件

  • ネイティブ モードの Firestore をデータベース サービスとして選択しているプロジェクトを使用します。選択はプロジェクトを作成するときに実施します。これは変更できません。プロジェクトで Firestore をネイティブ モードで使用していない場合、Firestore コンソールを開くと「このプロジェクトは別のデータベース サービスを使用しています」というメッセージが表示されます。
  • そのプロジェクトに Compute Engine Shielded VM インスタンスを配置して、整合性ベースライン測定値のソースとして機能させます。Shielded VM インスタンスは少なくとも 1 回は再起動される必要があります。
  • gcloud コマンドライン ツールがインストール済み
  • 次の手順に従って、Stackdriver Logging API と Cloud Functions API を有効にします。

    1. [API とサービス] に移動
    2. Cloud Functions API と Stackdriver Logging API が [有効化された API とサービス] リストに表示されているか確認します。
    3. どちらの API も表示されていない場合は、[API とサービスを追加] をクリックします。
    4. 必要に応じて API を検索して有効にします。

Pub/Sub トピックに整合性モニタリング エントリをエクスポートする

ログを使用して、Shielded VM インスタンスによって生成されたすべての整合性モニタリング ログエントリを Pub/Sub トピックにエクスポートします。このトピックを Cloud Functions のトリガーのデータソースとして使用して、整合性モニタリング イベントへのレスポンスを自動化します。

  1. Stackdriver Logging に移動
  2. [ラベルまたはテキスト検索でフィルタ] の右側にあるプルダウン矢印をクリックし、続いて [高度なフィルタに変換] をクリックします。
  3. 次の高度なフィルタを入力します。

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

    YOUR_PROJECT_ID はプロジェクトの ID に置き換えます。logName: の後にスペースが 2 つあることに注意してください。

  4. [フィルタを送信] をクリックします。

  5. [エクスポートを作成] をクリックします。

  6. [シンク名] に「integrity-monitoring」と入力します。

  7. [シンクサービス] で [Cloud Pub/Sub] を選択します。

  8. [シンクのエクスポート先] の右側にあるプルダウン矢印をクリックし、続いて [新しいトピックを作成する(Cloud Pub/Sub)] をクリックします。

  9. [名前] に「integrity-monitoring」と入力し、続いて [作成] をクリックします。

  10. [シンクを作成] をクリックします。

整合性の失敗に対応するための Cloud Functions のトリガーを作成する

Pub/Sub トピックのデータを読み取り、整合性検証に失敗した Shielded VM インスタンスを停止する Cloud Functions トリガーを作成します。

  1. 次のコードは Cloud Functions のトリガーを定義します。このコードを 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. main.py と同じ場所で、以下の依存関係において、requirements.txt という名前のファイルを作成してコピーします。

    google-api-python-client==1.6.6
    google-auth==1.4.1
    google-auth-httplib2==0.0.3
    
  3. ターミナル ウィンドウを開き、main.pyrequirements.txt を含むディレクトリに移動します。

  4. gcloud beta functions deploy コマンドを実行して次のトリガーをデプロイします。

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

    YOUR_PROJECT_ID はプロジェクトの ID に置き換えます。

既存の正常なベースライン測定値のデータベースを作成する

Firestore データベースを作成して、既存の正常な整合性ポリシーのベースライン測定値のソースを提供します。このデータベースを最新の状態に保つには、ベースライン測定値を手動で追加する必要があります。

  1. [VM インスタンス] ページに移動
  2. Shielded VM インスタンス ID をクリックして、[VM インスタンスの詳細] ページを開きます。
  3. [ログ] で、[Stackdriver Logging] をクリックします。
  4. 最新の lateBootReportEvent ログエントリを見つけます。
  5. ログエントリ > jsonPayload > lateBootReportEvent > policyMeasurements を展開します。
  6. lateBootReportEvent > policyMeasurements に含まれる要素の値に注意してください。
  7. Firestore コンソールに移動
  8. [コレクションを開始] を選択します。
  9. [コレクション ID] に「known_good_measurements」と入力します。
  10. [ドキュメント ID] に「baseline1」と入力します。
  11. [フィールド名] に、lateBootReportEvent > policyMeasurements の要素 0 に含まれる [pcrNum] フィールドの値を入力します。
  12. [フィールド タイプ] で [マップ] を選択します。
  13. hashAlgo、pcrNum、value という名前の 3 つの文字列フィールドを [マップ] フィールドに追加します。この値を lateBootReportEvent > policyMeasurements の要素 0 フィールドの値にします。
  14. さらにマップ フィールドを作成(lateBootReportEvent > policyMeasurements の要素ごとに 1 つ)します。作成したマップ フィールドに、最初のマップ フィールドと同じサブフィールドを指定します。このサブフィールドの値は、追加の要素ごとの値にマッピングされます。

    たとえば、Linux VM を使用している場合、作成が完了したコレクションは次のようになります。

    Linux 用の known_good_measurements コレクションを示す Firestore データベース。

    Windows VM を使用している場合は、より多くの測定値、すなわち次のようなコレクションが表示されます。

    Windows 用の known_good_measurements コレクションを示す Firestore データベース。

Cloud Functions トリガーを更新して既知の適切な基準値を学習する

  1. 次のコードにより、既知の正常な測定値のデータベースにある場合は、整合性検証に失敗したすべての Shielded VM インスタンスに新しいベースラインを習得させ、それ以外の場合はシャットダウンする Cloud Functions のトリガーが作成されます。このコードをコピーして、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. 次の依存関係をコピーして、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. ターミナル ウィンドウを開き、main.pyrequirements.txt を含むディレクトリに移動します。

  4. gcloud beta functions deploy コマンドを実行して次のトリガーをデプロイします。

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

    YOUR_PROJECT_ID はプロジェクトの ID に置き換えます。

  5. Cloud 関数のコンソール上で、旧版の shutdown_vm 関数を手動で削除します。

  6. Cloud Functions に移動

  7. shutdown_vm 関数を選択し、delete をクリックします。

整合性検証の失敗に対する自動応答を確認する

  1. まず、 が有効になっている実行中のインスタンスの Shielded VM オプションが有効になっているかどうかを確認します。有効になっていない場合は、Shielded VM イメージで新しいインスタンスを作成して(Ubuntu 18.04LTS)、セキュアブート オプションを有効にします。インスタンスに対して数セントが請求される場合があります。この処理は 1 時間以内に完了します。
  2. ここで、なんらかの理由により、手動でカーネルをアップグレードするとします。
  3. インスタンスに SSH で接続し、次のコマンドを使用して現在のカーネルを確認します。

    uname -sr
    

    Linux 4.15.0-1028-gcp などのコードが表示されます。

  4. https://kernel.ubuntu.com/~kernel-ppa/mainline/ から汎用カーネルをダウンロードします。

  5. ダウンロードしたコマンドを使用してインストールします。

    sudo dpkg -i *.deb
    
  6. VM を再起動します。

  7. マシンに SSH でログインできないため、VM が起動していないことがわかります。新しいカーネルの署名がセキュアブート ホワイトリストに含まれていないため、これは想定どおりの結果です。またこれによって、セキュアブートが承認されていない不正なカーネルによる変更を確かに防止することもわかります。

  8. しかし、今回は悪意のある行為によるカーネルのアップグレードではないので、セキュアブートを無効にして新しいカーネルを起動します。

  9. VM をシャットダウンし、セキュアブート オプションの選択を解除した後、VM を再起動します。

  10. マシンの起動が再び失敗します。しかし今回は、セキュアブート オプションが変更されたのと、新しいカーネル イメージが追加されたため、Cloud 関数によって自動的にシャットダウンされ、基準値とは異なる測定結果となります。(上記の結果は、Cloud 関数の Stackdriver ログで確認できます。)

  11. 悪意ある変更ではなく根本原因もわかっているため、lateBootReportEvent の測定結果を既知の正常な測定値の Firebase テーブルに追加できます(2 つの変更点があることに注意してください: (1)セキュアブート オプション(2)カーネル イメージ)。

    前の手順に沿って実行します。既知の正常な測定値のデータベースを作成し、最新の lateBootReportEvent にある実測値を使用して、Firestore データベースに新しい基準値を追加します。

    新しい更新済みの known_good_measurements コレクションを示す Firestore データベース。

  12. マシンを再起動します。[Stackdriver ログ] チェックボックスをオンにしても、lateBootReportEvent は false のままです。しかし、Cloud 関数が新しい測定値を信頼して再学習したため、マシンは正常に起動できるはずです。このことは、Cloud 関数の Stackdriver を確認することで確認できます。

  13. 次に、セキュアブートを無効にすると、カーネルにブートできるようになります。マシンに SSH でログインし、カーネルをもう一度確認すると、新しいカーネル バージョンが表示されます。

    uname -sr
    
  14. 最後に、このステップで使用したリソースとデータをクリーンアップします。

  15. このステップで作成をした場合は、追加の請求を回避するために、VM をシャットダウンします。

  16. [VM インスタンス] ページに移動

  17. このステップで追加した既知の正常な測定値を削除します。

  18. Firestore コンソールに移動