1 คะแนน โดย GN⁺ 4 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ssh-init-vm ใช้ cloud-init เพื่อใส่คีย์ส่วนตัวของโฮสต์ SSH ชั่วคราวสำหรับการเชื่อมต่อ SSH ครั้งแรกไปยัง VM ใหม่ เพื่อป้องกัน การโจมตีแบบคนกลาง และเชื่อถือคีย์นี้เพียงระหว่างที่กำลังสร้างหรือนำเข้าคีย์โฮสต์ระยะยาวเท่านั้น
  • ใช้งานได้แม้กับ VPS หรือคลาวด์ที่ไม่มีฟีเจอร์ป้องกันการเชื่อมต่อเฉพาะทางอย่าง Hetzner Cloud โดยสิ่งที่ต้องการมีเพียง cloud-init ที่รองรับอย่างแพร่หลาย
  • ในแนวทาง Trust On First Use ตามปกติ หากพิมพ์ yes ตอบคำถามของ ssh ที่ว่า “The authenticity of host [...] can't be established” ผู้โจมตีอาจพร็อกซีทราฟฟิกหรือจัดเตรียมเครื่องที่ปลอมตัวเป็น VM ของผู้ใช้ได้
  • หากใส่คีย์ส่วนตัวของโฮสต์ SSH ระยะยาวลงใน cloud-init userdata โดยตรง แม้จะช่วยยืนยันตัวตนในการเชื่อมต่อครั้งแรกได้ แต่ก็อาจทำให้ ข้อมูลคีย์ที่มีความอ่อนไหว รั่วไหลผ่านบริการเมตาดาตา, SSRF, ระบบของผู้ให้บริการคลาวด์ หรือเวิร์กสเตชันของผู้ดูแลระบบได้
  • ssh-init-vm จะเก็บคีย์ชั่วคราวไว้ในไดเรกทอรีชั่วคราวและไม่ใส่ลงใน ~/.ssh/known_hosts อีกทั้งไม่บันทึกเอาต์พุตของ VM ลงไปตรงๆ แต่พึ่งพา การหมุนเวียนคีย์โฮสต์ ของ OpenSSH เพื่อบันทึกคีย์ระยะยาว

ปัญหาการรั่วไหลของ cloud-init userdata

  • หากฉีดคีย์ส่วนตัวของโฮสต์ SSH ระยะยาวด้วย cloud-init ก็สามารถนำคีย์สาธารณะไปใส่ใน ~/.ssh/known_hosts เพื่อยืนยันการเชื่อมต่อครั้งแรกได้ แต่คีย์ส่วนตัวอาจรั่วออกได้หลายทาง
  • โปรเซสใดๆ ภายใน VM มักจะอ่าน บริการเมตาดาตา ได้ และดึง userdata ออกมาได้ โดยบน Hetzner VM สามารถเห็นเนื้อหา cloud-init ได้ที่ http://169.254.169.254/hetzner/v1/userdata
  • ผู้โจมตีสามารถใช้ SSRF เพื่อบังคับให้โปรเซสรั่วข้อมูลเมตาดาตาออกมาได้ และการป้องกันลักษณะนี้ก็ ไม่ได้ถูกบังคับใช้เสมอไป แม้ในสภาพแวดล้อมที่มีฟีเจอร์ป้องกันเฉพาะทาง
  • userdata อาจรั่วผ่านระบบอื่นของผู้ให้บริการคลาวด์ได้เช่นกัน โดย Hetzner ระบุไว้ชัดเจนในเอกสาร API สำหรับสร้างเซิร์ฟเวอร์ว่า ไม่ควรใช้เก็บ “passwords or other sensitive information”
  • เวิร์กสเตชันของผู้ดูแลระบบก็อาจเป็นจุดที่ cloud-init userdata ถูกเก็บค้างหรือไหลผ่านได้ ดังนั้นการใส่คีย์ส่วนตัวระยะยาวลงไปจึงสร้างความเสี่ยงที่คีย์จะถูกเปิดเผยตราบใดที่คีย์ยังใช้งานได้

การวิเคราะห์ความปลอดภัยและแบบจำลองภัยคุกคาม

  • สมมุติฐานคือเชื่อถือ โปรโตคอลและอิมพลีเมนเทชันของ OpenSSH และไม่พึ่งพาความสามารถของผู้ดูแลระบบในการตรวจจับการโจมตี
  • การป้องกันต่อผู้โจมตีบนเครือข่าย

    • สิ่งที่ปกป้องคือความถูกต้องสมบูรณ์ของเวิร์กสเตชันผู้ดูแลระบบและตัว VM
    • ผู้โจมตีคือคนกลางที่ควบคุมเครือข่ายได้ทั้งหมด และอาจรู้ cloud-init userdata ได้ในบางจุดหลังจากสคริปต์ทำงานสำเร็จหรือล้มเหลวแล้วและจบการทำงาน
    • การป้องกันยังใช้ได้เพราะผู้โจมตีไม่รู้ข้อมูลคีย์ในช่วงเวลาที่ข้อมูลนั้นยังมีมูลค่าต่อการโจมตี
    • สคริปต์ จะเก็บคีย์โฮสต์ SSH ชั่วคราวไว้ในไดเรกทอรีชั่วคราวเพื่อป้องกันการใช้งานโดยไม่ตั้งใจ และไม่ใส่คีย์โฮสต์ SSH ชั่วคราวลงใน ~/.ssh/known_hosts
  • กรณีเวิร์กสเตชันผู้ดูแลระบบถูกเจาะ

    • สิ่งที่ปกป้องจำกัดอยู่ที่ VM และคีย์ส่วนตัวของโฮสต์ SSH ระยะยาวของ VM
    • สมมุติว่าผู้โจมตีควบคุมทั้งเครือข่ายและเวิร์กสเตชันผู้ดูแลระบบได้ทั้งหมด แต่ไม่ได้เข้าถึง VM จริง
    • คีย์ส่วนตัวของโฮสต์ SSH ระยะยาวไม่เคยอยู่บนเวิร์กสเตชันของผู้ดูแลระบบ และเมื่อผู้โจมตีไม่เข้าถึง VM จริง ก็จะไม่ได้คีย์โฮสต์ระยะยาวของ VM
    • หากผู้โจมตีเข้าถึง VM จริงได้ ก็มีโอกาสสูงที่จะรู้คีย์โฮสต์ SSH เช่นด้วย ssh root@<VM> cat /etc/ssh/ssh_host_*
  • กรณี VM หรือผู้ให้บริการถูกเจาะ

    • สิ่งที่ปกป้องจำกัดอยู่ที่ความถูกต้องสมบูรณ์ของเวิร์กสเตชันผู้ดูแลระบบ
    • ผู้โจมตีควบคุมเครือข่ายได้ทั้งหมด และยังควบคุม VM หรือผู้ให้บริการได้ทั้งหมดด้วย
    • แม้ในกรณีนี้ก็ยังปกป้องความถูกต้องสมบูรณ์ของเวิร์กสเตชันผู้ดูแลระบบได้ ภายใต้สมมุติฐานว่า OpenSSH ยังปลอดภัย
    • เพื่อเพิ่มการป้องกัน สคริปต์ จะไม่เขียนเอาต์พุตของ VM ลง ~/.ssh/known_hosts ตรงๆ แต่พึ่งพา การหมุนเวียน คีย์ ของ OpenSSH เพื่อใส่คีย์โฮสต์ SSH ระยะยาว
    • วิธีนี้ช่วยป้องกันไม่ให้โฮสต์ที่ถูกเจาะป้อนข้อมูลอันตรายให้กับตัวแยกวิเคราะห์ known_hosts และทำให้มีเพียง คีย์ที่ VM ควบคุมอยู่จริง เท่านั้นที่ถูกบันทึกลงใน ~/.ssh/known_hosts
    • ยังรองรับตัวเลือกของ OpenSSH อย่าง HashKnownHosts และตัวเลือกที่เกี่ยวข้องในอนาคตได้อย่างถูกต้อง

เงื่อนไขที่ทำให้การโจมตีแบบคนกลางสำเร็จได้จริง

  • ความสำเร็จของการโจมตีแบบคนกลางขึ้นอยู่กับว่าผู้ใช้ตรวจพบจริงหรือไม่ว่าการเชื่อมต่อทั้งหมดตั้งแต่ต้นกำลังชี้ไปยังเครื่องผิดเครื่อง, ปฏิเสธการกรอกรหัสผ่านหรือไม่, และตั้งค่าการส่งต่อ agent หรือ X11 ของ ssh ไว้หรือไม่
  • หากอิงตามเงื่อนไขแบบย่อของ ssh-mitm การโจมตีน่าจะสำเร็จได้มากหากผู้โจมตีสามารถหลอกผู้ใช้ด้วยการให้สิทธิ์เข้าถึงเครื่องที่ผู้โจมตีควบคุม แทนที่จะเป็นโฮสต์เป้าหมายจริง
  • การโจมตีก็สำเร็จได้เช่นกันหากผู้โจมตีหลอกให้ผู้ใช้เปิดเผยข้อมูลที่ใช้ล็อกอินเข้าโฮสต์จริง
    • หากผู้ใช้ล็อกอินเข้าเครื่องของผู้โจมตีด้วย รหัสผ่าน ผู้โจมตีก็อาจสำเร็จได้
    • หากผู้ใช้ล็อกอินด้วยวิธีพิสูจน์ตัวตนใดก็ตาม แล้วพิมพ์รหัสผ่านที่พรอมป์ต์ ผู้โจมตีก็อาจสำเร็จได้
    • หากผู้ใช้ล็อกอินด้วยวิธีพิสูจน์ตัวตนใดก็ตาม และส่งต่อ การเชื่อมต่อ ssh-agent ผู้โจมตีก็อาจสำเร็จได้
  • หากไม่มีเงื่อนไขข้างต้น ผู้โจมตีจะต้องเข้าถึงโฮสต์จริงเพื่อหลอกผู้ใช้ แต่ก็มีแนวโน้มจะล้มเหลว เพราะไม่สามารถใช้เพียงข้อมูลที่ผู้ใช้ป้อนเพื่อล็อกอินเข้าโฮสต์จริงได้
  • หากผู้ใช้ส่งต่อการเชื่อมต่อ X11 ผู้โจมตีอาจโจมตีเวิร์กสเตชันผู้ดูแลระบบได้สำเร็จโดยไม่ขึ้นกับวิธีพิสูจน์ตัวตน

1 ความคิดเห็น

 
GN⁺ 4 시간 전
ความคิดเห็นจาก Lobste.rs
  • เจ๋งดีที่ทำให้เป็นอัตโนมัติได้ สำหรับวิธีแบบแมนนวล ผมคอยตรวจสอบ ลายนิ้วมือ SSH ของเซิร์ฟเวอร์ผ่านช่องทางแยกจากคอนโซลของผู้ให้บริการคลาวด์มาโดยตลอด
    เพราะไม่ได้ดูแลคลาวด์อินสแตนซ์จำนวนมากนัก ก็เลยไม่เป็นไรถ้าจะมีขั้นตอนแบบแมนนวลอยู่บ้างในกระบวนการ provisioning

    • วิธีนั้นก็ใช้ได้เหมือนกัน เพียงแต่ผู้ให้บริการที่ผมใช้อยู่ไม่ได้เชื่อมส่วนนี้ไว้ และตอนนั้นผมก็กำลังทำสคริปต์ automation สำหรับการตั้งค่าไปพร้อมกันด้วย
  • ถ้าคุณทำ DNS zone ให้เป็นอัตโนมัติไว้แล้ว ก็มีอีกแนวทางหนึ่ง: สร้างโทเค็นใช้ครั้งเดียวที่มีขอบเขตแคบมาก และอนุญาตเพียงให้สร้างเรคคอร์ดหนึ่งรายการบน my-server-hostname.example.net เป็นต้น
    ส่งโทเค็นนั้นเข้าไปยังเซิร์ฟเวอร์ด้วย cloud-init แล้วให้เซิร์ฟเวอร์อัปโหลด public SSH key ของตัวเองเป็นเรคคอร์ด SSHFP ใน DNS จากนั้นก็สามารถตั้งค่าให้ SSH client ตรวจสอบเรคคอร์ด SSHFP โดยอัตโนมัติได้ และ DNS zone จะต้องมี ลายเซ็น DNSSEC
    โฟลว์นี้ช่วยให้เซิร์ฟเวอร์ยังคงถือ private SSH host key ไว้ได้ โดยไม่ต้องหลีกเลี่ยงการหมุนคีย์ ผู้ให้บริการ DNS ส่วนใหญ่ไม่รองรับ access token แบบใช้ครั้งเดียวที่ละเอียดขนาดนี้ แต่คุณสามารถมีเว็บเซอร์วิสภายในแบบง่าย ๆ ที่ตรวจสอบโทเค็นนั้นก่อน แล้วค่อยเรียก API จริงด้วยโทเค็นถาวรที่ไม่มีการจำกัดขอบเขตแทนได้ เซิร์ฟเวอร์ SSH จะไม่สามารถเข้าถึงโทเค็นถาวรนั้น

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

    • ทำงานกับ OpenSSH สนุกเสมอ และผมแก้ รูปแบบเดือน/วัน แล้ว
  • เมื่อก่อนผมเคยทำบริการทดลองเพื่อสำรวจ ปัญหาไก่กับไข่ของ SSH แบบเดียวกัน
    มันจะสร้างเรคคอร์ด SSHFP DNS ตามคำขอ ถ้าสนใจก็ดูได้ที่ https://github.com/tedb/sshfp

  • ยินดีที่เห็นการพูดถึง บริการเมทาดาทา 169.254.169.254 ซึ่งค่อนข้างสอดคล้องกันในหมู่ผู้ให้บริการ VM ต่าง ๆ คุณดูได้จากหลายรายการ cloudinit/source/DataSource*.py ในซอร์สของ cloud-init
    ส่วนตัวแล้วผมเริ่มเหนื่อยกับการออกแบบและข้อจำกัดของ cloud-init มากขึ้นเรื่อย ๆ ผมสนใจการรวมการตั้งค่าระบบให้ใช้แนวทางเดียวกันได้ทั้งบน QEMU VM ในเครื่อง, เครื่องระยะไกล, คอนเทนเนอร์ และฮาร์ดแวร์จริง
    arch-boxes project แสดงให้เห็นว่า ArchLinux cloud-init image ถูกสร้างอย่างไร โดยมันเป็นเพียงชุด shell script ที่เรียบง่ายมาก หากนำวิธีนี้ไปประยุกต์กับ guestfish หรือ µvm ก็สามารถใช้ สคริปต์ชุดเดียวกันทุกประการ กับอิมเมจที่เข้ากันได้กับ OCI, ดิสก์อิมเมจสำหรับ QEMU หรือผู้ให้บริการคลาวด์, และการ provisioning เครื่องจริงเครื่องใหม่ได้
    ถ้าจับคู่กับ QEMU flags บางตัว ก็สามารถทำแนวทางเดียวกันได้โดยไม่ต้องพึ่ง cloud-init เท่าที่ผมทราบ systemd.system-credentials ยังไม่สามารถส่งผ่าน host key ชั่วคราวได้ มีเพียง credential สำหรับ ~/.ssh/authorized_keys อย่าง ssh.authorized_keys.root เท่านั้น
    แต่คุณสามารถสร้าง unit file ที่รันในขั้น initrd หรือรันร่วมกับ systemd-firstboot.service ได้ โดยจะใส่ unit file นี้ไว้ล่วงหน้าในอิมเมจ หรือฉีดเข้าไปชั่วคราวด้วย credential systemd.extra-unit.* แล้วเปิดใช้งานผ่านตัวเลือกเคอร์เนลคอมมานด์ไลน์ systemd.wants=… ก็ได้ ใน QEMU สามารถจำลองการมีอยู่ของบริการเมทาดาทาได้ด้วยรายการ -netdev user,id=metadata,net=169.254.0.0/16,dhcpstart=169.254.0.15,guestfwd=tcp:169.254.169.254:80-cmd:… อย่างไรก็ตาม มีความเป็นไปได้สูงว่าคุณจะต้องเปิดใช้งานอินเทอร์เฟซที่ถูกสร้างขึ้น ซึ่งก็น่าจะจัดการด้วย unit file ชั่วคราวได้เช่นกัน
    วิธีนี้ทำให้เมื่อทำการตั้งค่าระบบอย่างสม่ำเสมอข้าม “เครื่อง” หลายประเภท คุณจะได้ ความยืดหยุ่นพอสมควร โดยมีความซับซ้อนไม่มากนัก อันที่จริงสำหรับงานนี้โดยเฉพาะ ดูเหมือนว่าวิธีที่ดีที่สุดคือให้เครื่องมือสร้างอิมเมจสร้าง machine image ที่มี host key แบบคงที่ฝังมา แล้วติดตั้งสคริปต์หมุน host key แบบคัสตอมเป็นบริการ SystemD ที่จะรันตอนรีบูตครั้งแรกหรือตอนปิดเครื่อง

    • บน ArchLinux ถ้าเปิดใช้ systemd HOOK ใน /etc/mkinitcpio.conf คุณสามารถเขียน ไฟล์ SystemD unit สำหรับงานที่ต้องทำใน initrd ได้ และจริง ๆ แล้วก็ควรทำแบบนั้น
      พอได้ลองใช้จริง มันก็ยุ่งยากกว่าการเขียน {/etc,/usr/lib}/initcpio/hooks แค่นิดเดียว
      แต่การเปิด systemd-networking และ systemd-resolved ใน initrd นั้นค่อนข้างง่าย จึงสามารถให้ initrd รับหน้าที่เริ่มต้นระบบและจัดตารางงานก่อนสลับไปยัง root filesystem ได้
      แน่นอนว่าบนฮาร์ดแวร์จริงอย่างแล็ปท็อปอาจไม่ค่อยเหมาะ เพราะการเชื่อมต่อ Wi‑Fi ต้องพึ่งอะไรอย่าง NetworkManager แต่สำหรับ QEMU VM และ VM ที่โฮสต์อยู่ มันเข้ากันได้ดี และงานเริ่มต้นระบบจำนวนมากก็เข้ากับพื้นที่นี้ได้อย่างเป็นธรรมชาติ
      เป้าหมายคือไม่ต้องพึ่ง cloud-init, ไม่ถูกผูกกับผู้ให้บริการคลาวด์รายใดรายหนึ่ง, ได้ความสม่ำเสมอข้ามเครื่องจริง คอนเทนเนอร์ VM ในเครื่อง และ VM ที่โฮสต์อยู่, และลด dependency ลงจนแทบเหลือแค่ SystemD
    • ถ้าอยากได้เครื่องมือขนาดเล็กมากที่ขยายเฉพาะความสามารถที่ต้องการได้ บางที https://github.com/the-maldridge/shinit/ อาจเป็นไปได้เหมือนกัน