פודי Kubernetes כזהויות ענן: EKS IRSA מול GKE Workload Identity Federation
כיצד ServiceAccount של Kubernetes הופך לזהות ענן אמיתית ב-AWS וב-Google Cloud - שרשרת האמון של פדרציית OIDC שמאחורי EKS IRSA ו-GKE Workload Identity Federation, ממופה אחד לאחד, עם הקשיים שמפרים את זיכרון השריר שלכם.
גם AWS וגם Google Cloud פותרות את אותה בעיה באותה דרך: פוד מאמת את עצמו מול ממשקי API של הענן כעצמו, עם טוקן קצר-חיים וללא סוד מאוחסן, על ידי הפיכת ה-Kubernetes ServiceAccount שלו לזהות ענן מאוחדת (federated). ב-AWS התכונה היא IRSA (IAM Roles for Service Accounts); ב-Google Cloud זוהי Workload Identity Federation for GKE (ששמה שונה ב-2024 מ-"Workload Identity" בלבד). מתחת למכסה המנוע, שתיהן משתמשות באותו טריק של פדרציית OIDC - אך GKE מסתירה הרבה יותר מהמנגנון הפנימי, וזה משנה את האופן שבו מחברים, מנפים באגים ומתייחסים אליה.
הגרסה הקצרה, אם אתם קוראים רק פסקה אחת: האשכול חותם על טוקן קצר-חיים עבור ה-ServiceAccount של כל פוד, והענן מחליף את הטוקן הזה בפרטי גישה אמיתיים המוגדרים לזהות אחת. ב-AWS אתם רושמים IAM OIDC provider לכל אשכול, יוצרים IAM role, ומגבילים אותו באמצעות trust policy; ה-SDK בתוך הפוד קורא ל-AssumeRoleWithWebIdentity. ב-GKE אינכם רושמים כלום - לכל פרויקט יש workload identity pool אחד קבוע, מנוהל על ידי Google, בשם PROJECT_ID.svc.id.goog, כל ServiceAccount הוא אוטומטית principal בו, ושרת metadata מקומי של הצומת מבצע את החלפת הטוקנים כך שהאפליקציה שלכם פשוט משתמשת ב-Application Default Credentials כאילו היא פועלת על VM רגיל. שלושה דברים שונים מספיק כדי להפתיע אתכם: GKE דורשת שני מתגי הפעלה (אשכול וnode pool), GKE יכולה להעניק ל-Kubernetes ServiceAccount הרשאות ענן ללא אובייקט זהות ענן כלל, וכמו תמיד - משיכת תמונת הקונטיינר שלכם היא זהות שונה מזהות ה-workload שלכם.
הבעיה ששניהם פותרים
לפני פדרציה, מתן הרשאות ענן לפודים כלל אפשרויות גרועות: הטמעת מפתח סטטי בתמונה או ב-Secret (דליפות, כאב בסיבוב מפתחות), או הענקת ההרשאה לצומת כולו כך שכל פוד עליו יירש אותה (ללא בידוד, היקף רחב מדי באופן פרוע). פתרונות הביניים בצד AWS היו מיירטי IMDS של צמתים כמו kube2iam ו-kiam; בצד GCP היה זה הרכבת מפתח service-account של צומת לתוך פודים. כולם היו מסורבלים, רחבים מדי ומועדים לטעויות תזמון.
פדרציה מחליפה זאת באמון קריפטוגרפי. האשכול מקרין טוקן OIDC חתום וקצר-חיים המוגדר ל-ServiceAccount של הפוד. הענן מאמת את הטוקן הזה מול המפתחות המפורסמים של האשכול, ואם זהות הטוקן תואמת כלל שהגדרתם, הוא מחזיר פרטי גישה עבור זהות ענן אחת בדיוק. שום סוד אינו מאוחסן בשום מקום; הטוקן מסתובב אוטומטית (זמן חיים ברירת מחדל כשעה) וחסר תועלת מחוץ לאשכול.
המנגנון המשותף: פדרציית OIDC
כל הגדרת workload-identity, בכל אחד מהעננים, היא אותו משולש אמון:
- האשכול הוא OIDC issuer. הוא חושף discovery document ו-JWKS endpoint עם המפתחות הציבוריים שבהם הוא חותם על טוקני ServiceAccount. ה-issuer הזה הוא עוגן האמון.
- הענן בוטח ב-issuer הזה עבור זהות ServiceAccount ספציפית. AWS מבטאת את האמון לכל role; GCP מבטאת אותו פעם אחת לכל פרויקט, באופן קבוע, ו-Google מנהלת אותו עבורכם.
- ה-workload מחליף את הטוקן המוקרן בפרטי גישה לענן ומקבל פרטי גישה קצרי-חיים עבור זהות ענן יחידה.
כל השאר הוא עניין של שמות - וכמה משלב 2 אתם צריכים לבנות בעצמכם. כאן שני העננים נבדלים ביותר.
המיפוי אחד-לאחד
- צד Kubernetes. ב-AWS אתם מוסיפים הערה (annotate) ל-ServiceAccount עם
eks.amazonaws.com/role-arn: <role arn>. ב-GKE, במודל ה-ישיר המודרני, אינכם מוסיפים שום הערה ל-ServiceAccount כלל - אתם פשוט מפנים אליו כ-IAM principal. (מודל ההתחזות הישן יותר אכן משתמש בהערה; פירוט בהמשך.) - ה-"OIDC provider". ב-AWS זהו ה-OIDC issuer של אשכול EKS, אותו אתם רושמים פעם אחת ב-IAM כ-OIDC identity provider - לכל אשכול. ב-GKE זהו ה-workload identity pool של הפרויקט
PROJECT_ID.svc.id.goog, שנוצר אוטומטית ומנוהל על ידי Google; אינכם רושמים אותו לעולם, וכל ServiceAccount בכל אשכול בפרויקט הוא כבר principal בתוכו. - הזהות. AWS: IAM role. מודל GKE ישיר: אין אובייקט זהות ייעודי - ה-Kubernetes ServiceAccount עצמו הוא ה-principal. מודל GKE התחזות: Google service account שה-KSA מתחזה אליו.
- כלל האמון. AWS: ה-trust policy של ה-IAM role מצמיד את
sub = system:serviceaccount:<ns>:<sa>ואתaud = sts.amazonaws.com. GKE ישיר: אין מסמך אמון נפרד - אתם מעניקים role ישירות ל-principal identifier שמקודד את ה-namespace וה-ServiceAccount. GKE התחזות: אתם מעניקים ל-KSA את ה-roleroles/iam.workloadIdentityUserעל ה-Google service account. - ההרשאות. AWS: IAM policy המצורף ל-role. GKE: IAM allow-policy role bindings על משאב היעד (או הפרויקט) -
roles/storage.objectViewer,roles/secretmanager.secretAccessor, וכן הלאה. זהו הפיצול העמוק יותר: AWS מצמידה policy לזהות; GCP מעניקה role בהיקף המשאב. - ההחלפה. AWS: ה-SDK בתוך הפוד קורא ל-
sts:AssumeRoleWithWebIdentityעם הטוקן המוקרן. GKE: שרת ה-metadata של הצומת מבצע את ההחלפה באופן שקוף והאפליקציה שלכם משתמשת ב-Application Default Credentials ללא קוד החלפה כלל.
שאר הפוסט הזה סוקר כל ענן מקצה לקצה, ולאחר מכן מתעכב על ההבדלים שבפועל מבלבלים אנשים.
סקירה: AWS EKS IRSA, מקצה לקצה
החלקים, בסדר זרימת האמון:
1. ה-OIDC issuer של האשכול, בכתובת URL כמו https://oidc.eks.<region>.amazonaws.com/id/<hash>. אתם רושמים אותו פעם אחת ב-IAM כ-OIDC identity provider כך ש-IAM יבטח בטוקנים שהוא חותם עליהם.
2. ה-IAM role ו-trust policy שלו. ה-trust policy הופך את ה-role לניתן להנחה על ידי ServiceAccount אחד בלבד:
{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::<account>:oidc-provider/oidc.eks.<region>.amazonaws.com/id/<hash>" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.<region>.amazonaws.com/id/<hash>:sub": "system:serviceaccount:apps:checkout",
"oidc.eks.<region>.amazonaws.com/id/<hash>:aud": "sts.amazonaws.com"
}
}
}
הצמידו גם את :sub וגם את :aud - הצמדת ה-issuer בלבד תאפשר לכל ServiceAccount באשכול להניח את ה-role.
3. ה-IAM policy המצורף ל-role מעניק את מה שהאפליקציה צריכה (s3:GetObject על bucket אחד, וכן הלאה).
4. ה-ServiceAccount נושא הערה אחת:
apiVersion: v1
kind: ServiceAccount
metadata:
name: checkout
namespace: apps
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::<account>:role/checkout
5. ההקרנה. EKS מספקת את ה-Pod Identity Webhook המובנה באשכול. כאשר פוד משתמש ב-ServiceAccount עם ההערה, ה-webhook מזריק את AWS_ROLE_ARN ו-AWS_WEB_IDENTITY_TOKEN_FILE ומרכיב טוקן מוקרן (audience sts.amazonaws.com, מסתובב אוטומטית).
6. ההחלפה. ה-AWS SDK מבחין ב-AWS_WEB_IDENTITY_TOKEN_FILE, קורא ל-sts:AssumeRoleWithWebIdentity, ושומר במטמון את פרטי הגישה קצרי-החיים שהוחזרו. הקוד שלכם הוא פשוט boto3.client("s3").
חלופה חדשה יותר: EKS Pod Identity (2023) עושה את אותה עבודה באמצעות סוכן בתוך האשכול ו-API של אסוציאציה במקום IAM OIDC provider לכל אשכול ו-trust policy לכל role - מה ש, יש לציין, גורם לו להרגיש הרבה יותר כמו המודל המנוהל של GKE. גם IRSA וגם Pod Identity קיימים יחד בייצור; IRSA הוא זה שממופה בצורה נקייה למסגרת פדרציית OIDC, ולכן הוא זה שכדאי לזכור כאן.
סקירה: GKE Workload Identity Federation, מקצה לקצה
אותה זרימת אמון, הרבה פחות לבנות:
1. שני מתגי הפעלה. הפעילו את ה-workload identity pool על האשכול ואת שרת ה-metadata על הnode pool:
gcloud container clusters update <cluster> --workload-pool=<project-id>.svc.id.goog
gcloud container node-pools update <pool> --cluster=<cluster> --workload-metadata=GKE_METADATA
שניהם נדרשים. דגל האשכול מצרף את האשכול ל-pool של הפרויקט; דגל ה-node-pool פורס את GKE metadata server (DaemonSet על כל צומת) שיירט בקשות פרטי גישה. פוד על node pool ללא GKE_METADATA מקבל את זהות הצומת, לא את זהותו שלו - חזרה שקטה ורחבה מדי.
2. ה-pool כבר קיים. <project-id>.svc.id.goog נוצר אוטומטית עבור הפרויקט; אינכם רושמים OIDC provider לעולם, ואין מגבלת provider לכל חשבון שצריך להגיע אליה. כל ServiceAccount באשכולות הפרויקט הוא כבר principal.
3א. גישה ישירה (ברירת המחדל המודרנית). העניקו role ישירות ל-ServiceAccount, הממוען כ-IAM principal. אין Google service account, אין הערה:
gcloud storage buckets add-iam-policy-binding gs://<bucket> \
--role=roles/storage.objectViewer \
--member="principal://iam.googleapis.com/projects/<project-number>/locations/global/workloadIdentityPools/<project-id>.svc.id.goog/subject/ns/apps/sa/checkout"
נתיב ה-principal מקודד את ns/apps/sa/checkout - ה-namespace וה-ServiceAccount הם כלל האמון; אין מסמך אמון נפרד לכתוב.
3ב. התחזות (המודל הישן יותר, עדיין נדרש עבור שירותים בודדים). צרו Google service account, אפשרו ל-KSA להתחזות אליו, והוסיפו הערה ל-KSA:
gcloud iam service-accounts add-iam-policy-binding checkout@<project-id>.iam.gserviceaccount.com \
--role=roles/iam.workloadIdentityUser \
--member="serviceAccount:<project-id>.svc.id.goog[apps/checkout]"
apiVersion: v1
kind: ServiceAccount
metadata:
name: checkout
namespace: apps
annotations:
iam.gke.io/gcp-service-account: checkout@<project-id>.iam.gserviceaccount.com
כאן ה-Google service account מחזיק בהרשאות (באמצעות role bindings רגילים), וה-KSA שואל אותן.
4. ההחלפה. כאשר האפליקציה שלכם מבקשת פרטי גישה, GKE metadata server מיירט את הבקשה ל-http://metadata.google.internal, מאחזר ServiceAccount JWT מ-Kubernetes API עבור אותו פוד, מחליף אותו באמצעות Security Token Service ב-federated access token קצר-חיים (ברירת מחדל שעה, מתרענן באופן יזום לפני פקיעה), ומחזיר אותו. האפליקציה שלכם משתמשת ב-Application Default Credentials ובספריות הלקוח הסטנדרטיות של Google Cloud - היא מתנהגת בדיוק כאילו היא פועלת על GCE VM. אין webhook שמזריק משתני סביבה, אין משתנים בסגנון AZURE_*, ואין קריאת החלפה בקוד שלכם: קריאת Cloud Storage היא פשוט storage.Client() ב-Python.
ה-subject הוא עמוד התווך (בשני העננים)
הערך שחייב להתאים בדיוק, בשני העננים, הוא זהות ה-ServiceAccount:
- AWS מצמידה את ה-token subject
system:serviceaccount:<namespace>:<serviceaccount>ב-trust policy של ה-role. - GKE direct מקודדת את אותו זיווג בנתיב ה-principal
.../subject/ns/<namespace>/sa/<serviceaccount>. - GKE impersonation מקודדת אותו ב-member
serviceAccount:<project-id>.svc.id.goog[<namespace>/<serviceaccount>].
הכשל הנפוץ ביותר של "אינו מאמת כאיש" בכל אחד מהעננים הוא אי-התאמה כאן: ה-chart נפרס ל-namespace אחר, ה-ServiceAccount קיבל את שם ה-release במקום את שם האפליקציה, או שהפוד חזר ל-ServiceAccount ה-default. כאשר פדרציה נכשלת בשקט, בדקו תחילה את ה-namespace ואת שם ה-ServiceAccount.
הבדלים שבפועל יבלבלו אתכם
1. GKE רושמת את ה-"OIDC provider" עבורכם - פעם אחת, לכל פרויקט, לנצח. ב-EKS אתם רושמים IAM OIDC provider לכל אשכול, ובקנה מידה של צי (fleet scale) אתם יכולים להגיע למגבלה הרכה של 100 OIDC providers לכל חשבון AWS ולצטרך לעקוף אותה. ב-GKE יש בדיוק pool אחד לכל פרויקט (PROJECT_ID.svc.id.goog), שנוצר ומנוהל על ידי Google, ומשותף לכל אשכול בפרויקט. אין מה לרשום, אין מה להגביל. הצד השני של המטבע: האמון הוא כלל-פרויקטי כברירת מחדל, כך שאתם מגדירים את ההיקף באמצעות IAM bindings (לאיזה namespace ו-ServiceAccount אתם מעניקים הרשאה), לא על ידי חלוקת providers.
2. GKE מחליפה את הטוקן בשרת metadata מקומי של הצומת, לא באמצעות קריאת SDK בתוך הפוד. IRSA משנה את הפוד (webhook מזריק משתני סביבה וטוקן מוקרן) וה-SDK קורא ל-STS. GKE מיירטת את metadata.google.internal על הצומת ומחזירה פרטי גישה באופן שקוף, כך שהאפליקציה משתמשת ב-Application Default Credentials כאילו היא על VM רגיל. שתי השלכות: האפליקציה שלכם אינה זקוקה לשום קוד פרטי גישה ספציפי לענן, וה-node pool חייב ששרת ה-metadata יהיה מופעל (GKE_METADATA) אחרת הפוד יקבל בשקט את זהות הצומת במקום את זהותו שלו.
3. GKE יכולה להעניק ל-ServiceAccount הרשאות ענן ללא אובייקט זהות ענן. ב-IRSA תמיד קיים IAM role. במודל הישיר של GKE אין שום דבר ליצור בצד הענן מלבד ה-role binding עצמו - ה-Kubernetes ServiceAccount הוא ה-principal (principal://.../subject/ns/NS/sa/SA). מודל ההתחזות הישן יותר אכן מציג Google service account והערת iam.gke.io/gcp-service-account, ועדיין תזדקקו לו עבור קומץ השירותים שאינם מקבלים principal מאוחד ישירות - אך נסו קודם כל קישור ישיר.
4. GKE דורשת שני מתגים, כמו Azure. אשכול (--workload-pool) בתוספת node pool (--workload-metadata=GKE_METADATA). אם מפספסים את חצי ה-node-pool אין כשל, רק זהות שגויה (של הצומת). IRSA משלבת את ההקרנה לתוך EKS עצמו, כך שאין מתג שני מקביל.
5. משיכת התמונה היא זהות שונה מזהות ה-workload - בשני העננים. משיכת תמונה היא עבודת זהות הצומת: ב-GKE ה-Google service account של ה-node pool זקוק ל-roles/artifactregistry.reader כדי למשוך מ-Artifact Registry; ב-EKS ה-instance role של קבוצת הצמתים זקוק ל-AmazonEC2ContainerRegistryReadOnly (או שאתם משתמשים ב-pull secrets). Workload identity מיועד רק כאשר קוד האפליקציה קורא ל-API של ענן. שירות HTTP טהור שלעולם אינו מדבר עם SDK של ענן אינו זקוק ל-workload identity כלל - והענקת artifactregistry.reader ל-workload identity אינה עושה דבר עבור משיכת תמונה, מכיוון שהמשיכה מתרחשת לפני שהאפליקציה (והטוקן המאוחד שלה) בכלל פועלת.
6. הרשאות מצורפות באופן שונה. AWS מצמידה IAM policy ל-role - ההרשאה נעה עם הזהות. GCP מעניקה IAM role binding בהיקף משאב היעד (או הפרויקט) - ההרשאה חיה על הדבר שאליו ניגשים, לא על הזהות. אותו מצב סופי, מקום שונה לבדוק אותו: ב-AWS קראו את ה-policies המצורפים ל-role; ב-GCP רשמו את ה-IAM policy של המשאב (או השתמשו ב-Policy Analyzer עבור מה ש-principal יכול להגיע אליו).
מה קוד האפליקציה עושה
שני העננים מסתיימים ב"אין סוד, אין קוד פרטי גישה", אך הם מגיעים לשם בדרכים שונות:
- AWS: ה-webhook מגדיר את
AWS_ROLE_ARNו-AWS_WEB_IDENTITY_TOKEN_FILE; שרשרת ברירת המחדל של ה-SDK קוראת ל-AssumeRoleWithWebIdentity. הקוד שלכם:boto3.client("s3"). - GKE: שום דבר אינו מוזרק לפוד; שרשרת ה-Application Default Credentials של ה-SDK פונה לשרת ה-metadata של הצומת, שמבצע את ההחלפה. הקוד שלכם:
storage.Client().
בשני המקרים, פרטי הגישה קצרי-חיים ומתרעננים באופן שקוף. אין מה לסובב, אין מה לאחסן, ואין מה לדלוף.
רשימת בדיקה לניפוי באגים שעובדת בשני העננים
כאשר פוד "אינו יכול לאמת את עצמו", עברו על שרשרת האמון לפי הסדר:
- פדרציית אשכול/פרויקט מופעלת? EKS: ה-IAM OIDC provider רשום. GKE: לאשכול יש
--workload-poolול-node pool ישGKE_METADATA. ב-GKE, מתג חסר ב-node-pool הוא הכשל השקט הקלאסי - הפוד מקבל את זהות הצומת, לא את זהותו שלו. - ServiceAccount נכון? הפוד אכן משתמש ב-ServiceAccount שאתם חושבים שהוא משתמש בו (לא
default). בצעו exec ובדקו. - התאמת זהות? השוו את כלל האמון ל-namespace ול-ServiceAccount האמיתיים:
system:serviceaccount:<ns>:<sa>ב-AWS,.../subject/ns/<ns>/sa/<sa>(או[ns/sa]) ב-GKE. זוהי הכשל הנפוץ ביותר בשניהם. - החלפה עובדת? EKS: לפוד יש
AWS_WEB_IDENTITY_TOKEN_FILE. GKE:curl -H "Metadata-Flavor: Google" metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/emailמהפוד מחזיר את הזהות הצפויה. - הרשאות? רק לאחר שהזהות נפתרת: ה-IAM policy על ה-role (AWS) או ה-role binding על משאב היעד (GCP). תוצאה ריקה כאן היא כשל הרשאה, לא כשל אימות - מה שאומר לכם שהפדרציה עצמה עובדת.
אילו הסמכות מתעמקות בכך
Workload identity נמצא בדיוק בנקודת המפגש של Kubernetes, IAM בענן ופדרציית OIDC, ולכן הוא מופיע בשלושה מסלולי בחינה. אם אתם כבר מחזיקים בפרטי גישה של ענן אחד, אותו קונספט בענן השני הוא קפיצה קצרה.
בצד AWS:
- AWS Certified Solutions Architect - Associate (SAA-C03) - IAM roles, EKS, וכיצד זהויות מקבלות הרשאות ענן.
- AWS Certified Security - Specialty (SCS-C03) - IAM trust policies, פדרציית OIDC, והגדרת היקף הרשאות מינימלי (least-privilege scoping) (בדיוק שרשרת האמון של IRSA).
בצד Google Cloud:
- Google Cloud Associate Cloud Engineer - GKE, IAM, service accounts, ו-role bindings.
- Google Cloud Professional Cloud Security Engineer - workload identity, עומק IAM, אבטחת service-account, והרשאה מינימלית (least privilege).
בצד Kubernetes:
- CNCF Certified Kubernetes Administrator (CKA) - ServiceAccounts, projected tokens, ו-pod specs.
- CNCF Certified Kubernetes Security Specialist (CKS) - אבטחת ServiceAccount token, הרשאה מינימלית (least privilege), והפחתת חשיפת סודות סטטיים.
השורה התחתונה
IRSA ו-GKE Workload Identity Federation הם אותו רעיון עם רמות שונות של מנגנון פנימי: האשכול חותם על טוקן OIDC קצר-חיים עבור ServiceAccount, והענן מחליף אותו בפרטי גישה המוגדרים לזהות יחידה. AWS גורמת לכם להרכיב את האמון בעצמכם - OIDC provider לכל אשכול, IAM role, trust policy, טוקן מוקרן שה-SDK מחליף. GKE מספקת לכם pool מנוהל וכלל-פרויקטי, מאפשרת לכם לקשור role ישירות ל-Kubernetes ServiceAccount ללא אובייקט זהות ענן כלל, ומבצעת את ההחלפה בשרת metadata של צומת כך שהאפליקציה שלכם לעולם אינה רואה פרטי גישה. למדו את משולש האמון פעם אחת והתרגום יהיה מכני: role הופך ל-principal binding (או ל-service account שאתם מתחזים אליו), trust policy הופך למחרוזת IAM member, attached policy הופך ל-role binding על המשאב, ו-AssumeRoleWithWebIdentity הופך להחלפה בלתי נראית של שרת metadata. שמרו שלושה דברים בראש - שני המתגים של GKE, המודל הישיר שלה ללא אובייקט זהות, והעובדה שמשיכת תמונה היא זהות שונה לחלוטין - והסירובים שנראו בעבר אקראיים יתחילו להיקרא כעיצוב קוהרנטי ומכוון.