פתרון בעיות בחשבונות שירות ב-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 ממשקי API כמו monitoring.googleapis.com כוללים את הערכים הבאים אם ההרשאות המתאימות לגישה לממשקי ה-API האלה חסרות בחשבון השירות של הצומת:

    • מידת החוּמרה: ERROR
    • הודעה: Permission denied (or the resource may not exist)
  • יומנים של צמתים ספציפיים חסרים ב-Cloud Logging, וביומני ה-Pod של סוכן הרישום בצמתים האלה מופיעות 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 clusters

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

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

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

    אם הערך בשדה חשבון שירות הוא 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. מוצאים את השם של חשבון השירות שבו הצמתים משתמשים:

    • עבור אשכולות במצב Autopilot, מריצים את הפקודה הבאה:
    gcloud container clusters describe CLUSTER_NAME \
        --location=LOCATION \
        --flatten=autoscaling.autoprovisioningNodePoolDefaults.serviceAccount
    
    • לגבי אשכולות במצב רגיל, מריצים את הפקודה הבאה:
    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)'
    

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

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

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

  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 (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: חסרות הרשאות עריכה בחשבון

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

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