> ## 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.

# Vaultwarden: مدير كلمات مرور للفريق على سيرفرك
- URL: https://arabroot.io/articles/تثبيت-vaultwarden-مدير-كلمات-المرور-للفريق/
- Published: 2026-10-13T15:30:00.000Z
- Updated: 2026-10-14T21:30:17.000Z
- Description: كيف تثبت Vaultwarden بأمان وتنقل إليه كلمات مرور فريقك من ملف Excel، ثم تتأكد أن نسخته الاحتياطية تستعاد فعلاً.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, الأمن السيبراني, الاستضافة الذاتية

في [مقارنة مديري كلمات المرور Password Managers](https://arabroot.io/articles/bitwarden-%D8%A7%D9%85-vaultwarden-%D8%A7%D9%85-passbolt/) وصلنا إلى أن Vaultwarden هو الخيار المناسب لفريق صغير أو متوسط معه من يشغل سيرفراً، لأنه يعطيك المؤسسات والفرق والسياسات والوصول الطارئ دون ترخيص، والموظف يستخدم تطبيقات Bitwarden التي يعرفها. وفي هذا الدليل سوف نثبته كما يثبت في شركة حقيقية، ثم نرحل إليه ملف Excel لكلمات المرور كما وصفنا العملية في [إدارة كلمات المرور في المؤسسة](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D9%83%D9%84%D9%85%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B1%D9%88%D8%B1-%D9%81%D9%8A-%D8%A7%D9%84%D9%85%D8%A4%D8%B3%D8%B3%D8%A9/#process)، ثم نختبر النسخة الاحتياطية باستعادة حقيقية، ونكسر الأشياء التي تنكسر عادة في الإنتاج.

المواضيع في السلسلة:

1. [إدارة كلمات المرور في المؤسسة: من ملف Excel إلى خزنة](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D9%83%D9%84%D9%85%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B1%D9%88%D8%B1-%D9%81%D9%8A-%D8%A7%D9%84%D9%85%D8%A4%D8%B3%D8%B3%D8%A9/)
2. [Bitwarden أم Vaultwarden أم Passbolt؟ مدير كلمات مرور لفريقك](https://arabroot.io/articles/bitwarden-%D8%A7%D9%85-vaultwarden-%D8%A7%D9%85-passbolt/)
3. Vaultwarden: مدير كلمات مرور للفريق على سيرفرك (الموضوع الحالي)

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

- لماذا تحتاج الخزنة HTTPS حتى في الشبكة الداخلية، وماذا ترى إذا فتحتها عبر HTTP.
- التثبيت بـ Docker Compose بإصدار مثبت خلف Caddy مع جهة إصدار داخلية، ورمز صفحة الإدارة بصيغة Argon2، والبريد، وإغلاق التسجيل المفتوح.
- المؤسسة والمجموعات Collections والفرق Groups، ودعوة الموظفين وتأكيدهم.
- ترحيل ملف Excel حقيقي الشكل إلى صيغة CSV التي يقبلها Bitwarden، واستيراده بسطر الأوامر الرسمي.
- فرض التحقق الثنائي على الأعضاء، والوصول الطارئ، وسجل الأحداث، وتحديد المحاولات.
- النسخ الاحتياطي واختبار الاستعادة، ثم كسر الاستعادة بثلاث طرق، والتحديثات ووضع SSO.

## شكل التجربة

سوف نبني كل شيء على جهاز واحد بأسماء مصطنعة تحت النطاق `lab.test`، وكل Container يبدأ اسمه بـ `lab-pw-`، وكل منفذ على `127.0.0.1` فقط. والصورة التالية تبين المكونات:

![الموظفون بالمتصفح وتطبيقات Bitwarden وسطر الأوامر bw يتصلون عبر HTTPS على المنفذ 18443 بـ Caddy الذي يحمل شهادة من جهة إصدار داخلية tls internal، وCaddy يمرر الطلب عبر HTTP على المنفذ 80 إلى Vaultwarden 1.37.4، وVaultwarden يحفظ كل شيء في Volume واحد فيه db.sqlite3 وrsa_key.pem وattachments، ويرسل البريد إلى Mailpit](https://arabroot.io/content/images/2026/10/vaultwarden-01-architecture.webp)

كل الطرق إلى الخزنة تمر عبر HTTPS، والبيانات كلها في Volume واحد

- **Vaultwarden 1.37.4**: آخر إصدار وقت الكتابة، بقاعدة SQLite الافتراضية، وهي تكفي لفريق بالعشرات أو المئات.
- **Caddy 2.11.7**: الـ Reverse Proxy الذي ينهي TLS، بشهادة من جهة إصدار داخلية لأن الاسم داخلي، وقد شرحنا Caddy في [دليل تثبيته](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-caddy-reverse-proxy/)، والشهادات وجهات الإصدار في [الشهادات الرقمية](https://arabroot.io/articles/%D8%A7%D9%84%D8%B4%D9%87%D8%A7%D8%AF%D8%A7%D8%AA-%D8%A7%D9%84%D8%B1%D9%82%D9%85%D9%8A%D8%A9-tls-%D9%88-x509-%D9%88%D8%B5%D9%8A%D8%BA-pem-%D9%88-der/).
- **Mailpit 1.31.5**: سيرفر بريد للتجربة يستقبل كل الرسائل ويعرضها، بدلاً من بريد حقيقي.
- **Container للأدوات**: Node.js 24.21.0 مع سطر أوامر Bitwarden الرسمي 2026.9.1، لأننا لا نثبت npm على الجهاز نفسه.

## لماذا لا تعمل الخزنة Vault عبر HTTP؟

قد تبدو العملية من الوهلة الأولى بسيطة: سيرفر داخلي في شبكة الشركة، فلماذا لا نفتحه عبر HTTP ونوفر على أنفسنا الشهادات؟ والإجابة أن الخزنة في المتصفح Web Vault تشفر وتفك كل شيء داخل المتصفح بواجهة Web Crypto، والمتصفحات لا تعطي هذه الواجهة إلا للصفحات في سياق آمن Secure Context، أي HTTPS، أو `localhost`. ولذلك تقول [صفحة المشروع](https://github.com/dani-garcia/vaultwarden?ref=arabroot.io#usage) إن الخزنة تحتاج HTTPS. وقد فتحنا المنفذ `80` للـ Container مباشرة على المنفذ `18081` لهذه التجربة فقط، ثم فتحنا الخزنة من Chrome على `http://vw.lab.test:18081/` بعد أن وجهنا الاسم إلى الجهاز، وسألنا الصفحة عن حالتها:

```text
secure? false subtle? false
```

أي أن `window.isSecureContext` قيمته `false` و`crypto.subtle` غير موجود، والصفحة نفسها تعرض الرسالة التالية:

![رسالة Vaultwarden في المتصفح: You are not using a secure context which is required for the Subtle Crypto API to work. You need to enable HTTPS!](https://arabroot.io/content/images/2026/10/vaultwarden-02-http-error.webp)

عبر HTTP لا تفتح الخزنة أصلاً، وهذا مقصود

والأمر نفسه في سطر أوامر Bitwarden، فعندما وجهناه لاحقاً إلى عنوان HTTP رفض الاتصال قبل أن يرسل أي شيء:

```text
Unable to fetch ServerConfig from http://lab-pw-vw-r/api InsecureUrlNotAllowedError: Insecure URL not allowed. All URLs must use HTTPS.
```

لذلك لا تحاول الالتفاف على ذلك، فحتى لو فتحت الخزنة Vault من `localhost` عبر نفق SSH، فإن تطبيقات الجوال وإضافات المتصفح لدى الموظفين تحتاج عنواناً بشهادة يثقون بها. وفي الشبكة الداخلية لديك طريقان: اسم تحت نطاقك العام بشهادة Let's Encrypt عبر التحقق بالـ DNS، أو جهة إصدار داخلية تثبت شهادتها الرئيسية على أجهزة الموظفين، وهو ما نفعله في التجربة.

## التثبيت بـ Docker Compose

### رمز صفحة الإدارة بصيغة Argon2

صفحة الإدارة `/admin` في Vaultwarden تحمي نفسها برمز واحد هو `ADMIN_TOKEN`، ومن يملكه يستطيع أن يدعو مستخدمين ويحذفهم ويلغي تحققهم الثنائي، ولذلك لا تضعه نصاً صريحاً في ملف الإعداد، وإنما ضع هاش Argon2id له بصيغة PHC كما في [صفحة الويكي](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page?ref=arabroot.io#secure-the-admin%5Ftoken). وDocker Image نفسها تحمل أداة الهاش، والخيار `--preset owasp` يستخدم الحد الأدنى الذي توصي به OWASP:

```bash
docker run --rm -it vaultwarden/server:1.37.4 /vaultwarden hash --preset owasp
```

والمخرج سوف يكون كما يلي، بعد أن تكتب الرمز مرتين:

```text
Generate an Argon2id PHC string using the 'owasp' preset:

Password:
Confirm Password:

ADMIN_TOKEN='$argon2id$v=19$m=19456,t=2,p=1$2Rdk3wkrNeiO0lkbA8ZG3AKLQfh6ZP8H2o4o+c0XiUs$6NkNQBRCA/PentPIOj1t4jncZI4hNQ5ouy3GdxPx/jw'

Generation of the Argon2id PHC string took: 81.009646ms
```

ويجدر الإشارة هنا إلى أن الأداة تقرأ الرمز من طرفية TTY، ولأن تجربتنا تعمل من سكربت دون طرفية فقد شغلنا الأمر نفسه عبر الأداة `script` التي تعطيه طرفية، والمخرج أعلاه هو مخرجها كما هو. والهاش أعلاه لرمز التجربة فقط، فولد رمزك أنت. والآن نضع السطر في ملف `.env` بجانب ملف Compose، والعلامات المفردة حوله مهمة، لأنها تمنع Compose من تفسير علامات `$` داخل الهاش:

```ini
VAULTWARDEN_ADMIN_TOKEN='$argon2id$v=19$m=19456,t=2,p=1$2Rdk3wkrNeiO0lkbA8ZG3AKLQfh6ZP8H2o4o+c0XiUs$6NkNQBRCA/PentPIOj1t4jncZI4hNQ5ouy3GdxPx/jw'
```

### ملف Compose وملف Caddy

ملف Compose التالي يشغل الثلاثة، والإصدارات كلها مثبتة:

```yaml
name: lab-pw
services:
  vaultwarden:
    image: vaultwarden/server:1.37.4
    container_name: lab-pw-vw
    restart: unless-stopped
    environment:
      DOMAIN: "https://vw.lab.test:18443"
      ADMIN_TOKEN: ${VAULTWARDEN_ADMIN_TOKEN}
      SIGNUPS_ALLOWED: "false"
      INVITATIONS_ALLOWED: "true"
      ORG_CREATION_USERS: "it-admin@lab.test"
      ORG_GROUPS_ENABLED: "true"
      ORG_EVENTS_ENABLED: "true"
      EVENTS_DAYS_RETAIN: "365"
      PASSWORD_HINTS_ALLOWED: "false"
      SMTP_HOST: "lab-pw-mailpit"
      SMTP_PORT: "1025"
      SMTP_SECURITY: "off"
      SMTP_FROM: "vault@lab.test"
      SMTP_FROM_NAME: "Lab Vault"
    volumes:
      - lab-pw-vw-data:/data
    ports:
      - "127.0.0.1:18081:80"   # direct HTTP, only to show why the web vault needs HTTPS
    networks: [lab-pw-net]

  caddy:
    image: caddy:2.11.7-alpine
    container_name: lab-pw-caddy
    restart: unless-stopped
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - lab-pw-caddy-data:/data
    ports:
      - "127.0.0.1:18443:18443"
    networks:
      lab-pw-net:
        aliases: [vw.lab.test, vw-restore.lab.test]   # the second name is only for the restore test

  mailpit:
    image: axllent/mailpit:v1.31.5
    container_name: lab-pw-mailpit
    restart: unless-stopped
    ports:
      - "127.0.0.1:18025:8025"
    networks: [lab-pw-net]

networks:
  lab-pw-net:
    name: lab-pw-net

volumes:
  lab-pw-vw-data:
    name: lab-pw-vw-data
  lab-pw-caddy-data:
    name: lab-pw-caddy-data
```

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

- `DOMAIN` هو العنوان الذي يراه الموظف، بالمنفذ نفسه، والسيرفر يبني منه روابط الدعوات في البريد، فإذا كان خطأ وصلت الدعوات بروابط لا تعمل.
- `SIGNUPS_ALLOWED: "false"` يغلق التسجيل المفتوح، و`INVITATIONS_ALLOWED: "true"` يسمح لمديري المؤسسات بدعوة الموظفين، فلا يدخل أحد إلا بدعوة، وسوف نرى في [قسم الكسر](#break-signups) ماذا يحدث إذا نسيت السطر الأول.
- `ORG_CREATION_USERS` يحصر إنشاء المؤسسات في حساب فريق تقنية المعلومات، حتى لا ينشئ كل موظف مؤسسته الخاصة ويشارك فيها حسابات العمل بعيداً عن الإدارة.
- `ORG_GROUPS_ENABLED` يفعل الفرق Groups، وهي معطلة افتراضياً وموصوفة في [ملف الإعدادات](https://github.com/dani-garcia/vaultwarden/blob/1.37.4/.env.template?ref=arabroot.io) بأنها ميزة تجريبية، و`ORG_EVENTS_ENABLED` يفعل سجل الأحداث مع حفظه 365 يوماً.
- `PASSWORD_HINTS_ALLOWED: "false"` يمنع تلميحات كلمة المرور، وهذا ما تطلبه NIST SP 800-63B-4 كما ذكرنا في المقال الأول.
- المنفذ `18081` المفتوح مباشرة على Vaultwarden هو لتجربة HTTP أعلاه فقط، فاحذفه في الإنتاج حتى لا يتجاوز أحد الـ Reverse Proxy.
- الاسم الثاني `vw-restore.lab.test` على Caddy هو لاختبار الاستعادة في آخر المقال.

وملف Caddy كما يلي:

```text
{
	skip_install_trust
}

vw.lab.test:18443 {
	tls internal
	encode zstd gzip
	reverse_proxy lab-pw-vw:80 {
		header_up X-Real-IP {remote_host}
	}
}

# restore test: the restored copy, same internal CA
vw-restore.lab.test:18444 {
	tls internal
	reverse_proxy lab-pw-vw-r:80
}
```

في الإعداد أعلاه لاحظ أن `tls internal` يجعل Caddy يصدر الشهادة من جهة إصدار محلية ينشئها بنفسه، وأن `skip_install_trust` يمنعه من محاولة تثبيتها في مخزن الثقة داخل الـ Container، والسطر `header_up X-Real-IP {remote_host}` يرسل عنوان الكلاينت الحقيقي إلى Vaultwarden، لأن Vaultwarden يقرأ هذه الترويسة افتراضياً، ويستخدم العنوان في تحديد المحاولات وفي السجلات كما سوف نرى. والجزء الثاني من الملف هو لاختبار الاستعادة.

نشغل التجربة، ثم ننظر في سجل Vaultwarden:

```bash
docker compose up -d
docker logs lab-pw-vw
```

والمخرج سوف يكون كما يلي:

```text
/--------------------------------------------------------------------\
|                        Starting Vaultwarden                        |
|                           Version 1.37.4                           |
|--------------------------------------------------------------------|
| This is an *unofficial* Bitwarden implementation, DO NOT use the   |
| official channels to report bugs/features, regardless of client.   |
| Send usage/configuration questions or feature requests to:         |
|   https://github.com/dani-garcia/vaultwarden/discussions or        |
|   https://vaultwarden.discourse.group/                             |
| Report suspected bugs/issues in the software itself at:            |
|   https://github.com/dani-garcia/vaultwarden/issues/new            |
\--------------------------------------------------------------------/

[2026-10-14 07:43:49.266][vaultwarden::auth][INFO] Private key 'data/rsa_key.pem' created correctly
[2026-10-14 07:43:52.286][start][INFO] Rocket has launched from http://0.0.0.0:80
```

لاحظ السطر الذي ينشئ `rsa_key.pem` في أول تشغيل، فهذا الملف سوف نعود إليه في النسخ الاحتياطي، ولاحظ أن السجل لم يحذر من رمز إدارة نصي، لأننا وضعنا هاش Argon2.

### الثقة بجهة الإصدار الداخلية

نأخذ الشهادة الرئيسية لجهة الإصدار من Caddy، وهي التي تثبتها على أجهزة الموظفين بأداة إدارة الأجهزة، ونعطيها لـ Container الأدوات:

```bash
docker cp lab-pw-caddy:/data/caddy/pki/authorities/local/root.crt ./work/lab-root.crt
openssl x509 -in work/lab-root.crt -noout -subject -enddate
```

والمخرج سوف يكون كما يلي:

```text
subject=CN=Caddy Local Authority - 2026 ECC Root
notAfter=Aug 22 07:43:48 2036 GMT
```

ثم نشغل Container الأدوات على شبكة التجربة، ونثبت فيه سطر أوامر Bitwarden، ونخبر Node.js بالشهادة عبر المتغير `NODE_EXTRA_CA_CERTS` كما يطلب [توثيق سطر الأوامر](https://bitwarden.com/help/cli/?ref=arabroot.io):

```bash
docker run -d --name lab-pw-cli --network lab-pw-net -v "$(pwd)/work:/work" -w /work \
  -e NODE_EXTRA_CA_CERTS=/work/lab-root.crt node:24.21.0-alpine sleep infinity
docker exec lab-pw-cli sh -c "apk add --no-cache curl jq >/dev/null && npm install -g @bitwarden/cli@2026.9.1 && bw --version"
docker exec lab-pw-cli sh -c "curl -sS --cacert /work/lab-root.crt -o /dev/null -w '%{http_code} verify=%{ssl_verify_result}\n' https://vw.lab.test:18443/"
```

والمخرج سوف يكون كما يلي، بعد سطور npm:

```text
2026.9.1
200 verify=0
```

والقيمة `verify=0` تعني أن الشهادة تحققت بالشهادة الرئيسية الداخلية دون أي تجاوز.

## الدعوة بدلاً من التسجيل

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

```bash
curl -sS --cacert /work/lab-root.crt -X POST https://vw.lab.test:18443/identity/accounts/register/send-verification-email \
  -H 'Content-Type: application/json' \
  -d '{"email":"stranger@outside.test","name":"Stranger","receiveMarketingEmails":false}' -w '\nHTTP %{http_code}\n'
```

والمخرج سوف يكون كما يلي:

```text
{"message":"Registration not allowed or user already exists","validationErrors":{"":["Registration not allowed or user already exists"]},"errorModel":{"message":"Registration not allowed or user already exists","object":"error"},"error":"","error_description":"","exceptionMessage":null,"exceptionStackTrace":null,"innerExceptionMessage":null,"object":"error"}
HTTP 400
```

والآن ندعو أول مستخدم، وهو حساب فريق تقنية المعلومات `it-admin@lab.test`، من صفحة الإدارة، فنسجل الدخول إليها بالرمز ثم نرسل الدعوة:

```bash
curl -sS --cacert /work/lab-root.crt -c adm.jar -o /dev/null -w 'login HTTP %{http_code}\n' \
  -X POST https://vw.lab.test:18443/admin -d 'token=YOUR-ADMIN-TOKEN'
curl -sS --cacert /work/lab-root.crt -b adm.jar -X POST https://vw.lab.test:18443/admin/invite \
  -H 'Content-Type: application/json' -d '{"email":"it-admin@lab.test"}' \
  | jq -c '{email: .email, status: ._status, created: .creationDate}'
```

والمخرج سوف يكون كما يلي:

```text
login HTTP 200
{"email":"it-admin@lab.test","status":1,"created":"2026-10-14T07:48:21.105617Z"}
```

ووصلت إلى Mailpit رسالة «Join Vaultwarden» من `vault@lab.test` برابط يبدأ بالعنوان الصحيح `https://vw.lab.test:18443/#/accept-organization/`، وفيه توكن موقع بالمفتاح `rsa_key.pem`. وعندما فتحنا الرابط في المتصفح أخذنا إلى صفحة «Join Vaultwarden» التي تطلب كلمة المرور الرئيسية مرتين، بحد أدنى 12 حرفاً كما تقول الصفحة، مع خانة لفحص كلمة المرور في قواعد التسريبات، وقد ألغيناها في التجربة حتى لا يخرج أي طلب إلى خدمة خارجية. وبعد إنشاء الحساب ظهرت رسالة «Successfully accepted your invitation».

## المؤسسة والمجموعات والفرق

في Bitwarden وVaultwarden تبنى الصلاحيات من ثلاثة أشياء: المؤسسة Organization التي تملك العناصر، والمجموعات Collections التي تقسم العناصر، والفرق Groups التي تجمع الأعضاء وتعطيهم صلاحية على المجموعات. والصورة التالية تبين ما بنيناه في التجربة:

![الأعضاء sami وnora وomar كل منهم في فريق: IT وFinance وMarketing، وكل فريق يصل إلى مجموعة: IT بصلاحية Manage، والمالية بصلاحية Edit items، والتسويق بصلاحية Edit items، والمجموعات فيها 7 و4 و3 عناصر، والمالك it-admin يرى الكل](https://arabroot.io/content/images/2026/10/vaultwarden-03-org-model.webp)

الصلاحية تتغير في مكان واحد: عضوية الفريق

أنشأنا المؤسسة «Lab Company» من الخزنة في المتصفح Web Vault بحساب `it-admin`، وهو الحساب الوحيد المسموح له بإنشاء مؤسسة. وأما المجموعات فلم ننشئها بأيدينا، لأن الاستيراد في القسم التالي ينشئها من عمود `collections` في ملف CSV. وبعد الاستيراد أنشأنا من صفحة الفرق ثلاثة فرق، لكل فريق مجموعته وصلاحيته:

| الفريق Group | المجموعة Collection | الصلاحية          | لماذا                                               |
| ------------ | ------------------- | ----------------- | --------------------------------------------------- |
| IT           | IT                  | Manage collection | فريق تقنية المعلومات يضيف ويحذف أنظمته ويعطي الوصول |
| Finance      | المالية             | Edit items        | المالية تحدث كلمات مرورها، ولا تدير المجموعة        |
| Marketing    | التسويق             | Edit items        | التسويق يحدث حسابات المنصات عند تغييرها             |

وفي قائمة الصلاحيات خمسة خيارات:

- الخيار `View items`: يرى العناصر ويستخدمها.
- الخيار `View items, hidden passwords`: يستخدم العناصر دون أن تظهر له كلمات المرور.
- الخيار `Edit items`: يرى العناصر ويعدلها ويضيف إليها.
- الخيار `Edit items, hidden passwords`: يعدل العناصر دون أن تظهر له كلمات المرور.
- الخيار `Manage collection`: يدير المجموعة نفسها ومن يصل إليها.

والخيار «مع إخفاء كلمات المرور» Hidden Passwords يسمح للعضو أن يملأ كلمة المرور في المتصفح دون أن يراها، كما في [توثيق صلاحيات المجموعات](https://bitwarden.com/help/collection-permissions/?ref=arabroot.io)، وهو مناسب لحسابات مثل منصات التواصل التي يستخدمها موظف مؤقت. ولكن الصفحة نفسها تحذر من أن الإخفاء يصعب النسخ واللصق ولا يمنع الوصول تماماً، فتعامل مع كلمة المرور المخفية على أنها معروفة لمن يستخدمها، وغيرها عند مغادرته. والصورة التالية تبين الفرق بعد إنشائها:

![صفحة Groups في لوحة الإدارة Admin Console فيها ثلاثة فرق: Finance مع مجموعة المالية، وIT مع مجموعة IT، وMarketing مع مجموعة التسويق](https://arabroot.io/content/images/2026/10/vaultwarden-05-groups.webp)

ثلاثة فرق، لكل فريق مجموعته

ثم أرسلنا دعوات إلى ثلاثة موظفين من صفحة الأعضاء Members بدور User، وأضفنا كل واحد إلى فريقه من تبويب Groups في نافذة الدعوة: `sami` إلى IT، و`nora` إلى Finance، و`omar` إلى Marketing. وبعد أن قبل كل منهم الدعوة من بريده وأنشأ كلمة مروره الرئيسية، بقيت حالته «Accepted»، لأن مفتاح المؤسسة لا يصله حتى يؤكده المالك. والتأكيد ممكن من الخزنة في المتصفح Web Vault، أو من سطر الأوامر بحساب المالك كما فعلنا:

```bash
ORG=bb32980a-492d-46d4-9ba2-b0cd996ad725
bw list org-members --organizationid $ORG | jq -r '.[] | [.email, .status, .type] | @tsv'
for id in $(bw list org-members --organizationid $ORG | jq -r '.[] | select(.status==1) | .id'); do
  bw confirm org-member $id --organizationid $ORG; echo
done
bw list org-members --organizationid $ORG | jq -r '.[] | [.email, .status, .type] | @tsv'
```

والمخرج سوف يكون كما يلي:

```text
it-admin@lab.test	2	0
sami@lab.test	1	2
nora@lab.test	1	2
omar@lab.test	1	2

it-admin@lab.test	2	0
sami@lab.test	2	2
nora@lab.test	2	2
omar@lab.test	2	2
```

والحالة `1` تعني قبل الدعوة Accepted، و`2` تعني تم التأكيد Confirmed، والنوع `0` مالك Owner و`2` مستخدم User. وقد يتساءل البعض: لماذا هذه الخطوة الإضافية؟ والإجابة أن السيرفر لا يملك مفتاح المؤسسة، فالمالك هو من يشفره بالمفتاح العام للعضو الجديد، والتأكيد هو اللحظة التي يحدث فيها ذلك. وفي الخزنة بالمتصفح Web Vault يعرض التأكيد عبارة بصمة Fingerprint Phrase للمفتاح العام، وإذا أردت أن تتأكد أنك تعطي المفتاح للشخص الصحيح، فاطلب من الموظف عبر الهاتف أن يقرأ عليك العبارة من حسابه، والصورة التالية من تأكيد الوصول الطارئ Emergency Access لاحقاً، وفيها العبارة نفسها:

![نافذة Confirm user sami@lab.test تطلب التحقق من عبارة البصمة negation-lagging-widen-clutter-stiffly قبل المتابعة، مع زر Confirm](https://arabroot.io/content/images/2026/10/vaultwarden-08-emergency-confirm.webp)

عبارة البصمة تثبت أن المفتاح العام هو مفتاح الموظف نفسه

## ترحيل ملف Excel إلى الخزنة Vault

الآن نصل إلى ما بدأت به السلسلة: ملف Excel لكلمات المرور. صنعنا ملفاً مصطنعاً يشبه ما تجده في الشركات، بورقة لكل قسم: IT والمالية والتسويق، وفي أعلى كل ورقة عنوان مدمج «سري، لا ترسل الملف»، ثم سطر فارغ، ثم العناوين في السطر الثالث، وفيه المشاكل المعتادة: صفان لنفس الجدار الناري بكلمتي مرور مختلفتين، وكلمة مرور مكتوبة رقماً فحفظها Excel رقماً، وصف دون كلمة مرور لأنها «عند نورة»، وعنوان SSH بدلاً من رابط. والجدول التالي يبين ورقة IT كما صنعناها:

| النظام                 | الرابط                         | اسم المستخدم        | كلمة المرور   | ملاحظات                        | آخر تغيير  | المسؤول |
| ---------------------- | ------------------------------ | ------------------- | ------------- | ------------------------------ | ---------- | ------- |
| الجدار الناري OPNsense | https://fw.lab.test            | root                | Fw#2023!      |                                | 2023-03-02 | سامي    |
| الجدار الناري OPNsense | https://fw.lab.test            | root                | Fw#2024!      | الجديدة                        | 2024-09-15 | سامي    |
| Proxmox                | https://pve.lab.test:8006      | root@pam            | Pve-Lab-77    |                                | 2025-01-10 | سامي    |
| التخزين TrueNAS        | https://nas.lab.test           | admin               | admin123      | لم تتغير منذ التركيب           | 2022-06-01 |         |
| السويتش الرئيسي        | ssh 10.0.0.2                   | admin               | Sw!tch-01     |                                |            | سامي    |
| شبكة الضيوف Wi-Fi      |                                |                     | Welcome2024   | مكتوبة على اللوحة في الاستقبال | 2024-01-07 |         |
| مسجل النطاق            | https://registrar.example.com  | it@lab.test         | Dns-Reg-2025$ | رمز التحقق يصل إلى جوال سامي   | 2025-02-20 | سامي    |
| لوحة البريد            | https://mail-admin.example.com | postmaster@lab.test | Mail@Admin9   |                                | 2025-05-04 | سامي    |

`lab/make_sheet.py`: السكربت الذي صنع الملف بأوراقه الثلاث 

```python
# Builds passwords.xlsx: a made-up but typical "shared passwords" workbook, one sheet per department.
# All names, hosts and passwords are invented for the lab (lab.test / example.com).
from datetime import date
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill

HEAD = ["النظام", "الرابط", "اسم المستخدم", "كلمة المرور", "ملاحظات", "آخر تغيير", "المسؤول"]
SHEETS = {
    "IT": [
        ["الجدار الناري OPNsense", "https://fw.lab.test", "root", "Fw#2023!", "", date(2023, 3, 2), "سامي"],
        ["الجدار الناري OPNsense", "https://fw.lab.test", "root", "Fw#2024!", "الجديدة", date(2024, 9, 15), "سامي"],
        ["Proxmox", "https://pve.lab.test:8006", "root@pam", "Pve-Lab-77", "", date(2025, 1, 10), "سامي"],
        ["التخزين TrueNAS", "https://nas.lab.test", "admin", "admin123", "لم تتغير منذ التركيب", date(2022, 6, 1), ""],
        ["السويتش الرئيسي", "ssh 10.0.0.2", "admin", "Sw!tch-01", "", None, "سامي"],
        ["شبكة الضيوف Wi-Fi", "", "", "Welcome2024", "مكتوبة على اللوحة في الاستقبال", date(2024, 1, 7), ""],
        ["مسجل النطاق", "https://registrar.example.com", "it@lab.test", "Dns-Reg-2025$", "رمز التحقق يصل إلى جوال سامي", date(2025, 2, 20), "سامي"],
        ["لوحة البريد", "https://mail-admin.example.com", "postmaster@lab.test", "Mail@Admin9", "", date(2025, 5, 4), "سامي"],
    ],
    "المالية": [
        ["البنك (الخدمات المصرفية للشركات)", "https://bank.example.com/corporate", "LABCO001", "Bank-Lab#5521", "الرمز المؤقت على جوال المدير المالي", date(2025, 3, 1), "نورة"],
        ["نظام الرواتب", "https://payroll.example.com", "finance@lab.test", "Payroll2025", "", date(2025, 1, 1), "نورة"],
        ["بوابة المورد", "https://supplier.example.com", "labco", 4417, "رقم فقط", date(2024, 11, 3), "نورة"],
        ["بوابة الفوترة", "https://einvoice.example.com", "labco-einv", "", "كلمة المرور عند نورة", None, "نورة"],
    ],
    "التسويق": [
        ["حساب الشركة على X", "https://x.example.com", "@labco", "Labco-Social-1", "يستخدمه ثلاثة موظفين", date(2024, 4, 12), "عمر"],
        ["إعلانات البحث", "https://ads.example.com", "marketing@lab.test", "Ads!2025-lab", "", date(2025, 6, 30), "عمر"],
        ["النشرة البريدية", "https://news.lab.test/admin", "admin", "Newsletter#88", "", date(2025, 8, 8), "عمر"],
    ],
}

wb = Workbook()
wb.remove(wb.active)
for name, rows in SHEETS.items():
    ws = wb.create_sheet(name)
    ws.sheet_view.rightToLeft = True
    ws["A1"] = "كلمات مرور " + name + " (سري، لا ترسل الملف)"
    ws["A1"].font = Font(bold=True, size=14)
    ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=len(HEAD))
    ws.append([])
    ws.append(HEAD)
    for c in ws[3]:
        c.font = Font(bold=True); c.fill = PatternFill("solid", fgColor="FFF2CC")
    for r in rows:
        ws.append(r)
    for row in ws.iter_rows(min_row=4, min_col=6, max_col=6):
        for c in row: c.number_format = "yyyy-mm-dd"
wb.save("passwords.xlsx")
print("passwords.xlsx:", {n: len(r) for n, r in SHEETS.items()})
```

### من ورقة Excel إلى صيغة Bitwarden CSV

صيغة استيراد المؤسسة في [توثيق Bitwarden](https://bitwarden.com/help/condition-bitwarden-import/?ref=arabroot.io) ملف CSV عناوينه بالضبط كما يلي، وحالة الأحرف مهمة:

```text
collections,type,name,notes,fields,reprompt,login_uri,login_username,login_password,login_totp
```

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

```python
# Converts passwords.xlsx (one sheet per department) to a Bitwarden organization CSV.
# Each sheet becomes a collection; rows are checked and every problem is printed so you can fix it before import.
import csv, sys
from datetime import date, datetime
from openpyxl import load_workbook

SRC, OUT = sys.argv[1], sys.argv[2]
COLS = ["collections", "type", "name", "notes", "fields", "reprompt",
        "login_uri", "login_username", "login_password", "login_totp"]
wb = load_workbook(SRC, data_only=True)
items, problems = {}, []

for ws in wb.worksheets:
    rows = list(ws.iter_rows(values_only=True))
    hdr_i = next(i for i, r in enumerate(rows) if r and "كلمة المرور" in r)   # the header is not always row 1
    hdr = {v: i for i, v in enumerate(rows[hdr_i]) if v}
    for n, r in enumerate(rows[hdr_i + 1:], start=hdr_i + 2):
        get = lambda k: r[hdr[k]] if hdr.get(k) is not None and hdr[k] < len(r) else None
        system = get("النظام")
        if not system:
            continue
        pw, user, uri = get("كلمة المرور"), get("اسم المستخدم") or "", get("الرابط") or ""
        where = f"{ws.title}!{n}"
        if pw in (None, ""):
            problems.append(f"{where} {system}: no password in the sheet, ask the owner and add it by hand")
            pw = ""
        elif not isinstance(pw, str):
            problems.append(f"{where} {system}: password stored as a number ({pw!r}), check leading zeros")
            pw = str(pw)
        if pw and len(pw) < 12:
            problems.append(f"{where} {system}: {len(pw)} characters, rotate after import")
        if uri and not uri.startswith("http"):
            problems.append(f"{where} {system}: '{uri}' is not a URL, kept in notes")
            uri_note, uri = f"Address: {uri}", ""
        else:
            uri_note = ""
        changed = get("آخر تغيير")
        notes = "\n".join(x for x in [
            str(get("ملاحظات") or ""), uri_note,
            f"Owner: {get('المسؤول')}" if get("المسؤول") else "",
            f"Last changed (Excel): {changed:%Y-%m-%d}" if isinstance(changed, (date, datetime)) else "",
            f"Imported from {SRC} {where}"] if x)
        key = (ws.title, system, user)
        row = {"collections": ws.title, "type": "login", "name": system, "notes": notes, "fields": "",
               "reprompt": "1" if ws.title == "المالية" else "0",
               "login_uri": uri, "login_username": user, "login_password": pw, "login_totp": "",
               "_changed": changed if isinstance(changed, (date, datetime)) else date.min}
        if key in items:
            old = items[key]
            keep, drop = (row, old) if row["_changed"] >= old["_changed"] else (old, row)
            problems.append(f"{where} {system}: duplicate of another row, kept the one changed {keep['_changed']:%Y-%m-%d}")
            items[key] = keep
        else:
            items[key] = row

with open(OUT, "w", newline="", encoding="utf-8") as f:
    w = csv.DictWriter(f, fieldnames=COLS, extrasaction="ignore")
    w.writeheader()
    for it in items.values():
        w.writerow(it)

print(f"{len(items)} items in {len({k[0] for k in items})} collections -> {OUT}")
print(f"{len(problems)} problems:")
for p in problems:
    print("  -", p)
```

في السكربت أعلاه لاحظ التالي:

- الصفوف المكررة لنفس النظام واسم المستخدم داخل الورقة نفسها يبقى منها الأحدث بحسب عمود «آخر تغيير»، ويطبع السكربت أنه حذف الآخر.
- العمود «المسؤول» وتاريخ آخر تغيير ومكان الصف في الملف تنتقل إلى الملاحظات، حتى تعرف لاحقاً من أين جاء كل عنصر ومن تسأل عنه.
- عناصر المالية تأخذ `reprompt=1`، فيطلب التطبيق كلمة المرور الرئيسية مرة أخرى قبل أن يعرض كلمة المرور، وهذه طبقة صغيرة إضافية لحسابات البنك.
- كل كلمة مرور أقصر من 12 حرفاً تظهر في القائمة، ليس لأن الاستيراد يرفضها، وإنما لأنها أول ما تغيره بعد الاستيراد.

نشغل السكربتين في Container من Python ونثبت openpyxl داخله:

```bash
docker run --rm --name lab-pw-py -v "$(pwd)/work:/work" -w /work python:3.13.16-slim sh -c \
  "pip install --quiet --root-user-action=ignore --disable-pip-version-check openpyxl==3.1.5 &&
   python make_sheet.py && python xlsx_to_bitwarden.py passwords.xlsx org-import.csv"
```

والمخرج سوف يكون كما يلي:

```text
passwords.xlsx: {'IT': 8, 'المالية': 4, 'التسويق': 3}
14 items in 3 collections -> org-import.csv
13 problems:
  - IT!4 الجدار الناري OPNsense: 8 characters, rotate after import
  - IT!5 الجدار الناري OPNsense: 8 characters, rotate after import
  - IT!5 الجدار الناري OPNsense: duplicate of another row, kept the one changed 2024-09-15
  - IT!6 Proxmox: 10 characters, rotate after import
  - IT!7 التخزين TrueNAS: 8 characters, rotate after import
  - IT!8 السويتش الرئيسي: 9 characters, rotate after import
  - IT!8 السويتش الرئيسي: 'ssh 10.0.0.2' is not a URL, kept in notes
  - IT!9 شبكة الضيوف Wi-Fi: 11 characters, rotate after import
  - IT!11 لوحة البريد: 11 characters, rotate after import
  - المالية!5 نظام الرواتب: 11 characters, rotate after import
  - المالية!6 بوابة المورد: password stored as a number (4417), check leading zeros
  - المالية!6 بوابة المورد: 4 characters, rotate after import
  - المالية!7 بوابة الفوترة: no password in the sheet, ask the owner and add it by hand
```

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

- 15 صفاً أصبحت 14 عنصراً، لأن صفي الجدار الناري أصبحا عنصراً واحداً بكلمة المرور الأحدث.
- كلمة مرور بوابة المورد حفظها Excel رقماً، والسكربت حولها إلى نص ونبهك، لأن رقماً مثل `0441` يصبح `441` في Excel دون أن تلاحظ.
- بوابة الفوترة دون كلمة مرور، وهذه حالة «كلمة المرور في رأس شخص واحد» التي تحدثنا عنها في المقال الأول، والسكربت يتركها فارغة لتسأل صاحبها.
- عشرة تنبيهات بطول أقل من 12 حرفاً من 14 عنصراً، وهي قائمة التغيير بعد الاستيراد.

### الاستيراد بسطر الأوامر

نسجل الدخول بحساب المالك داخل Container الأدوات، ونحفظ مفتاح الجلسة في المتغير `BW_SESSION`، ثم نستورد الملف إلى المؤسسة بالخيار `--organizationid`:

```bash
bw config server https://vw.lab.test:18443
export BW_SESSION=$(bw login it-admin@lab.test "$MASTER_PASSWORD" --raw)
bw list organizations | jq -c '.[] | {id, name}'
bw import --organizationid bb32980a-492d-46d4-9ba2-b0cd996ad725 bitwardencsv /work/org-import.csv
bw sync >/dev/null
bw list org-collections --organizationid bb32980a-492d-46d4-9ba2-b0cd996ad725 | jq -r '.[] | .name'
bw list items --organizationid bb32980a-492d-46d4-9ba2-b0cd996ad725 | jq length
```

والمخرج سوف يكون كما يلي، وقد فصلنا سطوره لأن سطر الأوامر يطبع بعض رسائله دون سطر جديد في آخرها:

```text
Saved setting `config`.
{"id":"bb32980a-492d-46d4-9ba2-b0cd996ad725","name":"Lab Company"}
Imported /work/org-import.csv
Default collection
IT
التسويق
المالية
14
```

الاستيراد أنشأ المجموعات الثلاث من عمود `collections`، و«Default collection» أنشأها Vaultwarden مع المؤسسة. والصورة التالية تبين العناصر في لوحة الإدارة بعد الاستيراد:

![قائمة العناصر من نوع Login في لوحة إدارة المؤسسة Lab Company، فيها نظام الرواتب ومسجل النطاق ولوحة البريد وشبكة الضيوف وحساب الشركة على X وبوابة المورد وبوابة الفوترة والنشرة البريدية والسويتش الرئيسي، ولكل عنصر مجموعته: المالية أو IT أو التسويق](https://arabroot.io/content/images/2026/10/vaultwarden-04-org-vault.webp)

العناصر بأسمائها العربية ومجموعاتها كما جاءت من أوراق Excel

والسؤال الأهم: هل يرى كل موظف مجموعته فقط؟ سجلنا الدخول بكل واحد من الثلاثة في مجلد إعداد مستقل لسطر الأوامر، وعرضنا ما يراه مجمعاً بالمجموعة:

```bash
for u in sami nora omar; do
  export BITWARDENCLI_APPDATA_DIR=/root/bw-$u
  bw config server https://vw.lab.test:18443 >/dev/null
  S=$(bw login $u@lab.test "Lab-$u-Master-Pass-2026!" --raw)
  echo "== $u"; BW_SESSION=$S bw list items | jq -r 'group_by(.collectionIds[0]) | .[] | "\(length) items: " + ([.[].name] | join(", "))'
done
```

والمخرج سوف يكون كما يلي، بعد حذف رسالتي إنشاء مجلد الإعداد اللتين يطبعهما سطر الأوامر لكل مستخدم:

```text
== sami
7 items: Proxmox, التخزين TrueNAS, الجدار الناري OPNsense, السويتش الرئيسي, شبكة الضيوف Wi-Fi, لوحة البريد, مسجل النطاق
== nora
4 items: البنك (الخدمات المصرفية للشركات), بوابة الفوترة, بوابة المورد, نظام الرواتب
== omar
3 items: إعلانات البحث, النشرة البريدية, حساب الشركة على X
```

كل موظف يرى مجموعة فريقه فقط، وهذا هو الفرق الأول عن ملف Excel الذي يرى فيه كل من يفتحه كل الأوراق. والآن احذف الملف الأصلي ونسخه من المجلد المشترك والبريد، وابدأ بتغيير كلمات المرور التي في قائمة السكربت، كما في الخطوة الخامسة من [العملية](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D9%83%D9%84%D9%85%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B1%D9%88%D8%B1-%D9%81%D9%8A-%D8%A7%D9%84%D9%85%D8%A4%D8%B3%D8%B3%D8%A9/#process).

## فرض التحقق الثنائي على الأعضاء

الخزنة Vault التي تفتح بكلمة مرور واحدة تعيدنا إلى درس حادثة Snowflake في المقال الأول، ولذلك نفرض التحقق الثنائي على كل أعضاء المؤسسة. فعلنا أولاً التطبيق المصادق Authenticator App للمالك و`sami`، ولم نفعله لـ `nora` و`omar` عمداً، ثم فتحنا السياسة «Require two-step login» من إعدادات المؤسسة:

![نافذة Edit policy لسياسة Require two-step login مع تحذير بأن الأعضاء الذين ليسوا مالكين أو مديرين ولم يفعلوا التحقق الثنائي سوف يزالون من المؤسسة ويصلهم بريد بذلك، وخانة Turn on مفعلة](https://arabroot.io/content/images/2026/10/vaultwarden-06-policy-2fa.webp)

التحذير يقول «removed»، وما حدث فعلاً كان سحب الوصول Revoke كما سنرى

وبعد الحفظ عرضنا حالة الأعضاء من سطر الأوامر مع عمود التحقق الثنائي:

```text
it-admin@lab.test	2	0	true
sami@lab.test	2	2	true
nora@lab.test	-1	2	false
omar@lab.test	-1	2	false
```

والحالة `-1` تعني أن وصولهما سحب Revoked ولم يحذفا، ووصل إلى كل منهما بريد بعنوان «Your access to Lab Company has been revoked.» نصه:

```text
Your user account has been removed from the *Lab Company* organization because you do not have two-step login configured.
Before you can re-join this organization you need to set up two-step login on your user account.

You can enable two-step login in your account settings.
```

وعندما زامنت `nora` خزنتها Vault من سطر الأوامر أصبح عدد عناصرها صفراً:

```text
Syncing complete.
0
```

![تبويب Revoked في صفحة الأعضاء، فيه nora@lab.test في فريق Finance وomar@lab.test في فريق Marketing، وبجانب كل منهما علامة Revoked](https://arabroot.io/content/images/2026/10/vaultwarden-07-members-revoked.webp)

العضوان بلا تحقق ثنائي في تبويب Revoked بفرقهما كما هي

وفي النتيجة أعلاه لاحظ التالي:

- التحذير في النافذة يقول إن العضو «سوف يزال»، ونص البريد يقول «has been removed»، ولكن الحالة الفعلية سحب الوصول، والعضو بقي في فريقه، وهذا أفضل، لأن إعادته لا تحتاج دعوة جديدة.
- بعد أن فعلت `nora` التطبيق المصادق، أعاد المالك وصولها من قائمة العضو بالخيار «Restore access»، فزامنت ووجدت عناصر المالية الأربعة كما كانت.
- تسجيل الدخول بسطر الأوامر دون رمز أصبح يرفض، فالأمر `bw login sami@lab.test … --nointeraction` رد بـ `Code is required.`، ومع `--method 0 --code` ورمز من التطبيق المصادق رد بـ `You are logged in!`.
- أبلغ قبل أن تفعل السياسة، وأعط الموظفين أسبوعاً لتفعيل التحقق الثنائي، لأن من لم يفعله يفقد وصوله إلى حسابات فريقه في اللحظة نفسها.

## الوصول الطارئ Emergency Access

الوصول الطارئ Emergency Access يجعل شخصاً موثوقاً يطلب الوصول إلى خزنتك إذا غبت، فيحصل عليه بعد مدة انتظار تحددها أنت، أو قبلها إذا وافقت. وله نوعان بحسب [توثيق Bitwarden](https://bitwarden.com/help/emergency-access/?ref=arabroot.io): العرض View ويعطي قراءة كل عناصر خزنتك الشخصية، والاستيلاء Takeover ويجعله يضع كلمة مرور رئيسية جديدة لحسابك. ولاحظ تحذير الصفحة في Vaultwarden للمالك: إذا أعطيت Takeover لشخص وأنت مالك مؤسسة فسوف يحصل على صلاحياتك كمالك بعد الاستيلاء.

في التجربة أضاف المالك `sami` جهة اتصال طارئة بنوع View ومدة انتظار يومين، فوصل إلى `sami` بريد «Emergency access for it-admin@lab.test» فقبله، ثم أكده المالك بعبارة البصمة كما في الصورة السابقة، ثم طلب `sami` الوصول، فوصل إلى المالك البريد التالي:

```text
sami@lab.test has initiated an emergency access request to View your account. You may login on the web vault (https://vw.lab.test:18443) and manually approve or reject this request.

If you do nothing, the request will automatically be approved after 2 day(s).
```

وافق المالك على الطلب، ففتح `sami` الخزنة الطارئة، ووجد فيها عنصراً واحداً هو «Domain registrar recovery codes»، وهو الملاحظة الشخصية الوحيدة في خزنة المالك، ولم يجد أي عنصر من عناصر المؤسسة الأربعة عشر. وهذا أهم درس في هذا القسم: الوصول الطارئ Emergency Access يغطي الخزنة الشخصية فقط، أما حسابات المؤسسة فحمايتها من غياب شخص أن يكون للمؤسسة مالكان على الأقل، فأضف مالكاً ثانياً قبل أن تحتاجه.

## سجل الأحداث وتحديد المحاولات

بعد تفعيل `ORG_EVENTS_ENABLED` تسجل المؤسسة كل دخول وكل عرض لعنصر وكل تغيير في الأعضاء والسياسات، والصورة التالية من صفحة Event logs في التجربة:

![صفحة Event logs بأعمدة Timestamp وClient وMember وEvent، فيها إنشاء مرفق وعرض عنصر من سطر الأوامر بحساب it-admin، وتسجيلات دخول من الخزنة في المتصفح لـ sami وit-admin، وعرض nora لأربعة عناصر من سطر الأوامر](https://arabroot.io/content/images/2026/10/vaultwarden-09-events.webp)

كل عرض لعنصر يسجل باسم العضو والتطبيق والوقت

وفي السجل أعلاه لاحظ أن سطر الأوامر الذي يعرض قائمة العناصر يسجل «Viewed item» لكل عنصر، فـ `nora` عرضت أربعة عناصر في الثانية نفسها، فاقرأ السجل بهذا الفهم في أي تحقيق. وأما تحديد المحاولات فـ Vaultwarden يحد طلبات الدخول إلى صفحة الإدارة افتراضياً بثلاث محاولات متتالية، ثم محاولة كل 300 ثانية، بحسب `ADMIN_RATELIMIT_MAX_BURST` و`ADMIN_RATELIMIT_SECONDS` في ملف الإعدادات. أرسلنا خمسة رموز خاطئة متتالية:

```bash
for i in 1 2 3 4 5; do
  curl -s --cacert /work/lab-root.crt -o /dev/null -w "attempt $i: HTTP %{http_code}\n" \
    -X POST https://vw.lab.test:18443/admin -d "token=wrong-guess-$i"
done
```

والمخرج سوف يكون كما يلي:

```text
attempt 1: HTTP 401
attempt 2: HTTP 401
attempt 3: HTTP 401
attempt 4: HTTP 429
attempt 5: HTTP 429
```

وفي سجل Vaultwarden ظهرت المحاولات مع عنوان الكلاينت الحقيقي `192.168.32.5`، وهو عنوان Container الأدوات وليس عنوان Caddy، بفضل الترويسة `X-Real-IP`:

```text
[2026-10-14 12:23:09.302][vaultwarden::api::admin][ERROR] Invalid admin token. IP: 192.168.32.5
[2026-10-14 12:23:09.303][response][INFO] (post_admin_login) POST /admin/ application/x-www-form-urlencoded => 401 Unauthorized
[2026-10-14 12:23:09.351][request][INFO] POST /admin
[2026-10-14 12:23:09.396][vaultwarden::api::admin][ERROR] Invalid admin token. IP: 192.168.32.5
[2026-10-14 12:23:09.396][response][INFO] (post_admin_login) POST /admin/ application/x-www-form-urlencoded => 401 Unauthorized
```

وهذه السطور هي ما يقرؤه fail2ban ليحظر العنوان على مستوى الجدار الناري، وطريقة إعداده في [صفحة الويكي](https://github.com/dani-garcia/vaultwarden/wiki/Fail2Ban-Setup?ref=arabroot.io). ولو لم يمرر الـ Reverse Proxy عنوان الكلاينت لظهر عنوان Caddy في كل سطر، فيحظر fail2ban الـ Proxy نفسه ويقطع الخزنة عن الجميع. ولاحظ أن ملاحظات الإصدار [1.37.4](https://github.com/dani-garcia/vaultwarden/releases/tag/1.37.4?ref=arabroot.io) غيرت طريقة قراءة `X-Forwarded-For` إذا استخدمته بدلاً من `X-Real-IP`، فأصبح العنوان المعتمد هو آخر عنوان من اليمين ليس من الـ Proxies الموثوقة في `IP_HEADER_TRUSTED_PROXIES`، فإذا كان أمام Vaultwarden أكثر من Proxy فأضفها كلها. وأفضل من الحظر ألا تكون صفحة الإدارة متاحة من الإنترنت أصلاً، فاحصرها في شبكة الإدارة من الـ Reverse Proxy.

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

كل بيانات Vaultwarden في المجلد `/data`، ولكن ليس كل ما فيه بالأهمية نفسها، ولا يصح نسخ قاعدة SQLite وهي تعمل بأمر نسخ عادي، والسبب أن جزءاً من آخر التغييرات يكون في ملف `db.sqlite3-wal`. والصورة التالية تبين ما تأخذه، وما يحدث إذا نسيت جزءاً، والنتائج فيها من اختبار الاستعادة أدناه:

![جدول من أربعة صفوف: db.sqlite3 عبر vaultwarden backup فيه المستخدمون والمؤسسات والعناصر المشفرة، وإذا غاب لا توجد خزنة، وattachments فيه المرفقات المشفرة، وإذا غاب يظهر المرفق في القائمة ويفشل تنزيله بخطأ 404، وrsa_key.pem يوقع التوكنات والدعوات، وإذا غاب ينشئ السيرفر مفتاحاً جديداً وتسقط الجلسات بخطأ 401، وconfig.json وsends فيهما إعدادات صفحة الإدارة ومرفقات Send](https://arabroot.io/content/images/2026/10/vaultwarden-10-backup.webp)

قاعدة البيانات وحدها لا تكفي، فالمرفقات ومفتاح التوقيع خارجها

وبحسب [صفحة النسخ الاحتياطي في الويكي](https://github.com/dani-garcia/vaultwarden/wiki/Backing-up-your-vault?ref=arabroot.io) فقاعدة البيانات والمرفقات نسخها مطلوب، و`rsa_key*` و`config.json` نسخها موصى به، و`sends` و`icon_cache` اختياريان. ومنذ الإصدار 1.32.1 يحمل Vaultwarden أمراً ينسخ قاعدة SQLite بواجهة النسخ الآمن Online Backup API وهي تعمل. وأضفنا مرفقاً إلى عنصر الجدار الناري قبل النسخ، وهو ملف إعداد مصطنع اسمه `fw-config-2026-10.xml`، حتى نختبره في الاستعادة. والسكربت التالي يأخذ النسخة:

```bash
#!/bin/sh
# Backs up the Vaultwarden data volume: a consistent SQLite copy, the rsa key, attachments and config.json (if present).
set -eu
VOL=lab-pw-vw-data
OUT=${1:-./backups}
STAMP=$(date +%Y%m%d-%H%M%S)
mkdir -p "$OUT"
# 1. consistent copy of the live database (SQLite online backup API), written inside /data
DB=$(docker exec lab-pw-vw /vaultwarden backup | sed -n "s/.*'data\/\(db_[0-9_]*\.sqlite3\)'.*/\1/p")
# 2. pack the copy with the files that live outside the database
docker run --rm --name lab-pw-backup --entrypoint sh \
  -v "$VOL":/data:ro -v "$(cd "$OUT" && pwd)":/backup \
  vaultwarden/server:1.37.4 -c "
    cd /data
    FILES=\"$DB rsa_key.pem\"
    [ -d attachments ] && FILES=\"\$FILES attachments\"
    [ -d sends ] && FILES=\"\$FILES sends\"
    [ -f config.json ] && FILES=\"\$FILES config.json\"
    tar czf /backup/vaultwarden-$STAMP.tar.gz \$FILES
    tar tzvf /backup/vaultwarden-$STAMP.tar.gz"
# 3. remove the temporary copy from the live volume
docker exec lab-pw-vw rm "/data/$DB"
echo "backup: $OUT/vaultwarden-$STAMP.tar.gz"
```

في السكربت أعلاه لاحظ التالي:

- الأمر `/vaultwarden backup` يكتب نسخة متسقة من قاعدة البيانات داخل `/data`، والسكربت يأخذ اسمها من مخرجه.
- الـ Container المؤقت يركب الـ Volume للقراءة فقط `:ro`، فلا يستطيع السكربت أن يغير البيانات الحية بالخطأ.
- الأرشيف يحمل `rsa_key.pem` ورمز صفحة الإدارة إذا كان في `config.json`، فشفر الأرشيف قبل أن ترسله إلى أي مكان خارج السيرفر، كما توصي صفحة الويكي.

والمخرج سوف يكون كما يلي:

```text
-rw-r--r-- root/root    339968 2026-10-14 12:24 db_20261014_122432.sqlite3
-rw-r--r-- root/root      1679 2026-10-14 07:43 rsa_key.pem
drwxr-xr-x root/root         0 2026-10-14 12:23 attachments/
drwxr-xr-x root/root         0 2026-10-14 12:23 attachments/4a76e770-5811-4b91-8288-7d17e21d2008/
-rw-r--r-- root/root       113 2026-10-14 12:23 attachments/4a76e770-5811-4b91-8288-7d17e21d2008/84e65943fc84b17bc253
backup: ./backups/vaultwarden-20261014-152431.tar.gz
```

ولاحظ أن التاريخ في اسم الأرشيف بتوقيت الجهاز الذي شغل السكربت، والتاريخ داخل الـ Container بتوقيت UTC، ولذلك الفرق ثلاث ساعات. وشغل السكربت مرة كل يوم على الأقل، وانقل الأرشيف إلى مكان ثان، كما في [قاعدة 3-2-1](https://arabroot.io/articles/%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D8%A7%D9%84%D8%A7%D8%AD%D8%AA%D9%8A%D8%A7%D8%B7%D9%8A-%D8%A8%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-3-2-1-%D9%88%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1-%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%B9%D8%A7%D8%AF%D8%A9/).

## اختبار الاستعادة

النسخة التي لم تستعدها لا تعرف أنها تعمل، ولذلك نستعيدها إلى Volume جديد وسيرفر ثان بجانب الأصلي، ثم نسجل الدخول إليه ونقرأ العناصر والمرفق. والسكربت التالي يفك الأرشيف في Volume فارغ، ويجعل نسخة قاعدة البيانات `db.sqlite3`، ويحذف أي ملف WAL قديم كما تطلب صفحة الويكي:

```bash
#!/bin/sh
# Restores a backup archive into an empty volume: the database copy becomes db.sqlite3, no stale WAL file.
set -eu
ARCHIVE=$1
VOL=${2:-lab-pw-vw-restore}
DIR=$(cd "$(dirname "$ARCHIVE")" && pwd); FILE=$(basename "$ARCHIVE")
docker volume create "$VOL" >/dev/null
docker run --rm --name lab-pw-restore --entrypoint sh \
  -v "$VOL":/data -v "$DIR":/backup:ro vaultwarden/server:1.37.4 -c "
    cd /data
    tar xzf /backup/$FILE
    mv db_*.sqlite3 db.sqlite3
    rm -f db.sqlite3-wal db.sqlite3-shm
    ls -l /data"
```

نشغله على الأرشيف:

```bash
sh restore.sh ./backups/vaultwarden-20261014-152431.tar.gz lab-pw-vw-restore
```

والمخرج سوف يكون كما يلي:

```text
total 340
drwxr-xr-x 3 root root   4096 Oct 14 12:23 attachments
-rw-r--r-- 1 root root 339968 Oct 14 12:24 db.sqlite3
-rw-r--r-- 1 root root   1679 Oct 14 07:43 rsa_key.pem
```

ثم نشغل سيرفراً ثانياً على الـ Volume المستعاد، خلف الاسم `vw-restore.lab.test` في Caddy، لأن سطر الأوامر يرفض HTTP كما رأينا:

```bash
docker run -d --name lab-pw-vw-r --network lab-pw-net -e DOMAIN=https://vw-restore.lab.test:18444 \
  -e SIGNUPS_ALLOWED=false -e ORG_GROUPS_ENABLED=true -v lab-pw-vw-restore:/data vaultwarden/server:1.37.4
```

ونسجل الدخول بحساب `sami` ورمزه من التطبيق المصادق، ونعرض عناصره، وننزل المرفق ونقرؤه:

```bash
export BITWARDENCLI_APPDATA_DIR=/root/bw-restore
bw config server https://vw-restore.lab.test:18444
export BW_SESSION=$(bw login sami@lab.test "$MASTER_PASSWORD" --method 0 --code "$TOTP_CODE" --raw)
bw list items | jq -r '.[].name'
ID=$(bw list items --search OPNsense | jq -r '.[0].id')
bw get attachment fw-config-2026-10.xml --itemid $ID --output /tmp/restored.xml
cat /tmp/restored.xml
```

والمخرج سوف يكون كما يلي:

```text
Saved setting `config`.
Proxmox
التخزين TrueNAS
الجدار الناري OPNsense
السويتش الرئيسي
شبكة الضيوف Wi-Fi
لوحة البريد
مسجل النطاق
Saved /tmp/restored.xml
<opnsense><note>lab config export</note></opnsense>
```

الاستعادة نجحت بالكامل: الحساب وكلمة مروره الرئيسية، والتحقق الثنائي الذي فعله `sami` بعد إنشاء حسابه، وعناصر مجموعة IT السبعة، والمرفق بمحتواه. والآن نكسر الاستعادة لنرى ماذا يحدث لمن نسخ قاعدة البيانات فقط.

## كسر التجربة

### نسخة دون مجلد المرفقات

أوقفنا السيرفر المستعاد، وأبعدنا المجلد `attachments` كأنه لم يكن في النسخة، ثم شغلنا السيرفر وحاولنا تنزيل المرفق بالجلسة نفسها:

```bash
docker stop lab-pw-vw-r
docker run --rm --name lab-pw-tmp --entrypoint sh -v lab-pw-vw-restore:/data vaultwarden/server:1.37.4 \
  -c "mv /data/attachments /data/attachments.hidden"
docker start lab-pw-vw-r
bw sync
bw list items --search OPNsense | jq -c '.[0].attachments[] | {fileName, size}'
bw get attachment fw-config-2026-10.xml --itemid $ID --output /tmp/restored2.xml; echo "exit=$?"
```

والمخرج سوف يكون كما يلي:

```text
Syncing complete.
{"fileName":"fw-config-2026-10.xml","size":"113"}
A 404 error occurred while downloading the attachment.exit=1
```

وفي سجل السيرفر:

```text
[2026-10-14 12:30:14.074][response][INFO] (get_attachment) GET /api/ciphers/<cipher_id>/attachment/<attachment_id> => 200 OK
[2026-10-14 12:30:14.099][request][INFO] GET /attachments/4a76e770-5811-4b91-8288-7d17e21d2008/84e65943fc84b17bc253?token=eyJ0eXAiOiJKV1QiLCJhbGci
[2026-10-14 12:30:14.099][response][INFO] (attachments) GET /attachments/<cipher_id>/<file_id> => 404 Not Found
```

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

### نسخة دون rsa\_key.pem

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

```bash
docker stop lab-pw-vw-r
docker run --rm --name lab-pw-tmp --entrypoint sh -v lab-pw-vw-restore:/data vaultwarden/server:1.37.4 \
  -c "mv /data/attachments.hidden /data/attachments && rm /data/rsa_key.pem"
docker start lab-pw-vw-r
docker logs lab-pw-vw-r 2>&1 | grep -i "private key" | tail -1
bw sync
```

والمخرج سوف يكون كما يلي:

```text
[2026-10-14 12:30:32.117][vaultwarden::auth][INFO] Private key 'data/rsa_key.pem' created correctly
Unauthorized response received for request to /accounts/revision-date. Attempting request again.
Syncing failed: {"response":{"error":{"code":401,"reason":"Unauthorized","description":"The request requires user authentication."}},"statusCode":401}
```

وفي سجل السيرفر السبب:

```text
[2026-10-14 12:30:41.698][vaultwarden::auth][ERROR] Error decoding JWT: Error(InvalidSignature)
```

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

### التسجيل المفتوح

شغلنا السيرفر المستعاد مرة أخرى مع `SIGNUPS_ALLOWED=true`، وهي القيمة الافتراضية إذا لم تكتب السطر في ملف Compose، ثم كررنا محاولة التسجيل من بريد خارجي:

```bash
curl -sS --cacert /work/lab-root.crt -X POST https://vw-restore.lab.test:18444/identity/accounts/register/send-verification-email \
  -H 'Content-Type: application/json' \
  -d '{"email":"stranger@outside.test","name":"Stranger","receiveMarketingEmails":false}' -w '\nHTTP %{http_code}\n' | cut -c1-200
```

والمخرج سوف يكون كما يلي، وقد قصرنا السطر الأول بالأمر `cut` لأنه توكن طويل:

```text
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3OTE5ODEwOTMsImV4cCI6MTc5MTk4Mjg5MywiaXNzIjoiaHR0cHM6Ly92dy1yZXN0b3JlLmxhYi50ZXN0OjE4NDQ0fHJlZ2lzdGVyX3ZlcmlmeSIsInN1YiI6InN0cmFuZ2VyQG91dHNpZGUudGVzdCI
HTTP 200
```

بدلاً من الرفض الذي رأيناه في البداية، أعطى السيرفر توكن تسجيل مباشرة، لأن هذا السيرفر بلا بريد مضبوط فلا يوجد تحقق من البريد، وبه يكمل أي شخص يصل إلى العنوان إنشاء حساب. والحساب وحده لا يرى شيئاً من المؤسسة، ولكنه يعطي الغريب مكاناً على سيرفرك يخزن فيه ما يشاء، ويعطيه واجهات أكثر يختبرها. لذلك اكتب `SIGNUPS_ALLOWED=false` دائماً، وتأكد منه بالأمر أعلاه بعد كل تحديث للإعدادات.

## التحديثات

ثبت الإصدار في ملف Compose كما فعلنا بـ `vaultwarden/server:1.37.4`، ولا تستخدم `latest`، والسبب أنك تريد أن تقرأ ملاحظات الإصدار قبل أن يصل إليك، وملاحظات 1.37.4 مثال جيد: سبع ثغرات أمنية، وتغيير في طريقة قراءة عنوان الكلاينت خلف أكثر من Proxy، وحذف نقاط قديمة يعتمد عليها سطر أوامر قديم. وطريقة التحديث التي نوصي بها هي التالية:

1. اقرأ صفحة الإصدار الجديد في [صفحة الإصدارات](https://github.com/dani-garcia/vaultwarden/releases?ref=arabroot.io)، وانتبه لقسم «Upgrade notes» و«Security Fixes».
2. شغل سكربت النسخ الاحتياطي، وتأكد أن الأرشيف موجود.
3. غير رقم الإصدار في ملف Compose، ثم `docker compose pull` و`docker compose up -d`.
4. افتح الخزنة Vault من المتصفح، وزامن من سطر الأوامر، ونزل مرفقاً.

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

## الدخول الموحد SSO

يدعم Vaultwarden الدخول عبر OpenID Connect منذ الإصدار 1.35.0، فيمكن أن يدخل الموظفون عبر authentik الذي ثبتناه في [دليل 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/)، وتفعله المتغيرات `SSO_ENABLED` و`SSO_AUTHORITY` و`SSO_CLIENT_ID` و`SSO_CLIENT_SECRET` بحسب [صفحة SSO في الويكي](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-SSO-support-using-OpenId-Connect?ref=arabroot.io). ولكن انتبه لثلاث نقاط تذكرها الصفحة نفسها:

- كلمة المرور الرئيسية تبقى مطلوبة ولا يتحكم فيها مزود الهوية، فـ SSO هنا يتحكم في من يدخل، وتعطيل الموظف في مزود الهوية يمنعه من الدخول من جديد، ولكنه لا يغني عن سحب وصوله في المؤسسة.
- `SSO_AUTHORITY` يجب أن يطابق الحقل `issuer` في وثيقة الاكتشاف حرفاً بحرف.
- تفعيل `SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION` مع `SSO_SIGNUPS_MATCH_EMAIL` قد يسمح بالاستيلاء على حساب، فلا تفعلهما معاً.

ولم نفعل SSO في هذه التجربة، فربطه بـ authentik وما يحدث للجلسات عند سحب الدور موضوع مستقل، وقد شرحنا الفكرة العامة في [إدارة الأدوار عبر التطبيقات](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D8%A7%D8%AF%D9%88%D8%A7%D8%B1-%D8%B9%D8%A8%D8%B1-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA%D9%83-%D9%85%D9%86-authentik/).

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

نحذف ما أنشأناه بالاسم فقط، ثم Docker Images التي نزلناها للتجربة إذا لم تكن تستخدمها في شيء آخر:

```bash
docker rm -f lab-pw-vw-r lab-pw-cli lab-pw-caddy lab-pw-vw lab-pw-mailpit
docker volume rm lab-pw-vw-data lab-pw-caddy-data lab-pw-vw-restore
docker network rm lab-pw-net
docker rmi vaultwarden/server:1.37.4 node:24.21.0-alpine
```

## أخطاء شائعة

- **ترك `SIGNUPS_ALLOWED` بقيمته الافتراضية**: القيمة الافتراضية `true`، فكل من يصل إلى العنوان يستطيع أن ينشئ حساباً.
- **رمز الإدارة نصاً صريحاً**: من يقرأ ملف `.env` أو مخرج `docker inspect` يصبح مدير السيرفر، فاستخدم هاش Argon2، وأغلق `/admin` من خارج شبكة الإدارة.
- **نسخ `db.sqlite3` وحده**: دون المرفقات تفقد الملفات بصمت، ودون `rsa_key.pem` تسقط كل الجلسات والدعوات.
- **نسخ قاعدة البيانات بأمر نسخ عادي وهي تعمل**: استخدم `/vaultwarden backup`، وعند الاستعادة احذف أي `db.sqlite3-wal` قديم.
- **مالك واحد للمؤسسة**: الوصول الطارئ Emergency Access لا يغطي عناصر المؤسسة كما رأينا، فأضف مالكاً ثانياً.
- **فرض التحقق الثنائي دون إبلاغ**: كل عضو بلا تحقق ثنائي يفقد وصوله في اللحظة نفسها.
- **الـ Proxy لا يمرر عنوان الكلاينت**: فيظهر عنوان الـ Proxy في السجل، ويحظره fail2ban فيقطع الخدمة عن الجميع.
- **استخدام `latest`**: تصلك تغييرات تكسر الإعداد دون أن تقرأ ملاحظاتها.

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- الخزنة في المتصفح لا تعمل عبر HTTP لأن Web Crypto تحتاج سياقاً آمناً، وسطر أوامر Bitwarden يرفض HTTP أيضاً، فضع Vaultwarden خلف Reverse Proxy بشهادة يثق بها الموظفون، عامة أو من جهة إصدار داخلية.
- رمز الإدارة بهاش Argon2، والتسجيل مغلق بـ `SIGNUPS_ALLOWED=false`، والدخول بالدعوة فقط، وإنشاء المؤسسات محصور في حساب تقنية المعلومات.
- العضو في فريق، والفريق يصل إلى مجموعة، والعضو لا يحصل على مفتاح المؤسسة حتى يؤكده المالك.
- ترحيل ملف Excel يحتاج أكثر من حفظه بصيغة CSV: سطر العناوين ليس دائماً الأول، والصفوف تتكرر، والأرقام تفقد أصفارها، وكل كلمة مرور في الملف تحتاج تغييراً بعد الاستيراد.
- سياسة التحقق الثنائي تسحب وصول من لم يفعله فوراً، والوصول الطارئ يغطي الخزنة الشخصية وليس عناصر المؤسسة.
- النسخة الاحتياطية هي قاعدة البيانات والمرفقات و`rsa_key.pem`، واختبارها يعني أن تنزل مرفقاً من السيرفر المستعاد، وليس أن تفتح الخزنة Vault فقط.

[![](https://arabroot.io/content/images/2026/10/password-management-cover-cover.webp)دليلإدارة كلمات المرور في المؤسسة: من ملف Excel إلى خزنةلماذا لا يصلح ملف Excel لكلمات مرور الشركة، وكيف تنقل الحسابات المشتركة إلى خزنة بصلاحيات وسجل وخطة طوارئ.](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D9%83%D9%84%D9%85%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B1%D9%88%D8%B1-%D9%81%D9%8A-%D8%A7%D9%84%D9%85%D8%A4%D8%B3%D8%B3%D8%A9/)[![](https://arabroot.io/content/images/2026/10/password-manager-tools-cover-cover.webp)دليلBitwarden أم Vaultwarden أم Passbolt؟ مدير كلمات مرور لفريقكمقارنة بين مديري كلمات المرور للفرق في طريقة المشاركة والترخيص والسعر والمزايا، ودليل يساعدك على اختيار الأنسب لشركتك.](https://arabroot.io/articles/bitwarden-%D8%A7%D9%85-vaultwarden-%D8%A7%D9%85-passbolt/)[![](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/caddy-cover-cover.webp)دليلتثبيت Caddy على خادمك: Reverse Proxy بشهادات HTTPS تلقائيةكيف تثبت Caddy على سيرفرك ليمنح كل موقع شهادة HTTPS تلقائياً، وتربط به تطبيقاتك بملف إعداد من بضعة أسطر.](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-caddy-reverse-proxy/)[![](https://arabroot.io/content/images/2026/10/backup-321-cover-cover.webp)دليلالنسخ الاحتياطي بقاعدة 3-2-1 واختبار الاستعادةكيف تحدد كم بيانات تقبل أن تخسر، وتبني نسخاً لا يستطيع المخترق حذفها، وتتأكد أنك تستطيع استعادتها فعلاً.](https://arabroot.io/articles/%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D8%A7%D9%84%D8%A7%D8%AD%D8%AA%D9%8A%D8%A7%D8%B7%D9%8A-%D8%A8%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-3-2-1-%D9%88%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1-%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%B9%D8%A7%D8%AF%D8%A9/)[![](https://arabroot.io/content/images/2026/10/passkeys-cover-cover.webp)دليلالدخول دون كلمة مرور: شرح Passkeys وWebAuthnلماذا لا تحميك رموز SMS وTOTP من التصيد الحديث، وكيف تحل Passkeys المشكلة بمفتاح خاص لا يغادر جهازك.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AF%D8%AE%D9%88%D9%84-%D8%AF%D9%88%D9%86-%D9%83%D9%84%D9%85%D8%A9-%D9%85%D8%B1%D9%88%D8%B1-passkeys-%D9%88-fido2/)

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

- أكتوبر 2026: كتابة المقال، وتنفيذ التجربة في 14 أكتوبر 2026 على Docker Engine 29.8.0 مع Vaultwarden 1.37.4 (الخزنة في المتصفح 2026.7.0)، وCaddy 2.11.7، وMailpit 1.31.5، وسطر أوامر Bitwarden 2026.9.1 على Node.js 24.21.0، وPython 3.13.16 مع openpyxl 3.1.5، وChrome 154 للخزنة في المتصفح، وكل الحسابات والعناوين وكلمات المرور مصطنعة للتجربة.