انتقل إلى المحتوى الرئيسي

الوحدة 14 — الأمان والمستخدمون والنسخ الاحتياطيّة والتشغيل اليوميّ

اكتمل محرّك Veille، وفهرسته جاهزة، والاستعلامات تجري كما ينبغي. تريد إيناس الآن ضمانتَين قبل عرض أيّ شيء أمام عميل. الأولى ألّا تعدّل واجهة كريم برمجيًّا مقالًا بالخطأ، ولا تحذف بأمر خاطئ فهرسًا كاملًا. والثانية ألّا يمحو عطبٌ عتاديٌّ أو نسخة سيّئة من الحقيبة ستّة أشهر من التّجميعات المحفوظة في Kibana واللّوحات الّتي بنتها Léa. تُرسي هذه الوحدة الأدوار ومفاتيح API وsnapshots والرّوتين اليوميّ للمراقبة الّتي تجعل عنقودًا يصمد على المدى الطّويل، سواء أمام حادث برمجيّ أو خطأ إنسانيّ.

ما تُؤمّنه الحقيبة أصلًا

افتح docker-compose.yml في الحقيبة واقرأ كتلة elasticsearch: الأمان مُفعَّل افتراضيًّا في المختبر، وهذا ما تُغفله كثير من الشّروحات القديمة. أربعة أسطر تكفي لفهم حال العنقود.

- xpack.security.enabled=true
- xpack.security.http.ssl.enabled=false
- xpack.security.transport.ssl.enabled=false
- ELASTIC_PASSWORD=${ELASTIC_PASSWORD}

يعني ذلك ثلاثة أمور. كلّ طلب مُصادَق عليه: من دون بيانات اعتماد، يردّ Elasticsearch بـ 401 security_exception. كلمة سرّ المستخدم الفائق هي المكتوبة في ملفّ .env (veille2026 افتراضيًّا)، تُضبَط عند أوّل تشغيل وتُحفَر في المُجلَّد es-data. HTTP يمرّ بلا تشفير: التبادلات بين veille-kibana وveille-python وveille-es غير مشفّرة، وهذا مقبول في شبكة Docker معزولة، لكنّه غير مقبول بمجرّد كشف المنفذ 9200 إلى الخارج.

تكمّل خدمة veille-setup الصّورة: تنتظر أن يصبح Elasticsearch healthy، ثمّ تستدعي واجهة /_security/user/kibana_system/_password لمواءمة كلمة السرّ الدّاخليّة للمستخدم النّظاميّ مع KIBANA_PASSWORD. لست إذن مضطرًّا يومًا إلى نسخ token يدويًّا بين الخدمتَين.

HTTP بلا TLS = مختبر فقط

تُرجّح الحقيبة سهولة القراءة: يعمل curl أو ./lab.sh es بلا شهادة. في الإنتاج، ينبغي التبديل إلى HTTPS. توفّر Elastic أداةً هي elasticsearch-certutil (داخل الصّورة: bin/elasticsearch-certutil ca ثمّ cert) تُنشئ سلطة التّصديق وشهادات العقدة، ثمّ يُفعَّل xpack.security.http.ssl.enabled=true وتُحمَّل الملفّات إلى المستوعب. تبقى هذه الدّورة على HTTP؛ خطوات التّبديل موثَّقة على elastic.co وتُطبَّق في ساعة واحدة.

إنشاء دور ومستخدم للقراءة فقط لكريم

لا تحتاج واجهة كريم إلّا إلى قراءة الفهرس news: لا كتابة أبدًا، ولا حذف، ولا مساس بالـmapping. نبني له دورًا مخصّصًا، ثمّ مستخدمًا يحمل هذا الدّور. افتح Kibana Dev Tools.

POST _security/role/veille_lecture
{
"cluster": ["monitor"],
"indices": [
{
"names": ["news"],
"privileges": ["read", "view_index_metadata"]
}
]
}

يُجيز read استعمال _search و_count و_msearch و_mget؛ ويُتيح view_index_metadata استرجاع الـmapping والإعدادات، وهذا لا غنى عنه لعميل يريد معرفة اسم الحقل الزّمنيّ. أمّا monitor على مستوى العنقود فيسمح بـ _cluster/health: مسبار حياة التّطبيق لن يحتاج إلى إعادة المصادقة بـ elastic.

POST _security/user/api_karim
{
"password": "karim2026",
"roles": ["veille_lecture"],
"full_name": "API Veille (Karim)",
"email": "karim@veille.example"
}

يجيب Elasticsearch بـ {"created": true}. تحقّق فورًا بندائَين من الطّرفيّة، داخل المستوعب veille-es الّذي يتضمّن curl:

docker exec veille-es curl -s -u api_karim:karim2026 \
http://localhost:9200/news/_count

الخرج المتوقّع:

{"count":200853,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0}}

القراءة تمرّ. اختبر الآن رفض الكتابة:

docker exec veille-es curl -s -u api_karim:karim2026 \
-H 'Content-Type: application/json' \
-X PUT http://localhost:9200/news/_doc/999999 \
-d '{"headline":"tentative","date":"2026-09-09"}'

الخرج المتوقّع:

{"error":{"root_cause":[{"type":"security_exception","reason":"action [indices:data/write/index] is unauthorized for user [api_karim] with effective roles [veille_lecture] on indices [news]"}],"status":403}}

403 security_exception: الدّور يفعل تمامًا ما طُلب منه. هذا هو الانعكاس الأوّل للاختبار كلّما أنشأت مستخدمًا — تحقّق ممّا يمرّ وممّا يجب أن يُرفَض.

سرد الأدوار والمستخدمين

يُعيد GET _security/role/veille_lecture وGET _security/user/api_karim التّعريف JSON كاملًا. ويسرد GET _security/_query/user جميع المستخدمين مع ترقيم الصّفحات. في Kibana، تُقدّم Management → Stack Management → Security → Users / Roles الشّيء نفسه بالفأرة.

مفاتيح API للمصادقة الآليّة

تعمل كلمة السرّ، لكنّ كلّ خدمة تستعملها تحتاج إلى معرفتها كاملة، وإبطالها نظيفًا يستلزم إعادة النّشر الشّاملة. تحلّ مفاتيح API المشكلة: تحصل كلّ خدمة على مفتاحها الخاصّ، بدور مرتبط وتاريخ انتهاء صلاحيّة. يُبطَل مفتاح واحد دون المساس بالباقي.

POST _security/api_key
{
"name": "api-karim-lecture",
"expiration": "90d",
"role_descriptors": {
"veille_lecture": {
"cluster": ["monitor"],
"indices": [
{
"names": ["news"],
"privileges": ["read", "view_index_metadata"]
}
]
}
}
}

يجيب Elasticsearch بكائن فيه ثلاثة حقول مهمّة: id وapi_key و**encoded**. يحوي حقل encoded سلفًا التّسلسل id:api_key مُرمَّزًا بـ Base64، جاهزًا للتّمرير في ترويسة HTTP.

{
"id" : "V0cU5oQBz2ExampleId",
"name" : "api-karim-lecture",
"expiration" : 1770000000000,
"api_key" : "abcDEF...",
"encoded" : "VjBjVTVvUUJ6MkV4YW1wbGVJZDphYmNERUY..."
}

يستدعي التّطبيق Elasticsearch بلصق قيمة encoded بعد ApiKey :

docker exec veille-es curl -s \
-H "Authorization: ApiKey VjBjVTVvUUJ6MkV4YW1wbGVJZDphYmNERUY..." \
http://localhost:9200/news/_search?size=1

أمران مفيدان يوميًّا. يسرد GET _security/api_key?owner=true المفاتيح الّتي أنشأتها أنت مع تاريخ انتهائها. ويُبطل DELETE _security/api_key بجسم {"ids": ["V0cU5oQBz2ExampleId"]} مفتاحًا مسرَّبًا فورًا.

Kibana: نظرة على الفضاءات (Spaces)

تُقدّم Kibana فضاءات — بمثابة مجلّدات معزولة للكائنات المحفوظة: لوحات القيادة، وData Views، ومرئيّات Lens. الوصول عبر Management → Stack Management → Spaces. يمكن لفريق Veille إنشاء فضاء «Léa» لا يضمّ إلّا لوحات العملاء، وفضاء «Sami» مخصّصًا لآفاق التّشغيل. يستطيع دور Kibana حصر مستخدم في فضاء أو أكثر بمستوى وصول مختلف (read على فضاء العملاء، all على الفضاء الدّاخليّ). فائدة الفضاءات مزدوجة: أمنيّة لأنّها تخفي على العميل تفاصيل التّشغيل، وتنظيميّة لأنّ كلّ فريق يجد لوحاته ولا يتيه بين عشرات الملفّات المشتركة. يخرج التّحكّم الدّقيق بالصّلاحيّات (RBAC) في Kibana عن نطاق هذه الوحدة؛ يكفي أن تعرف أنّه موجود ومجّانيّ في رخصة basic.

نسخ Elasticsearch احتياطيًّا بمستودع fs

يحفظ Elasticsearch البيانات تدريجيًّا في snapshot مُخزَّن في مستودع (repository). أبسط مستودع من نوع fs — مجرّد مسار محلّيّ. قاعدتان يجب معرفتهما قبل البدء.

  • يجب الإعلان عن المسار في العقدة عبر إعداد path.repo. من دون ذلك، يرفض PUT _snapshot/… الإنشاء.
  • يجب أن يكون المسار متاحًا لجميع العقد في عنقود متعدّد العقد. على عنقود بعقدة واحدة، يكفي مُجلَّد Docker محلّيّ.

إضافة المستودع إلى المستوعب

عدّل docker-compose.yml، وفي كتلة elasticsearch، أضف سطر بيئة والحجم المقابل:

services:
elasticsearch:
environment:
# ... الأسطر الموجودة ...
- path.repo=/usr/share/elasticsearch/snapshots
volumes:
- es-data:/usr/share/elasticsearch/data
- es-snapshots:/usr/share/elasticsearch/snapshots

volumes:
es-data:
es-snapshots:
# ... الباقي دون تغيير ...

أعِد تشغيل Elasticsearch دون محو البيانات:

./lab.sh down
./lab.sh up

سيمحو ./lab.sh reset الحجم es-data — وستفقد الفهرس news. أمّا down ثمّ up فيُحافظ على الحجوم.

لماذا down/up لا restart

لا يعيد restart مستوعبٍ قراءة إعدادات Compose، فلن يُؤخَذ path.repo بعين الاعتبار. أمّا ./lab.sh down ثمّ ./lab.sh up فيُعيد إنشاء المستوعب بالإعدادات الجديدة، مع الحفاظ على الحجوم الموجودة.

إنشاء المستودع وأوّل snapshot

من Kibana Dev Tools:

PUT _snapshot/veille_repo
{
"type": "fs",
"settings": {
"location": "/usr/share/elasticsearch/snapshots",
"compress": true
}
}

الجواب المتوقّع:

{"acknowledged":true}

أطلق أوّل snapshot، متزامنًا لرؤية النّتيجة فورًا:

PUT _snapshot/veille_repo/snap1?wait_for_completion=true
{
"indices": "news",
"include_global_state": false
}

على كامل مجموعة News، تستغرق النّسخة عشر ثوان تقريبًا وتُعيد:

{
"snapshot": {
"snapshot": "snap1",
"state": "SUCCESS",
"indices": ["news"],
"shards": {"total": 1, "failed": 0, "successful": 1}
}
}

أمران للمراقبة. يسرد GET _snapshot/veille_repo/_all الـsnapshots. ويُفصّل GET _snapshot/veille_repo/snap1/_status البايتات المنسوخة والمدّة.

الاستعادة إلى فهرس مُعاد تسميته

نادرًا ما نستعيد فوق فهرس موجود: خطر شديد. يقدّم Elasticsearch إسقاطًا مع rename_pattern وrename_replacement يُعيد تسمية الفهارس أثناء الاستعادة على الفور.

POST _snapshot/veille_repo/snap1/_restore
{
"indices": "news",
"rename_pattern": "news",
"rename_replacement": "news_restaure",
"include_global_state": false
}

تحقّق:

GET _cat/indices/news*?v

الخرج المتوقّع:

health status index          uuid ... docs.count store.size
green open news ... 200853 ...
green open news_restaure ... 200853 ...

فهرسان جنبًا إلى جنب، بالمستندات نفسها والـmapping نفسه. تستطيع المقارنة والتّحقّق، ثمّ تحويل اسم مستعار (alias) نحو news_restaure عبر _aliases (وردت في الوحدة 8) من دون قطع الخدمة.

نسخ Neo4j Community احتياطيًّا

يوفّر Neo4j Community أمرًا أصليًّا هو neo4j-admin database dump، يكتب ملفًّا ثنائيًّا. يجب إيقاف قاعدة البيانات لهذه الطّبعة؛ توفّر طبعة Enterprise dump على الهواء، لا نحن.

docker exec veille-neo4j cypher-shell -u neo4j -p veille2026 \
-d system "STOP DATABASE neo4j;"

docker exec veille-neo4j neo4j-admin database dump neo4j \
--to-path=/dumps

يُحوّل STOP DATABASE القاعدة إلى offline، ويكتب dump الملفَّ neo4j.dump في /dumps. ليكون هذا المسار متاحًا من الجهاز المضيف، ركّبه حجمًا في docker-compose.yml :

services:
neo4j:
volumes:
# ... الأسطر الموجودة ...
- ./neo4j/dumps:/dumps

(بعد التّعديل: ./lab.sh down ثمّ ./lab.sh up، كما في Elasticsearch.) أعِد تشغيل القاعدة:

docker exec veille-neo4j cypher-shell -u neo4j -p veille2026 \
-d system "START DATABASE neo4j;"

تتمّ الاستعادة بـ neo4j-admin database load neo4j --from-path=/dumps --overwrite-destination=true، والقاعدة موقوفة كذلك.

مستخدمو Neo4j Community

تعرف الطّبعة المجتمعيّة المستخدمين، لا الأدوار الدّقيقة. يمكنك إنشاء حساب وتغيير كلمة سرّه، لكنّ أيّ حساب يبقى في المستوى نفسه من الامتيازات ما دام مسجَّلًا في قاعدة النّظام. افتح cypher-shell على قاعدة system :

./lab.sh cypher-shell

ثمّ:

:use system
CREATE USER karim SET PASSWORD 'karim2026' CHANGE NOT REQUIRED;
ALTER USER karim SET PASSWORD 'karim2026-b' CHANGE NOT REQUIRED;
SHOW USERS;

الخرج المتوقّع:

+---------------------------------------------------------------+
| user | roles | passwordChangeRequired | suspended |
+---------------------------------------------------------------+
| "karim" | ["PUBLIC"] | FALSE | FALSE |
| "neo4j" | ["admin"] | FALSE | FALSE |
+---------------------------------------------------------------+
RBAC دقيق = Enterprise

إسناد دور لكريم يُخوّله قراءة تسميات معيّنة أو اجتياز علاقات محدّدة يستلزم GRANT MATCH { … } ON GRAPH … TO role، وهذا من طبعة Enterprise. على Community، الفصل الوحيد الممكن هو «admin» (يُنشأ عند التّشغيل) مقابل «PUBLIC» (وصول كامل إلى القاعدة افتراضيًّا). لعزل حقيقيّ في الإنتاج، خطّط للانتقال إلى Enterprise أو انقل منطق التّرخيص إلى واجهة التّطبيق.

المراقبة اليوميّة

ثلاثة أوامر تُشكّل لوحة قيادة سامي في الصّباح. يتّسع كلّ منها في سطر داخل Dev Tools.

GET _cat/nodes?v&h=name,heap.percent,ram.percent,cpu,disk.used_percent

الخرج المتوقّع:

name      heap.percent ram.percent cpu disk.used_percent
veille-es 27 82 3 41.7

يجب أن يبقى heap.percent دون 75 ٪ في الوضع الطّبيعيّ. فوق 85 ٪ بشكل دائم، تُبطئ عمليّات الجمع الاستعلامات؛ لا بدّ من رفع الـheap (المتغيّر ES_JAVA_OPTS في docker-compose.yml) أو تخفيف الحمل. ويجب أن يبقى disk.used_percent دون 85 ٪ : فوق ذلك، يُطبّق Elasticsearch تلقائيًّا عتبة flood_stage ويحوّل الفهارس إلى القراءة فقط (تُعطّل الحقيبة هذه العتبة للمختبر، لكنّها فعّالة في الإنتاج).

GET _cat/indices?v

الخرج المتوقّع:

health status index  pri rep docs.count store.size
green open news 1 0 200853 130mb

ثلاث قيم يجب مراقبتها لكلّ فهرس: health (green = سليم، yellow = نُسَخ ناقصة، red = shard رئيسيّ مفقود، الحالة تُعالَج في الوحدة 15)، وdocs.count (يتحرّك عند الكتابة)، وstore.size (ينمو مع الوقت؛ إن انتفخ فهرس بلا حدّ، فكّر في ILM).

GET _nodes/stats/jvm

خرج مفصَّل على الـJVM : mem.heap_used_in_bytes وgc.collectors.young.collection_time_in_millis. زمن GC young المتصاعد هو أوّل مؤشّر على heap ضيّق.

من ناحية المضيف، أمران يكمّلان:

docker stats --no-stream
./lab.sh status
./lab.sh logs elasticsearch --tail=50

يُعطي docker stats وحدة المعالجة، والذّاكرة، والشّبكة من زاوية Docker — مفيد لرصد مستوعب يستهلك أكثر من حدّه (mem_limit: 2g في compose عندنا). يُلخّص ./lab.sh status حال المستوعبات، ويتتبّع ./lab.sh logs سجلّات خدمة معيّنة.

ممارسات جيّدة لكلمات السرّ

ثلاث قواعد مستفادة من الأفواج السّابقة، مكتوبة بالحرف في env.example:

  • حروف وأرقام فقط في .env. سيؤوّل الصّدفة (shell) أيّ ! أو $ أو @ عند تصدير lab.sh للمتغيّرات، ولن تكون كلمة السرّ الّتي يستقبلها Elasticsearch هي المكتوبة في الملفّ.
  • طول أدنى معقول — اثنا عشر محرفًا في الإنتاج؛ كلمة سرّ الحقيبة (veille2026) اتّفاق تربويّ، يجب تغييرها قبل أيّ استعمال حقيقيّ.
  • ./lab.sh reset إلزاميّ بعد التّغيير. تُكتب كلمات السرّ في الحجم es-data عند أوّل تشغيل (لـ elastic) وفي neo4j-data (لـ neo4j). لا يستعيدها down/up وحده؛ يجب محو الحجوم بـ reset، وهذا يحذف أيضًا الفهرس news — فكّر في snapshot مسبق.

جرّب 1 — دور للأخصّائيّة Léa

أنشئ دورًا veille_analyse يُتيح لـ Léa قراءة الفهرس news وإنشاء كائناتها الخاصّة في Kibana وقراءتها وتعديلها. ثمّ أنشئ مستخدمًا lea بهذا الدّور وتحقّق من قدرتها على البحث في Dev Tools مع عجزها عن حذف الفهرس news.

الحلّ

تُعرِّف Kibana صلاحيّة مسمّاة على مجموع خصائص التّحليل (kibana-.kibana) تُركَّب من جانب الدّور. الأسهل هو إنشاء الدّور من الواجهة Stack Management → Roles → Create role بإضافة صلاحيّة Kibana Analytics: All. من Dev Tools، يمكن إنشاء دور Elasticsearch بالصّلاحيّات نفسها على الفهرس:

POST _security/role/veille_analyse
{
"cluster": ["monitor"],
"indices": [
{ "names": ["news"], "privileges": ["read", "view_index_metadata"] }
]
}

ثمّ المستخدم:

POST _security/user/lea
{
"password": "lea2026abcd",
"roles": ["veille_analyse", "kibana_admin"],
"full_name": "Léa Bernier"
}

الدّور المدمج kibana_admin يغطّي جانب Kibana. اختبار سلبيّ:

docker exec veille-es curl -s -u lea:lea2026abcd \
-X DELETE http://localhost:9200/news

المتوقّع:

{"error":{"type":"security_exception","reason":"action [indices:admin/delete] is unauthorized ..."},"status":403}

جرّب 2 — Snapshot قبل، استعادة بعد

أطلق snapshot باسم snap_avant للفهرس news وحده، احذف ثلاثة مستندات (DELETE news/_doc/1، 2، 3)، تحقّق من انخفاض _count، ثمّ استعِد فقط في news_restaure وقارِن العدّادَين.

الحلّ
PUT _snapshot/veille_repo/snap_avant?wait_for_completion=true
{ "indices": "news", "include_global_state": false }

DELETE news/_doc/1
DELETE news/_doc/2
DELETE news/_doc/3

GET news/_count

الجواب: {"count": 200850}.

POST _snapshot/veille_repo/snap_avant/_restore
{
"indices": "news",
"rename_pattern": "news",
"rename_replacement": "news_restaure",
"include_global_state": false
}

GET news_restaure/_count

الجواب: {"count": 200853}. استعادت العمليّة المستندات المحذوفة، من دون طمس فهرس الإنتاج. يكفي بعدها تحويل اسم مستعار نحو news_restaure أو إعادة حقن المستندات الثّلاثة الغائبة بـ _reindex مُرشَّح.

جرّب 3 — لوحة القيادة الصّباحيّة

اكتب سلسلة الاستعلامات الثّلاثة الّتي يُطلقها سامي كلّ صباح للتّحقّق من عمل Veille. يجب أن تتّسع في ثلاث كتل Dev Tools وتُغطّي: صحّة العنقود، الفهرس الرّئيسيّ، موارد العقدة.

الحلّ
GET _cluster/health
GET _cat/indices/news?v
GET _cat/nodes?v&h=name,heap.percent,ram.percent,cpu,disk.used_percent

ثلاثة أسطر نتيجة واضحة: status وdocs.count وheap.percent. يمكن لسامي حفظ هذه الاستعلامات الثّلاثة في سِجلّ Dev Tools (تبقى فيه) وإعادة تشغيلها بـ Ctrl+Entrée.

النقاط الأساسيّة

  • تُفعّل الحقيبة xpack.security.enabled=true وتُبقي HTTP بلا تشفير: خيار مختبر؛ في الإنتاج، فعِّل TLS بأداة elasticsearch-certutil.
  • أنشئ دورًا لكلّ استعمال (veille_lecture لواجهة API)، ثمّ مستخدمًا أو مفتاح API لكلّ خدمة؛ اختبر دائمًا ما يجب أن يمرّ وما يجب أن يُرفَض.
  • تُستعمل مفاتيح API (POST _security/api_key) مع الترويسة Authorization: ApiKey <encoded> ؛ يُبطَل المفتاح وحده بلا مساس بالباقي.
  • تستلزم snapshots في Elasticsearch الإعلان عن path.repo في إعدادات العقدة، ثمّ إنشاء مستودع PUT _snapshot/... ؛ تُجنّبك الاستعادة المُعاد تسميتها (rename_pattern/rename_replacement) طمسَ فهرس حيّ.
  • يُنسَخ Neo4j Community احتياطيًّا بأمر neo4j-admin database dump وقاعدته موقوفة؛ تبقى الأدوار الدّقيقة (RBAC) حكرًا على Enterprise.
  • المراقبة اليوميّة: _cat/nodes?v&h=... و_cat/indices?v و_nodes/stats/jvm وdocker stats و./lab.sh status و./lab.sh logs.
  • كلمات السرّ في .env : حروف وأرقام فقط؛ يستوجب أيّ تعديل ./lab.sh reset (فكّر في snapshot قبل ذلك).

استكشاف الأخطاء

  • يُعيد PUT _snapshot/veille_repo الخطأ repository_verification_exception → لم يُصرَّح path.repo في المستوعب أو أنّ الحجم غير مركَّب. راجع docker-compose.yml، ثمّ ./lab.sh down و./lab.sh up.
  • يُجيب neo4j-admin database dump بـ « database is not offline » → لم تُوقَف القاعدة. افتح cypher-shell على system وشغّل STOP DATABASE neo4j; قبل dump.
  • يُرجع curl -u api_karim:... الخطأ 401 security_exception → كلمة السرّ عُدِّلت في Dev Tools ولم تُحدَّث في الزّبون، أو نُسخت مع فراغ في النّهاية. أعِد POST _security/user/api_karim/_password لمواءمتها.
  • بعد تغيير ELASTIC_PASSWORD في .env، يرفض ./lab.sh up المصادقة → كلمة السرّ مكتوبة سلفًا في الحجم es-data. ./lab.sh reset ثمّ ./lab.sh up — بعد snapshot إن كنت تريد الاحتفاظ بـ news.

للاستزادة