פתרון בעיות בחשבונות שירות ב-GKE

הרשאות שמוגדרות בצורה שגויה או שחסרות בחשבונות שירות של Google Kubernetes Engine ‏ (GKE) עלולות לגרום לבעיות שונות, כמו כשל ברישום של צמתים או חוסר אפשרות של עומסי עבודה לגשת ל Google Cloud שירותים.

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

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

הקצאת התפקיד הנדרש ל-GKE לחשבונות שירות של צמתים

באשכולות GKE שפועלת בהם גרסה 1.33 של Kubernetes או גרסה מוקדמת יותר, לחשבונות השירות של IAM שמשמשים את הצמתים של GKE צריכות להיות כל ההרשאות שכלולות בתפקיד ה-IAM‏ Kubernetes Engine Default Node Service Account ‏(roles/container.defaultNodeServiceAccount). אם לחשבון שירות של צומת GKE חסרה אחת מההרשאות האלה או יותר, מערכת GKE לא יכולה לבצע משימות מערכת כמו:

יכול להיות שלחשבונות השירות של הצמתים לא יהיו הרשאות נדרשות מסוימות, מהסיבות הבאות:

  • הארגון אוכף את iam.automaticIamGrantsForDefaultServiceAccounts האילוץ של מדיניות הארגון, שמונע מ- Google Cloud להעניק באופן אוטומטי תפקידי IAM לחשבונות שירות של IAM שמוגדרים כברירת מחדל.
  • תפקיד ה-IAM שאתם מקצים לחשבונות שירות של צמתים בהתאמה אישית לא כולל את כל ההרשאות הנדרשות שכלולות בתפקיד roles/container.defaultNodeServiceAccount.

אם לחשבון השירות של הצומת חסרות ההרשאות שנדרשות ל-GKE, יכול להיות שיופיעו שגיאות והודעות כמו אלה:

  • במסוף Google Cloud , בדף Kubernetes clusters, מוצגת הודעת השגיאה Grant critical permissions בעמודה Notifications עבור אשכול ספציפי.
  • במסוף Google Cloud , בדף פרטי האשכול של אשכול ספציפי, מופיעה הודעת השגיאה הבאה:

    Grant roles/container.defaultNodeServiceAccount role to Node service account to allow for non-degraded operations.
    
  • ביומני הפעילות של האדמין ביומני הביקורת של Cloud, ל- Google Cloud APIs כמו monitoring.googleapis.com יש את הערכים הבאים אם ההרשאות המתאימות לגישה לממשקי ה-API האלה חסרות בחשבון השירות של הצומת:

    • מידת החוּמרה: ERROR
    • הודעה: Permission denied (or the resource may not exist)
  • יומנים של צמתים ספציפיים חסרים ב-Cloud Logging, וביומני ה-Pod של סוכן ה-Logging בצמתים האלה מופיעות שגיאות 401. כדי לקבל את היומנים של ה-Pod, מריצים את הפקודה הבאה:

    [[ $(kubectl logs -l k8s-app=fluentbit-gke -n kube-system -c fluentbit-gke | grep -cw "Received 401") -gt 0 ]] && echo "true" || echo "false"
    

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

כדי לפתור את הבעיה, צריך להעניק את התפקיד Kubernetes Engine Default Node Service Account ‏(roles/container.defaultNodeServiceAccount) בפרויקט לחשבון השירות שגורם לשגיאות. בוחרים באחת מהאפשרויות הבאות:

console

כדי למצוא את השם של חשבון השירות שבו משתמשים הצמתים:

  1. עוברים לדף Kubernetes clusters:

    כניסה לאשכולות של Kubernetes

  2. ברשימת האשכולות, לוחצים על שם האשכול שרוצים לבדוק.

  3. מוצאים את השם של חשבון השירות של הצומת. תצטרכו את השם הזה בהמשך.

    • עבור אשכולות במצב אוטומטי, בקטע Security, מוצאים את השדה חשבון שירות.
    • לגבי אשכולות במצב רגיל:
    1. לוחצים על הכרטיסייה Nodes (צמתים).
    2. בטבלה Node pools (מאגרי צמתים), לוחצים על שם של מאגר צמתים. נפתח הדף פרטי מאגר צמתים.
    3. בקטע Security, מוצאים את השדה Service account.

    אם הערך בשדה חשבון שירות הוא default, הצמתים משתמשים בחשבון השירות שמוגדר כברירת מחדל של Compute Engine. אם הערך בשדה הזה לא default, הצמתים משתמשים בחשבון שירות בהתאמה אישית.

כדי להעניק לחשבון השירות את התפקיד Kubernetes Engine Default Node Service Account:

  1. נכנסים לדף Welcome:

    מעבר לדף Welcome

  2. בשדה מספר הפרויקט, לוחצים על העתקה ללוח.

  3. נכנסים לדף IAM:

    כניסה לדף IAM

  4. לוחצים על Grant access.

  5. בשדה New principals, מציינים את השם של חשבון השירות של הצומת. אם הצמתים משתמשים בחשבון השירות שמוגדר כברירת מחדל של Compute Engine, צריך לציין את הערך הבא:

    PROJECT_NUMBER-compute@developer.gserviceaccount.com
    

    מחליפים את PROJECT_NUMBER במספר הפרויקט שהעתקתם.

  6. בתפריט Select a role בוחרים בתפקיד Kubernetes Engine Default Node Service Account.

  7. לוחצים על Save.

כדי לוודא שהתפקיד הוקצה:

  1. בדף IAM, לוחצים על הכרטיסייה View by roles (תצוגה לפי תפקידים).
  2. מרחיבים את הקטע Kubernetes Engine Default Node Service Account (חשבון השירות של צומת ברירת המחדל ב-Kubernetes Engine). מוצגת רשימה של חשבונות משתמשים שיש להם את התפקיד הזה.
  3. ברשימת החשבונות הראשיים, מחפשים את חשבון השירות של הצומת.

gcloud

  1. מוצאים את השם של חשבון השירות שבו הצמתים משתמשים:

    • עבור אשכולות במצב אוטומטי, מריצים את הפקודה הבאה:
    gcloud container clusters describe CLUSTER_NAME \
        --location=LOCATION \
        --flatten=autoscaling.autoprovisioningNodePoolDefaults.serviceAccount
    
    • באשכולות במצב Standard, מריצים את הפקודה הבאה:
    gcloud container clusters describe CLUSTER_NAME \
        --location=LOCATION \
        --format="table(nodePools.name,nodePools.config.serviceAccount)"
    

    אם הפלט הוא default, הצמתים משתמשים בחשבון השירות שמוגדר כברירת מחדל של Compute Engine. אם הפלט לא default, הצמתים משתמשים בחשבון שירות בהתאמה אישית.

  2. איך מוצאים את מספר הפרויקט Google Cloud :

    gcloud projects describe PROJECT_ID \
        --format="value(projectNumber)"
    

    מחליפים את PROJECT_ID במזהה הפרויקט.

    הפלט אמור להיראות כך:

    12345678901
    
  3. מקצים לחשבון השירות את התפקיד roles/container.defaultNodeServiceAccount:

    gcloud projects add-iam-policy-binding PROJECT_ID \
        --member="SERVICE_ACCOUNT_NAME" \
        --role="roles/container.defaultNodeServiceAccount"
    

    מחליפים את SERVICE_ACCOUNT_NAME בשם של חשבון השירות שמצאתם בשלב הקודם. אם הצמתים משתמשים בחשבון השירות שמוגדר כברירת מחדל של Compute Engine, צריך לציין את הערך הבא:

    serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com
    

    מחליפים את PROJECT_NUMBER במספר הפרויקט מהשלב הקודם.

  4. כדי לוודא שההרשאה ניתנה בהצלחה:

    gcloud projects get-iam-policy PROJECT_ID \
        --flatten="bindings[].members" --filter=bindings.role:roles/container.defaultNodeServiceAccount \
        --format='value(bindings.members)'
    

    הפלט הוא השם של חשבון השירות.

זיהוי אשכולות שבהם לחשבונות השירות של הצמתים חסרות הרשאות

כדי לזהות אשכולות Autopilot ו-Standard שכוללים חשבונות שירות של צמתים עם הרשאות חסרות, אפשר להשתמש בהמלצות GKE של NODE_SA_MISSING_PERMISSIONS סוג המשנה של הכלי להמלצות. הכלי Recommender מזהה רק אשכולות שנוצרו ב-1 בינואר 2024 או אחריו. כדי למצוא ולתקן את ההרשאות החסרות באמצעות שירות ההמלצות:

  1. אפשר למצוא המלצות פעילות בפרויקט עבור סוג המשנה של שירות ההמלצות NODE_SA_MISSING_PERMISSIONS:

    gcloud recommender recommendations list \
        --recommender=google.container.DiagnosisRecommender \
        --location LOCATION \
        --project PROJECT_ID \
        --format yaml \
        --filter="recommenderSubtype:NODE_SA_MISSING_PERMISSIONS"
    

    מחליפים את מה שכתוב בשדות הבאים:

    • ‫LOCATION: המיקום שבו רוצים למצוא המלצות.
    • ‫PROJECT_ID: מזהה הפרויקט ב- Google Cloud .

    הפלט דומה לפלט הבא, שמציין שלצביר יש חשבון שירות של צומת עם הרשאות חסרות:

    associatedInsights:
    # lines omitted for clarity
    recommenderSubtype: NODE_SA_MISSING_PERMISSIONS
    stateInfo:
      state: ACTIVE
    targetResources:
    - //container.googleapis.com/projects/12345678901/locations/us-central1/clusters/cluster-1
    

    יכול להיות שיחלפו עד 24 שעות עד שההמלצה תופיע. הוראות מפורטות זמינות במאמר בנושא תובנות והמלצות.

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

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

זיהוי כל חשבונות השירות של הצמתים שחסרות להם הרשאות

אתם יכולים להריץ סקריפט שמחפש במאגרי הצמתים באשכולות Standard ו-Autopilot של הפרויקט, חשבונות שירות של צמתים שאין להם את ההרשאות הנדרשות ל-GKE. הסקריפט הזה משתמש ב-CLI של gcloud ובכלי jq. כדי לראות את הסקריפט, מרחיבים את הקטע הבא:

הצגת התסריט

#!/bin/bash

# Set your project ID
project_id=PROJECT_ID
project_number=$(gcloud projects describe "$project_id" --format="value(projectNumber)")
declare -a all_service_accounts
declare -a sa_missing_permissions

# Function to check if a service account has a specific permission
# $1: project_id
# $2: service_account
# $3: permission
service_account_has_permission() {
  local project_id="$1"
  local service_account="$2"
  local permission="$3"

  local roles=$(gcloud projects get-iam-policy "$project_id" \
          --flatten="bindings[].members" \
          --format="table[no-heading](bindings.role)" \
          --filter="bindings.members:\"$service_account\"")

  for role in $roles; do
    if role_has_permission "$role" "$permission"; then
      echo "Yes" # Has permission
      return
    fi
  done

  echo "No" # Does not have permission
}

# Function to check if a role has the specific permission
# $1: role
# $2: permission
role_has_permission() {
  local role="$1"
  local permission="$2"
  gcloud iam roles describe "$role" --format="json" | \
  jq -r ".includedPermissions" | \
  grep -q "$permission"
}

# Function to add $1 into the service account array all_service_accounts
# $1: service account
add_service_account() {
  local service_account="$1"
  all_service_accounts+=( ${service_account} )
}

# Function to add service accounts into the global array all_service_accounts for a Standard GKE cluster
# $1: project_id
# $2: location
# $3: cluster_name
add_service_accounts_for_standard() {
  local project_id="$1"
  local cluster_location="$2"
  local cluster_name="$3"

  while read nodepool; do
    nodepool_name=$(echo "$nodepool" | awk '{print $1}')
    if [[ "$nodepool_name" == "" ]]; then
      # skip the empty line which is from running `gcloud container node-pools list` in GCP console
      continue
    fi
    while read nodepool_details; do
      service_account=$(echo "$nodepool_details" | awk '{print $1}')

      if [[ "$service_account" == "default" ]]; then
        service_account="${project_number}-compute@developer.gserviceaccount.com"
      fi
      if [[ -n "$service_account" ]]; then
        printf "%-60s| %-40s| %-40s| %-10s| %-20s\n" $service_account $project_id  $cluster_name $cluster_location $nodepool_name
        add_service_account "${service_account}"
      else
        echo "cannot find service account for node pool $project_id\t$cluster_name\t$cluster_location\t$nodepool_details"
      fi
    done <<< "$(gcloud container node-pools describe "$nodepool_name" --cluster "$cluster_name" --zone "$cluster_location" --project "$project_id" --format="table[no-heading](config.serviceAccount)")"
  done <<< "$(gcloud container node-pools list --cluster "$cluster_name" --zone "$cluster_location" --project "$project_id" --format="table[no-heading](name)")"

}

# Function to add service accounts into the global array all_service_accounts for an Autopilot GKE cluster
# Autopilot cluster only has one node service account.
# $1: project_id
# $2: location
# $3: cluster_name
add_service_account_for_autopilot(){
  local project_id="$1"
  local cluster_location="$2"
  local cluster_name="$3"

  while read service_account; do
      if [[ "$service_account" == "default" ]]; then
        service_account="${project_number}-compute@developer.gserviceaccount.com"
      fi
      if [[ -n "$service_account" ]]; then
        printf "%-60s| %-40s| %-40s| %-10s| %-20s\n" $service_account $project_id  $cluster_name $cluster_location $nodepool_name
        add_service_account "${service_account}"
      else
        echo "cannot find service account" for cluster  "$project_id\t$cluster_name\t$cluster_location\t"
      fi
  done <<< "$(gcloud container clusters describe "$cluster_name" --location "$cluster_location" --project "$project_id" --format="table[no-heading](autoscaling.autoprovisioningNodePoolDefaults.serviceAccount)")"
}


# Function to check whether the cluster is an Autopilot cluster or not
# $1: project_id
# $2: location
# $3: cluster_name
is_autopilot_cluster() {
  local project_id="$1"
  local cluster_location="$2"
  local cluster_name="$3"
  autopilot=$(gcloud container clusters describe "$cluster_name" --location "$cluster_location" --format="table[no-heading](autopilot.enabled)")
  echo "$autopilot"
}


echo "--- 1. List all service accounts in all GKE node pools"
printf "%-60s| %-40s| %-40s| %-10s| %-20s\n" "service_account" "project_id" "cluster_name" "cluster_location" "nodepool_name"
while read cluster; do
  cluster_name=$(echo "$cluster" | awk '{print $1}')
  cluster_location=$(echo "$cluster" | awk '{print $2}')
  # how to find a cluster is a Standard cluster or an Autopilot cluster
  autopilot=$(is_autopilot_cluster "$project_id" "$cluster_location" "$cluster_name")
  if [[ "$autopilot" == "True" ]]; then
    add_service_account_for_autopilot "$project_id" "$cluster_location"  "$cluster_name"
  else
    add_service_accounts_for_standard "$project_id" "$cluster_location"  "$cluster_name"
  fi
done <<< "$(gcloud container clusters list --project "$project_id" --format="value(name,location)")"

echo "--- 2. Check if service accounts have permissions"
unique_service_accounts=($(echo "${all_service_accounts[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '))

echo "Service accounts: ${unique_service_accounts[@]}"
printf "%-60s| %-40s| %-40s| %-20s\n" "service_account" "has_logging_permission" "has_monitoring_permission" "has_performance_hpa_metric_write_permission"
for sa in "${unique_service_accounts[@]}"; do
  logging_permission=$(service_account_has_permission "$project_id" "$sa" "logging.logEntries.create")
  time_series_create_permission=$(service_account_has_permission "$project_id" "$sa" "monitoring.timeSeries.create")
  metric_descriptors_create_permission=$(service_account_has_permission "$project_id" "$sa" "monitoring.metricDescriptors.create")
  if [[ "$time_series_create_permission" == "No" || "$metric_descriptors_create_permission" == "No" ]]; then
    monitoring_permission="No"
  else
    monitoring_permission="Yes"
  fi
  performance_hpa_metric_write_permission=$(service_account_has_permission "$project_id" "$sa" "autoscaling.sites.writeMetrics")
  printf "%-60s| %-40s| %-40s| %-20s\n" $sa $logging_permission $monitoring_permission $performance_hpa_metric_write_permission

  if [[ "$logging_permission" == "No" || "$monitoring_permission" == "No" || "$performance_hpa_metric_write_permission" == "No" ]]; then
    sa_missing_permissions+=( ${sa} )
  fi
done

echo "--- 3. List all service accounts that don't have the above permissions"
if [[ "${#sa_missing_permissions[@]}" -gt 0 ]]; then
  printf "Grant roles/container.defaultNodeServiceAccount to the following service accounts: %s\n" "${sa_missing_permissions[@]}"
else
  echo "All service accounts have the above permissions"
fi

הסקריפט הזה חל על כל אשכולות GKE בפרויקט.

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

שחזור חשבון השירות שמוגדר כברירת מחדל בפרויקט Google Cloud

יכול להיות שחשבון השירות שמוגדר כברירת מחדל ב-GKE, ‏ container-engine-robot, ינותק בטעות מפרויקט. התפקיד "סוכן שירות של Kubernetes Engine" (roles/container.serviceAgent) הוא תפקיד בניהול זהויות והרשאות גישה (IAM) שנותן לחשבון השירות את ההרשאות לנהל את המשאבים של האשכול. אם תסירו את הקישור הזה של התפקיד מחשבון השירות, חשבון השירות שמוגדר כברירת מחדל יבוטל מהפרויקט, וזה עלול למנוע מכם לפרוס אפליקציות ולבצע פעולות אחרות באשכול.

כדי לבדוק אם חשבון השירות הוסר מהפרויקט, אתם יכולים להשתמש במסוף Google Cloud או ב-Google Cloud CLI.

המסוף

gcloud

  • מריצים את הפקודה הבאה:

    gcloud projects get-iam-policy PROJECT_ID
    

    מחליפים את PROJECT_ID במזהה הפרויקט.

אם container-engine-robot לא מופיע בלוח הבקרה או בפקודה בין חשבונות השירות, התפקיד לא קשור לחשבון.

כדי לשחזר את הקישור של התפקיד Kubernetes Engine Service Agent (roles/container.serviceAgent), מריצים את הפקודות הבאות:

PROJECT_NUMBER=$(gcloud projects describe "PROJECT_ID" \
    --format 'get(projectNumber)') \
gcloud projects add-iam-policy-binding PROJECT_ID \
    --member "serviceAccount:service-${PROJECT_NUMBER}@container-engine-robot.iam.gserviceaccount.com" \
    --role roles/container.serviceAgent

מוודאים שאיגוד התפקידים שוחזר:

gcloud projects get-iam-policy PROJECT_ID

אם השם של חשבון השירות מופיע לצד התפקיד container.serviceAgent, קישור התפקיד שוחזר. לדוגמה:

- members:
  - serviceAccount:service-1234567890@container-engine-robot.iam.gserviceaccount.com
  role: roles/container.serviceAgent

הפעלת חשבון השירות שמוגדר כברירת מחדל ב-Compute Engine

חשבון השירות שמשמש למאגר הצמתים הוא בדרך כלל חשבון השירות שמוגדר כברירת מחדל ב-Compute Engine. אם חשבון השירות שמוגדר כברירת מחדל מושבת, יכול להיות שהצמתים לא יירשמו באשכול.

כדי לבדוק אם חשבון השירות מושבת בפרויקט, אפשר להשתמש במסוףGoogle Cloud או ב-CLI של gcloud.

המסוף

gcloud

  • מריצים את הפקודה הבאה:
gcloud iam service-accounts list  --filter="NAME~'compute' AND disabled=true"

אם חשבון השירות מושבת, מריצים את הפקודות הבאות כדי להפעיל אותו:

  1. מאתרים את מספר הפרויקט ב- Google Cloud :

    gcloud projects describe PROJECT_ID \
        --format="value(projectNumber)"
    

    מחליפים את PROJECT_ID במזהה הפרויקט.

    הפלט אמור להיראות כך:

    12345678901
    
  2. מפעילים את חשבון השירות:

    gcloud iam service-accounts enable PROJECT_NUMBER-compute@developer.gserviceaccount.com
    

    מחליפים את PROJECT_NUMBER במספר הפרויקט מהפלט של השלב הקודם.

מידע נוסף מופיע במאמר בנושא פתרון בעיות ברישום צמתים.

שגיאה 400/403: חסרות הרשאות עריכה בחשבון

אם חשבון השירות נמחק, יכול להיות שתופיע הודעת שגיאה על הרשאות עריכה חסרות. במאמר שגיאה 400/403: חסרות הרשאות עריכה בחשבון מוסבר איך לפתור את השגיאה הזו.

המאמרים הבאים