SSD Nodes Learn 🎉 VPS เริ่ม $5.50/เดือน
คู่มือ Matt Connorโดย Matt Connor

วิธีใช้ Ansible Vault เข้ารหัสข้อมูลลับใน Git อย่างปลอดภัย

เรียนรู้วิธีจัดการรหัสผ่านและ API token ใน Ansible ด้วยการเข้ารหัสไฟล์ vars หรือสตริงเฉพาะจุด พร้อมเทคนิคการแยกสภาพแวดล้อม staging และ production รวมถึงการ rekey อย่างถูกวิธี

สิ่งที่ Ansible Vault ป้องกันและสิ่งที่ไม่สามารถป้องกันได้

Ansible Vault ทำการเข้ารหัสข้อมูลลับภายใน repository ของ playbook ของคุณ ดังนั้นสิ่งที่ git จัดเก็บจึงเป็น ciphertext แทนที่จะเป็นรหัสผ่านแบบ plaintext คำสั่ง ansible-vault จะเข้ารหัสทั้งไฟล์หรือค่าเพียงค่าเดียวภายในไฟล์ โดยใช้ symmetric key ที่สร้างจากรหัสผ่านที่คุณกำหนด Ansible จะถอดรหัสเนื้อหานั้นในหน่วยความจำเมื่อ play ทำงาน ทำให้ตัวแปรดังกล่าวมีพฤติกรรมเหมือนตัวแปรอื่นๆ ทั่วไป

โมเดลนี้มีขอบเขตที่ชัดเจนหนึ่งประการ Vault ป้องกันข้อมูลลับ ในขณะที่จัดเก็บอยู่ใน repository เท่านั้น เมื่อ task เริ่มทำงาน ค่าดังกล่าวจะเป็น plaintext ในหน่วยความจำ, ใน template ที่ถูก render, ในอาร์กิวเมนต์ของโมดูล และในผลลัพธ์การทำงาน เว้นแต่คุณจะสั่งระงับไว้ ทุกคนที่สามารถรัน playbook ได้ย่อมถือรหัสผ่านของ vault ดังนั้น vault จึงให้การปกปิดข้อมูลจากบุคคลภายนอกทีม ไม่ใช่การควบคุมการเข้าถึงรายบุคคลภายในทีม

หากคุณยังไม่ได้เขียน playbook ให้เริ่มต้นที่ การเขียน Ansible playbook ครั้งแรกบน VPS แล้วค่อยกลับมาเมื่อ playbook นั้นต้องการรหัสผ่าน

เข้ารหัสทั้งไฟล์ หรือเข้ารหัสเฉพาะสตริง?

ansible-vault encrypt จะแทนที่ไฟล์เดิมด้วยข้อความที่เข้ารหัส (ciphertext) ไฟล์นั้นจะกลายเป็นบล็อกข้อความ base64 ภายใต้บรรทัดส่วนหัวที่ขึ้นต้นด้วย $ANSIBLE_VAULT ให้ใช้คำสั่งนี้เมื่อไฟล์นั้นไม่มีข้อมูลอื่นนอกจากความลับ

ansible-vault encrypt_string จะเข้ารหัสค่าเพียงค่าเดียวและแสดงผลเป็น YAML snippet เพื่อให้คุณนำไปวางในไฟล์ vars ปกติ ชื่อตัวแปรจะยังคงอ่านได้และมีเพียงค่าของตัวแปรเท่านั้นที่เป็น ciphertext ให้ใช้คำสั่งนี้เมื่อมีความลับวางปะปนอยู่กับค่าตั้งค่าที่เป็นข้อความธรรมดา

ความแตกต่างที่สำคัญในการทำงานประจำวันคือเรื่องของ diff ไฟล์ที่ใช้ vault จะถูกเข้ารหัสใหม่ด้วย salt สุ่มชุดใหม่ทุกครั้งที่คุณบันทึก ทำให้ทุกไบต์ของ ciphertext เปลี่ยนไป ดังนั้น git diff จะแสดงผลเพียงบล็อกที่อ่านไม่รู้เรื่องบล็อกหนึ่งถูกแทนที่ด้วยอีกบล็อกหนึ่ง ซึ่งหมายความว่าผู้ตรวจสอบจะไม่สามารถทราบได้เลยว่าคุณได้เปลี่ยนรหัสผ่านเพียงตัวเดียวหรือเขียนไฟล์ใหม่ทั้งหมด สำหรับ encrypt_string ความลับแต่ละรายการจะเป็นบล็อกแยกกันภายในไฟล์ข้อความธรรมดา ดังนั้น diff จะแสดงให้เห็นชัดเจนว่าตัวแปรใดที่เปลี่ยนแปลงไปและคงส่วนอื่นของไฟล์ไว้ตามเดิม

รูปแบบการเข้ารหัสแบบ inline มีข้อจำกัดเมื่อถึงเวลาหมุนเวียนรหัสผ่าน (rotation): ansible-vault rekey จะไม่จัดการกับบล็อกแบบ inline ให้เลือกใช้รูปแบบไฟล์เมื่อรายการความลับมีจำนวนมากและมีการเปลี่ยนแปลงน้อย และเลือกใช้รูปแบบ inline เมื่อไฟล์มีการผสมผสานระหว่างความลับกับตัวแปรทั่วไป และคุณต้องการให้การตรวจสอบโค้ด (code review) มีความหมายชัดเจน

โครงสร้าง 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 }}"

Role และ template จะใช้ db_password โดยไม่จำเป็นต้องทราบว่าค่าดังกล่าวมาจากที่ใด ซึ่งช่วยรักษา การแยกส่วนระหว่าง playbook และ role ให้เป็นระเบียบ ไฟล์ vars.yml ที่เป็นข้อความธรรมดายังทำหน้าที่เป็นดัชนีที่ค้นหาได้ในตัว: grep -r vault_ group_vars/ จะแสดงรายการความลับทั้งหมดที่ repository คาดหวังไว้ โดยไม่จำเป็นต้องถอดรหัสใดๆ ทั้งสิ้น สิ่งที่ต้องแลกคือการเพิ่มชื่ออีกหนึ่งชื่อต่อความลับหนึ่งรายการ และหากพิมพ์ชื่อ vault_ ผิดพลาด ระบบจะแจ้งว่าเป็นตัวแปรที่ไม่ได้กำหนดค่าไว้ในขณะรันไทม์ แทนที่จะเป็นข้อผิดพลาดทางไวยากรณ์

การเข้ารหัสตัวแปรหนึ่งค่าด้วย encrypt_string

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

พิมพ์ค่าความลับแล้วกด Ctrl-D ตัว --stdin-name จะอ่านค่าจาก standard input ซึ่งช่วยป้องกันไม่ให้ค่าดังกล่าวถูกบันทึกลงในไฟล์ประวัติการใช้งาน shell ของคุณ ส่วนรูปแบบอื่นที่ใส่ค่าลงในบรรทัดคำสั่งโดยตรงจะทำให้ shell บันทึกค่าเหล่านั้นไว้ในประวัติ

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 ทราบว่าข้อมูลนี้เป็น ciphertext ไม่ใช่ข้อความธรรมดา ส่วนหัวของข้อมูลจะระบุเวอร์ชันของรูปแบบ อัลกอริทึมการเข้ารหัส และป้ายกำกับ vault ID ที่ใช้ในการเข้ารหัส หากค่าใดถูกเข้ารหัสโดยไม่มี vault ID จะมีส่วนหัวเป็น 1.1 ซึ่งไม่มีป้ายกำกับระบุไว้ แม้จะยังใช้งานได้ตามปกติ แต่จะให้ข้อมูลที่น้อยลงเกี่ยวกับที่มาของรหัสผ่านนั้น

รหัสผ่านของ vault ถูกเก็บไว้ที่ไหน

อยู่นอก repository นี่คือกฎข้อเดียวที่ไม่มีข้อยกเว้น

--ask-vault-pass จะถามรหัสผ่านหนึ่งครั้งต่อการรันและไม่จัดเก็บข้อมูลใดๆ วิธีนี้เหมาะสำหรับแล็ปท็อป แต่ไม่เหมาะสำหรับงาน cron job หรือ CI runner

ไฟล์รหัสผ่านคือไฟล์ข้อความธรรมดาที่บรรทัดแรกคือรหัสผ่าน ให้สร้างไฟล์เปล่าขึ้นมาโดยกำหนดสิทธิ์การเข้าถึงให้เข้มงวด จากนั้นจึงใส่รหัสผ่านผ่านโปรแกรมแก้ไขข้อความ เพื่อให้รหัสผ่านไม่ถูกบันทึกลงในประวัติการใช้งาน shell ของคุณ:

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

การพิมพ์ flag ดังกล่าวซ้ำในทุกคำสั่งอาจทำให้ลืมได้ง่าย ดังนั้นให้ตั้งค่าไว้เพียงครั้งเดียวใน ansible.cfg ที่ root ของ repository:

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

การตั้งค่าเดียวกันนี้จะอ่านค่าจาก environment variable ANSIBLE_VAULT_PASSWORD_FILE ซึ่งเป็นวิธีที่งาน CI ปกติใช้ส่งผ่านรหัสผ่าน โดยงาน CI จะเขียนรหัสผ่านจากที่เก็บข้อมูลประจำตัวของตนเองลงในไฟล์ในไดเรกทอรีชั่วคราว จากนั้น export ตัวแปรดังกล่าว และลบไฟล์ทิ้งเมื่อการทำงานสิ้นสุดลง ให้เพิ่มรูปแบบชื่อไฟล์ลงใน .gitignore ด้วย เนื่องจาก path ใน ansible.cfg จะถูก commit ลงในระบบ และไม่ช้าก็เร็วจะมีคนสร้างไฟล์จริงขึ้นมาภายใน checkout

หากไฟล์รหัสผ่านสามารถรันได้ (executable) Ansible จะรันไฟล์นั้นและอ่านรหัสผ่านจาก standard output แทนการอ่านไฟล์เป็นข้อความ นี่คือวิธีที่คุณจะดึงรหัสผ่าน vault จาก system keyring หรือ cloud secret manager โดยไม่ต้องเขียนลงดิสก์เลย สคริปต์ที่ใช้ผ่าน --vault-id มีข้อกำหนดเพิ่มเติมดังนี้: ชื่อไฟล์ต้องลงท้ายด้วย -client หรือ -client ตามด้วยนามสกุลไฟล์, ต้องสามารถรันได้, ต้องรองรับตัวเลือก --vault-id และต้องพิมพ์รหัสผ่านออกมาทาง standard output

Vault ID สองชุดสำหรับ staging และ production

Vault ID คือป้ายกำกับที่แนบไปกับรหัสผ่านของ vault ซึ่งเขียนในรูปแบบ label@source โดยแหล่งที่มาคือ prompt ซึ่งเป็นพาธไปยังไฟล์รหัสผ่านหรือพาธไปยังสคริปต์ของไคลเอนต์ ป้ายกำกับเหล่านี้ช่วยให้ repository เดียวสามารถเก็บความลับภายใต้รหัสผ่านมากกว่าหนึ่งชุดได้ ดังนั้นรหัสผ่านของ 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

ส่งผ่าน ID ทุกตัวที่การทำงานนั้นๆ จำเป็นต้องใช้:

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 ID มาตั้งแต่ต้นเท่านั้น

เมื่อมีการโหลด ID มากกว่าหนึ่งชุด ansible-vault encrypt จะไม่ทราบอีกต่อไปว่าควรใช้รหัสผ่านใดในการเข้ารหัส ให้ระบุชื่อด้วย --encrypt-vault-id prod หรือตั้งค่า vault_encrypt_identity ใน ansible.cfg เพื่อให้ repository มีค่าเริ่มต้น

ผลตอบแทนที่ได้รับคือขอบเขตของการปรับใช้ งาน CI ที่ปรับใช้ staging จะได้รับเฉพาะรหัสผ่านของ staging เท่านั้น ดังนั้นหาก runner ถูกบุกรุก ก็จะไม่สามารถอ่านข้อมูลประจำตัวของ production ได้ เมื่อคุณเริ่มรัน play บน กลุ่มเซิร์ฟเวอร์ Linux จากเครื่องควบคุมเครื่องเดียว การแยกส่วนนี้คือความแตกต่างระหว่างเหตุการณ์เล็กน้อยกับเหตุการณ์ที่ใหญ่มาก

การเปลี่ยนรหัสผ่าน vault เมื่อมีพนักงานลาออก

การเปลี่ยนรหัสผ่าน (Rekeying) คือการเปลี่ยนรหัสผ่านของ vault และเข้ารหัสเนื้อหาใหม่ด้วยรหัสผ่านชุดใหม่ กระบวนการนี้ไม่ได้เป็นการย้อนกลับการเปลี่ยนแปลงใดๆ ผู้ที่เคยถือรหัสผ่านเดิมยังคงสามารถถอดรหัสสำเนาของ repository ที่พวกเขาเก็บไว้ได้ รวมถึงทุก commit เก่าในสำเนาเหล่านั้น ดังนั้นให้ถือว่ารหัสผ่าน vault ชุดเดิมนั้นถูกยกเลิกทันทีที่ผู้ถือรหัสผ่านลาออก และให้ดำเนินการหมุนเวียนรหัสผ่านตามลำดับดังนี้

  1. เปลี่ยนข้อมูลรับรอง (credentials) จริงบนเซิร์ฟเวอร์และในบริการของบุคคลที่สาม ขั้นตอนนี้คือขั้นตอนที่เพิกถอนสิทธิ์การเข้าถึงอย่างแท้จริง
  2. ใส่ค่าใหม่ลงในไฟล์ vault ด้วย ansible-vault edit
  3. ทำการ rekey ไฟล์ที่เข้ารหัสทั้งหมดด้วยรหัสผ่าน vault ชุดใหม่
  4. ส่งมอบรหัสผ่าน vault ชุดใหม่ให้กับผู้ที่ยังจำเป็นต้องใช้งานผ่านช่องทางอื่นที่ไม่ใช่ repository
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 จะถามรหัสผ่านใหม่เพียงครั้งเดียวแทนการอ่านจากดิสก์ ให้ใช้ label เดิมเว้นแต่คุณจะมีเหตุผลที่ต้องเปลี่ยน เนื่องจาก label จะถูกเขียนลงในส่วนหัวของทุกไฟล์ที่คำสั่งทำการเขียนทับ

นี่คือจุดที่รูปแบบ inline ทำให้คุณต้องมีภาระเพิ่มขึ้น ansible-vault rekey ทำงานกับไฟล์ที่เข้ารหัสไว้ทั้งหมด ดังนั้นบล็อก !vault ที่อยู่ในไฟล์ vars แบบ plaintext จะไม่ถูกแตะต้อง ให้ค้นหาบล็อกเหล่านั้นก่อน จากนั้นสร้างใหม่แต่ละรายการด้วย encrypt_string ภายใต้รหัสผ่านชุดใหม่

grep -rl '!vault' group_vars/ host_vars/

นี่คือข้อแลกเปลี่ยนทั้งหมด บล็อกแบบ inline ช่วยให้คุณอ่าน diff ได้ง่ายและแลกมาด้วยการต้องทำด้วยตนเองเมื่อถึงเวลาหมุนเวียนรหัสผ่าน ส่วนไฟล์ที่เข้ารหัสทั้งหมดจะหมุนเวียนได้ด้วยคำสั่งเดียวแต่จะไม่ให้ข้อมูลที่เป็นประโยชน์ใดๆ ในระหว่างการตรวจสอบ

เหตุใดความลับยังคงปรากฏในผลลัพธ์ของคุณ

Vault จะทำงานเสร็จสิ้นทันทีที่ค่าถูกถอดรหัส Ansible จะรายงานผลลัพธ์ของงาน และโมดูลที่แสดงอาร์กิวเมนต์ของตนเองจะนำข้อมูลประจำตัวนั้นติดไปในรายงานด้วย การรันแบบ verbose, การใช้ --diff ในงาน template, งานที่ล้มเหลวแล้วแสดงอาร์กิวเมนต์ออกมา หรือ callback plugin ที่เขียนผลลัพธ์ลงไฟล์ ล้วนทำให้ข้อมูลที่เป็น plaintext หลุดออกมาได้ การเข้ารหัสไฟล์ไม่ได้ช่วยป้องกันสิ่งเหล่านี้เลย

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 จะระงับผลลัพธ์ของงานนั้นไม่ให้แสดงใน output ทำให้ log บันทึกเพียงว่างานได้รันไปแล้วโดยไม่บันทึกสิ่งที่จัดการไป ให้ตั้งค่านี้โดยเฉพาะกับลูป เพราะลูปจะรายงานผลลัพธ์หนึ่งรายการต่อหนึ่งไอเทม และการลูปผ่านรายการข้อมูลประจำตัวจะรายงานรายการทั้งหมดออกมา

มีอีกสี่จุดที่ความลับซึ่งถูกถอดรหัสแล้วอาจรั่วไหลได้ ซึ่ง no_log ไม่ครอบคลุมถึง:

  • ไฟล์ที่สร้างจาก template จะสืบทอด mode และ owner ที่คุณกำหนดไว้ ให้ตั้งค่า mode: "0600" และระบุเจ้าของที่เฉพาะเจาะจงให้กับทุกสิ่งที่เก็บข้อมูลประจำตัว มิฉะนั้นความลับนั้นจะกลายเป็นไฟล์ที่ทุกคนอ่านได้บนโฮสต์ปลายทาง
  • ความลับที่ส่งผ่านไปยัง ansible.builtin.command หรือ ansible.builtin.shell จะปรากฏในรายการ process บนโฮสต์ปลายทางขณะที่คำสั่งกำลังทำงาน ซึ่งผู้ใช้ในเครื่องคนอื่นสามารถอ่านได้ ให้ส่งผ่านไฟล์หรือตัวแปร environment แทน
  • การทำ Fact caching จะเขียน fact ที่รวบรวมได้ลงดิสก์บนเครื่องควบคุม ดังนั้นตัวแปรที่ลงทะเบียนไว้ซึ่งเก็บความลับอาจไปอยู่ในไฟล์แคชที่ไม่มีใครนึกว่าเป็นข้อมูลอ่อนไหว
  • ความลับเดียวกันมักจะอยู่ในจุดที่สอง เช่น ไฟล์ environment ที่ container อ่าน กฎเกณฑ์ในส่วนนั้นจะแยกต่างหาก และ การเก็บข้อมูลประจำตัวไม่ให้ปนในไฟล์ Compose env จะครอบคลุมถึงประเด็นดังกล่าว

no_log ทำให้การดีบั๊กยากขึ้น ซึ่งนั่นคือจุดประสงค์ของมัน ให้ลบออกชั่วคราวบนโฮสต์ทดสอบเมื่อมีงานทำงานผิดพลาด และใส่กลับคืนก่อนที่จะนำการเปลี่ยนแปลงไปใช้ในระบบ production

อ่านและแก้ไขไฟล์ที่เข้ารหัสโดยไม่ทิ้งข้อมูลแบบ plaintext ไว้เบื้องหลัง

ansible-vault view group_vars/prod/vault.yml จะถอดรหัสข้อมูลลงในโปรแกรมอ่านไฟล์ (pager) โดยไม่มีการเขียนข้อมูลลงดิสก์ ส่วน ansible-vault edit จะถอดรหัสลงในไฟล์ชั่วคราว เปิดไฟล์นั้นด้วย $EDITOR ของคุณ และเข้ารหัสกลับเมื่อคุณปิดไฟล์ ควรเลือกใช้ทั้งสองวิธีนี้แทน ansible-vault decrypt ซึ่งจะทิ้งไฟล์ plaintext ไว้ใน working tree การเผลอ stage ไฟล์ vault ที่ถอดรหัสแล้วเป็นสาเหตุที่พบบ่อยที่สุดที่ทำให้ข้อมูลรับรองจริงหลุดไปอยู่ใน public repository

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 จะแสดงความลับของระบบงานจริง (production secrets) ออกมาใน terminal ของคุณ ซึ่งจะทำให้ข้อมูลเหล่านั้นไปปรากฏในประวัติการเลื่อนหน้าจอ (scrollback) และในการแชร์หน้าจอใดๆ นี่เป็นความสะดวกเฉพาะที่สำหรับบุคคลเดียวบนเครื่องเดียวเท่านั้น ดังนั้นควรเก็บ git config ไว้ในเครื่อง และพึงระลึกว่าการ checkout ของผู้อื่นจะมีพฤติกรรมที่แตกต่างออกไป เว้นแต่พวกเขาจะตั้งค่าแบบเดียวกันไว้

เมื่อ Vault ไม่ใช่เครื่องมือที่เหมาะสมอีกต่อไป

Vault เป็นรูปแบบไฟล์ที่เก็บรหัสผ่านหนึ่งชุดต่อหนึ่งป้ายกำกับ ซึ่งโครงสร้างนี้เป็นตัวกำหนดขีดจำกัดในการใช้งาน คุณควรย้ายไปใช้ระบบจัดเก็บความลับ (secret store) ที่แท้จริงเมื่อพบสถานการณ์ต่อไปนี้:

  • คุณต้องการการเข้าถึงแยกตามรายบุคคล ทุกคนที่รัน playbook จะใช้รหัสผ่านชุดเดียวกัน และ Vault ID จะแบ่งการเข้าถึงตามสภาพแวดล้อมเท่านั้น ไม่ใช่ตามตัวบุคคล
  • คุณต้องการบันทึกการตรวจสอบ (audit trail) Vault ไม่มีการบันทึกว่าใครเป็นผู้ถอดรหัสข้อมูลใดหรือถอดรหัสเมื่อใด
  • คุณต้องการการหมุนเวียนรหัสผ่านตามกำหนดเวลา Vault ไม่มีการตั้งวันหมดอายุและไม่มีการทำ versioning ดังนั้นจึงไม่มีสิ่งใดแจ้งเตือนคุณว่าข้อมูลรับรอง (credential) ไม่ได้ถูกเปลี่ยนมาเป็นเวลาสองปีแล้ว
  • ตัวแอปพลิเคชันเองต้องการความลับในขณะรันไทม์ บริการที่ต้องอ่านรหัสผ่านฐานข้อมูลตอนเริ่มต้นระบบไม่ควรต้องอ่านค่าดังกล่าวจาก repository ที่ใช้สำหรับ deployment

รูปแบบการทำงานจะเปลี่ยนไปในทางตรงกันข้าม Ansible จะหยุดจัดเก็บความลับและเริ่มดึงข้อมูลเหล่านั้นในขณะรันไทม์ผ่าน lookup plugin โดยดึงจาก HashiCorp Vault (ผลิตภัณฑ์คนละตัวที่มีชื่อคล้ายกันจนสับสน), บริการจัดการความลับของผู้ให้บริการคลาวด์ หรือ keyring บนเครื่องควบคุม (control machine) ตัว repository จะเก็บเพียง path ส่วนตัวจัดเก็บจะเก็บค่าจริงและบันทึก log การเข้าถึง สำหรับทีมขนาดเล็ก การใช้โปรแกรมจัดการรหัสผ่านแบบ self-hosted ที่มี API เช่น เซิร์ฟเวอร์ Vaultwarden สามารถรองรับงานเดียวกันได้ในขนาดที่เหมาะสมกว่า

มีข้อมูลรับรองชุดหนึ่งที่อยู่นอกเหนือจากระบบเหล่านี้ คือ SSH key ที่เครื่องควบคุมของคุณใช้เพื่อเชื่อมต่อไปยังเซิร์ฟเวอร์ ซึ่งไม่ใช่ปัญหาของ Vault เพราะ Ansible จำเป็นต้องใช้กุญแจนี้ก่อนที่ play ใดๆ จะเริ่มทำงาน ให้จัดการด้วย agent และ passphrase ตามแนวทางของ พื้นฐานการจัดการ SSH key

FAQ

ฉันควรเข้ารหัสไฟล์ vars ทั้งไฟล์หรือเฉพาะสตริงที่เป็นความลับ?

ให้เข้ารหัสทั้งไฟล์เมื่อไฟล์นั้นเก็บเฉพาะข้อมูลที่เป็นความลับเท่านั้น เพราะคำสั่งเดียวสามารถหมุนเวียน (rotate) ข้อมูลทั้งหมดได้และโครงสร้างไฟล์ยังคงเรียบง่าย ให้ใช้ ansible-vault encrypt_string เมื่อข้อมูลที่เป็นความลับวางอยู่ข้างตัวแปรทั่วไป เพราะจะทำให้เฉพาะค่าที่เข้ารหัสเท่านั้นที่มีการเปลี่ยนแปลงใน diff และผู้ตรวจสอบสามารถเห็นได้ว่าตัวแปรใดถูกแก้ไข ข้อแลกเปลี่ยนคือเรื่องการหมุนเวียนรหัสผ่าน ansible-vault rekey ครอบคลุมทั้งไฟล์และไม่ยุ่งกับบล็อก !vault แบบ inline ดังนั้นบล็อกเหล่านั้นจะต้องถูกสร้างใหม่ด้วยตนเองภายใต้รหัสผ่านใหม่

ควรเก็บไฟล์รหัสผ่าน Ansible Vault ไว้ที่ไหน?

ให้เก็บไว้นอก repository โดยตั้งค่าสิทธิ์เป็น 0600 ไว้ที่พาธเช่น ~/.ansible/vault-prod.txt ระบุตำแหน่งไฟล์ด้วย --vault-password-file หรือตั้งค่า vault_password_file ภายใต้ [defaults] ใน ansible.cfg หรือตั้งค่า ANSIBLE_VAULT_PASSWORD_FILE ใน environment variable สำหรับ CI ให้ job เขียนรหัสผ่านจาก credential store ของระบบเองลงในไฟล์ชั่วคราว จากนั้น export ตัวแปรและลบไฟล์ทิ้งเมื่อ job สิ้นสุด หากไฟล์นั้นสามารถรันได้ (executable) Ansible จะรันไฟล์ดังกล่าวและอ่านรหัสผ่านจาก standard output ซึ่งช่วยให้คุณดึงรหัสผ่านมาจาก keyring แทนการเก็บไว้บนดิสก์

ฉันจะใช้รหัสผ่าน vault ที่แตกต่างกันสำหรับ staging และ production ได้อย่างไร?

กำหนดป้ายกำกับ (label) ให้กับรหัสผ่านแต่ละชุดด้วย --vault-id staging@/path/to/file และ --vault-id prod@/path/to/file และเข้ารหัสไฟล์ของแต่ละ environment ภายใต้ป้ายกำกับของตนเอง ส่ง ID ทั้งสองชุดในขณะรัน หรือระบุไว้ใน vault_identity_list ภายใต้ [defaults] โดยปกติ Ansible จะพยายามใช้รหัสผ่านทุกชุดที่มีจนกว่าจะถอดรหัสไฟล์ได้สำเร็จ ดังนั้นให้ตั้งค่า vault_id_match = True หากคุณต้องการให้ Ansible พยายามใช้เฉพาะรหัสผ่านที่มีป้ายกำกับตรงกับส่วนหัวของไฟล์เท่านั้น เมื่อมีการโหลด ID หลายชุด ให้เลือก ID ที่ใช้สำหรับเข้ารหัสด้วย --encrypt-vault-id

Ansible Vault ป้องกันไม่ให้รหัสผ่านปรากฏในผลลัพธ์การรันหรือไม่?

ไม่ Vault ป้องกันเฉพาะข้อมูลที่เป็นความลับขณะจัดเก็บอยู่ใน repository เท่านั้น เมื่อ task เริ่มทำงาน ค่าดังกล่าวจะอยู่ในรูปแบบข้อความปกติ (plaintext) และการรันแบบ verbose หรือ task ที่ล้มเหลวอาจนำข้อมูลนั้นไปปรากฏใน log ได้ ให้เพิ่ม no_log: true ในทุก task ที่จัดการกับ credential ตั้งค่า mode และ owner ที่เข้มงวดให้กับไฟล์ใดก็ตามที่คุณทำ template ออกมา และหลีกเลี่ยงการส่งข้อมูลที่เป็นความลับผ่านอาร์กิวเมนต์ของคำสั่ง เพราะข้อมูลเหล่านั้นจะมองเห็นได้ในรายการ process บนโฮสต์ปลายทางในขณะที่คำสั่งกำลังทำงานอยู่