> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# Kanidm وLLDAP: دليل مستخدمين خفيف بديل Active Directory
- URL: https://arabroot.io/articles/kanidm-و-lldap-بديل-خفيف-ل-active-directory/
- Published: 2026-10-25T20:56:31.000Z
- Updated: 2026-10-26T08:17:16.000Z
- Description: جربنا LLDAP وKanidm في Docker بمستخدمين عرب ومخرجات LDAP حقيقية، لترى متى يكفيك دليل خفيف مجاني بدلاً من Active Directory.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, الأمن السيبراني, الاستضافة الذاتية, لينكس والسيرفرات, Kanidm

لنفرض أن لديك مكتب استشارات هندسية في الخبر فيه خمسة عشر موظفاً، كلهم على أجهزة Mac وLinux، وتطبيقاته Nextcloud للملفات وGitea للمشاريع وGrafana لمراقبة السيرفرات وVPN للعمل من المواقع، وكلها تسأل «هل لديك LDAP؟». والإجابة المعتادة في المؤسسات هي Active Directory، ولكنه يعني سيرفر Windows ورخصاً لكل مستخدم وإدارة لا يحتاجها فريق لا يملك جهاز Windows واحداً. وفي [شرح LDAP ودليل المستخدمين](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-ldap-%D9%88%D8%AF%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D9%85%D8%B3%D8%AA%D8%AE%D8%AF%D9%85%D9%8A%D9%86/) بنينا الدليل على OpenLDAP ورأينا أنه مرن ولكنه يحتاج إلى خبرة في إعداداته. وفي هذا المقال نجرب بديلين خفيفين صمما لهذه الحالة بالضبط: LLDAP وKanidm.

ومسؤول التقنية في المكتب شخص واحد، يقسم وقته بين السيرفرات وأجهزة الموظفين والطابعات، ولذلك سؤاله ليس «أي الأداتين أقوى»، وإنما ثلاثة أسئلة عملية: أي الأداتين أستطيع أن أشغلها وأنساها؟ وكيف أغلق حساب مهندسة تغادر المكتب في نفس اليوم؟ وكيف أضيف المصادقة الثنائية التي طلبها العميل الحكومي للمشروع الجديد؟ وسوف نتابع هذه الأسئلة الثلاثة مع الأداتين، بتجربتين حقيقيتين على Docker، إحداهما لما يحتاجه التطبيق، والثانية لما يحدث في يوم المغادرة.

وفي هذا المقال سوف نتناول:

- ماذا يقدم Active Directory فعلاً، وما الذي يحتاجه فريق صغير منه.
- LLDAP: دليل LDAP بواجهة ويب في Container صغير، مع تجربة كاملة ومخرجات `ldapsearch`.
- Kanidm: مزود هوية ودليل معاً، مع OIDC ومفاتيح المرور وLDAPS للقراءة، وتجربة كاملة أيضاً.
- الأسماء العربية في الدليلين، والذاكرة والقرص كما قسناها.
- تكلفة البديل التجاري، ومتى تختار كل واحد.

## ماذا يقدم Active Directory، وماذا تحتاج منه؟

Active Directory ليس دليل LDAP فقط، فهو مجموعة خدمات مترابطة على سيرفر Windows يسمى متحكم النطاق Domain Controller: دليل المستخدمين والأجهزة، والمصادقة بـ Kerberos، وDNS الداخلي، وسياسات المجموعات Group Policy التي تفرض الإعدادات على أجهزة Windows، وضم الأجهزة إلى النطاق Domain Join. وإذا كانت مؤسستك تعمل على أجهزة Windows فهذه الخدمات هي السبب الحقيقي لوجوده، ولا يغني عنها LLDAP ولا Kanidm.

أما إذا كان كل ما تحتاجه من «AD» هو أن تسأل التطبيقات عن المستخدم وكلمة مروره ومجموعاته، فأنت تحتاج إلى جزء صغير منه فقط، وهنا يأتي البديلان. والصورة التالية تلخص ما رأيناه في التجربة:

![ثلاثة أعمدة: LLDAP دليل LDAP فقط بواجهة ويب على المنفذ 3890 وذاكرة قرابة 3 ميجابايت ولا يوجد SSO ولا MFA، وKanidm مزود هوية ودليل مع OAuth2 وOpenID Connect وWebAuthn وTOTP وLDAPS للقراءة فقط على 3636 وذاكرة قرابة 45 ميجابايت، وActive Directory مع Kerberos وLDAP وDNS وGPO وضم الأجهزة ورخص Windows Server وCAL](https://arabroot.io/content/images/2026/10/kanidm-lldap-01-map.webp)

LLDAP يجيب عن سؤال LDAP فقط، وKanidm يضيف الدخول الموحد، وActive Directory يدير أجهزة Windows

|                         | LLDAP                                                                                                                                          | Kanidm                                                                                                                   |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| الترخيص                 | [GPL-3.0](https://github.com/lldap/lldap/blob/main/LICENSE?ref=arabroot.io)                                                                    | [MPL-2.0](https://github.com/kanidm/kanidm/blob/master/LICENSE.md?ref=arabroot.io)                                       |
| الإصدار الذي جربناه     | [v0.6.3](https://github.com/lldap/lldap/releases/tag/v0.6.3?ref=arabroot.io) في 30 أبريل 2026، والتطوير مستمر في الفرع الرئيسي حتى سبتمبر 2026 | [1.11.2](https://github.com/kanidm/kanidm/releases/tag/v1.11.2?ref=arabroot.io) في 11 سبتمبر 2026، وقبله 1.11.1 في أغسطس |
| الذاكرة في تجربتنا      | قرابة 3 ميجابايت                                                                                                                               | قرابة 45 ميجابايت                                                                                                        |
| القرص في تجربتنا        | 152 كيلوبايت (SQLite وملف الإعداد)                                                                                                             | 2.3 ميجابايت (قاعدة البيانات والشهادات)                                                                                  |
| حجم الـ Image           | 97 ميجابايت                                                                                                                                    | 327 ميجابايت، وأدوات الإدارة في Image منفصلة                                                                             |
| LDAP                    | على 3890، والإدارة من الواجهة                                                                                                                  | LDAPS على 3636 للقراءة فقط                                                                                               |
| الدخول الموحد           | لا يوجد، ويوضع خلف authentik أو Authelia                                                                                                       | OAuth2 وOpenID Connect مدمجان                                                                                            |
| المصادقة متعددة العوامل | لا توجد                                                                                                                                        | WebAuthn وTOTP                                                                                                           |

### شكلان للبنية، وليس أداتين فقط

قبل أن ندخل في التفاصيل، لاحظ أن الاختيار بين LLDAP وKanidm ليس اختياراً بين أداتين متشابهتين، وإنما بين شكلين للبنية. LLDAP دليل فقط، فإذا أراد المكتب صفحة دخول موحدة ومصادقة ثنائية فسوف يضع أمامه مزود هوية مثل [Authelia](https://arabroot.io/articles/authelia-%D8%AF%D8%AE%D9%88%D9%84-%D9%85%D9%88%D8%AD%D8%AF-%D9%88%D9%85%D8%B5%D8%A7%D8%AF%D9%82%D8%A9-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%85%D8%A7%D9%85-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA%D9%83/) أو [authentik](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-authentik-%D9%84%D9%84%D8%AF%D8%AE%D9%88%D9%84-%D8%A7%D9%84%D9%85%D9%88%D8%AD%D8%AF/) يقرأ المستخدمين منه، أما Kanidm فيجمع الدليل ومزود الهوية في برنامج واحد. والصورة التالية تبين الشكلين:

![الشكل الأول: المستخدم يدخل إلى مزود الهوية authelia أو authentik الذي يعرض صفحة الدخول وMFA وOIDC ويقرأ المستخدمين من LLDAP عبر LDAP على المنفذ 3890، والتطبيقات Nextcloud وGitea عبر OIDC من المزود والتطبيق القديم عبر LDAP من LLDAP مباشرة. الشكل الثاني: Kanidm وحده فيه صفحة الدخول وWebAuthn وTOTP وOAuth2 وOpenID Connect والمستخدمون والمجموعات وLDAPS للقراءة على 3636 ودخول Linux عبر PAM، والتطبيقات كلها منه](https://arabroot.io/content/images/2026/10/kanidm-lldap-04-stacks.webp)

LLDAP يحتاج مزود هوية أمامه للدخول الموحد، وKanidm يقوم بالدورين

والفرق في التشغيل واضح: الشكل الأول برنامجان تحدثهما وتراقبهما وتنسخهما احتياطياً، ولكن كل منهما بسيط وله واجهة رسومية، والشكل الثاني برنامج واحد، ولكن إدارته من سطر الأوامر غالباً كما سنرى، ومفاهيمه تحتاج إلى قراءة قبل البدء. ولمكتب فيه مسؤول تقني واحد يعرف Docker جيداً ولا يحب سطر الأوامر الطويل، الشكل الأول أقرب، ولمن يريد أقل عدد من الأجزاء المتحركة ويقبل أن يتعلم أداة واحدة جيداً، الثاني أقرب.

## LLDAP: دليل LDAP بواجهة ويب

[LLDAP](https://github.com/lldap/lldap?ref=arabroot.io) مكتوب بلغة Rust، ويقول مشروعه صراحة إن هدفه ليس سيرفر LDAP كاملاً، وإنما واجهة LDAP مبسطة للمصادقة يكفيها أغلب التطبيقات، ومعها واجهة ويب لإدارة المستخدمين والمجموعات. والقرار المهم في تصميمه أن الإدارة تتم من الواجهة أو من واجهة GraphQL، وليس بأوامر LDAP.

### التشغيل

الإعداد كله في متغيرات البيئة: سر توقيع جلسات الواجهة، وبذرة مفتاح التشفير، وكلمة مرور المدير، والجذر Base DN:

```bash
docker network create dir-lab
docker volume create lldap-data
docker run -d --name lldap --network dir-lab --network-alias lldap.example.com -p 17170:17170 \
  -v lldap-data:/data \
  -e LLDAP_JWT_SECRET="$(openssl rand -hex 32)" \
  -e LLDAP_KEY_SEED="$(openssl rand -hex 32)" \
  -e LLDAP_LDAP_USER_PASS="$LLDAP_ADMIN_PW" \
  -e LLDAP_LDAP_BASE_DN=dc=example,dc=com \
  -e TZ=UTC \
  lldap/lldap:v0.6.3
```

في الإعداد أعلاه لاحظ التالي:

- `LLDAP_KEY_SEED` يولد منه مفتاح التشفير الخاص بالسيرفر، فإذا فقدته مع قاعدة البيانات فلن يستطيع أحد الدخول بكلمات المرور القديمة، لذلك احفظه مع النسخة الاحتياطية في مكان آمن.
- كلمة مرور المدير من متغير `LLDAP_ADMIN_PW` عندك في الشيل، ولا تكتبها في ملف Compose يرفع إلى Git.
- المنفذ 3890 لـ LDAP لم ننشره على الجهاز، فالتطبيقات تصل إليه عبر شبكة Docker فقط، والواجهة على 17170.

عند أول تشغيل ينشئ LLDAP قاعدة SQLite والمجموعات الأساسية وحساب المدير، والسجل يقول:

```text
WARN     ┝━ 🚧 [warn]: Could not find an admin user, trying to create the user "admin" with the config-provided password
INFO     ┝━ ｉ [info]: Successfully (re)set password for "admin"
INFO     ┝━ ｉ [info]: Starting the LDAP server on port 3890
INFO     ┕━ ｉ [info]: Starting the API/web server on port 17170
```

### المستخدمون والمجموعات والأسماء العربية

تستطيع أن تنشئ المستخدمين من الواجهة، ولكننا استخدمنا واجهة GraphQL لأنها ما سوف تستخدمه في سكربت التعيين. نحصل أولاً على توكن المدير، ثم ننشئ المستخدمة والمجموعة ونضيفها إليها:

```bash
U=http://localhost:17170
TOKEN=$(curl -s $U/auth/simple/login -H 'Content-Type: application/json' \
  -d "{\"username\":\"admin\",\"password\":\"$LLDAP_ADMIN_PW\"}" | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])")
gql() { curl -s $U/api/graphql -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json; charset=utf-8' --data-binary "$1"; echo; }
gql '{"query":"mutation { createUser(user: {id: \"sara\", email: \"sara@example.com\", displayName: \"سارة علي\", firstName: \"Sara\", lastName: \"Ali\"}) { id creationDate } }"}'
gql '{"query":"mutation { createGroup(name: \"developers\") { id displayName } }"}'
gql '{"query":"mutation { addUserToGroup(userId: \"sara\", groupId: 4) { ok } }"}'
```

```text
{"data":{"createUser":{"id":"sara","creationDate":"2026-10-25T20:58:31.913496228Z"}}}
{"data":{"createGroup":{"id":4,"displayName":"developers"}}}
{"data":{"addUserToGroup":{"ok":true}}}
```

ثم نضع كلمات المرور بالأداة `lldap_set_password` الموجودة داخل الـ Image، ونفعل الشيء نفسه لحساب الخدمة `svc-gitea` الذي أضفناه إلى المجموعة الجاهزة `lldap_strict_readonly` ليقرأ الدليل دون أن يعدله:

```bash
docker exec -it lldap /app/lldap_set_password --base-url http://localhost:17170 \
  --admin-username admin --admin-password "$LLDAP_ADMIN_PW" --username sara --password "$SARA_PW"
```

```text
Successfully changed sara's password
```

⚠️

في أول محاولة أرسلنا الاسم العربي من Git Bash على Windows مباشرة في سطر الأوامر، فوصل إلى LLDAP علامات استفهام `???? ???`، لأن Windows حول النص إلى ترميز النظام قبل أن يصل إلى curl. فإذا رأيت ذلك فاكتب جسم الطلب في ملف بترميز UTF-8 وأرسله بـ `--data-binary @file.json`، وهذا ما فعلناه ثم صححنا الاسمين بالعملية `updateUser`.

والواجهة تعرض الأسماء العربية كما هي، مع أن الواجهة نفسها بالإنجليزية فقط:

![شاشة Users في LLDAP 0.6.3 وفيها المستخدمون admin وomar وsara وsvc-gitea، وفي عمود Display name الاسمان عمر حسن وسارة علي بالعربية](https://arabroot.io/content/images/2026/10/kanidm-lldap-02-lldap-users.webp)

قائمة المستخدمين في LLDAP، والاسم المعروض بالعربية

### ماذا يرى التطبيق عبر LDAP؟

من Container آخر على نفس الشبكة فيه `ldap-utils`، نجرب أولاً دون حساب:

```bash
ldapsearch -x -LLL -H ldap://lldap.example.com:3890 -b dc=example,dc=com "(uid=sara)"
```

```text
ldap_bind: Inappropriate authentication (48)
	additional info: Anonymous bind not allowed
```

أي أن LLDAP يرفض القراءة دون دخول من البداية، بخلاف الإعداد الافتراضي الذي رأيناه في OpenLDAP على Debian. ثم بحساب الخدمة:

```bash
ldapsearch -x -LLL -H ldap://lldap.example.com:3890 -D uid=svc-gitea,ou=people,dc=example,dc=com -W \
  -b dc=example,dc=com "(uid=sara)" cn mail memberOf
```

```text
dn: uid=sara,ou=people,dc=example,dc=com
cn:: 2LPYp9ix2Kkg2LnZhNmK
mail: sara@example.com
memberOf: cn=developers,ou=groups,dc=example,dc=com

# pagedresults: estimate=1 cookie=
```

لاحظ السطر `cn:: 2LPYp9ix2Kkg2LnZhNmK` بنقطتين، فصيغة LDIF تكتب أي قيمة فيها أحرف غير ASCII بترميز base64، وهي «سارة علي» بعد فكها بـ `base64 -d`. والتطبيقات تقرأ القيمة الأصلية دون مشكلة، أما أنت فتحتاج إلى فكها عند القراءة بعينك. والمجموعة من جهتها:

```text
dn: cn=developers,ou=groups,dc=example,dc=com
cn: developers
entryuuid: 5e9fd69d-1e79-3283-aa3b-b3a03f126429
member: uid=sara,ou=people,dc=example,dc=com
objectclass: groupOfUniqueNames
objectclass: groupOfNames
uid: developers
uniquemember: uid=sara,ou=people,dc=example,dc=com

# pagedresults: estimate=1 cookie=
```

ويعرض LLDAP المجموعة بصنفين `groupOfNames` و`groupOfUniqueNames` معاً، حتى يعمل مع التطبيقات التي تتوقع أياً منهما. ثم الربط بكلمة مرور سارة، ثم بكلمة خاطئة:

```text
Result: Success (0)
Additional info: dn:uid=sara,ou=people,dc=example,dc=com
ldap_bind: Invalid credentials (49)
```

وأخيراً، ماذا لو حاول التطبيق أن يعدل البريد عبر LDAP، حتى بحساب المدير؟

```text
ldap_modify: Server is unwilling to perform (53)
	additional info: Unsupported operation: `Replace` for `mail`
modifying entry "uid=sara,ou=people,dc=example,dc=com"
```

وهذا متوقع من تصميم LLDAP، فالتعديل من الواجهة أو GraphQL، وهذا يكفي لأغلب التطبيقات لأنها تقرأ ولا تكتب، ولكن انتبه إذا كان لديك تطبيق يتوقع أن يكتب في الدليل.

### صفات خاصة بالمكتب، وفلتر كما يكتبه Nextcloud

المكتب يريد أن يعرف التطبيق قسم كل موظف، والقسم ليس من الصفات الأساسية في LLDAP. ومن الإصدار 0.6 يسمح LLDAP بإضافة صفات خاصة User Attributes إلى المخطط من الواجهة أو من GraphQL. وفي الجلسة الثانية من التجربة أعدنا بناء الدليل من الصفر، وأرسلنا الطلبات من سكربت Python صغير يكتب جسم كل طلب بترميز UTF-8، حتى لا تتكرر مشكلة الأسماء العربية التي ذكرناها:

```python
# lldap2.py: second LLDAP session over GraphQL; every request body is UTF-8 JSON
import json, sys, urllib.request
U = 'http://localhost:18805/api/graphql'
TOKEN = open(sys.argv[1]).read().strip()
def gql(q, v=None):
    body = json.dumps({'query': q, 'variables': v or {}}, ensure_ascii=False).encode('utf8')
    r = urllib.request.Request(U, body, {'Authorization': 'Bearer ' + TOKEN, 'Content-Type': 'application/json; charset=utf-8'})
    out = urllib.request.urlopen(r).read().decode('utf8')
    print(out)
gql('mutation { addUserAttribute(name: "department", attributeType: STRING, isList: false, isVisible: true, isEditable: false) { ok } }')
gql('mutation($u: CreateUserInput!) { createUser(user: $u) { id } }',
    {'u': {'id': 'sara', 'email': 'sara@example.com', 'displayName': 'سارة علي', 'firstName': 'Sara', 'lastName': 'Ali',
           'attributes': [{'name': 'department', 'value': ['legal']}]}})
gql('mutation($u: CreateUserInput!) { createUser(user: $u) { id } }',
    {'u': {'id': 'svc-nextcloud', 'email': 'svc-nextcloud@example.com', 'displayName': 'Nextcloud service'}})
gql('mutation { createGroup(name: "nextcloud_users") { id } }')
gql('{ groups { id displayName } }')
```

```text
{"data":{"addUserAttribute":{"ok":true}}}
{"data":{"createUser":{"id":"sara"}}}
{"data":{"createUser":{"id":"svc-nextcloud"}}}
{"data":{"createGroup":{"id":4}}}
{"data":{"groups":[{"id":1,"displayName":"lldap_admin"},{"id":2,"displayName":"lldap_password_manager"},{"id":3,"displayName":"lldap_strict_readonly"},{"id":4,"displayName":"nextcloud_users"}]}}
```

ثم أضفنا سارة إلى المجموعة `nextcloud_users` وحساب الخدمة `svc-nextcloud` إلى `lldap_strict_readonly`، وبحثنا بنفس الطريقة التي يبحث بها Nextcloud عندما تعطيه فلتراً يحصر الدخول في مجموعة واحدة:

```bash
ldapsearch -x -LLL -o ldif_wrap=no -H ldap://lldap.example.com:3890 \
  -D uid=svc-nextcloud,ou=people,dc=example,dc=com -W -b ou=people,dc=example,dc=com \
  "(&(objectClass=person)(memberOf=cn=nextcloud_users,ou=groups,dc=example,dc=com))" uid mail department cn
```

```text
dn: uid=sara,ou=people,dc=example,dc=com
cn:: 2LPYp9ix2Kkg2LnZhNmK
department: legal
mail: sara@example.com
uid: sara

# pagedresults: estimate=1 cookie=
```

في المخرج أعلاه لاحظ التالي:

- الصفة الجديدة `department: legal` تظهر في LDAP مباشرة باسمها، فيستطيع أي تطبيق أن يقرأها دون أي إعداد إضافي.
- الفلتر `memberOf` يعمل في LLDAP دون أي إضافة، بخلاف OpenLDAP الذي احتاج فيه إلى تحميل الإضافة memberof بنفسنا.
- السطر الأخير `pagedresults` يعني أن LLDAP يدعم تقسيم النتائج إلى صفحات Paged Results، وهو ما تطلبه بعض التطبيقات عندما يكون عدد المستخدمين كبيراً.

#### كيف يقرأ التطبيق هذا المدخل؟

قد تبدو مخرجات LDAP مجرد قائمة، ولكن كل سطر فيها قرار يتخذه التطبيق. ففي المدخل الكامل الذي رأيناه في الجلسة الأولى ظهرت صفات لا تجدها في OpenLDAP بنفس الأسماء، مثل `first_name` و`last_name` بجانب `givenname` و`sn` القياسيتين، لأن LLDAP يعرض الاسمين معاً حتى يعمل مع التطبيقات التي تتوقع أياً منهما. والصفة `entryuuid` هي المعرف الثابت الذي لا يتغير إذا تغير اسم المستخدم، وهي ما يجب أن تربط بها التطبيقات حساباتها إن استطاعت، لأن الاسم قد يتغير عند تصحيح خطأ إملائي أو زواج أو تغيير في سياسة الأسماء. و`createtimestamp` وقت إنشاء الحساب، وهو مفيد في المراجعة الدورية لتعرف الحسابات الجديدة منذ آخر مراجعة. أما الأصناف `inetOrgPerson` و`posixAccount` و`mailAccount` و`person` فهي التي يبحث بها التطبيق عادة، فإذا كتبت في Nextcloud فلتراً بالصنف `inetOrgPerson` أو `person` وجد المستخدمين في الحالتين. والدرس العملي أن تفتح دائماً مخرج `ldapsearch` لمستخدم واحد قبل أن تكتب إعداد LDAP في أي تطبيق، فهو يخبرك بأسماء الصفات الحقيقية بدلاً من أن تخمنها من توثيق تطبيق كتب لـ Active Directory.

### يوم المغادرة في LLDAP: لا يوجد زر تعطيل

الآن السؤال الثاني للمكتب: سارة تغادر اليوم. في OpenLDAP رأينا القفل بـ ppolicy، وفي LLDAP بحثنا عن خيار مشابه في الواجهة فلم نجده، فسألنا واجهة GraphQL نفسها عن كل العمليات التي تقبلها، وعن الحقول التي يقبلها تعديل المستخدم:

```text
['createUser', 'createGroup', 'createGroupWithDetails', 'updateUser', 'updateGroup', 'addUserToGroup', 'removeUserFromGroup', 'deleteUser', 'deleteGroup', 'addUserAttribute', 'addGroupAttribute', 'deleteUserAttribute', 'deleteGroupAttribute', 'addUserObjectClass', 'addGroupObjectClass', 'deleteUserObjectClass', 'deleteGroupObjectClass']
['id', 'email', 'displayName', 'firstName', 'lastName', 'avatar', 'removeAttributes', 'insertAttributes']
```

لا توجد عملية لتعطيل الحساب، ولا حقل للحالة في تعديل المستخدم، فالخيارات الوحيدة هي حذف المستخدم، أو حذفه من المجموعات، أو تغيير كلمة مروره. فجربنا الطريقة التي تبدو أخف، وهي حذف سارة من `nextcloud_users`، ثم أعدنا نفس البحث، فلم يرجع أي مستخدم. ولكن عندما جربنا كلمة مرور سارة نفسها:

```bash
ldapwhoami -x -H ldap://lldap.example.com:3890 -D uid=sara,ou=people,dc=example,dc=com -W
```

```text
Result: Success (0)
Additional info: dn:uid=sara,ou=people,dc=example,dc=com
```

كلمة المرور ما زالت تعمل، وهذا يعني أن أي تطبيق يتحقق من كلمة المرور عبر LLDAP دون أن يفلتر بالمجموعة، مثل VPN مربوط بـ LDAP بإعداد بسيط، سوف يقبلها. لذلك يجب أن تكون المغادرة في LLDAP حذفاً للحساب أو تغييراً لكلمة مروره إلى قيمة عشوائية، مع إنهاء جلساتها في مزود الهوية الذي أمامه، ومع إغلاق ما لا يمر على LDAP أصلاً، مثل مفاتيح SSH والرموز Tokens داخل Gitea، كما شرحنا في [دورة حياة الحساب JML](https://arabroot.io/articles/%D8%AF%D9%88%D8%B1%D8%A9-%D8%AD%D9%8A%D8%A7%D8%A9-%D8%AD%D8%B3%D8%A7%D8%A8-%D8%A7%D9%84%D9%85%D9%88%D8%B8%D9%81-jml/).

### التشفير وكلمات المرور في LLDAP

في التجربة تحدث التطبيق مع LLDAP على المنفذ 3890 دون تشفير، لأن الحاويتين على نفس شبكة Docker الداخلية. ولكن الربط البسيط Simple Bind يرسل كلمة مرور الموظف كما هي، فإذا كان VPN المكتب أو أي تطبيق على سيرفر آخر يتحدث مع LLDAP عبر الشبكة، فيجب أن يكون الاتصال مشفراً. وملف الإعداد الذي ينشئه LLDAP عند أول تشغيل في `/data/lldap_config.toml` يحمل قسماً لذلك، وهذا نصه كما وجدناه:

```ini
[ldaps_options]
## Whether to enable LDAPS.
#enabled=true
## Port on which to listen.
#port=6360
## Certificate file.
#cert_file="/data/cert.pem"
## Certificate key file.
#key_file="/data/key.pem"
```

أي أن LDAPS معطل افتراضياً، ويفعل بشهادة ومفتاح على المنفذ 6360، ويمكن ضبط كل خيار بمتغير بيئة بالصيغة التي يذكرها الملف نفسه، مثل `LLDAP_LDAPS_OPTIONS__PORT`. وفي نفس الملف قسم `[smtp_options]` فيه الخيار `enable_password_reset`، وهو الذي يجعل الموظف يطلب رابط إعادة تعيين كلمة المرور بنفسه من صفحة الدخول بدلاً من أن يطلبها من المسؤول، بشرط أن تعد بيانات سيرفر البريد. وقاعدة البيانات نفسها يحددها السطر `database_url`، وهو SQLite افتراضياً، ويمكن أن يكون MySQL أو PostgreSQL بحسب صفحة المشروع، وهذا مفيد إذا أردت أن تنسخ LLDAP احتياطياً مع قاعدة بيانات تديرها أصلاً.

أما كلمات المرور فيحفظها LLDAP بطريقة تصفها صفحة المشروع بأنها إثبات دون معرفة Zero-knowledge Proof، والنتيجة العملية التي تذكرها الصفحة أن الخدمات التي تحتاج إلى قراءة هاش كلمة المرور نفسه لتتحقق منه محلياً لا تعمل معه. فإذا كان لديك تطبيق قديم يطلب صفة `userPassword` ليقارنها بنفسه، فاختبره قبل أن تقرر، لأن LLDAP لن يعطيه إياها.

### النسخ الاحتياطي والتحديث

كل بيانات LLDAP في التجربة كانت في ملف SQLite واحد `/data/users.db` بحجم 139264 بايت، ومعه ملف الإعداد. والنسخ الاحتياطي هو نسخ هذا الملف بعد إيقاف الـ Container أو بأداة SQLite التي تنسخ قاعدة بيانات تعمل، ومعه شيء لا يجب أن تنساه: قيمة `LLDAP_KEY_SEED`، لأن المفتاح الذي يحمي كلمات المرور يولد منها، والنسخة الاحتياطية دونها لا تفتح كلمات المرور. أما التحديث فهو تغيير رقم الإصدار في الـ Image، ويحدث LLDAP مخطط قاعدة البيانات وحده عند التشغيل كما رأينا في السجل عند أول تشغيل، فخذ نسخة من الملف قبل كل تحديث.

### نقاط القوة ونقاط الضعف

- **نقاط القوة:** صغير جداً، وإعداده في دقائق، وواجهته واضحة لمن لا يعرف LDAP، ويرفض القراءة دون دخول من البداية، ويعمل مع SQLite أو MySQL أو PostgreSQL، ويرسل رابط إعادة تعيين كلمة المرور بالبريد إذا أعددت SMTP.
- **نقاط الضعف:** ليس مزود هوية، فلا يوجد دخول موحد ولا MFA، ولذلك يستخدم غالباً خلف authentik أو Authelia اللذين يقرآن منه المستخدمين ويضيفان MFA. والتعديل عبر LDAP محدود، والإصدار المستقر الأخير من أبريل 2026، والخدمات التي تحتاج إلى هاش كلمة المرور نفسها لا تعمل معه لأن طريقة تخزينه لا تسمح بذلك بحسب صفحة المشروع.

## Kanidm: مزود هوية ودليل في برنامج واحد

[Kanidm](https://kanidm.github.io/kanidm/stable/?ref=arabroot.io) أيضاً بلغة Rust، ولكن فكرته مختلفة: هو مزود هوية كامل فيه OAuth2 وOpenID Connect ومفاتيح المرور WebAuthn وTOTP، ودخول Linux عبر PAM، وRADIUS لشبكات Wi-Fi، ويعرض بياناته عبر LDAPS للقراءة فقط للتطبيقات القديمة، كما في [قائمة الميزات](https://kanidm.github.io/kanidm/stable/supported%5Ffeatures.html?ref=arabroot.io). وكلمات المرور فيه مخزنة بـ Argon2id. أي أنه يجمع ما يفعله authentik وLLDAP معاً.

### مفاهيم Kanidm التي تحتاجها قبل أول أمر

أغلب الوقت الذي يضيعه من يجرب Kanidm أول مرة يذهب في مفاهيم لا تشبه ما تعوده من LDAP أو authentik، لذلك نمر عليها كما رأيناها في التجربة:

- **الحسابان الإداريان:** `admin` يدير النظام نفسه، مثل إعدادات النطاق، و`idm_admin` يدير الأشخاص والمجموعات والتطبيقات. والفصل مقصود، فالمسؤول الذي يضيف موظفاً كل أسبوع لا يحتاج صلاحية تغيير النطاق، وكل أوامرنا في التجربة كانت بحساب `idm_admin`.
- **الاسم الكامل spn:** كل مدخل في Kanidm له اسم بالشكل `sara@idm.example.com`، وهو ما يظهر في DN في LDAPS وفي سجلات الدخول، ويقابل ما يسميه Kerberos Principal.
- **الأشخاص Persons وحسابات الخدمة Service Accounts:** الشخص إنسان يدخل بكلمة مرور ومفتاح مرور، وحساب الخدمة لتطبيق يتصل بتوكن API، ولا يخلط بينهما، فلا يوجد «مستخدم وهمي» لكل تطبيق كما في LLDAP.
- **المجموعات الداخلية:** رأينا في مخرج `person get` أن سارة عضو تلقائياً في `idm_all_persons` و`idm_all_accounts`، وهذه مجموعات يستخدمها Kanidm نفسه في صلاحياته، والمجموعات التي تنشئها أنت مثل `developers` تظهر في `directmemberof`.
- **POSIX:** خصائص Linux مثل `gidnumber` لا تضاف إلا عندما تفعلها لشخص أو مجموعة، ومعها كلمة مرور منفصلة لدخول Linux وLDAPS، كما سنرى.
- **صلاحية الحساب Validity:** لكل شخص تاريخ يبدأ منه وتاريخ ينتهي عنده، ونستخدمها في يوم المغادرة.

وهذه المفاهيم هي ما يجعل Kanidm أدق من LLDAP في الأمان، وهي نفسها ما يجعل منحنى التعلم أطول، لذلك خصص لها ساعة قراءة في [كتاب Kanidm](https://kanidm.github.io/kanidm/stable/?ref=arabroot.io) قبل أن تثبته في مكتب حقيقي.

### التشغيل: TLS من أول دقيقة

Kanidm لا يعمل دون TLS ولا حتى في التجربة، فالإعداد يشترط الشهادة والمفتاح. وهذا ملف `server.toml` الذي استخدمناه، مبنياً على [صفحة إعداد السيرفر](https://kanidm.github.io/kanidm/stable/server%5Fconfiguration.html?ref=arabroot.io):

```toml
version = "2"
bindaddress = "0.0.0.0:8443"
ldapbindaddress = "0.0.0.0:3636"
domain = "idm.example.com"
origin = "https://idm.example.com:18806"
db_path = "/data/kanidm.db"
tls_chain = "/data/chain.pem"
tls_key = "/data/key.pem"
log_level = "info"
```

ثم ننشئ الـ Container دون تشغيله، وننسخ الإعداد، ونولد شهادة تجريبية بالأمر `cert-generate`، كما في [دليل البداية السريعة](https://kanidm.github.io/kanidm/stable/evaluation%5Fquickstart.html?ref=arabroot.io):

```bash
docker volume create kanidm-data
docker create --name kanidm --network dir-lab --network-alias idm.example.com -p 18806:8443 -v kanidm-data:/data kanidm/server:1.11.2
docker cp server.toml kanidm:/data/server.toml
docker run --rm -v kanidm-data:/data kanidm/server:1.11.2 kanidmd cert-generate
docker start kanidm
```

```text
INFO     ｉ [info]: Running in certificate generate mode ...
INFO     ｉ [info]: certificate generation complete
...
INFO     ｉ [info]: Starting LDAPS interface ldaps://0.0.0.0:3636 ...
INFO     ｉ [info]: Ready to rock! 🪨  UI available at: https://idm.example.com:18806/ | event_tag_id: 3
```

في الإعداد أعلاه لاحظ التالي:

- `domain` و`origin` يجب أن يتطابقا، والـ origin هو العنوان الذي يكتبه المستخدم في المتصفح، لأن مفاتيح المرور WebAuthn مربوطة به، ولذلك وضعنا فيه المنفذ 18806 الذي نشرناه على الجهاز، وفتحنا الواجهة بالاسم `idm.example.com` وليس `localhost`.
- `ldapbindaddress` معطل افتراضياً، وفعلناه لنجرب LDAPS، فإذا لم يكن عندك تطبيق يحتاج LDAP فاتركه معطلاً.
- الشهادة من `cert-generate` للتجربة فقط، وفي الإنتاج ضع شهادة حقيقية من Let's Encrypt أو من جهة التوقيع الداخلية.
- السجل حذر من أن الملفات في `/data` مقروءة للجميع وأن الخدمة تعمل كـ root، فاضبط صلاحيات المجلد وشغله بمستخدم غير root في الإنتاج.

### حسابا الإدارة، وأداة سطر الأوامر

في Kanidm حسابان للإدارة: `admin` لإعدادات النظام، و`idm_admin` لإدارة الأشخاص والمجموعات، ونحصل على كلمة مرور كل منهما بالأمر `recover-account`، والمخرج يطبعها مرة واحدة فاحفظها في مدير كلمات المرور:

```bash
docker exec -it kanidm kanidmd recover-account admin
docker exec -it kanidm kanidmd recover-account idm_admin
```

والإدارة اليومية من أداة `kanidm` في الـ Image المنفصلة `kanidm/tools:1.11.2`، ونخبرها بعنوان السيرفر وبجهة التوقيع في الملف `~/.config/kanidm`:

```toml
uri = "https://idm.example.com:8443"
ca_path = "/kdata/ca.pem"
```

```bash
kanidm login --name idm_admin
kanidm person create sara "سارة علي" -D idm_admin
kanidm person update sara --mail sara@example.com -D idm_admin
kanidm group create developers -D idm_admin
kanidm group add-members developers sara -D idm_admin
kanidm person get sara -D idm_admin
```

```text
Login Success for idm_admin@idm.example.com
Successfully created display_name="سارة علي" username=sara
Success
Successfully created group 'developers'
Successfully added ["sara"] to group "developers"
---
class: account
class: memberof
class: object
class: person
directmemberof: idm_all_persons@idm.example.com
directmemberof: idm_all_accounts@idm.example.com
directmemberof: developers@idm.example.com
displayname: سارة علي
mail: sara@example.com
memberof: idm_all_persons@idm.example.com
memberof: idm_all_accounts@idm.example.com
memberof: idm_people_self_name_write@idm.example.com
memberof: developers@idm.example.com
name: sara
spn: sara@idm.example.com
uuid: 019a0fbd-5ee3-4f1c-b0fd-6312fae2f40f
```

ولاحظ الفرق بين `directmemberof` وهي المجموعات التي أضفتها أنت، و`memberof` التي تضم معها المجموعات التي ينتمي إليها كل شخص تلقائياً، مثل `idm_all_persons`. والصورة التالية من لوحة الإدارة في الواجهة:

![شاشة Person Management في واجهة Kanidm للنطاق idm.example.com وفيها المستخدمان omar وsara مع معرف Uuid لكل منهما، وفي القائمة الجانبية Persons وGroups وOauth2](https://arabroot.io/content/images/2026/10/kanidm-lldap-03-kanidm-persons.webp)

لوحة إدارة الأشخاص في Kanidm 1.11.2

### LDAPS في Kanidm، وكلمة المرور المنفصلة

هنا أهم فرق عملي: كلمة المرور التي يدخل بها الشخص إلى الواجهة ليست هي التي يستخدمها في LDAP. فلكي يربط شخص عبر LDAP يجب أن تفعل له خصائص POSIX ثم تضع له كلمة مرور خاصة بها:

```bash
kanidm person posix set sara -D idm_admin
kanidm group posix set developers -D idm_admin
kanidm person posix set-password sara -D idm_admin
```

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

```bash
export LDAPTLS_CACERT=/etc/ldap/certs/kanidm-ca.pem
ldapsearch -x -LLL -o ldif_wrap=no -H ldaps://idm.example.com:3636 -b dc=idm,dc=example,dc=com "(name=sara)"
```

```text
dn: spn=sara@idm.example.com,dc=idm,dc=example,dc=com
class: account
class: memberof
class: object
class: person
class: posixaccount
directmemberof: spn=idm_all_persons@idm.example.com,dc=idm,dc=example,dc=com
directmemberof: spn=idm_all_accounts@idm.example.com,dc=idm,dc=example,dc=com
directmemberof: spn=developers@idm.example.com,dc=idm,dc=example,dc=com
displayname:: 2LPYp9ix2Kkg2LnZhNmK
gidnumber: 2061693967
memberof: spn=idm_all_persons@idm.example.com,dc=idm,dc=example,dc=com
memberof: spn=idm_all_accounts@idm.example.com,dc=idm,dc=example,dc=com
memberof: spn=idm_people_self_name_write@idm.example.com,dc=idm,dc=example,dc=com
memberof: spn=developers@idm.example.com,dc=idm,dc=example,dc=com
name: sara
spn: sara@idm.example.com
uuid: 019a0fbd-5ee3-4f1c-b0fd-6312fae2f40f
```

في المخرج أعلاه لاحظ التالي:

- الجذر مأخوذ من `domain` تلقائياً: `dc=idm,dc=example,dc=com`، وDN الشخص مبني على `spn` وليس `uid`.
- القراءة دون دخول مسموحة، ولكن البريد `mail` لم يظهر، فـ Kanidm يعرض للمجهول الصفات العامة فقط.
- `gidnumber` أنشأه Kanidm تلقائياً عند تفعيل POSIX، وهو ما يحتاجه دخول Linux.
- الاسم العربي في `displayname` بترميز base64 كما رأينا في LLDAP.

ثم الربط بالاسم القصير مباشرة، وهي ميزة مريحة لأن التطبيق لا يحتاج إلى البحث عن DN أولاً، ثم بكلمة خاطئة، ثم محاولة تعديل:

```bash
ldapwhoami -x -H ldaps://idm.example.com:3636 -D sara -W
```

```text
u: sara@idm.example.com
ldap_bind: Invalid credentials (49)
ldap_result: Protocol error (2)
modifying entry "spn=sara@idm.example.com,dc=idm,dc=example,dc=com"
```

والتعديل رجع بخطأ بروتوكول، لأن LDAP في Kanidm للقراءة فقط بحسب تصميمه، وكل تعديل يمر عبر الأداة أو الواجهة أو واجهة REST. وجربنا أيضاً LDAP دون تشفير على نفس المنفذ فرد الكلاينت بـ `Can't contact LDAP server (-1)`، فلا يوجد في Kanidm مسار غير مشفر أصلاً.

### Kanidm كمزود OpenID Connect لـ Nextcloud

السؤال الثالث للمكتب كان المصادقة الثنائية للمشروع الحكومي، وهنا يظهر الفرق الحقيقي: Kanidm لا يحتاج مزود هوية أمامه. ففي الجلسة الثانية أنشأنا لـ Nextcloud عميل OAuth2، وحصرنا الدخول فيه بمجموعة، بهذا السكربت الذي شغلناه داخل حاوية الأدوات:

```bash
#!/bin/sh
# k2.sh: second Kanidm session: a person, an OAuth2 client for Nextcloud, discovery, expiry, reset token
export NO_COLOR=1
kanidm login --name idm_admin >/dev/null 2>&1
kanidm login --name admin >/dev/null 2>&1
echo '== person'
kanidm person create sara "Sara Ali" -D idm_admin 2>&1 | grep -v permissions
kanidm person update sara --mail sara@example.com -D idm_admin 2>&1 | grep -v permissions
kanidm group create nextcloud_users -D idm_admin 2>&1 | grep -v permissions
kanidm group add-members nextcloud_users sara -D idm_admin 2>&1 | grep -v permissions
echo '== reset token'
kanidm person credential create-reset-token sara 3600 -D idm_admin 2>&1 | grep -v permissions | grep -vi 'qr\|█\|▄\|▀' | head -8
echo '== oauth2'
kanidm system oauth2 create nextcloud "Nextcloud" https://cloud.example.com -D idm_admin 2>&1 | grep -v permissions
kanidm system oauth2 add-redirect-url nextcloud https://cloud.example.com/apps/user_oidc/code -D idm_admin 2>&1 | grep -v permissions
kanidm system oauth2 update-scope-map nextcloud nextcloud_users openid profile email -D idm_admin 2>&1 | grep -v permissions
kanidm system oauth2 get nextcloud -D idm_admin 2>&1 | grep -v permissions
echo '== expiry'
kanidm person validity show sara -D idm_admin 2>&1 | grep -v permissions
kanidm person validity expire-at sara 2026-10-26T12:00:00+03:00 -D idm_admin 2>&1 | grep -v permissions
kanidm person validity show sara -D idm_admin 2>&1 | grep -v permissions
```

وهذا جزء العميل من المخرج:

```text
---
class: account
class: key_object
class: key_object_internal
class: key_object_jwe_a128gcm
class: key_object_jwt_es256
class: memberof
class: oauth2_resource_server
class: oauth2_resource_server_basic
class: object
displayname: Nextcloud
key_internal_data: 5e70f528e1f8: valid jws_es256 0
key_internal_data: e310c802f63e: valid jwe_a128gcm 0
name: nextcloud
oauth2_rs_basic_secret: hidden
oauth2_rs_origin: https://cloud.example.com/apps/user_oidc/code
oauth2_rs_origin_landing: https://cloud.example.com/
oauth2_rs_scope_map: nextcloud_users@idm.example.com: {"email", "openid", "profile"}
oauth2_strict_redirect_uri: true
spn: nextcloud@idm.example.com
uuid: 16efd64f-74b4-492d-bac1-1d9689da47ce
```

في المخرج أعلاه لاحظ التالي:

- `oauth2_rs_scope_map` يربط المجموعة `nextcloud_users` بالنطاقات Scopes المسموحة، ومن ليس في المجموعة لا يحصل على أي توكن لهذا التطبيق، أي أن قاعدة «من يدخل إلى Nextcloud» مكتوبة في Kanidm نفسه، وليست فلتراً في كل تطبيق.
- `oauth2_strict_redirect_uri: true` يعني أن Kanidm يقبل عنوان العودة المسجل حرفاً بحرف فقط، وهي الحماية التي شرحنا سببها في [شرح OAuth وOpenID Connect](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-oauth-%D9%88-openid-connect-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/).
- `oauth2_rs_basic_secret: hidden` فالأداة لا تعرض السر في المخرج العادي، ويعرض بأمر منفصل عندما تحتاج أن تنسخه إلى Nextcloud.
- `key_internal_data` يبين أن لكل عميل مفاتيحه الخاصة: ES256 للتوقيع وA128GCM للتشفير.

ثم طلبنا رابط الاكتشاف Discovery الذي يكفي Nextcloud ليعرف كل شيء آخر:

```bash
curl -sk https://idm.example.com:18806/oauth2/openid/nextcloud/.well-known/openid-configuration
```

```text
issuer : https://idm.example.com:18806/oauth2/openid/nextcloud
authorization_endpoint : https://idm.example.com:18806/ui/oauth2
token_endpoint : https://idm.example.com:18806/oauth2/token
userinfo_endpoint : https://idm.example.com:18806/oauth2/openid/nextcloud/userinfo
jwks_uri : https://idm.example.com:18806/oauth2/openid/nextcloud/public_key.jwk
id_token_signing_alg_values_supported : ['ES256']
code_challenge_methods_supported : ['S256']
scopes_supported : ['email', 'openid', 'profile']
```

![Kanidm فيه عميل OAuth2 اسمه nextcloud مع oauth2_rs_origin_landing https://cloud.example.com/ وعنوان العودة apps/user_oidc/code وscope_map للمجموعة nextcloud_users بالنطاقات openid وprofile وemail وoauth2_strict_redirect_uri true، وNextcloud بتطبيق user_oidc يقرأ كل شيء من رابط الاكتشاف، وتحته ما رد به Kanidm: authorization_endpoint وtoken_endpoint وES256 وS256](https://arabroot.io/content/images/2026/10/kanidm-lldap-05-oauth.webp)

لكل عميل في Kanidm عنوان issuer خاص به، والتوقيع ES256، وPKCE بطريقة S256 فقط

ولاحظ ثلاث نقاط من الرد تهم من يربط التطبيقات: أن الـ `issuer` خاص بكل عميل ويحمل اسمه، فإذا كتبت في التطبيق عنواناً عاماً للمزود فلن يجد الاكتشاف، وأن التوقيع ES256 فقط، فالتطبيق القديم الذي لا يعرف إلا RS256 لن يتحقق من التوكن حتى تفعل له التوقيع القديم في إعداد العميل، وأن PKCE بطريقة `S256` هو الوحيد المدعوم، وهذا ما يجب أن يكون في 2026.

### سياسة الحساب: قواعد أشد لمجموعة واحدة

العميل الحكومي لا يطلب MFA فقط، وإنما يطلب أيضاً ألا تبقى جلسة المهندس مفتوحة طويلاً وأن تكون كلمات المرور طويلة. وفي Kanidm تكتب هذه القواعد كسياسة حساب Account Policy على مجموعة، ويطبقها على كل أعضائها، وإذا انطبقت على شخص أكثر من سياسة طبق Kanidm الأشد في كل إعداد، كما يشرح [توثيق سياسة الحساب](https://kanidm.github.io/kanidm/stable/accounts/account%5Fpolicy.html?ref=arabroot.io). فطبقنا على مجموعة `nextcloud_users` ثلاث قواعد:

```text
$ kanidm group account-policy enable nextcloud_users -D idm_admin
Group enabled for account policy.
$ kanidm group account-policy auth-expiry nextcloud_users 28800 -D idm_admin
Updated authsession expiry.
$ kanidm group account-policy password-minimum-length nextcloud_users 12 -D idm_admin
Updated password minimum length.
$ kanidm group account-policy privilege-expiry nextcloud_users 900 -D idm_admin
Updated privilege session expiry.
---
auth_password_minimum_length: 12
authsession_expiry: 28800
class: account_policy
class: group
class: object
member: sara@idm.example.com
name: nextcloud_users
privilege_expiry: 900
spn: nextcloud_users@idm.example.com
uuid: 7d4612b1-b033-4662-9a61-233b9e7bc9f9
```

في المخرج أعلاه لاحظ التالي:

- `authsession_expiry: 28800` يعني أن جلسة الدخول تنتهي بعد ثماني ساعات، أي يوم عمل، ثم يطلب Kanidm المصادقة من جديد، وهذا هو قرار مدة الجلسة الذي ناقشناه في [شرح الدخول الموحد](https://arabroot.io/articles/%D9%85%D8%A7-%D9%87%D9%88-%D8%A7%D9%84%D8%AF%D8%AE%D9%88%D9%84-%D8%A7%D9%84%D9%85%D9%88%D8%AD%D8%AF-sso-%D9%88%D8%A7%D9%84%D9%85%D8%B5%D8%B7%D9%84%D8%AD%D8%A7%D8%AA/#lifetimes) مكتوباً في المزود نفسه.
- `privilege_expiry: 900` يخص الصلاحيات الإدارية، فعندما يرفع الشخص جلسته إلى جلسة تعديل يعيد فيها المصادقة، تبقى صلاحية التعديل 15 دقيقة فقط، والتوثيق يحدها بساعة كحد أعلى.
- `auth_password_minimum_length: 12` للحسابات التي تستخدم كلمة مرور، وهو الإعداد الوحيد لكلمات المرور في السياسة، لأن Kanidm يفضل مفاتيح المرور أصلاً.
- التوثيق يذكر أيضاً حداً أدنى لنوع بيانات الدخول، من أي نوع إلى MFA إلى مفتاح مرور إلى مفتاح مرور معتمد من قائمة أجهزة محددة، وهذا ما تستخدمه لتفرض العامل الثاني على مجموعة المشروع الحكومي وحدها.

ولا يوجد في LLDAP ما يشبه هذا، لأن الجلسات والعامل الثاني ليست من عمله، فتكتب هذه القواعد في مزود الهوية الذي أمامه.

### إضافة مفاتيح المرور: رابط إعادة تعيين بدلاً من كلمة مرور

في Kanidm لا يضع المسؤول كلمة مرور للموظف، وإنما يرسل له رابطاً يضيف به هو بيانات دخوله، كلمة مرور مع TOTP أو مفتاح مرور Passkey:

```bash
kanidm person credential create-reset-token sara -D idm_admin
```

```text
The person can use one of the following to allow the credential reset
This link: https://idm.example.com:8443/ui/reset?token=…
Or run this command: kanidm person credential use-reset-token …
This token will expire at: 2026-10-26T08:53:43Z
```

اختصرنا الرمز في المخرج لأنه سر، وكان مع الرابط رمز QR يمسحه الموظف بهاتفه. والرابط صالح ساعة واحدة، وهذا تصميم أفضل من كلمة مرور مؤقتة يرسلها المسؤول بالبريد، لأن المسؤول لا يعرف بيانات الدخول النهائية أبداً، ولأن الموظف يستطيع أن يسجل مفتاح المرور على جهازه مباشرة، وهو ما يحقق طلب العميل الحكومي دون أي برنامج آخر. وفي المحاولة الأولى أضفنا مدة الصلاحية رقماً بعد الاسم فرفضتها الأداة برسالة `unexpected argument '3600' found`، فالمدة الافتراضية ساعة في هذا الإصدار.

### يوم المغادرة في Kanidm: تاريخ انتهاء للحساب

بخلاف LLDAP، يملك Kanidm مفهوم صلاحية الحساب Account Validity بتاريخ بداية وتاريخ نهاية. فأعطينا سارة كلمة مرور POSIX، وجعلنا حسابها ينتهي بعد ثلاث دقائق:

```bash
kanidm person validity expire-at sara 2026-10-26T07:57:44Z -D idm_admin
kanidm person validity show sara -D idm_admin
```

```text
Success
user: sara
valid after: any time
expire: 2026-10-26T07:57:44Z
```

قبل الموعد، في الساعة 07:56:23، نجح الربط عبر LDAPS:

```text
u: sara@idm.example.com
```

وبعد الموعد، في الساعة 07:57:52، بنفس الأمر ونفس كلمة المرور:

```text
ldap_bind: Invalid credentials (49)
Account is expired or not yet valid.
```

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

![في LLDAP لا يوجد زر تعطيل وعمليات GraphQL لا تحوي تعطيلاً، وحذف سارة من مجموعة nextcloud_users أخفاها من فلتر Nextcloud، ولكن كلمة مرورها ما زالت تعمل بالأمر ldapwhoami، والحل حذف الحساب أو تغيير كلمة مروره، وفي Kanidm الأمر kanidm person validity expire-at، وقبل الموعد 07:56:23 نجح الدخول، وبعد الموعد 07:57:52 رد ldap_bind Invalid credentials 49 وفي السجل Account is expired or not yet valid](https://arabroot.io/content/images/2026/10/kanidm-lldap-06-leaver.webp)

Kanidm ينهي الحساب في موعده، وLLDAP يحتاج حذفاً أو تغيير كلمة المرور

### النسخ الاحتياطي في Kanidm

يأخذ Kanidm نسخة احتياطية كاملة بأمر واحد داخل حاوية السيرفر:

```bash
kanidmd database backup -c /data/server.toml /data/backup.json
```

```text
INFO     ｉ [info]: Running in backup mode ...
INFO     ｉ [info]: Backup success!
```

وكان الملف في تجربتنا 15063 بايت، ورغم اسمه `.json` فقد ظهر مضغوطاً عندما عرضنا بدايته، فلا تتوقع أن تقرأه بعينك. وتعرض مساعدة `kanidmd` أيضاً خيارات للنسخ التلقائي أثناء العمل Online Backup بمسار وعدد نسخ وجدول زمني، فاستخدمها بدلاً من سكربت خاص، وانسخ الملفات إلى خارج السيرفر، ومعها ملفات الشهادات والإعداد.

### دخول Linux نفسه عبر Kanidm

للمكتب أيضاً ثلاثة سيرفرات Linux للمحاكاة الهندسية، ويريد أن يدخل إليها المهندسون بحساباتهم في Kanidm. وهنا يأتي برنامج `kanidm-unixd` الذي يشرحه [توثيق PAM وnsswitch](https://kanidm.github.io/kanidm/stable/integrations/pam%5Fand%5Fnsswitch.html?ref=arabroot.io): خدمة على كل سيرفر تحل أسماء المستخدمين والمجموعات من Kanidm عبر مكتبات nsswitch، وتتحقق من الدخول عبر PAM، وتحتفظ بنسخة مؤقتة من المستخدمين وبيانات الدخول للمستخدمين ذوي الشبكة غير المستقرة، وتنشئ مجلد المستخدم عند أول دخول. وإعداده في `/etc/kanidm/unixd`، ومنه الخيار `pam_allowed_login_groups` الذي يحدد المجموعات المسموح لها بالدخول إلى هذا السيرفر. ولاحظ شرطين رأينا أثرهما في التجربة: أن الشخص والمجموعة يجب أن تفعل لهما خصائص POSIX حتى يظهرا في `getent passwd`، وأن الشخص يحتاج إلى كلمة مرور UNIX، وهي نفسها كلمة المرور التي استخدمناها في LDAPS. ولم نشغل `kanidm-unixd` في هذه التجربة لأنه يعمل كخدمة systemd على السيرفر نفسه، وهذا موضوع مقال مستقل إن شاء الله.

### نقاط القوة ونقاط الضعف

- **نقاط القوة:** مزود هوية ودليل في برنامج واحد بذاكرة قرابة 45 ميجابايت، وOIDC ومفاتيح المرور مدمجة، وTLS إلزامي، وArgon2id لكلمات المرور، ودخول Linux عبر PAM، والقراءة المجهولة محصورة في الصفات العامة، وإصداراته منتظمة.
- **نقاط الضعف:** أغلب الإدارة من سطر الأوامر، والواجهة الإدارية في 1.11.2 ما زالت محدودة كما في الصورة، وكلمة مرور POSIX المنفصلة تربك المستخدمين في البداية، وLDAP للقراءة فقط، والمفاهيم مثل `spn` و`idm_admin` تحتاج إلى قراءة التوثيق قبل أن تبدأ.

## هل تستطيع أن تجمع الاثنين؟

قد يتساءل البعض بعد كل ما سبق: لماذا لا أشغل الاثنين، Kanidm للدخول الموحد ومفاتيح المرور، وLLDAP للتطبيقات التي تحتاج LDAP يقبل كل شيء؟ والإجابة أن ذلك ممكن تقنياً، ولكنه يعيد المشكلة التي بدأنا بها: مكانان للحسابات، وكل مغادرة تحتاج إغلاقاً في المكانين، وكل تغيير في المجموعات يجب أن ينسخ بينهما. وKanidm يعرض LDAPS للقراءة، وهذا يكفي أغلب التطبيقات القديمة التي تحتاج أن تبحث عن المستخدم وتتحقق من كلمة مروره، فإذا كان التطبيق يحتاج أن يكتب في الدليل فهذه حالة نادرة تستحق أن تسأل هل التطبيق نفسه مناسب. والقاعدة التي نقترحها: مصدر واحد للحسابات، وما عداه يقرأ منه. فإذا اخترت LLDAP فمزود الهوية يقرأ منه، وإذا اخترت Kanidm فالتطبيقات تقرأ منه، ولا يكون لأي موظف كلمة مرور في مكانين.

## كم يكلف البديل التجاري؟

الأسعار التالية من صفحات Microsoft الرسمية في 26 أكتوبر 2026، وهي أسعار مقترحة بالدولار تختلف بحسب الدولة والموزع:

| الخيار                       | السعر                       | ملاحظات                                                                                                                                                                    |
| ---------------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Windows Server 2025 Standard | 1,176 دولاراً لرخصة 16 نواة | ومعها رخصة وصول CAL لكل مستخدم أو جهاز، كما في [صفحة الأسعار](https://www.microsoft.com/en-us/windows-server/pricing?ref=arabroot.io)، ومتحكمان للنطاق على الأقل للتوافرية |
| Microsoft Entra ID P1        | 7 دولارات للمستخدم شهرياً   | باشتراك سنوي، كما في [صفحة Entra](https://www.microsoft.com/en-us/security/business/microsoft-entra-pricing?ref=arabroot.io)                                               |
| Microsoft Entra ID P2        | 10 دولارات للمستخدم شهرياً  | باشتراك سنوي                                                                                                                                                               |
| LLDAP وKanidm                | مجانيان                     | تكلفتك هي السيرفر ووقت الإدارة والنسخ الاحتياطي                                                                                                                            |

فلفريق من 15 شخصاً يكلف Entra ID P1 وحده 1,260 دولاراً في السنة، دون حساب أجهزة Windows أو الدعم. واحسب مع البديل المجاني وقت من يدير السيرفر وينسخه احتياطياً، والسبب أن هذا الوقت هو تكلفته الحقيقية، ومع ذلك يبقى الفرق كبيراً عندما لا تحتاج إلى أجهزة Windows أصلاً.

## كم تأخذ الأداتان فعلاً؟

قسنا الذاكرة بالأمر `docker stats` مرتين: في الجلسة الأولى بعد إنشاء ثلاثة مستخدمين أخذ LLDAP قرابة 3 ميجابايت وKanidm قرابة 45 ميجابايت، وفي الجلسة الثانية بعد عشرات الطلبات عبر GraphQL وLDAP أخذ LLDAP قرابة 17 ميجابايت وKanidm قرابة 43 ميجابايت. والرقم الدقيق ليس مهماً، وإنما الدرس: الأداتان صغيرتان إلى حد أن أصغر سيرفر افتراضي في السوق يتحملهما مع تطبيقات المكتب، بخلاف Active Directory الذي يحتاج سيرفر Windows كاملاً، أو FreeIPA الذي يطلب توثيقه 2 جيجابايت لسيرفر تجربة كما شرحنا في [مقال FreeIPA](https://arabroot.io/articles/freeipa-%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D9%87%D9%88%D9%8A%D8%A7%D8%AA-%D9%88%D8%A7%D8%AC%D9%87%D8%B2%D8%A9-linux/). أما القرص فكان أقل من 3 ميجابايت للأداتين معاً بما فيها الشهادات، والنسخة الاحتياطية من Kanidm 15 كيلوبايت. لذلك فالقرار بينهما لا يحسمه حجم السيرفر، وإنما شكل البنية الذي تريده ومن سيديره.

## الربط بالضوابط

إذا كان المكتب يعمل في مشاريع لجهات حكومية ضمن نطاق [الضوابط الأساسية للأمن السيبراني ECC](https://arabroot.io/articles/%D8%A7%D9%84%D8%B6%D9%88%D8%A7%D8%A8%D8%B7-%D8%A7%D9%84%D8%A7%D8%B3%D8%A7%D8%B3%D9%8A%D8%A9-%D9%84%D9%84%D8%A7%D9%85%D9%86-%D8%A7%D9%84%D8%B3%D9%8A%D8%A8%D8%B1%D8%A7%D9%86%D9%8A-nca-ecc-%D8%A8%D9%84%D8%BA%D8%A9-%D8%A8%D8%B3%D9%8A%D8%B7%D8%A9/)، فهناك ثلاثة ضوابط يمسها هذا الاختيار مباشرة، كما في [نص الوثيقة](https://cdn.nca.gov.sa/api/files/public/upload/86e09090-44e4-481f-bc28-355673607654%5FECC--2024-EN.pdf?ref=arabroot.io):

- **2-2-3-2 التحقق متعدد العوامل للدخول عن بعد:** مع LLDAP يأتي من مزود الهوية الذي أمامه، ومع Kanidm من مفاتيح المرور أو TOTP مباشرة، وفي الحالتين تأكد أن VPN نفسه يمر على العامل الثاني، وليس على LDAP بكلمة المرور فقط.
- **2-2-3-5 المراجعة الدورية للهويات والصلاحيات:** قائمة أعضاء كل مجموعة تخرج من الأداتين بأمر واحد، وتاريخ انتهاء الحساب في Kanidm يجعل المراجعة أسهل لأن حسابات المتعاقدين تنتهي وحدها.
- **1-9-5 إلغاء صلاحيات العاملين عند انتهاء العلاقة:** وهنا الفرق الذي رأيناه في التجربة، فالإلغاء في LLDAP يحتاج حذفاً أو تغيير كلمة المرور، وفي Kanidm يكفي تاريخ الانتهاء، وفي الحالتين اكتب الإجراء ووثق تنفيذه.

والأداتان تعملان على سيرفرات المكتب نفسه، فبيانات الموظفين لا تخرج إلى خدمة سحابية خارجية، وهذا ما يفضله كثير من العملاء الحكوميين في مشاريعهم.

## إذا كان لديك دليل قديم

ليست كل المكاتب تبدأ من الصفر، فبعضها لديه OpenLDAP قديم أعده شخص غادر، أو Samba يعمل كنطاق. والطريقة العملية في الحالتين أن تبني الدليل الجديد بجانب القديم، ثم تنقل التطبيقات إليه واحداً واحداً، كما فعلنا مع الدخول الموحد. ويذكر توثيق Kanidm في قائمة ميزاته أنه يستطيع مزامنة المحتوى من LDAP بالمعيار RFC 4533 ومن FreeIPA، أي أنه يستطيع أن يقرأ المستخدمين من الدليل القديم حتى يكتمل الانتقال، أما LLDAP فالانتقال إليه يكون بإنشاء المستخدمين عبر GraphQL من ملف تصدره من الدليل القديم، وكلمات المرور لا تنتقل، فخطط لرابط إعادة تعيين يرسل للجميع. ولاحظ في الحالتين أن أسماء المستخدمين يجب أن تبقى كما هي، لأن التطبيقات تربط الملفات والصلاحيات بالاسم أو بالمعرف، وتغيير الاسم في يوم الانتقال يجعل كل موظف يبدأ في Nextcloud بحساب فارغ.

## ثلاثة مكاتب، ثلاثة قرارات

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

- **مكتب الاستشارات في قصتنا:** خمسة عشر موظفاً، Nextcloud وGitea وGrafana وVPN، ومشروع حكومي يطلب MFA ومدة جلسة قصيرة. والأنسب له Kanidm وحده، لأنه يعطيه OIDC ومفاتيح المرور وسياسة حساب لمجموعة المشروع وتاريخ انتهاء للمتعاقدين في برنامج واحد، والثمن أن يتعلم المسؤول سطر الأوامر.
- **شركة ناشئة تعمل أصلاً بـ authentik:** لديها مزود هوية يعمل، ولكن تطبيقاً قديماً يحتاج LDAP. والأنسب لها LLDAP كدليل خلف authentik، أو LDAP الذي يقدمه authentik نفسه، بدلاً من أن تغير مزود الهوية كله من أجل تطبيق واحد.
- **شركة فيها أجهزة Windows للمحاسبة والإدارة:** هنا لا يغني أي من الاثنين عن Active Directory أو Samba كمتحكم نطاق، كما يشرح [توثيق Samba](https://www.samba.org/samba/docs/?ref=arabroot.io)، لأن ما تحتاجه هو سياسات المجموعات وضم الأجهزة، وليس دليلاً للتطبيقات فقط.

وفي الحالات الثلاث، القرار الذي لا يتغير هو أن يكون للحساب مكان واحد ينشأ فيه ويغلق، وأن تجرب يوم المغادرة قبل أن يأتي، كما فعلنا في التجربة.

## أيهما تختار؟

- **اختر LLDAP** إذا كان لديك أو ستضع مزود هوية مثل authentik أو Authelia، وتحتاج دليلاً بسيطاً للتطبيقات التي تسأل LDAP، ويديره شخص لا يريد أن يتعلم LDAP.
- **اختر Kanidm** إذا كنت تبدأ من الصفر وتريد برنامجاً واحداً للدخول الموحد ومفاتيح المرور والدليل ودخول Linux، ولا يزعجك سطر الأوامر.
- **اختر Active Directory أو Samba** إذا كانت أجهزة الموظفين Windows وتحتاج إلى سياسات المجموعات وضم الأجهزة.
- **اختر FreeIPA** إذا كانت لديك سيرفرات Linux كثيرة تحتاج إلى Kerberos وقواعد sudo مركزية.

[![](https://arabroot.io/content/images/2026/10/ldap-directory-cover-cover.webp)دليلLDAP ودليل المستخدمين: كيف تعرف التطبيقات من هو المستخدم؟نبني دليل مستخدمين على OpenLDAP ونرى بمخرجات ldapsearch الحقيقية كيف تتحقق التطبيقات من كلمة المرور والمجموعات، وأين يخطئ أغلب من يبدأ.](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-ldap-%D9%88%D8%AF%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D9%85%D8%B3%D8%AA%D8%AE%D8%AF%D9%85%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/freeipa-cover-cover.webp)دليلFreeIPA: إدارة هويات وأجهزة Linux في المؤسسةما يجمعه FreeIPA من دليل وKerberos وقواعد دخول وsudo مركزية لسيرفرات Linux، وما حدث عندما حاولنا تشغيله في Docker.](https://arabroot.io/articles/freeipa-%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D9%87%D9%88%D9%8A%D8%A7%D8%AA-%D9%88%D8%A7%D8%AC%D9%87%D8%B2%D8%A9-linux/)[![](https://arabroot.io/content/images/2026/10/authentik-cover-cover-1.webp)دليلتثبيت authentik للدخول الموحد SSO إلى تطبيقاتككيف تثبت authentik حتى يدخل كل موظف إلى تطبيقات الفريق بحساب واحد، مع فرض المصادقة الثنائية على الجميع.](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-authentik-%D9%84%D9%84%D8%AF%D8%AE%D9%88%D9%84-%D8%A7%D9%84%D9%85%D9%88%D8%AD%D8%AF/)[![](https://arabroot.io/content/images/2026/10/sso-basics-cover-cover.webp)دليلما هو الدخول الموحد SSO؟ المصطلحات التي تحتاجها أولاًخريطة لمصطلحات الدخول الموحد SSO مع تجربة حقيقية تبين كيف تفتح جلسة واحدة في مزود الهوية تطبيقين، وماذا يبقى بعد الخروج.](https://arabroot.io/articles/%D9%85%D8%A7-%D9%87%D9%88-%D8%A7%D9%84%D8%AF%D8%AE%D9%88%D9%84-%D8%A7%D9%84%D9%85%D9%88%D8%AD%D8%AF-sso-%D9%88%D8%A7%D9%84%D9%85%D8%B5%D8%B7%D9%84%D8%AD%D8%A7%D8%AA/)

## إزالة التجربة

```bash
docker rm -f lldap kanidm
docker volume rm lldap-data kanidm-data
docker network rm dir-lab
```

## الخلاصة

- Active Directory يدير أجهزة Windows، فإذا كان كل ما تحتاجه دليلاً تسأله التطبيقات عبر LDAP فلديك بديلان خفيفان مفتوحا المصدر.
- LLDAP 0.6.3 بترخيص GPL-3.0 دليل LDAP بواجهة ويب، أخذ في تجربتنا قرابة 3 ميجابايت من الذاكرة، ويرفض القراءة دون دخول، ولكنه ليس مزود هوية ولا يقبل التعديل عبر LDAP.
- Kanidm 1.11.2 بترخيص MPL-2.0 مزود هوية ودليل معاً مع OIDC وWebAuthn، أخذ قرابة 45 ميجابايت، وLDAPS فيه للقراءة فقط وبكلمة مرور POSIX منفصلة، وTLS إلزامي من البداية.
- الأسماء العربية تعمل في الاثنين، وتظهر في مخرجات LDIF بترميز base64، فأرسلها من ملف UTF-8 إذا كنت على Windows.
- Entra ID P1 يكلف 7 دولارات للمستخدم شهرياً، وWindows Server 2025 Standard يبدأ من 1,176 دولاراً مع رخص CAL، والقرار الحقيقي هو هل لديك أجهزة Windows تحتاج إلى إدارة.

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتنفيذ التجربة على LLDAP 0.6.3 وKanidm 1.11.2.
- أكتوبر 2026: توسيع المقال بقصة مكتب الاستشارات، وتجربة ثانية على نفس الإصدارين: صفات خاصة وفلتر Nextcloud في LLDAP، ويوم المغادرة في الأداتين، وKanidm كمزود OpenID Connect، ورابط إعادة التعيين، والنسخ الاحتياطي، وثلاث صور جديدة.