לדלג לתוכן

סודות

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

אף נקודת קצה של API אינה מחזירה ערך סוד מאוחסן. אף ממשק משתמש אינו מציע כפתור "הצג". מי שאיבד ערך מחליף אותו — זו אותה קריאה שיצרה אותו, דרך אותו טופס. אין זו החלטת מדיניות: נתיב הקריאה פשוט אינו קיים בקוד. (REQ-1558)


תחביר ההפניה

שלוש צורות הפניה תקפות בכל מקום שבו Provisa מפענחת אישורי גישה:

צורה מפוענחת מתוך מי יכול להשתמש בה
${env:VAR_NAME} סביבת תהליך השרת תצורת פריסה בלבד
${secret:NAME} כספת הארגון — משותפת לכל החברים כל שדה המקבל הפניה לאישור גישה
${user:NAME} הכספת האישית של המבצע כל שדה המקבל הפניה לאישור גישה

הפענוח הוא 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.


המאגר המובנה של 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 של הארגון כך שתפנה אליו:

vendor key field → ${secret:OPENAI_KEY}

ההפניה נשמרת מוצפנת ב-org_secrets. בזמן השאילתה Provisa מפענחת את ${secret:OPENAI_KEY} מול כספת הארגון ומוסרת את המפתח המילולי ל-SDK של הספק. סבב של רשומת הכספת נכנס לתוקף מיד — ללא שינוי תצורה בצד הגדרות הארגון. [tool-verified: provisa/core/org_secrets.py:64-79]


גישת מנהל פלטפורמה

למנהל פלטפורמה המפעיל את מישור הבקרה אין קריאה של ערכי סוד של אף ארגון. השומר org_settings מסרב במפורש ל-cross_org ולעקיפת הפלטפורמה: ניהול מחזור החיים של ארגון אינו קריאה של אישורי הגישה שאותו ארגון שומר. השרת אוכף זאת באופן בלתי תלוי בממשק המשתמש. (REQ-1361) [tool-verified: provisa/api/admin/secrets_router.py:53-83]


ראו גם