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

איך משתמשים בטריגר של פונקציות 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 בפרויקט הזה, שתשמש כמקור למדידות של בסיס התקינות. צריך להפעיל מחדש את מופע מכונה וירטואלית מוגנת לפחות פעם אחת.
  • התקנת כלי שורת הפקודה gcloud.
  • כדי להפעיל את Cloud Logging ואת Cloud Run Functions API, פועלים לפי השלבים הבאים:

    1. במסוף Google Cloud , נכנסים לדף 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

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

Logs Explorer

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

    כניסה ל-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 במסוף Google Cloud .

    כניסה ל-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 במסוף Google Cloud .

    לדף VM instances

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

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

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

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

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

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

    כניסה למסוף Firestore

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

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

  10. בשדה Document ID (מזהה המסמך), מקלידים 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 במסוף Google Cloud .

    כניסה לדף Cloud Functions

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

אימות התשובות האוטומטיות לבעיות באימות התקינות

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

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

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

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

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

    לדף VM instances

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

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

    כניסה לדף Firestore