סודות¶
שמות נכנסים. ערכים לעולם אינם יוצאים חזרה.
אף נקודת קצה של API אינה מחזירה ערך סוד מאוחסן. אף ממשק משתמש אינו מציע כפתור "הצג". מי שאיבד ערך מחליף אותו — זו אותה קריאה שיצרה אותו, דרך אותו טופס. אין זו החלטת מדיניות: נתיב הקריאה פשוט אינו קיים בקוד. (REQ-1558)
תחביר ההפניה¶
שלוש צורות הפניה תקפות בכל מקום שבו Provisa מפענחת אישורי גישה:
| צורה | מפוענחת מתוך | מי יכול להשתמש בה |
|---|---|---|
${env:VAR_NAME} |
סביבת תהליך השרת | תצורת פריסה בלבד |
${secret:NAME} |
כספת הארגון — משותפת לכל החברים | כל שדה המקבל הפניה לאישור גישה |
${user:NAME} |
הכספת האישית של המבצע | כל שדה המקבל הפניה לאישור גישה |
בפריסה מרובת דיירים, שמרו את אישורי הפלטפורמה בשירות סודות ולא במשתני סביבה. מנהל דייר יכול לכתוב ${env:...} בשדה אישורים משלו, כך שכל ערך בסביבת התהליך נמצא בהישג ידו של כל דייר. השאירו בסביבה רק את האישורים של צד-השרת של הסודות עצמו.
הפענוח הוא fail-closed לכל אורכו. שם ספק לא מוכר, שם שלא הוגדר, וקצה עורפי בלתי נגיש — כולם מעלים שגיאה. הפניה שלא ניתן היה לפענח לעולם אינה מוחלפת בשקט במחרוזת ריקה. (REQ-1557) [tool-verified: provisa/core/secrets.py:92-117]
פורמט השם¶
שמות סודות חייבים להתאים ל-[A-Za-z_][A-Za-z0-9_]* — אותיות, ספרות וקווים תחתונים, המתחילים באות או בקו תחתון. האילוץ מעשי: ${secret:NAME} מנותח על ידי דקדוק ההפניות, הקורא עד ל-} הסוגר. שם המכיל סוגר מסולסל, רווח או נקודתיים היה מייצר הפניה שמתפרשת כדבר אחר. [tool-verified: provisa/core/secrets_store.py:61]
שתי כספות, שירות אחד¶
לכל ארגון יש שתי כספות. שתיהן חיות בתוך אותו שירות סודות. (REQ-1560)
כספת הארגון — אישור הגישה שמנהל ארגון מאחסן כאן הוא משותף. כל חבר המפנה אל ${secret:DATABASE_TOKEN} מקבל את אותו ערך. זה מיועד לאישורי גישה שהארגון הוא הבעלים שלהם: סיסמת מסד נתונים משותפת, מפתח חשבון שירות, אסימון פריסה. כספת הארגון דורשת את היכולת org_settings לקריאה או לכתיבה.
כספת אישית — אישור גישה המאוחסן כאן שייך לאדם אחד בדיוק. כששני אנשים מחזיקים כל אחד ב-GIT_TOKEN, ${user:GIT_TOKEN} מתפענח לזה מביניהם שפועל כרגע. אותו טקסט הפניה מוסר לכל אדם את אישור הגישה שלו עצמו. מי שלא אחסן דבר מקבל שגיאה, לא את ערכו של מישהו אחר. שום יכולת אינה שולטת בכספת האישית — החזקת אישור גישה משלך אינה זכות יתר שמנהל מעניק. וגם אין תחביר בקשה לציון כספת של אדם אחר. [tool-verified: provisa/api/admin/secrets_router.py:86-103]
ההיקף הוא חלק מההפניה, לא הרשאה סביבה. ${secret:NAME} ו-${user:NAME} לעולם אינם עונים זה במקום זה.
בחירת שירות סודות¶
Admin ← Security ← Secrets service. הפאנל גלוי לכל מי שמחזיק ביכולת platform_settings. כל קצה עורפי שהבנייה מכירה מופיע ברשימה, בין אם ה-SDK מותקן ובין אם לאו. שורה מעומעמת מספרת לך איזו חבילת Python חסרה — הפאנל נוקב בשמה במקום להסתיר את האפשרות לחלוטין.
חמישה קצוות עורפיים נשלחים:
| מפתח | תווית | דורש |
|---|---|---|
provisa |
Provisa (built-in, encrypted) | דבר; זוהי ברירת המחדל |
hashicorp_vault |
HashiCorp Vault (KV v2) | hvac |
aws_secrets_manager |
AWS Secrets Manager | boto3 |
gcp_secret_manager |
Google Secret Manager | google-cloud-secret-manager |
azure_key_vault |
Azure Key Vault (secrets) | azure-keyvault-secrets |
[tool-verified: provisa/core/secrets_registry.py:161-299]
הבחירה היא fail-closed: קצה עורפי לא מוכר או לא זמין מעלה שגיאה בהפעלה במקום ליפול בשקט לאחר. (REQ-1557)
אישור הגישה של הקצה העורפי עצמו¶
אישור גישת החיבור של קצה עורפי מרכזי הוא תצורת תהליך. הוא מגיע מ-${env:...} בלבד — לעולם לא מ-${secret:...}. שירות סודות שאישור הגישה שלו עצמו חי בתוכו אינו ניתן לפתיחה, ולכן שרשרת האמון מסתיימת בסביבת המארח מתוך תכנון. הרישום אוכף זאת: כל ערך תצורה במפרט של קצה עורפי מפוענח עם providers=("env",) לפני שהקצה העורפי נבנה. [tool-verified: provisa/core/secrets_registry.py:128-141]
דוגמה — תצורת Vault ב-provisa.yaml:
secrets:
provider: hashicorp_vault
hashicorp_vault:
url: https://vault.internal:8200
token: ${env:VAULT_TOKEN} # process env only — never ${secret:...}
mount: secret
שירות מרכזי מול המובנה¶
כששירות מרכזי מוגדר, Provisa קוראת ממנו אך אינה כותבת אליו. השירות המרכזי הוא הבעלים של יצירת רשומות ומחיקתן — פעולות אלה שייכות לכלים שלו עצמו. עמוד הסודות אומר זאת ואינו מציע כפתור יצירה. (REQ-1557)
כשהקצה העורפי המובנה provisa פעיל, עמוד הסודות ניתן לכתיבה מלאה: יצירה, החלפה ומחיקה מממשק המשתמש או דרך ה-API.
הסודות של ארגון בשירות מרכזי¶
פריסה שמחזיקה ארגונים רבים וקוראת שירות מרכזי קוראת אותו עבור כל ארגון בתוך הקידומת של אותו ארגון. הפניה נשארת השם הקצר (${secret:db#password}); הקידומת מתווספת בקריאה. (REQ-1557) [tool-verified: provisa/core/secrets_providers.py, org_namespace]
| שירות | היכן נשמר הסוד db של הארגון acme |
|---|---|
| HashiCorp Vault (KV v2) | orgs/acme/db |
| AWS Secrets Manager | orgs/acme/db |
| GCP Secret Manager | org-acme--db |
| Azure Key Vault | o4-acme-db (o, אורך מזהה הארגון, המזהה עם _ שנכתב כ--, ואז השם) |
הפניה נדחית, בשמה, כאשר:
- שמה היה יוצא מהקידומת: הוא מתחיל ב-
/, מכיל מקטע..או קו נטוי הפוך, או מכיל/ב-GCP או ב-Azure, שבהם השמות שטוחים; - שמה ארוך מדי עבור השירות לאחר הוספת הקידומת (GCP 255 תווים, Azure 127); הדבר נדחה בעת שמירת ההפניה, עם ציון המגבלה;
- אף ארגון אינו קשור לפעולה שקוראת אותה.
פריסה של ארגון אחד קוראת הפניה בדיוק כפי שנכתבה, ללא קידומת: השירות שאליו היא מחוברת הוא של אותו ארגון, ואין צורך להעביר דבר.
מעבר לקידומת. בפריסה של ארגונים רבים, כל סוד שארגון מפנה אליו חייב לשבת תחת הקידומת של אותו ארגון. העתיקו כל אחד לשם בכלים של השירות עצמו, ואז קצרו את ההפניה לשם שבתוך הקידומת (${secret:teams/data/db} שנשמר ב-orgs/acme/db הופך ל-${secret:db}). הפניה שהסוד שלה לא הועבר נכשלת כלא נמצאה; היא אינה נקראת מהמקום שבו היה.
המאגר המובנה של Provisa¶
ברירת המחדל כששום שירות מרכזי אינו מוגדר. כל שורה ב-secrets_store מחזיקה blob של מעטפה מוצפנת — העמודה value היא בינארית, לא טקסט, ומפתח הפענוח חי בסביבת התהליך, לא במסד הנתונים. עותק של מישור הבקרה ללא מפתח האב של הפריסה מחזיק טקסט מוצפן ותו לא. (REQ-1558)
ההצפנה לעולם אינה אופציונלית. כששום מפתח הצפנה ברמת התהליך אינו מוגדר, המאגר נופל אל מחזיק מפתחות מקומי. אם למארח אין מחזיק מפתחות שיחזיק מפתח, המאגר מסרב לכתוב במקום לאחסן את הערך בגלוי. [tool-verified: provisa/core/secrets_store.py:130-159]
צורת האחסון [tool-verified: provisa/core/schema_admin.py:493-505]:
| עמודה | סוג | מטרה |
|---|---|---|
org_id |
Text | הארגון שהוא הבעלים של סוד זה |
owner_id |
Text | "*" לכספת הארגון; מזהה משתמש לכספת אישית |
name |
Text | שם ההפניה |
value |
LargeBinary | blob של מעטפה מוצפנת |
description |
Text | למה הסוד משמש — לעולם אינו נגזר מהערך |
updated_by |
Text | מי קבע אותו לאחרונה |
ארגון שיצר מפתח הצפנה משלו מקבל את סודותיו עטופים תחת אותו מפתח; אחרת הם עטופים תחת מפתח הפריסה. ראו הצפנה במנוחה.
העמודה value אינה נבחרת בשום שאילתת רשימה. [tool-verified: provisa/core/secrets_store.py:214-235]
נקודות קצה של ה-API¶
כל הנתיבים נמצאים תחת /admin/orgs/{org_id}. כספת הארגון דורשת org_settings באותו ארגון. הכספת האישית אינה דורשת יכולת כלשהי — הבעלים נקרא מתוך הזהות המאומתת; אין פרמטר בקשה לציון כספת של מישהו אחר.
| שיטה | נתיב | מה היא עושה |
|---|---|---|
GET |
/secrets |
רשימת שמות והפניות בכספת הארגון |
PUT |
/secrets/{name} |
יצירה או החלפה של סוד ארגוני אחד |
DELETE |
/secrets/{name} |
מחיקת סוד ארגוני אחד |
GET |
/my-secrets |
רשימת השמות וההפניות האישיים של הקורא |
PUT |
/my-secrets/{name} |
יצירה או החלפה של אחד מסודות הקורא |
DELETE |
/my-secrets/{name} |
מחיקת אחד מסודות הקורא |
כל תגובה מחזירה מטא-דאטה — שם, תיאור, updated_at, updated_by, ומחרוזת ה-reference להדבקה — אך לעולם לא את הערך. גוף ה-PUT נושא את value (חובה) ואת description (אופציונלי). החלפה היא אותה קריאה כמו יצירה: השם הוא הזהות, לא מזהה נפרד.
כל כתיבה נרשמת ביומן הביקורת. רשומת היומן נוקבת בשם המבצע ובשם הסוד. הערך אינו נרשם, אפילו לא אורכו. [tool-verified: provisa/api/admin/secrets_router.py:106-117]
היכן ${secret:NAME} מתפענח¶
הפענוח מתרחש בתוך פעולה כבולה להקשר, לא בזמן ייבוא או בהפעלה. המאגר קורא ומפענח את סודות הארגון פעם אחת בתחילת אותה פעולה ומחזיק את המפה ב-ContextVar למשך קיומה. מחוץ לפעולה כבולה, ${secret:NAME} מעלה שגיאה. (REQ-1557) [tool-verified: provisa/core/secrets_store.py:269-290]
שני אתרי קריאה מכוננים את הכבילה:
פעולות remote של Git. כשכתובת ה-URL של ה-remote במאגר של ארגון מכילה הפניית ${secret:...} או ${user:...} — למשל, אסימון push המוטמע בכתובת — נתב הסביבות כובל הן את כספת הארגון והן את הכספת האישית של המשתמש הפועל סביב קריאת git. הצורה ${user:GIT_TOKEN} פירושה שקומיט נוחת תחת אישור הגישה של מי שדחף אותו, לא של חשבון שירות משותף. [tool-verified: provisa/api/admin/environments_router.py:1263]
קריאות של מפתח API של ספק AI. כש-Provisa קוראת מפתח ספק LLM של ארגון והמפתח מאוחסן כהפניית ${secret:NAME}, bound_to_request_org מכונן את כספת הארגון עבור אותה בקשה. ההפניה מפוענחת בדרך החוצה; טקסט ההפניה עצמו לעולם אינו נשלח לספק. (REQ-1580) [tool-verified: provisa/core/org_secrets.py:76-79]
מפתחות ספק AI ארגוניים כהפניות לסודות¶
מפתח ספק ה-AI של ארגון (Anthropic, OpenAI ואחרים) יכול להישמר כהפניית ${secret:NAME} במקום כמפתח מילולי. (REQ-1580)
אחסנו תחילה את המפתח בכספת הארגון:
PUT /admin/orgs/{org_id}/secrets/OPENAI_KEY
{ "value": "sk-...", "description": "OpenAI production key" }
לאחר מכן קבעו את תצורת ה-AI של הארגון כך שתפנה אליו:
ההפניה נשמרת מוצפנת ב-org_secrets. בזמן השאילתה Provisa מפענחת את ${secret:OPENAI_KEY} מול כספת הארגון ומוסרת את המפתח המילולי ל-SDK של הספק. סבב של רשומת הכספת נכנס לתוקף מיד — ללא שינוי תצורה בצד הגדרות הארגון. [tool-verified: provisa/core/org_secrets.py:64-79]
אישורי הגישה של המאגרים של הפריסה עצמה¶
סיסמאות המאגרים ש-Provisa מפעילה (PostgreSQL, Redis, MinIO, חשבון המנהל של Grafana והסוד המשותף של אשכול Trino) אינן סודות באף אחת מהכספות. הן נוצרות לכל פריסה ונשמרות בידי הפריסה: ב-~/.provisa/store-credentials.env (מצב 600), ב-state של Terraform, או ב-Secret של Kubernetes ש-credentials.existingSecret של ה-Helm chart מציין. ראו הקשחת פריסה.
גישת מנהל פלטפורמה¶
למנהל פלטפורמה המפעיל את מישור הבקרה אין קריאה של ערכי סוד של אף ארגון. השומר org_settings מסרב במפורש ל-cross_org ולעקיפת הפלטפורמה: ניהול מחזור החיים של ארגון אינו קריאה של אישורי הגישה שאותו ארגון שומר. השרת אוכף זאת באופן בלתי תלוי בממשק המשתמש. (REQ-1361) [tool-verified: provisa/api/admin/secrets_router.py:53-83]
ראו גם¶
- מודל אבטחה — בקרת גישה משוכבת, אימות ורישום ביקורת
- מדריך תצורה — תחביר
${env:VAR}לאישורי גישה ברמת התהליך