אוטומציה של תגובות לכשלים באימות התקינות

במאמר הזה מוסבר איך להשתמש בטריגר של פונקציות Cloud Run כדי לבצע פעולות אוטומטיות על אירועים של מעקב אחר תקינות של מכונות וירטואליות מוגנות.

סקירה כללית

ניטור התקינות אוסף מדידות ממכונות וירטואליות מוגנות ומציג אותן ב-Cloud Logging. אם מדידות התקינות משתנות בין אתחולים של מופע של מכונה וירטואלית מוגנת, אימות התקינות נכשל. הכשל הזה מתועד כאירוע ביומן, ומוצג גם ב-Cloud Monitoring.

לפעמים, מדידות השלמות של מכונות וירטואליות מוגנות משתנות מסיבה לגיטימית. לדוגמה, עדכון מערכת עשוי לגרום לשינויים צפויים בקרנל של מערכת ההפעלה. לכן, ניטור התקינות מאפשר לכם להנחות מכונה וירטואלית מוגנת ללמוד בסיס חדש של מדיניות התקינות במקרה של כשל צפוי באימות התקינות.

במדריך הזה, קודם ניצור מערכת אוטומטית פשוטה שמשביתה מכונות וירטואליות מוגנות שנכשלות באימות שלמות:

  1. ייצוא כל האירועים של מעקב אחר תקינות לנושא Pub/Sub.
  2. יוצרים טריגר של פונקציות Cloud Run שמשתמש באירועים בנושא הזה כדי לזהות ולכבות מכונות וירטואליות מוגנות שנכשלות באימות התקינות.

אחר כך, אפשר להרחיב את המערכת כך שהיא תציג הנחיות למכונות וירטואליות מוגנות שלא עוברות אימות תקינות, כדי ללמוד את בסיס ההשוואה החדש אם הוא תואם למדידה טובה ידועה, או כדי לכבות אותן אם לא.

  1. יוצרים מסד נתונים של Firestore כדי לשמור קבוצה של מדידות בסיסיות שלמות וטובות.
  2. מעדכנים את הטריגר של פונקציות Cloud Run כך שאם מכונות וירטואליות מוגנות לא עוברות אימות של תקינות, המערכת תלמד את נתוני הבסיס החדשים אם הם נמצאים במסד הנתונים, או תכבה את המכונות.

אם בחרתם להטמיע את הפתרון המורחב, כך משתמשים בו:

  1. בכל פעם שיש עדכון שצפוי לגרום לכשל באימות מסיבה מוצדקת, מריצים את העדכון הזה במופע יחיד של מכונה וירטואלית מוגנת בקבוצת המופעים.
  2. משתמשים באירוע האתחול המאוחר מהמכונה הווירטואלית המעודכנת כמקור, ומוסיפים את המדידות החדשות של בסיס המדיניות לנתונים על ידי יצירת מסמך חדש באוסף known_good_measurements. מידע נוסף זמין במאמר בנושא יצירת מסד נתונים של מדידות בסיסיות תקינות.
  3. מעדכנים את שאר מכונות ה-VM המוגנות. הטריגר גורם למופעים הנותרים ללמוד את נקודת הבסיס החדשה, כי אפשר לאמת אותה כטובה. מידע נוסף מופיע במאמר בנושא עדכון הטריגר של פונקציות Cloud Run כדי ללמוד את בסיס הנתונים הטוב הידוע.

דרישות מוקדמות

  • משתמשים בפרויקט שבו נבחר Firestore במצב Native כשירות מסד הנתונים. אתם בוחרים את האפשרות הזו כשאתם יוצרים את הפרויקט, ואי אפשר לשנות אותה. אם בפרויקט שלכם לא נעשה שימוש ב-Firestore במצב Native, תוצג ההודעה 'This project uses another database service' (הפרויקט הזה משתמש בשירות אחר של מסד נתונים) כשפותחים את מסוף Firestore.
  • מכונה וירטואלית מוגנת של Compute Engine בפרויקט הזה, שתשמש כמקור למדידות בסיסיות של תקינות. צריך להפעיל מחדש את מופע ה-VM המוגן לפחות פעם אחת.
  • התקנת כלי שורת הפקודה gcloud.
  • כדי להפעיל את ממשקי ה-API של Cloud Logging ו-Cloud Run Functions:

    1. במסוף Cloud de Confiance , נכנסים לדף APIs & Services.

      כניסה אל APIs & Services

    2. בודקים אם Cloud Functions API ו-Stackdriver Logging API מופיעים ברשימה Enabled APIs and services.

    3. אם אחד מממשקי ה-API לא מופיע, לוחצים על Add APIs and Services.

    4. מחפשים את ממשקי ה-API ומפעילים אותם לפי הצורך.

ייצוא של רשומות ביומן של מעקב אחר תקינות לנושא Pub/Sub

בעזרת Logging תוכלו לייצא את כל רשומות היומן של ניטור התקינות שנוצרו על ידי מכונות וירטואליות מוגנות לנושא Pub/Sub. אתם משתמשים בנושא הזה כמקור נתונים להפעלת פונקציות Cloud Run כדי לאוטומט את התגובות לאירועים של ניטור תקינות.

Logs Explorer

  1. נכנסים לדף Logs Explorer במסוף Cloud de Confiance .

    כניסה ל-Cloud Logging

  2. בבונה השאילתות, מזינים את הערכים הבאים.

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

  3. לוחצים על הפעלת המסנן.

  4. לוחצים על More actions ואז על Create sink.

  5. בדף יצירת יעד לניתוב יומנים:

    1. בקטע פרטי יעד, בשדה שם היעד, מזינים integrity-monitoring ולוחצים על הבא.
    2. בקטע Sink destination, מרחיבים את Sink Service ובוחרים באפשרות Cloud Pub/Sub.
    3. מרחיבים את האפשרות Select a Cloud Pub/Sub topic ובוחרים באפשרות Create a topic.
    4. בתיבת הדו-שיח Create a topic, בשדה Topic ID, מזינים integrity-monitoring ולוחצים על Create topic.
    5. לוחצים על הבא ואז על יצירת מאגר.

Logs Explorer

  1. נכנסים לדף Logs Explorer במסוף Cloud de Confiance .

    כניסה ל-Cloud Logging

  2. לוחצים על אפשרויות ואז על חזרה לגרסה הקודמת של Logs Explorer.

  3. מרחיבים את האפשרות Filter by label or text search (סינון לפי תווית או חיפוש טקסט) ואז לוחצים על Convert to advanced filter (המרה למסנן מתקדם).

  4. מזינים את המסנן המתקדם הבא:

    resource.type="gce_instance"
    AND logName:  "projects/YOUR_PROJECT_ID/logs/compute.googleapis.com/shielded_vm_integrity"
    
    שימו לב שיש שני רווחים אחרי logName:.

  5. לוחצים על שליחת המסנן.

  6. לוחצים על יצירת ייצוא.

  7. בשדה Sink Name (שם יעד), מזינים integrity-monitoring.

  8. בשדה Sink Service, בוחרים באפשרות Cloud Pub/Sub.

  9. מרחיבים את Sink Destination ולוחצים על Create new Cloud Pub/Sub topic.

  10. בשדה Name (שם), מזינים integrity-monitoring ולוחצים על Create (יצירה).

  11. לוחצים על Create Sink.

יצירת טריגר של פונקציות Cloud Run כדי להגיב לכשלים בשלמות

יוצרים טריגר של פונקציות Cloud Run שקורא את הנתונים בנושא Pub/Sub ומפסיק כל מופע של מכונה וירטואלית מוגנת שלא עובר את אימות השלמות.

  1. הקוד הבא מגדיר את הטריגר של פונקציות Cloud Run. מעתיקים אותו לקובץ בשם 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.py ואת requirements.txt.

  4. מריצים את הפקודה gcloud beta functions deploy כדי לפרוס את הטריגר:

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

יצירת מסד נתונים של מדידות בסיס טובות

יוצרים מסד נתונים של Firestore כדי לספק מקור של מדידות בסיסיות של מדיניות יושרה ידועה. כדי שהנתונים במסד הנתונים הזה יהיו עדכניים, צריך להוסיף אליו מדידות בסיסיות באופן ידני.

  1. נכנסים לדף VM instances במסוף Cloud de Confiance .

    לדף VM instances

  2. לוחצים על מזהה המופע של המכונה הווירטואלית המוגנת כדי לפתוח את הדף פרטי המכונה הווירטואלית.

  3. בקטע יומנים, לוחצים על Stackdriver Logging.

  4. מאתרים את הרשומה האחרונה ביומן lateBootReportEvent.

  5. מרחיבים את רשומת היומן > jsonPayload > lateBootReportEvent > policyMeasurements.

  6. שימו לב לערכים של הרכיבים שמופיעים ב-lateBootReportEvent > policyMeasurements.

  7. נכנסים לדף Firestore במסוף Cloud de Confiance .

    כניסה למסוף Firestore

  8. בוחרים באפשרות התחלת הגבייה.

  9. בשדה Collection ID, מזינים known_good_measurements.

  10. בשדה מזהה מסמך, מקלידים baseline1.

  11. בשדה שם השדה, מקלידים את ערך השדה pcrNum מהרכיב 0 ב-lateBootReportEvent > policyMeasurements.

  12. בקטע סוג השדה, בוחרים באפשרות מפה.

  13. מוסיפים שלושה שדות מחרוזת לשדה המפה, בשמות hashAlgo,‏ pcrNum ו-value, בהתאמה. הערכים שלהם יהיו הערכים של השדות של רכיב 0 ב-lateBootReportEvent > policyMeasurements.

  14. יוצרים עוד שדות מיפוי, אחד לכל אלמנט נוסף ב-lateBootReportEvent > policyMeasurements. נותנים להם את אותם שדות משנה כמו לשדה המיפוי הראשון. הערכים בשדות המשנה האלה צריכים להיות זהים לאלה שבכל אחד מהרכיבים הנוספים.

    לדוגמה, אם אתם משתמשים ב-VM של Linux, בסיום התהליך האוסף אמור להיראות בערך כך:

    מסד נתונים של Firestore שבו מוצגת קולקציית known_good_measurements שהושלמה עבור Linux.

    אם אתם משתמשים ב-VM של Windows, תראו יותר מדידות, ולכן האוסף ייראה בערך כך:

    מסד נתונים ב-Firestore שבו מוצג אוסף known_good_measurements שהושלם עבור Windows.

עדכון הטריגר של פונקציות Cloud Run כדי ללמוד את נקודת הבסיס הטובה הידועה

  1. הקוד הבא יוצר טריגר לפונקציות Cloud Run שגורם לכל מכונה וירטואלית מוגנת שלא עוברת אימות של תקינות ללמוד את בסיס ההשוואה החדש אם הוא נמצא במסד הנתונים של המדידות הטובות הידועות, או לכבות את עצמה. צריך להעתיק את הקוד הזה ולהשתמש בו כדי להחליף את הקוד הקיים ב-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. מעתיקים את יחסי התלות הבאים ומשתמשים בהם כדי להחליף את הקוד הקיים ב-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.py ואת requirements.txt.

  4. מריצים את הפקודה gcloud beta functions deploy כדי לפרוס את הטריגר:

    gcloud beta functions deploy relearn_if_known_good \
        --project PROJECT_ID \
        --runtime python37 \
        --trigger-resource integrity-monitoring \
        --trigger-event google.pubsub.topic.publish
  5. מוחקים ידנית את הפונקציה הקודמת shutdown_vm במסוף Cloud Functions.

  6. נכנסים לדף Cloud Functions במסוף Cloud de Confiance .

    כניסה לדף Cloud Functions

  7. בוחרים בפונקציה shutdown_vm ולוחצים על סמל המחיקה.

אימות התשובות האוטומטיות לנסיונות כושלים של אימות תקינות

  1. קודם כל, בודקים אם יש לכם מכונה וירטואלית פעילה עם הפעלה מאובטחת כמו אפשרות של מכונה וירטואלית מוגנת. אם לא, אפשר ליצור מופע חדש עם תמונת מכונה וירטואלית מוגנת (Ubuntu 18.04LTS) ולהפעיל את האפשרות Secure Boot. יכול להיות שתחויבו בכמה סנטים על המופע (השלב הזה יכול להסתיים תוך שעה).
  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). זה מה שצפוי, כי החתימה של ליבת המערכת החדשה לא נמצאת ברשימת ההיתרים של האתחול המאובטח שלנו. ההדגמה הזו מראה גם איך הפעלה מאומתת יכולה למנוע שינוי לא מורשה או זדוני בליבה.

  8. אבל מכיוון שאנחנו יודעים שהפעם שדרוג הליבה לא זדוני ושהוא אכן נעשה על ידינו, אנחנו יכולים להשבית את הפעלה מאובטחת כדי להפעיל את הליבה החדשה.

  9. משביתים את ה-VM, מבטלים את הסימון של האפשרות Secure Boot (אתחול מאובטח) ומפעילים מחדש את ה-VM.

  10. ההפעלה של המכונה אמורה להיכשל שוב. אבל הפעם היא מושבתת אוטומטית על ידי פונקציית הענן שיצרנו, כי האפשרות Secure Boot השתנתה (גם בגלל תמונת הליבה החדשה), והיא גרמה לכך שהמדידה תהיה שונה מהמדידה הראשונית. (אפשר לבדוק את זה ביומן Stackdriver של הפונקציה ב-Cloud).

  11. אנחנו יודעים שזה לא שינוי זדוני, ואנחנו יודעים מה הגורם הבסיסי, ולכן אנחנו יכולים להוסיף את המדידה הנוכחית ב-lateBootReportEvent לטבלת המדידות התקינות הידועות ב-Firebase. (חשוב לזכור שמשנים שני דברים: 1. הפעלה מאובטחת, אפשרות 2. תמונת ליבה).

    כדי להוסיף בסיס חדש לנתוני ההשוואה למסד הנתונים של Firestore, צריך לפעול לפי השלב הקודם יצירת מסד נתונים של נתוני השוואה טובים ידועים באמצעות המדידה בפועל בגרסה האחרונה של lateBootReportEvent.

    מסד נתונים ב-Firestore שבו מוצג אוסף חדש של known_good_measurements שהושלם.

  12. מפעילים מחדש את המחשב. כשבודקים את יומן Stackdriver, רואים שהערך של lateBootReportEvent עדיין false, אבל המכונה אמורה עכשיו לבצע אתחול בהצלחה, כי פונקציית הענן סמכה על המדידה החדשה ולמדה אותה. אפשר לאמת את זה על ידי בדיקה ב-Stackdriver של הפונקציה של Cloud.

  13. אחרי שמשביתים את האתחול המאובטח, אפשר לבצע אתחול לליבה. מבצעים SSH למכונה ובודקים שוב את ליבת מערכת ההפעלה. תופיע הגרסה החדשה של ליבת מערכת ההפעלה.

    uname -sr
    
  14. לבסוף, ננקה את המשאבים ואת הנתונים שבהם השתמשנו בשלב הזה.

  15. כדי להימנע מחיובים נוספים, צריך לכבות את המכונה הווירטואלית אם יצרתם אותה בשלב הזה.

  16. נכנסים לדף VM instances במסוף Cloud de Confiance .

    לדף VM instances

  17. מסירים את המידות הטובות הידועות שהוספתם בשלב הזה.

  18. נכנסים לדף Firestore במסוף Cloud de Confiance .

    כניסה לדף Firestore