SSD Nodes Learn 🎉 VPS من $5.50/شهر
الأدلة Matt Connorبقلم Matt Connor

كيفية تشفير الأسرار في مستودع Git باستخدام Ansible Vault

تعلّم تشفير ملف vars أو قيمة واحدة، فصل أسرار staging عن production، وإعادة تشفيرها بأمان، مع معرفة ما يحميه Ansible Vault وما لا يحميه.

ما الذي يحميه Ansible Vault وما الذي لا يحميه

يشفّر Ansible Vault الأسرار داخل مستودع playbook، لذلك يخزّن git نصاً مشفراً بدلاً من كلمة مرور واضحة. يشفّر الأمر ansible-vault ملفاً كاملاً أو قيمة واحدة داخل ملف، باستخدام مفتاح متماثل مشتق من كلمة مرور تختارها. يفك Ansible تشفير هذا المحتوى في الذاكرة عند تشغيل play، لذلك يتصرف المتغير مثل أي متغير آخر.

لهذا النموذج حد واضح. يحمي Vault سراً أثناء تخزينه في المستودع ولا يحميه بعد ذلك. عند تشغيل مهمة، تكون القيمة نصاً واضحاً في الذاكرة، وفي القالب الناتج، وفي وسيطات الوحدة، وفي مخرجات التشغيل، ما لم تمنع ظهورها. كل من يستطيع تشغيل playbook يملك كلمة مرور vault، لذلك يوفّر vault السرية تجاه الأشخاص خارج الفريق، وليس التحكم في الوصول لكل شخص داخل الفريق.

إذا لم تكتب playbook بعد، فابدأ بـ أول playbook لـ Ansible على VPS ثم عد عندما يحتاج ذلك playbook إلى كلمة مرور.

تشفير ملف كامل أم سلسلة نصية واحدة؟

يستبدل ansible-vault encrypt الملفَ بنص مشفّر. يصبح الملف كتلة واحدة من نص base64 تحت سطر رأس يبدأ بـ $ANSIBLE_VAULT. استخدمه عندما لا يحتوي الملف إلا على الأسرار.

يشفّر ansible-vault encrypt_string قيمة واحدة ويطبع مقتطف YAML تلصقه في ملف vars عادي. يبقى اسم المتغير مقروءاً، بينما تكون القيمة وحدها نصاً مشفّراً. استخدمه عندما توجد الأسرار بجانب إعدادات نصية عادية.

الفرق المهم في العمل اليومي هو diff. يُعاد تشفير ملف vault باستخدام salt عشوائي جديد في كل مرة تحفظه فيها، لذلك تتغير كل بايتات النص المشفّر. يعرض git diff عندها كتلة غير قابلة للقراءة استُبدلت بكتلة أخرى غير قابلة للقراءة، ما يعني أن المراجع لا يستطيع معرفة ما إذا كنت قد غيّرت كلمة مرور واحدة أو أعدت كتابة الملف. أما مع encrypt_string، فكل سر يكون كتلة مستقلة داخل ملف نصي عادي، لذلك يعرض diff المتغير الذي تغيّر بدقة ويترك بقية الملف دون تغيير.

للنموذج المضمّن تكلفة تظهر عند تدوير الأسرار: لا يتعامل ansible-vault rekey مع الكتل المضمّنة. اختر نموذج الملف عندما تكون قائمة الأسرار طويلة ولا تتغير كثيراً. واختر النموذج المضمّن عندما يجمع الملف بين الأسرار والمتغيرات العادية، وتريد أن تكون مراجعة الشفرة ذات معنى.

تخطيط group_vars الذي يوضح ما هو محمي

يحمّل Ansible ملف group_vars/<group>.yml، كما يحمّل كل ملف داخل دليل group_vars/<group>/. صيغة الدليل هي ما تحتاج إليه، لأنها تتيح للمجموعة نفسها احتواء ملف نص عادي وملف مشفّر جنباً إلى جنب.

inventory/
  hosts.ini
group_vars/
  all/
    vars.yml
    vault.yml
  web/
    vars.yml
    vault.yml
host_vars/
  db01/
    vars.yml
    vault.yml
playbooks/
  site.yml

كل vault.yml مشفّر. وكل vars.yml بنص عادي. يمكن للقارئ معرفة القيم المحمية من دون فتح أي ملف، لأن اسم الملف يوضح ذلك.

الجزء الثاني من النمط هو الإحالة غير المباشرة. داخل الملف المشفّر، أضف البادئة vault_ إلى كل متغير.

vault_db_password: "a real password"
vault_grafana_admin_token: "a real token"

ثم أشر إلى هذه الأسماء من ملف النص العادي المجاور له.

db_password: "{{ vault_db_password }}"
grafana_admin_token: "{{ vault_grafana_admin_token }}"

تستخدم الأدوار والقوالب db_password، ولا تعرف أبداً مصدر القيمة، ما يحافظ على وضوح الفصل بين playbook والدور. ويعمل ملف النص العادي vars.yml أيضاً كفهرس قابل للبحث: يسرد grep -r vault_ group_vars/ كل سر يتوقعه المستودع، من دون فك تشفير أي شيء. والتكلفة هي اسم إضافي واحد لكل سر، بينما يظهر الخطأ المطبعي في اسم vault_ وقت التشغيل على شكل متغير غير معرّف، وليس كخطأ في بناء الجملة.

تشفير متغير واحد باستخدام encrypt_string

ansible-vault encrypt_string --vault-id prod@~/.ansible/vault-prod.txt \
  --stdin-name 'vault_db_password'

اكتب السر، ثم اضغط Ctrl-D. يقرأ --stdin-name القيمة من الإدخال القياسي، ما يمنع تسجيلها في ملف سجل أوامر الصدفة. أما الصيغة الأخرى فتضع القيمة في سطر الأوامر، حيث تسجلها الصدفة:

ansible-vault encrypt_string --vault-id prod@~/.ansible/vault-prod.txt \
  'a real password' --name 'vault_db_password'

في كلتا الحالتين، يطبع الأمر كتلة YAML. الصقها في ملف vars كما طُبعت تماماً، لأن المسافة البادئة تحت الوسم !vault جزء من القيمة.

vault_db_password: !vault |
          $ANSIBLE_VAULT;1.2;AES256;prod
          6638643965323633646262656665306333616466396630323136393465356136396436383331
          3131303163306665326539353837343663313762616561306534373963383531613664393332

يُخبر الوسم !vault محمّل YAML بأن القيمة scalar عبارة عن نص مشفّر، لا نص عادي. يتضمن الرأس إصدار التنسيق، والخوارزمية، وتصنيف vault ID الذي شُفّرت به القيمة. تحمل القيمة المشفّرة من دون vault ID رأس 1.1 بلا تصنيف، وتظل صالحة للاستخدام، لكنها توفّر معلومات أقل عن مصدر كلمة المرور.

أين تُحفظ كلمة مرور vault؟

خارج المستودع. هذه هي القاعدة الوحيدة التي لا استثناءات لها.

يطلب --ask-vault-pass كلمة المرور مرة واحدة في كل تشغيل ولا يخزّن شيئاً. يناسب ذلك حاسوباً محمولاً، ولا يناسب مهمة cron أو مشغّل CI.

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

mkdir -p ~/.ansible
install -m 600 /dev/null ~/.ansible/vault-prod.txt
$EDITOR ~/.ansible/vault-prod.txt

مرّر الملف إلى أي أمر باستخدام --vault-password-file:

ansible-playbook -i inventory/hosts.ini playbooks/site.yml \
  --vault-password-file ~/.ansible/vault-prod.txt

من السهل نسيان تكرار هذا الخيار في كل أمر، لذلك عيّنه مرة واحدة في ansible.cfg عند جذر المستودع.

[defaults]
inventory = inventory/hosts.ini
vault_password_file = ~/.ansible/vault-prod.txt

يقرأ الإعداد نفسه من متغير البيئة ANSIBLE_VAULT_PASSWORD_FILE، وهذه هي الطريقة المعتادة التي توفّر بها مهمة CI كلمة المرور. تكتب المهمة كلمة المرور من مخزن بيانات الاعتماد الخاص بها في ملف داخل دليل مؤقت، ثم تصدّر المتغير وتحذف الملف عند انتهاء التشغيل. أضف نمط اسم الملف إلى .gitignore أيضاً، لأن المسار في ansible.cfg مُضمَّن في المستودع، وعاجلاً أو آجلاً سينشئ أحدهم الملف الفعلي داخل نسخة العمل.

إذا كان ملف كلمات المرور قابلاً للتنفيذ، يشغّله Ansible ويقرأ كلمة المرور من مخرجاته القياسية بدلاً من قراءة الملف كنص. بهذه الطريقة تستخرج كلمة مرور vault من حلقة مفاتيح النظام أو من مدير أسرار سحابي دون كتابتها على القرص إطلاقاً. يحتوي البرنامج النصي المستخدم عبر --vault-id على متطلبات إضافية: يجب أن ينتهي اسمه بـ -client أو بـ -client متبوعاً بامتداد، ويجب أن يكون قابلاً للتنفيذ، وأن يقبل خيار --vault-id، وأن يطبع كلمة المرور إلى المخرجات القياسية.

معرّفا vault: staging وproduction

معرّف vault هو تسمية مرتبطة بكلمة مرور vault، وتُكتب بالشكل label@source. يمكن أن يكون المصدر prompt، أي مسار ملف كلمات المرور أو مسار برنامج نصي للعميل. تتيح التسميات لمستودع واحد الاحتفاظ بالأسرار المحمية بأكثر من كلمة مرور، لذلك لا تفتح كلمة مرور staging الملف الخاص بـproduction.

ansible-vault encrypt --vault-id staging@~/.ansible/vault-staging.txt \
  group_vars/staging/vault.yml
ansible-vault encrypt --vault-id prod@~/.ansible/vault-prod.txt \
  group_vars/prod/vault.yml

مرّر كل معرّف قد يحتاج إليه التشغيل:

ansible-playbook playbooks/site.yml \
  --vault-id staging@~/.ansible/vault-staging.txt \
  --vault-id prod@~/.ansible/vault-prod.txt

أو أدرجها مرة واحدة في ansible.cfg:

[defaults]
vault_identity_list = staging@~/.ansible/vault-staging.txt, prod@~/.ansible/vault-prod.txt

هناك سلوك يفاجئ بعض المستخدمين. تكون التسمية، افتراضياً، تلميحاً وليست قفلاً. يجرّب Ansible كل سر يحتفظ به حالياً على الملف إلى أن ينجح أحدها في فك تشفيره، لذلك يظل الملف الذي يحمل التسمية staging قابلاً للفتح إذا كانت كلمة مرور production هي المفتاح الصحيح. اضبط vault_id_match = True ضمن [defaults]، أو استخدم متغير البيئة ANSIBLE_VAULT_ID_MATCH، لكي يستخدم Ansible السر الذي تطابق تسميته ترويسة الملف فقط. يحتاج هذا التحقق إلى ترويسة 1.2، لذلك ينطبق فقط على المحتوى الذي شُفّر باستخدام معرّف vault من البداية.

عند تحميل أكثر من معرّف واحد، لا يعود ansible-vault encrypt يعرف كلمة المرور التي يجب استخدامها للتشفير. حدّدها باستخدام --encrypt-vault-id prod، أو اضبط vault_encrypt_identity في ansible.cfg لكي يكون للمستودع إعداد افتراضي.

الفائدة هي تحديد نطاق النشر. يحصل job في CI ينشر staging على كلمة مرور staging فقط، لذلك لا يستطيع runner مخترق قراءة بيانات اعتماد production. عندما تشغّل playbooks على مجموعة من خوادم Linux من جهاز تحكم واحد، يكون هذا الفصل هو الفارق بين حادثة محدودة وحادثة واسعة النطاق.

إعادة إنشاء مفتاح الخزنة عند مغادرة أحد الأشخاص

تغيّر إعادة إنشاء المفتاح كلمة مرور الخزنة، وتعيد تشفير المحتوى باستخدام كلمة المرور الجديدة. لكنها لا تلغي أي شيء. يظل بإمكان كل من امتلك كلمة المرور القديمة في أي وقت فك تشفير أي نسخة من المستودع احتفظ بها، بما في ذلك كل commit قديم في تلك النسخة. لذلك اعتبر كلمة مرور الخزنة مكشوفةً منذ لحظة مغادرة حاملها، ونفّذ التدوير بالترتيب التالي.

  1. غيّر بيانات الاعتماد الفعلية على الخوادم وفي خدمات الجهات الخارجية. هذه هي الخطوة التي تلغي الوصول فعلياً.
  2. ضع القيم الجديدة في ملفات الخزنة باستخدام ansible-vault edit.
  3. أعد إنشاء مفتاح كل ملف مشفّر باستخدام كلمة مرور خزنة جديدة.
  4. سلّم كلمة مرور الخزنة الجديدة إلى الأشخاص الذين ما زالوا يحتاجون إليها، عبر قناة ليست المستودع.
ansible-vault rekey --vault-id prod@~/.ansible/vault-prod-old.txt \
  --new-vault-id prod@prompt \
  group_vars/prod/vault.yml host_vars/db01/vault.yml

يقبل rekey عدة ملفات في أمر واحد، ويطلب --new-vault-id prod@prompt كلمة المرور الجديدة مرة واحدة بدلاً من قراءتها من القرص. أبقِ التسمية نفسها ما لم يكن لديك سبب لتغييرها، لأن التسمية تُكتب في ترويسة كل ملف يعيد الأمر كتابته.

هنا تظهر كلفة الصيغة المضمّنة. يعمل ansible-vault rekey على ملفات مشفّرة بالكامل، لذلك تظل كتلة !vault الموجودة داخل ملف vars نصي واضح دون تغيير. اعثر عليها أولاً، ثم أنشئ كل ملف منها مجدداً باستخدام encrypt_string وكلمة المرور الجديدة:

grep -rl '!vault' group_vars/ host_vars/

هذه هي المفاضلة كاملةً. تمنحك الكتل المضمّنة فروقاً قابلة للقراءة، لكنها تتطلب مراجعة يدوية عند وقت التدوير. أما الملفات المشفّرة بالكامل فتُدوَّر بأمر واحد، لكنها لا تمنحك شيئاً مفيداً عند المراجعة.

لماذا لا يزال السر يظهر في مخرجاتك

ينتهي دور Vault بمجرد فك تشفير القيمة. يعرض Ansible نتيجة المهمة، والوحدة التي تطبع وسيطاتها تنقل بيانات الاعتماد إلى ذلك التقرير. وسيحتوي التشغيل المطوّل، أو --diff في مهمة قالب، أو مهمة فاشلة تطبع وسائطها، أو إضافة callback تكتب المخرجات إلى ملف، على النص الواضح. لم يؤثر تشفير الملف في أي من هذه المواضع.

no_log: true هو المفتاح. عيّنه في أي مهمة تتلقى بيانات اعتماد.

- name: Write the application environment file
  ansible.builtin.template:
    src: app.env.j2
    dest: /etc/myapp/app.env
    owner: myapp
    group: myapp
    mode: "0600"
  no_log: true

يحجب Ansible بعد ذلك نتيجة هذه المهمة من المخرجات، ولذلك يسجل السجل أن المهمة نُفذت دون تسجيل ما عالجته. عيّنه في الحلقات خصوصاً، لأن الحلقة تسجل نتيجة واحدة لكل عنصر، والحلقة التي تتعامل مع قائمة بيانات اعتماد تسجل القائمة بأكملها.

هناك أربعة مواضع أخرى يمكن أن يتسرب منها السر بعد فك تشفيره، ولا يغطي no_log أياً منها:

  • يرث الملف الذي أُنشئ من قالب قيم mode وowner التي حددتها له. عيّن mode: "0600" ومالكاً محدداً لأي ملف يحتوي على بيانات اعتماد، وإلا أصبح السر قابلاً للقراءة من الجميع على الخادم الهدف.
  • يظهر السر الممرر إلى ansible.builtin.command أو ansible.builtin.shell في قائمة العمليات على الخادم الهدف أثناء تشغيل الأمر، حيث يمكن لأي مستخدم محلي قراءته. مرّره عبر ملف أو متغير بيئة بدلاً من ذلك.
  • يكتب التخزين المؤقت للحقائق الحقائق التي جُمعت إلى القرص على جهاز التحكم، ولذلك قد ينتهي متغير مسجل يحتوي على سر داخل ملف تخزين مؤقت لا يعتبره أحد حساساً.
  • يوجد السر نفسه عادةً في موضع ثانٍ، مثل ملف بيئة يقرأه container. وتكون القواعد هناك منفصلة، ويتناول إبقاء بيانات الاعتماد خارج ملفات بيئة Compose هذا الجانب.

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

قراءة الملفات المشفّرة وتحريرها من دون ترك نسخة نصية مكشوفة

يفك ansible-vault view group_vars/prod/vault.yml التشفير داخل عارض صفحات ولا يكتب شيئاً إلى القرص. ويفك ansible-vault edit التشفير داخل ملف مؤقت، ويفتح $EDITOR، ثم يعيد التشفير عند إغلاقه. استخدم كليهما بدلاً من ansible-vault decrypt، الذي يترك ملفاً نصياً مكشوفاً داخل شجرة العمل. إن إضافة ملف خزنة مفكوك التشفير إلى منطقة التجهيز عن طريق الخطأ هي الطريقة الأكثر شيوعاً لوصول بيانات اعتماد حقيقية إلى مستودع عام.

يمكن لـGit إنشاء diff قابل للقراءة للملفات المشفّرة بالكامل عبر فك تشفيرها أثناء المعالجة:

git config --local diff.ansible-vault.textconv "ansible-vault view --vault-password-file ~/.ansible/vault-prod.txt"
printf '%s\n' 'group_vars/**/vault.yml diff=ansible-vault' >> .gitattributes

افهم وظيفة ذلك قبل تفعيله. سيطبع git diff الآن الأسرار الخاصة ببيئة الإنتاج في الطرفية، ما يضعها في سجل التمرير وفي أي مشاركة للشاشة. هذا إجراء محلي مخصص لشخص واحد وعلى جهاز واحد، لذا أبقِ git config محلياً، وتوقّع أن تتصرف نسخ المستودع لدى الآخرين بطريقة مختلفة ما لم يضبطوا الإعداد نفسه.

عندما لا يعود Vault الأداة المناسبة

Vault هو تنسيق ملفات يحتوي على كلمة مرور واحدة لكل تسمية، وهذا الهيكل يحدد الحالات التي لا يعود مناسباً لها. انتقل إلى مخزن أسرار فعلي عند تحقق أي مما يلي.

  • تحتاج إلى صلاحيات لكل شخص. كل من يشغّل الـplaybook يمتلك كلمة المرور نفسها، وتفصل vault IDs الوصول حسب البيئة فقط، وليس حسب الأشخاص.
  • تحتاج إلى سجل تدقيق. لا يسجّل Vault من فك تشفير أي قيمة أو وقت حدوث ذلك.
  • تحتاج إلى تدوير مجدول. لا يوفّر Vault انتهاء صلاحية أو إصدارات، لذلك لا يوجد ما يخبرك بأن بيانات الاعتماد لم تتغير منذ عامين.
  • يحتاج التطبيق نفسه إلى السر أثناء التشغيل. يجب ألا تكون الخدمة التي تقرأ كلمة مرور قاعدة البيانات عند الإقلاع تقرأها من مستودع النشر.

ينعكس النمط عندئذٍ. يتوقف Ansible عن تخزين الأسرار ويبدأ في جلبها أثناء التشغيل من خلال lookup plugin، وذلك من HashiCorp Vault (وهو منتج مختلف يحمل اسماً مشابهاً بشكل مربك)، أو من مدير أسرار لدى مزود سحابي، أو من keyring على جهاز التحكم. يحتوي المستودع على المسار، ويحتوي المخزن على القيمة، بينما يحتفظ المخزن بسجل الوصول. بالنسبة إلى فريق صغير، يوفّر مدير كلمات مرور مستضاف ذاتياً مع API، مثل خادم Vaultwarden، الوظيفة نفسها على نطاق أصغر.

تبقى إحدى بيانات الاعتماد خارج كل ذلك. مفتاح SSH الذي يستخدمه جهاز التحكم للوصول إلى الخوادم ليس مشكلة تخص vault، لأن Ansible يحتاج إليه قبل أن يتمكن أي play من التشغيل. أدِره باستخدام agent وعبارة مرور، وفق ما يرد في أساسيات إدارة مفاتيح SSH.

FAQ

هل يجب تشفير ملف vars بأكمله أم سلسلة السر فقط؟

شفّر الملف بأكمله عندما لا يحتوي إلا على الأسرار، لأن أمراً واحداً يدوّرها كلها ويحافظ على بساطة البنية. استخدم ansible-vault encrypt_string عندما توجد الأسرار بجانب متغيرات عادية، لأن قيمة واحدة مشفّرة فقط ستتغير في الفرق، ويمكن للمراجع معرفة المتغير الذي عُدِّل. المفاضلة هنا هي عملية التدوير. يتعامل ansible-vault rekey مع الملفات بأكملها ويترك كتل !vault المضمّنة دون تغيير، لذلك يجب إعادة إنشائها يدوياً باستخدام كلمة المرور الجديدة.

أين يجب تخزين ملف كلمة مرور Ansible Vault؟

خزّنه خارج المستودع، مع ضبط الوضع على 0600، في مسار مثل ~/.ansible/vault-prod.txt. حدّد مساره باستخدام --vault-password-file، أو اضبط vault_password_file ضمن [defaults] في ansible.cfg، أو اضبط ANSIBLE_VAULT_PASSWORD_FILE في البيئة. في CI، اجعل المهمة تكتب كلمة المرور من مخزن بيانات الاعتماد الخاص بها إلى ملف مؤقت، ثم صدّر المتغير واحذف الملف عند انتهاء المهمة. إذا كان الملف قابلاً للتنفيذ، يشغّله Ansible ويقرأ كلمة المرور من الإخراج القياسي، ما يتيح جلبها من keyring بدلاً من تخزينها على القرص.

كيف أستخدم كلمات مرور مختلفة لـVault في بيئتي staging وproduction؟

امنح كل كلمة مرور تسمية باستخدام --vault-id staging@/path/to/file و--vault-id prod@/path/to/file، وشِفّر ملفات كل بيئة باستخدام تسميتها الخاصة. مرّر المعرّفين في وقت التشغيل، أو أدرجهما في vault_identity_list ضمن [defaults]. يحاول Ansible افتراضياً استخدام كل سر يحتفظ به إلى أن ينجح أحدها في فك تشفير الملف، لذلك اضبط vault_id_match = True إذا أردت أن يحاول فقط استخدام السر الذي تطابق تسميته التسمية الموجودة في ترويسة الملف. عند تحميل عدة معرّفات، حدّد المعرّف المستخدم للتشفير عبر --encrypt-vault-id.

هل يمنع Ansible Vault ظهور كلمة المرور في مخرجات التشغيل؟

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