دليل المشرف

التشغيل الأول

  1. سجّل الدخول برمز الإعداد الأولي (api.bootstrap.* في مخطط Helm). وبعد تشغيل التسجيل يُسجَّل كل ما تفعله في سجل التدقيق باسم الفاعل bootstrap-admin. ويتوقف الرمز عن العمل بعد إعداد SSO وإعادة تشغيل الـ API. وهذه هي النهاية المقصودة لعمره. وهو لأول تسجيل دخول فقط. وتستبدله لوحة التحكم وnezal login --token مرة واحدة بجلسة. وينهي تسجيل الدخول الثاني به الأول كأي حساب آخر. أنشئ حسابك الخاص بعد ذلك واستخدمه.

  2. شغّل تسجيل التدقيق من سجل التدقيق > التسجيل ومدة الاحتفاظ. لا يسجّل التثبيت الجديد شيئًا حتى يشغّله المشرف. وتشغيله هو أول مدخل في السجل ويذكر من فعل ذلك. شغّله الآن في أي تثبيت تقرأ سجله جهة رقابية حتى تُسجَّل كل الخطوات التالية. وانظر قسم تشغيل التسجيل أو إيقافه.

  3. الترخيص: الصق المفتاح الذي تسلّمته من نزال. يُتحقق منه محليًا دون أي اتصال بنا ولذلك يعمل في الكلستر المعزول عن الإنترنت. وتعرض الحالة valid العميل وعدد المقاعد وتاريخ الانتهاء والميزات. وبعد الانتهاء تبدأ فترة grace مدتها 30 يومًا (7 أيام للنسخة التجريبية) يستمر خلالها كل شيء في العمل وتنبّه لوحة التحكم إلى ذلك. وبعد فترة السماح لا تُقبل بيئات جديدة ولا مستخدمون جدد. لا يُحذف أي شيء أبدًا بسبب الترخيص ولا يتوقف أي شيء قائم عن العمل.

  4. SMTP قبل أن تدعو أحدًا. في شاشة الأمان والبريد أدخل المضيف والمنفذ ونمط التأمين (starttls أو tls أو بلا تأمين) ثم بيانات الاعتماد وعنوان المرسل. ويثبت Send test أن الإعداد يعمل قبل أن تعتمد عليه أي دعوة. وكلمة المرور للكتابة فقط: تعود محجوبة وتعيد لوحة التحكم القيمة المحجوبة كما هي. لذلك لا يمكن أن يستبدل حفظ النموذج كلمة مرور صالحة بكلمة فارغة.

  5. SSO أو الحسابات المحلية أو كلاهما. OIDC (المُصدِر ومعرّف العميل وسرّه ومطالبة المجموعات) أو SAML (بيانات IdP الوصفية بصيغة XML وزوج مفاتيح SP). واربط مجموعات مزوّد الهوية بدور المشرف وبمساحات العمل بصفة مطوّر أو قائد فريق.

  6. سياسة MFA في أعلى شاشة الأمان والبريد. عند تفعيل enforced يجب على كل حساب محلي أن يسجّل تطبيق مصادقة TOTP قبل أن يستطيع فعل أي شيء. ويعمل أي تطبيق مصادقة مجاني ولا يُتصل بأي شيء خارج الكلستر. ولا تتأثر حسابات SSO لأن العامل الثاني لها من اختصاص مزوّد الهوية لديك.

  7. مساحات العمل: مساحة عمل واحدة لكل فريق. وتقابل مساحة العمل مساحة أسماء للتطبيق. وفيها يوجد الأساس المشترك والبيئات والسياسة الخاصة بذلك الفريق.

  8. السياسة لكل مساحة عمل. وهي تحدد:

    • عدد البيئات لكل مستخدم ولكل فريق
    • حصة المعالج والذاكرة
    • المدة (TTL) الافتراضية والقصوى
    • السكون عند الخمول وفترة السماح بعد انتهاء المدة
    • تشغيل الطرفية أو إيقافها مع مهلة الخمول والمدة القصوى للجلسة

    ويفرضها webhook القبول في المشغّل على كل عملية كتابة. لذلك تسري على kubectl أيضًا لا على لوحة التحكم وحدها.

  9. سجل صور لكل مساحة عمل من شاشة سجلات الصور. وهو مطلوب: لا يمكن إنشاء تطبيق في مساحة عمل قبل ربط سجل صور بها. اربطه مرة واحدة ببيانات اعتماده. وأول سجل تربطه هو الافتراضي.

    يستخدم التطبيق السجل الافتراضي ما لم تختر له سجلًا مربوطًا آخر من حقل السجل في نافذته. وتطلب النافذة المستودع أولًا ثم السجل والسجل الافتراضي مختار مسبقًا. ولا يمكن فصل سجل اختاره تطبيق ما حتى يختار التطبيق سجلًا آخر.

    وللسجل الافتراضي وظيفتان. تدفع عمليات البناء صورها إليه لأن العقدة لا تستطيع تشغيل صورة لا توجد إلا على قرص عقدة أخرى. وببيانات اعتماده تسحب pods مساحة العمل صورها.

    ولا يعيد السجل كتابة أسماء الصور. فالصورة تعمل من السجل الذي يذكره مخططك أو ملفات manifests لديك بالضبط. وفي الكلستر المعزول وجّه تلك الأسماء إلى سجل الصور الخاص بك. وقيم image.repository في المخطط هي الموضع المعتاد لذلك.

    والسجل الذي يحتاج إلى كلمة مرور يعمل لنسخ المطوّرين أيضًا. يكتب نزال بيانات الاعتماد في Secret في Kubernetes وينسخها المشغّل إلى مساحة أسماء كل بيئة لأن الـ pod لا يستطيع الإشارة إلا إلى Secret في مساحة أسمائه. ويستطيع المشغّل قراءة Secret أُخبر باسمه ولا يستطيع سرد الأسرار. راجع ADR 0012 لتعرف ما يعنيه ذلك للمراجعة الأمنية.

  10. الوكلاء الموثوقون إذا كان الـ API خلف متحكم ingress أو موزّع حمل. اضبط api.trustedProxies على شبكة pods ذلك الوكيل. وإلى أن تفعل ذلك يُتجاهل X-Forwarded-For تمامًا ويسجّل سجل التدقيق عنوان الوكيل بدلًا من عنوان المستخدم. وهذا الإعداد الافتراضي مقصود: فتصديق الترويسة دون شرط يتيح لأي مستدعٍ أن يكتب عنوانًا عشوائيًا في السجل المقاوم للتلاعب الذي يعتمد عليه المحقق.

  11. تأكد من أن التثبيت يستجيب: helm test nezal -n nezal-system.

الأشخاص: الحسابات المحلية والدعوات والعامل الثاني

يأتي المستخدمون من مزوّد الهوية لديك أو يُدعَون مباشرة أو من الاثنين معًا.

الدعوة: المستخدمون والفرق > دعوة. أدخل بريدًا إلكترونيًا ومساحة عمل ودورًا وحدّد ما إذا كان الشخص مشرفًا. ومساحة العمل مطلوبة وتبدأ بالمساحة التي أنت فيها. ويرى المطوّر المدعو تلك المساحة وتطبيقاتها فور تسجيل دخوله. وتعرض قائمة المستخدمين مساحات عمل كل شخص وأدواره. ويتلقى المدعو رابطًا لتعيين كلمة مرور. ويشغل الحساب المدعو مقعدًا منذ لحظة إنشائه لا منذ أول تسجيل دخول. ولولا ذلك لاستطاع مشرف أن يدعو عددًا غير محدود من الأشخاص ويجعلهم جميعًا يعملون قبل أن ينتبه عدّاد المقاعد.

العامل الثاني: TOTP للحسابات المحلية. ويعمل أي تطبيق مصادقة مجاني مثل Google Authenticator أو Microsoft Authenticator أو FreeOTP. ولا يُتصل بأي شيء خارج الكلستر ولا توجد خدمة مدفوعة. ويعرض التسجيل مرة واحدة عشرة رموز استرداد يُستخدم كل منها مرة واحدة بدل رمز الهاتف. وعند فرض MFA يُرشَد الحساب المحلي الجديد خلال التسجيل عند أول تسجيل دخول. ويمسح Reset MFA تسجيل المستخدم ويُلزمه بإعداده من جديد عند تسجيل دخوله التالي حتى لو لم يكن MFA مفروضًا. وهذا هو الحل عند فقدان الهاتف وهو مسجَّل في سجل التدقيق. وليس لحسابات SSO ولرمز الإعداد كلمة مرور في نزال ولذلك لا عامل ثانيًا لها هنا. وتذكر صفحة الحساب ذلك.

تحديد المعدل مفعّل ولا يحتاج إلى إعداد: عشر محاولات دخول فاشلة لحساب واحد خلال خمس عشرة دقيقة تقفل ذلك الحساب خمس عشرة دقيقة. وستون محاولة فاشلة من عنوان واحد في المدة نفسها تحجب ذلك العنوان. والحساب المدعو الذي لم يعيّن كلمة مرور بعد يخضع لتحديد المعدل أيضًا.

الجلسات والرموز: يرى كل شخص جلساته النشطة في شاشة الحساب مع الجهاز وآخر وقت استُخدمت فيه كل جلسة. ويستطيع إلغاء أي منها. ويستثني "تسجيل الخروج من كل مكان" الجلسة التي طلبته فلا يُخرج أحد نفسه عن طريق الخطأ. ويستطيع المشرف إلغاء كل جلسات مستخدم آخر. ويسري ذلك عند الطلب التالي لذلك المستخدم لا عند انتهاء صلاحية رمزه. وتُصدَر رموز CLI من لوحة التحكم وتظهر في القائمة نفسها.

إزالة شخص: حذف المستخدم يلغي جلساته ورموزه ويحرّر مقعده. والمستخدم الذي لا يزال يملك بيئات يُرفض حذفه مع ذكر عددها: فإزالته بصمت تترك بيئات لا يصل إليها أحد وحصة لا يُسأل عنها أحد. احذف بيئاته أو انقل ملكيتها أولًا ثم احذف الحساب.

الأدوار

المطوّر قائد الفريق المشرف
البيئات الخاصة: الإنشاء والحذف والمزامنة والتمديد مع السجلات والطرفية نعم نعم نعم
بيئات الفريق قراءة إدارة وتمديد وحذف نعم
حصة الفريق وأعضاؤه وتقاريره لا نعم نعم
التطبيقات والأساس المشترك والسياسات قراءة ربط وترقية نعم
المستخدمون والترخيص وSSO وSIEM وتصدير سجل التدقيق لا لا نعم

جلسات الطرفية والسجلات مقصورة دائمًا على مالك البيئة ولا تُفتح أبدًا على الأساس المشترك. ولا يقرأ أحد غيره سجلات البيئة ولا قائد الفريق ولا المشرف لأن السجل يحمل كل ما يطبعه التطبيق. ويستطيع المشرف أو السياسة إيقاف الطرفية لكل فريق.

حدود الطرفية

طرفية الويب هي أعلى ما يستطيع المطوّر فعله هنا من حيث الصلاحيات. وهي الإجراء الوحيد الذي لا يترك في العادة أثرًا مفصلًا: فكل إجراء آخر إدخال في سجل التدقيق يذكر ما فُعل أما الطرفية فإدخال واحد يقول إنها فُتحت. ولهذا تُكتب كل جلسة في سجل التدقيق مع من فتحها وأي pod دخلت إليه ومتى ولماذا انتهت. وتضع السياسة حدودًا للجلسة نفسها:

حقل السياسة الافتراضي ما يفعله
terminalIdleTimeout 15m يغلق الجلسة التي لم يمرّ فيها إدخال ولا إخراج طوال هذه المدة. والقيمة "0" تعطّله
terminalMaxDuration 4h يغلق الجلسة بعد هذه المدة من بدايتها مهما كانت نشطة. والقيمة "0" تعطّله

والجلسة التي يغلقها حدّ تذكر ذلك: فقائمة الجلسات وإدخال التدقيق يحملان كلاهما السبب (idle أو max أو cap أو user) لأن "خرجت الطرفية" و"أغلقناها بعد ساعة من الخمول" حقيقتان مختلفتان لمن يقرأ السجل لاحقًا.

ولمخرجات الجلسة سقف أيضًا. وبلوغ السقف ينهي الجلسة والعملية المنفلتة تبلغه في ثوانٍ. وهذه هي الحالة التي وُضع السقف لأجلها وتنتهي الجلسة بالسبب cap ليذكر السجل أي حدّ أغلقها.

المستودعات والتطبيقات

اربط المستودع أولًا من شاشة المستودعات: عنوانه URL والمرجع الافتراضي وطريقة المصادقة (رمز HTTPS أو مفتاح SSH أو بلا مصادقة لمرآة عامة). ويستنسخه Test قبل أن يعتمد عليه أي شيء. وتُحفظ بيانات الاعتماد في Secret في Kubernetes داخل مساحة أسماء التطبيق يحمل الوسم nezal.dev/git-credentials=true ولا يمكن قراءتها مرة أخرى عبر الـ API. ويُمنح المشغّل صلاحية الوصول إلى ذلك الـ Secret وحده باسمه ولا يملك أي صلاحية على الأسرار على مستوى الكلستر.

ويمكنك أن تضبط سرّ webhook على المستودع. وهو ما يُقارن به X-Hub-Signature-256 من GitHub أو X-Gitlab-Token من GitLab على POST /webhooks/{workspace}/{repository}. وبهذا يحصل طلب السحب على بيئة خاصة به. وهو للكتابة فقط مثل بيانات اعتماد git.

ثم اربط التطبيق بالإشارة إلى ذلك المستودع وإلى مسار فيه (مخطط Helm أو Kustomize أو ملفات manifests عادية). ويولّد المشغّل الملفات ويسرد الخدمات ويذكر في status.warnings كل ما لم يستطع نقله. اقرأ هذه التحذيرات في صفحة التطبيق: ففيها يُقال صراحة "هذه الخدمة بلا فحص صحة". وضع علامة shared على قواعد البيانات وطوابير الرسائل حتى لا تنسخها البيئات أبدًا.

ترقية الإصدار تعني تغيير مرجع المصدر. وتُسجَّل كل ترقية في تاريخ إصدارات التطبيق مع الـ commit والوقت ومن نفّذها. والتراجع هو ترقية مرجع أقدم من تلك القائمة. وتستمر البيئات في مشاركة الأساس المشترك ولا تملك منه إلا خدماتها المستبدلة.

بيئات طلبات السحب (الميزة المرخّصة per-pr): اضبط prEnvironments على all أو على label مع اسم وسم. وعندها يُنشئ فتح طلب السحب البيئة pr-<number>-<application> وتزامنها كل عملية push ويحذفها إغلاق طلب السحب. ومالكها webhook لا شخص بعينه فلا تستهلك من حصة البيئات الشخصية لأحد.

نشر رابط المعاينة في طلب السحب. فعّل Post the preview URL to the pull request على المستودع فيضبط نزال حالة commit (nezal/preview) ويكتب تعليقًا واحدًا. ثم يعدّل ذلك التعليق نفسه مع كل push بدلًا من تكراره. فعشرات التعليقات الآلية المتطابقة هي ما يدفع الناس إلى كتم هذه التكاملات.

وهذا الخيار معطّل افتراضيًا لأن الكتابة في طلبات السحب لديك ليست أمرًا يُفترض سلفًا. ويحتاج إلى رمز يستطيع الكتابة في المستودع: فمفتاح النشر للقراءة فقط يتصل ويستنسخ دون مشكلة لكنه لا يستطيع التعليق. وعندها يتخطى نزال النشر بهدوء بدلًا من ملء السجل. ويتعرّف نزال على GitHub (ومنه Enterprise) وGitLab من عنوان المستودع. أما أي خادم git آخر فلا طلبات سحب فيه يُنشر إليها ولا يُحاوَل أي شيء.

وتظهر كل عملية نشر في سجل التدقيق باسم forge.post. ولا يُتصل بأي جهة جديدة: فهذا هو خادم git نفسه الذي يشير إليه المستودع أصلًا.

روابط المعاينة

تنشر كل بيئة <environment>--<application>.<domain>. وعند تفعيل previewAuth.enabled (وهو الافتراضي) يجب أن يكون الزائر قد سجّل الدخول إلى نزال وأن تكون لديه صلاحية قراءة تلك البيئة. وملف cookie مقصور على بيئة واحدة وصالح ثماني ساعات.

اتركه مفعّلًا. فإذا أوقفته أصبح مضيف المعاينة متاحًا لكل من يصل إلى الـ ingress. وفي الشبكة الداخلية لشركة يعني ذلك كل موظف وكل حاسوب محمول لدى المتعاقدين وكل حمل عمل آخر في الكلستر. والمطوّر الذي يختبر على بيانات تشبه بيانات الإنتاج سيكشفها لهم جميعًا دون أن يعلم.

التدقيق والأدلة

كل إجراء لكل مستخدم إدخال واحد يحمل sha256 مرتبطًا بالإدخال السابق. ويتتبّع Audit > Verify السلسلة كلها. وينزّل Export حزمة من أسطر JSON مع ملف manifest موقَّع بمفتاح التثبيت (والمفتاح العام على /api/v1/audit/public-key). ويمكنك إرسال الإدخالات فور حدوثها إلى SIEM لديك عبر syslog (TCP أو UDP وفق RFC 5424) أو HTTP JSON من الإعدادات > SIEM. وتُسرد جلسات الطرفية في شاشة جلسات الطرفية مع من فتح كل جلسة وأي pod دخلت إليه ولماذا انتهت.

تشغيل التسجيل أو إيقافه

التسجيل متوقف في التثبيت الجديد. ولا يُكتب شيء في سجل التدقيق حتى يشغّله المشرف من سجل التدقيق > التسجيل ومدة الاحتفاظ. وتذكر صفحة سجل التدقيق ونظرة المشرف العامة ذلك ما دام متوقفًا. ويبقى التسجيل مفعّلًا في التثبيت المُرقّى الذي يحوي مدخلات حتى لا توقف الترقية سجلًا كان يعمل. ويُكتب تشغيله وإيقافه دائمًا مع اسم من فعل ذلك ومن أين. فيرى المدقق كل فترة لم يكن السجل فيها يسجّل. اتركه مفعّلًا حيث تقرأ جهة رقابية السجل.

مدة الاحتفاظ

يوجد سجل التدقيق في قاعدة بيانات الـ API على وحدة التخزين. ولمنعه من ملء وحدة التخزين اضبط عدد أيام في قيم Helm. وتُحذف المدخلات الأقدم من ذلك مرة كل ساعة.

helm upgrade nezal oci://registry-1.docker.io/nezal/nezal -n nezal-system --reuse-values \
  --set audit.retention.days=365
القيمة الافتراضي ما تفعله
audit.retention.days 0 تحذف المدخلات الأقدم من هذا العدد من الأيام. والقيمة 0 تحفظ كل المدخلات.
audit.retention.onlyArchived false القيمة true تحذف فقط المدخلات التي يحفظها أرشيف موقّع. فلا يغادر شيء قاعدة البيانات قبل أن تُحفظ نسخة منه في مكان آخر.

ولا يكسر حذف المدخلات القديمة عملية التحقق:

وتُعاد المساحة المحررة للمدخلات الجديدة. ولا يصغر ملف قاعدة البيانات على القرص.

الأرشفة

تصدّر الأرشفة كل ما هو أقدم من تاريخ فاصل إلى حزمة أدلة موقّعة تحفظها حيث تحفظ أدلتك. وتسجّل أن ذلك النطاق قد حُفظ. أرشف النطاق قبل أن تحذفه مدة الاحتفاظ إذا لزمك حفظه أطول من audit.retention.days.

curl -X POST -H "Authorization: Bearer $TOKEN" \
  "https://nezal.acme.internal/api/v1/audit/archive?olderThanDays=365&location=vault://audit/2025" \
  -o nezal-audit-archive.multipart

القيمة الافتراضية لـ olderThanDays هي 365. ويُسجَّل location كما تكتبه تمامًا ولا يُقرأ أبدًا. فالأرشيف الذي يستطيع نزال الوصول إليه أرشيف يستطيع نزال تعديله. وأرشفة نطاق فارغ ليست خطأ ولا تسجّل شيئًا. لذلك يستطيع استدعاء مجدول أن يعمل في أسبوع هادئ.

ويسرد GET /api/v1/audit/archives ما أُرشف. ويعرض النطاق الذي يغطيه كل أرشيف مع تجزئة رأسه وتجزئة رأس الأرشيف الذي قبله.

ويحمل كل أرشيف ملف manifest فيه sha256 للإدخالات وتوقيع ed25519 على ذلك الملف. ويستطيع المدقق التحقق من الاثنين بعد سنوات دون الحاجة إلى هذا التثبيت. فملف manifest يحمل المفتاح العام الذي وُقّع به.

دورة الحياة

تنتهي البيئات عند انقضاء مدتها (TTL): تُقلَّص النسخ إلى الصفر ويعرض رابط المعاينة صفحة "منتهية". وبعد فترة السماح تُحذف البيئة. والبيئات الخاملة تدخل في السكون. وأول طلب يوقظها. وكل انتقال من هذه الانتقالات Event في Kubernetes وإدخال في سجل التدقيق.

الترخيص والمقاعد

تعرض شاشة الترخيص العميل ومدة الترخيص والميزات. وتعرض أيضًا عدد المقاعد مقابل المستخدمين النشطين. والمقعد هو شخص سجّل الدخول أو لديه دعوة معلّقة.

المقعد لشخص واحد. يستخدم الحساب شخص واحد في كل مرة. وينهي تسجيل الدخول من متصفح جلسة المتصفح الأخرى للحساب نفسه وينهي nezal login جلسة سطر الأوامر الأخرى. ويُبلَّغ من خرج بالسبب. والحساب الذي يتكرر تسجيل الدخول به من أماكن أخرى يُعلَّم حساب مشترك في المستخدمين ويظهر في النظرة العامة والترخيص. ويُكتب في سجل التدقيق باسم auth.sharedAccount. امنح كل شخص حسابه الخاص. والدخول الموحّد يفعل ذلك تلقائيًا لأن لكل شخص هويته. وتظهر أماكن تسجيل دخول الشخص في الجلسات على صفّه في المستخدمين وفي صفحة حسابه.

والمقاعد مرنة. فتجاوز العدد يُظهر تحذيرًا وزيادة 10% مقبولة تمامًا. وبعد ذلك تُرفض الدعوات الجديدة ويستمر كل من يعمل أصلًا. فالشركة لا تريد أن يتوقف مطوّروها في منتصف السبرنت بسبب عدّاد. وتجاوز العدد موضوع للنقاش عند التجديد وليس انقطاعًا في الخدمة.

وينزّل Usage report تقريرًا موقَّعًا بالمقاعد النشطة شهريًا طوال مدة الترخيص. وعلى أساسه يجري التفاوض على التجديد. ولا يحتاج إلى شبكة ولا يكشف شيئًا خارج الكلستر.

التنبيه قبل انتهاء الترخيص

ينبّه نزال إلى الانتهاء عبر أربع قنوات في وقت واحد لأن إحداها على الأقل تكون في العادة غير متاحة في بيئة خاضعة للتنظيم. ويبدأ قبل الانتهاء بثلاثين يومًا. ثم يكرر التنبيه قبل 14 يومًا ثم قبل 7 أيام و3 أيام ويوم واحد ومرة أخيرة في يوم الانتهاء نفسه. وبعد ذلك ينبّه كل أسبوع خلال فترة السماح ومرة واحدة عند انتهائها. أما النسخة التجريبية فتنبّه قبل 7 أيام و3 أيام ويوم واحد فقط. فمدتها كلها أقصر من أول تنبيه في الترخيص المدفوع.

ويُرسل كل تنبيه مرة واحدة. فإعادة تشغيل الـ API لا تعيد إرساله. وتثبيت مفتاح مجدَّد يبدأ التسلسل من جديد للمدة الجديدة.

القناة ما تحتاج إلى إعداده أين تظهر
البريد الإلكتروني مرحّل SMTP كل مشرف مفعّل إضافة إلى العناوين في Settings → Licence notices
Prometheus جمع (scrape) لـ /metrics nezal_licence_seconds_until_expiry ويصبح سالبًا بعد الانتهاء
Event في Kubernetes لا شيء kubectl -n nezal-system get events على ConfigMap nezal-licence
شريط لوحة التحكم لا شيء كل صفحة ولكل شخص بدءًا من 30 يومًا قبل الانتهاء
CLI لا شيء سطر واحد على stderr مع كل أمر بدءًا من 30 يومًا قبل الانتهاء

ومستلمو البريد هم كل مشرف مفعّل إضافة إلى من يُضبط في PUT /settings/licence-notify. وتوجد هذه القائمة لأن من يوافقون على التجديد يعملون في العادة في المشتريات ونادرًا ما يملكون حساب مشرف. والمشرف المعطَّل لا يصله تنبيه.

وإذا لم يوجد مرحّل SMTP تبقى القنوات الأربع الأخرى تعمل ويسجّل سجل التدقيق licence.notice مع smtp: false. وبذلك يكون لعبارة "لم يُخبرنا أحد" جواب في الحالتين.

ولا يخرج أي شيء من هذا كله من الكلستر. فالبريد يمر عبر مرحّل العميل نفسه. ولا يتصل نزال بالجهة المورّدة في أي مرحلة.

المراقبة

يقدّم الـ API والمشغّل كلاهما مقاييس Prometheus على /metrics. وهي دون مصادقة على شبكة pods ولا تُعرض عبر الـ ingress. ولا يُرسل شيء إلى أي مكان: فخادم Prometheus لديك يجمعها كما يجمع أي شيء آخر في الكلستر.

ولا يحمل أي مقياس اسم مستخدم أو عميل أو بيئة أو مساحة عمل. فالوسوم تنتهي في لوحات المتابعة وفي التخزين طويل الأمد. لذلك الوسم مكان تذهب إليه البيانات وتبقى فيه. ثم إن سلسلة لكل بيئة تعني عددًا غير محدود من القيم (cardinality).

السلسلة ما تخبرك به
nezal_api_requests_total{route,method,status} معدل الطلبات والأخطاء. وroute هو النمط وليس المسار أبدًا
nezal_api_request_duration_seconds{route,method} زمن الاستجابة في صورة مدرّج تكراري
nezal_api_requests_in_flight عدد الطلبات المتزامنة الآن
nezal_environments{phase} البيئات حسب المرحلة كما يراها الـ API
nezal_builds_running عمليات البناء الجارية
nezal_terminal_sessions_active الطرفيات المفتوحة الآن
nezal_sessions_active جلسات الدخول التي لم تنتهِ
nezal_audit_head_seq أحدث إدخال في سجل التدقيق. ويتوقف عن الارتفاع إذا توقف السجل
nezal_data_volume_free_bytes / _total_bytes المساحة على وحدة التخزين التي تحمل قاعدة البيانات والمفاتيح
nezal_licence_seconds_until_expiry سالب بعد الانتهاء
nezal_evidence_pending_uploads ملفات الأدلة التي لم تصل بعد إلى تخزين الكائنات لديك

قواعد التنبيه التي تستحق الإعداد

هذه هي القواعد الخمس المهمة. وكل ما عداها مكانه لوحة المتابعة.

groups:
  - name: nezal
    rules:
      # The licence stops new work after its grace period. Thirty days is enough for a
      # purchase order to go through; the product also emails, but not every site has SMTP.
      - alert: NezalLicenceExpiringSoon
        expr: nezal_licence_seconds_until_expiry < 30 * 86400
        for: 1h
        annotations: {summary: "Nezal licence expires in under 30 days"}

      # The volume holds the database and the keys. When it fills, the database
      # becomes read-only and the audit log stops with it.
      - alert: NezalDataVolumeLow
        expr: nezal_data_volume_free_bytes / nezal_data_volume_total_bytes < 0.15
        for: 15m
        annotations: {summary: "Nezal data volume below 15% free"}

      - alert: NezalAPIErrorRate
        expr: |
          sum(rate(nezal_api_requests_total{status=~"5.."}[5m]))
            / sum(rate(nezal_api_requests_total[5m])) > 0.05
        for: 10m
        annotations: {summary: "Over 5% of Nezal API requests are failing"}

      # A stalled audit head is either a very quiet install or a broken one, and you need to
      # know which. Pair it with the error-rate alert: quiet plus errors is not quiet.
      - alert: NezalAuditLogStalled
        expr: increase(nezal_audit_head_seq[6h]) == 0
        for: 30m
        annotations: {summary: "No Nezal audit entries in six hours"}

      # Evidence that never reaches your object storage is evidence you do not have.
      - alert: NezalEvidenceUploadsPending
        expr: nezal_evidence_pending_uploads > 0
        for: 1h
        annotations: {summary: "Nezal evidence files are not reaching object storage"}

وينشر المشغّل سلاسله الخاصة (nezal_environments_by_phase وعدد عمليات توليد الملفات ومددها). والاختلاف بين ما يراه المشغّل وما يراه الـ API إشارة في حد ذاته: فهما يقرآن الكائنات نفسها بطريقين مختلفين.

الحصول على الدعم

لا يرسل نزال أي بيانات قياس عن بُعد. لذلك لا نملك ما ننظر فيه عندما يحدث خطأ. وحزمة الدعم هي البديل: ملف واحد فيه ما كان مهندس الدعم سيقضي يومًا كاملًا في طلبه.

الإدارة → التثبيت → نزّل حزمة الدعم أو من سطر الأوامر عندما تكون لوحة التحكم نفسها هي المشكلة:

kubectl exec -n nezal-system deploy/nezal-api -- \
  /nezal-api support-bundle > nezal-support.json

وتحتوي الحزمة على:

إنشاء الحزمة لا يرسل شيئًا إلى أي مكان. ويذكر السطر الأول من الملف ذلك. اقرأه ثم أرفقه بطلب الدعم بنفسك. فهذا هو الغرض كله من أن تكون الصيغة ملف JSON واحدًا يمكن قراءته لا أرشيفًا.

وتُحجب بيانات الاعتماد والرموز والمفاتيح وترويسات Authorization أينما ظهرت. ويشمل ذلك أسطر السجل. فهي تُحجب لحظة تسجيلها لا لحظة كتابة الحزمة. ويزرع اختبار بيانات اعتماد واقعية عبر الـ API ويُفشل البناء إذا وصل أي منها إلى حزمة. ومع ذلك اقرأ الملف قبل أن ترسله. فهو يصف تثبيتك بالتفصيل وأنت وحدك من يقرر هل ترضى بمشاركة هذا التفصيل.

وتقرأ نسخة سطر الأوامر قاعدة البيانات مباشرة فتعمل حتى عندما لا يستجيب الـ API. لكنها لا تستطيع تضمين السجلات الحديثة للعملية الجارية. أما التنزيل من لوحة التحكم فيستطيع ذلك.

النسخ الاحتياطي والاستعادة

يحمل PVC الخاص بالـ API قاعدة بيانات SQLite ومفتاحي session وaudit-export. ويحمل الوسم helm.sh/resource-policy: keep فلا يمسّه helm uninstall. وحذفه لا يكون إلا بأمر kubectl delete pvc متعمَّد.

خذ النسخة الاحتياطية بالأمر لا بنسخ الملف. فنسخ ملف قاعدة البيانات وهو مفتوح قد ينتج نسخة احتياطية لا تصلح للاستخدام:

kubectl exec -n nezal-system deploy/nezal-api -- \
  /nezal-api backup --out - > "nezal-$(date +%F).db.enc"

النسخة الاحتياطية مشفّرة. فهي تحتوي على كل تجزئة كلمة مرور وكل سرّ عامل ثانٍ مخزَّن وسجل التدقيق كاملًا. وحين تصل إلى تخزين الكائنات لا يكون المسؤول عن حفظها بالضرورة هو من يدير نزال. والتشفير AES-256-GCM بمفتاح طوله 32 بايتًا في الـ Secret المسمّى nezal-api-keys بجوار مفاتيح التوقيع.

إذا فقدت ذلك الـ Secret أصبحت كل نسخة احتياطية غير قابلة للقراءة. لا يوجد طريق للاسترداد ولا توجد عن قصد نسخة مودعة لدى طرف ثالث (escrow). انسخ الـ Secret احتياطيًا على حدة وقبل أن تحتاج إليه. واحفظه في مكان لا تصل إليه أدوات النسخ الاحتياطي لقاعدة البيانات.

ويكتب --plaintext نسخة احتياطية غير مشفّرة للعميل الذي تفرض عليه إدارة المفاتيح لديه ذلك. ويذكر الأمر بوضوح ما يحتويه ذلك الملف.

ويبثّ --out - النسخة الاحتياطية إلى stdout. وهذا هو طريق استرجاعها وليس مجرد تسهيل: فالأمر kubectl cp يستدعي tar داخل الحاوية وصورة الـ API من نوع distroless بلا صدفة ولا tar. لذلك لا يمكن أن يعمل kubectl cp معها أبدًا. وكل ما يقوله الأمر عن نفسه يذهب إلى stderr فلا يحتوي الملف المُعاد توجيهه إلا على قاعدة البيانات.

تحقق منها قبل أن تثق بها. يفتح verify-backup الملف بواسطة SQLite لذلك يحتاج إلى ملف حقيقي لا إلى أنبوب (pipe). فشغّله حيث توجد النسخة الاحتياطية الآن:

nezal-api verify-backup --in "nezal-$(date +%F).db.enc" --key-file ./backup.key

وداخل الكلستر يُعثر على المفتاح تلقائيًا في وحدة تخزين البيانات. لذلك لا يحتاج kubectl exec -n nezal-system deploy/nezal-api -- /nezal-api verify-backup --in … إلى --key-file. ويذكر verify-backup المفتاح الذي أُنشئ به كل ملف. وبذلك يمكن مطابقة نسخة احتياطية بلا تسمية يُعثر عليها لاحقًا مع المفتاح الصحيح.

وصورة الـ API من نوع distroless و/nezal-api هو الملف التنفيذي الوحيد فيها. لذلك يجري الفحص خارج الكلستر على النسخة التي أخذتها للتو. ويتحقق الـ CronJob المجدول من كل نسخة احتياطية يكتبها. وهذا هو الطريق المفضّل.

النسخة الاحتياطية التي لم يفتحها أحد أمنية وليست نسخة احتياطية.

ويستخدم backup الأمر VACUUM INTO في SQLite وهو آمن أثناء الكتابة في قاعدة البيانات. ويأتي المخطط مع CronJob يفعل كل ذلك وفق جدول ويكتب إلى مطالبة تخزين (claim) خاصة به. راجع backup في values.yaml. وهذا هو الطريق المفضّل. أما الأوامر اليدوية أعلاه فهي لنسخة احتياطية تُؤخذ قبل تغيير متعمَّد.

الاستعادة عن قصد تسلسل موثَّق وليست أمرًا فرعيًا. فأمر الاستعادة الذي يمكن تشغيله على قاعدة بيانات حية وسيلة لتدميرها:

  1. قلّص الـ API إلى الصفر: kubectl scale -n nezal-system deploy/nezal-api --replicas=0.

  2. انسخ النسخة الاحتياطية فوق nezal.db في مجلد البيانات من pod يربط مطالبة التخزين نفسها. ومع تقليص الـ API إلى الصفر لا يحتجز المطالبة شيء آخر. لذلك يكفي pod قصير العمر يربط nezal-api-data:

    kubectl cp ./nezal-2026-09-11.db nezal-system/<restore-pod>:/var/lib/nezal/nezal.db
    

    احذف أولًا أي ملف nezal.db-wal أو nezal.db-shm بجواره: فهما يخصّان قاعدة البيانات التي تستبدلها ولولا حذفهما لحاول SQLite إعادة تطبيقهما على الملف المستعاد.

  3. أعد التوسيع إلى نسخة واحدة وتأكد في لوحة التحكم من أن سلسلة التدقيق تتحقق.

والأساس المشترك والبيئات والسياسات كائنات Kubernetes. خذ لها نسخة احتياطية بالأداة التي يستخدمها الكلستر لديك أصلًا. ويُعاد بناؤها أيضًا من المستودع. لذلك قاعدة البيانات هي الجزء المهم.