ได้ Windows Server 2016 มาแต่ RDP: ลง OpenSSH ผ่าน WinRM ไม่ต้องเข้าจอ
ถ้าเครื่องที่ได้รับสิทธิ์ดูแลมีแค่ RDP กับ WinRM เปิดอยู่ และฝั่งเราตั้ง TrustedHosts ไม่ได้ ทางลัดที่ได้ผลคือใช้ Python คุย WinRM ตรง ๆ แล้วใช้มันบูตสแตรป SSH ให้ครั้งเดียว หลังจากนั้นก็ไม่ต้องพึ่ง RDP อีก
สรุปใน 1 นาที
- อาการ/ปัญหา
- ได้ Windows Server 2016 มาแค่ RDP กับ WinRM ไม่มี OpenSSH และตั้ง TrustedHosts ไม่ได้
- ใครควรอ่าน
- คนที่ได้สิทธิ์ดูแลเครื่อง Windows Server รุ่นเก่าและอยากเข้าด้วย SSH
- ทางแก้แบบสั้น
- ใช้ Python pywinrm แบบ NTLM สั่ง PowerShell ลง Win32-OpenSSH จาก MSI ตั้งพอร์ตและ key โดยไม่ต้องเปิดหน้าจอ RDP
พอดีผมได้ Windows Server 2016 มาเครื่องหนึ่ง เป็นเครื่องที่เจ้าของอนุญาตให้ผมเข้าไปดูแลและทำงานได้ มาพร้อมช่องทาง RDP กับบัญชี Administrator ช่องทางที่ใช้ได้มีแค่ RDP (3389) กับ WinRM แบบ HTTP (5985) ส่วน SSH (22) ยังไม่มี งานที่ต้องทำเป็นงานบรรทัดคำสั่งล้วน ๆ การนั่งเปิดจอ RDP เพื่อพิมพ์คำสั่งทีละบรรทัดทั้งช้าและทำซ้ำไม่ได้ ผมเลยอยากได้ SSH
ปัญหาคือ Windows Server 2016 ไม่มี OpenSSH เป็น capability ในตัว คำสั่ง Add-WindowsCapability ที่ใช้ได้ผลบน Windows 10/11 และ Server 2019 ขึ้นไป ใช้ที่นี่ไม่ได้ ส่วนทางเข้าไปสั่งงานโดยไม่เปิดจอก็มีเงื่อนไขของมัน บทความนี้เล่าว่าผมข้ามสองเรื่องนี้ได้ยังไง และข้อควรระวังเรื่องความปลอดภัยที่ควรทำหลังจบงาน
หมายเหตุเรื่องขอบเขต บทความนี้ใช้กับเครื่องที่ตัวเองมีสิทธิ์ใช้งานหรือได้รับอนุญาตจากเจ้าของแล้วเท่านั้น IP, ชื่อเครื่อง, พอร์ตและชื่อ key ในบทความเป็นตัวอย่าง ส่วนลำดับขั้นตอนและข้อสรุปเรื่อง WinRM กับ OpenSSH คือของที่ผมทำจริง ขอบอกไว้ตรงนี้ว่าคำสั่งบรรทัดละเอียดของบางขั้น (flag ของ msiexec, การตั้ง firewall, การตั้ง shell) ผมไม่ได้จดไว้เป๊ะ ๆ เลยเขียนในรูปแบบมาตรฐานและติดป้ายว่าเป็นตัวอย่างทั่วไป ควรลองกับ VM ก่อนใช้กับเครื่องจริง
ทางลัดที่ใช้ไม่ได้ สองทาง
ทางแรกคือ Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0 ซึ่งเป็นวิธีที่ผมเขียนไว้ในบทความ สั่งงานอัตโนมัติบนเครื่อง Windows ระยะไกลผ่าน SSH + Task Scheduler ใช้ได้เฉพาะรุ่นใหม่ บน Server 2016 ไม่มีตัวเลือกนี้ ต้องลง Win32-OpenSSH ของ Microsoft ที่แจกเป็นไฟล์ MSI บน GitHub แทน
ทางที่สองคือ PowerShell remoting ปกติ (Enter-PSSession) จากเครื่องผม ซึ่งเจอกำแพงที่ฝั่ง client เพราะ WinRM ไม่มี TLS และเครื่องปลายทางไม่ได้อยู่ในโดเมนเดียวกัน PowerShell ฝั่ง client จะบังคับให้เพิ่มเครื่องนั้นเข้า TrustedHosts ก่อน การตั้งค่านี้ต้องใช้สิทธิ์ admin บนเครื่องผมเอง ซึ่งเครื่องที่ผมใช้ทำงานอยู่ไม่ให้
ทางที่ใช้ได้: pywinrm แบบ NTLM
ไลบรารี Python ชื่อ pywinrm คุย WinRM ด้วยตัวเอง ไม่ได้ใช้ PowerShell remoting ของ Windows ฝั่ง client เลยไม่ต้องตั้ง TrustedHosts และไม่ต้องมีสิทธิ์ admin บนเครื่องที่รันสคริปต์ แค่ต้องมี Python กับรหัสของบัญชีบนเครื่องปลายทาง ผมเก็บรหัสไว้ในตัวแปรสภาพแวดล้อม ไม่ฝังในไฟล์
# ฝั่งเครื่องเรา (ตัวอย่าง)
pip install pywinrm
export WIN_PASS='<PASSWORD>' # ตั้งในเทอร์มินัลที่ใช้ ไม่เขียนลงสคริปต์
# winrun.py — ส่ง PowerShell ไปรันบน host-a ผ่าน WinRM (ตัวอย่างรูปแบบมาตรฐานของไลบรารี)
import os, sys, winrm
s = winrm.Session(
"http://203.0.113.50:5985/wsman",
auth=("Administrator", os.environ["WIN_PASS"]),
transport="ntlm",
)
def ps(script: str) -> int:
r = s.run_ps(script)
sys.stdout.write(r.std_out.decode("utf-8", "replace"))
sys.stderr.write(r.std_err.decode("utf-8", "replace"))
return r.status_code
if __name__ == "__main__":
sys.exit(ps(sys.argv[1]))
$ python winrun.py '$PSVersionTable.PSVersion.ToString(); hostname'
5.1.x.x
host-a
ผลลัพธ์ข้างบนเป็นตัวอย่างรูปแบบ คือถ้าต่อติดจะได้ PowerShell 5.1 กลับมา ซึ่งตรงกับสิ่งที่ Server 2016 มี ตั้งแต่ตรงนี้ผมสั่งอะไรก็ได้โดยไม่เปิดจอ RDP เลย
WinRM ที่พอร์ต 5985 เป็น HTTP ธรรมดา ไม่มี TLS ห่อ ผมใช้มันเป็นแค่ตัวบูตสแตรปและเก็บไว้เป็นทางสำรอง เมื่อ SSH ใช้ได้แล้วควรปิดหรือจำกัดพอร์ตนี้ด้วย IP ต้นทาง รายละเอียดอยู่ท้ายบทความ

ลง OpenSSH ทั้งกระบวนการผ่าน WinRM
ขั้นตอนทั้งหมดผมสั่งผ่านสคริปต์ด้านบน โดยไม่เปิดจอ RDP เลย ลำดับคือ
- ดาวน์โหลดไฟล์ MSI ของ Win32-OpenSSH จากหน้า release ของ Microsoft บน GitHub ลงเครื่องปลายทาง
- ติดตั้งแบบเงียบด้วย
msiexec - ตั้งให้
sshdเริ่มเองตอนบูต แล้วกำหนดพอร์ตเป็นเลขอื่นที่ไม่ใช่ 22 ในsshd_config - เปิด firewall ของ Windows ให้พอร์ตนั้น
- วาง public key ใน
administrators_authorized_keysเพราะบัญชีเป็น Administrator - เพิ่ม alias ใน
~/.ssh/configแล้วssh host-aเข้าได้โดยไม่ถามรหัส
ในบทความใช้ 2200 เป็นเลขพอร์ตตัวอย่าง ส่วนคำสั่งต่อไปนี้เป็นรูปแบบทั่วไปที่คนทำกันทั่วไป ไม่ใช่บันทึกของผมแบบตัวต่อตัว
# 1-2) ตัวอย่างทั่วไป: ดาวน์โหลดและติดตั้ง MSI — ควรทดสอบบน VM Server 2016 ก่อน
python winrun.py '
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$u = "https://github.com/PowerShell/Win32-OpenSSH/releases/download/<tag>/OpenSSH-Win64-<version>.msi"
Invoke-WebRequest -Uri $u -OutFile C:\Windows\Temp\openssh.msi -UseBasicParsing
Start-Process msiexec.exe -ArgumentList "/i C:\Windows\Temp\openssh.msi /qn" -Wait
Get-Service sshd | Format-List Name,Status,StartType
'
บรรทัด SecurityProtocol = Tls12 สำคัญตรงที่ PowerShell 5.1 บน Server 2016 อาจเริ่มด้วย TLS รุ่นเก่าที่ GitHub ไม่รับ แล้วคำสั่งดาวน์โหลดจะล้มด้วย error เรื่อง secure channel ข้อนี้เป็นความรู้ทั่วไปของ Windows ยุคนั้น ไม่ใช่สิ่งที่ผมวัดบนเครื่องนี้ ถ้าลิงก์ GitHub เปลี่ยนรูปแบบชื่อไฟล์ ให้เปิดหน้า release ดูชื่อจริงก่อน
# 3-4) ตัวอย่างทั่วไป: ตั้งบริการ ตั้งพอร์ต เปิด firewall — ควรทดสอบบน VM ก่อน
python winrun.py '
Set-Service sshd -StartupType Automatic
$cfg = "C:\ProgramData\ssh\sshd_config"
(Get-Content $cfg) -replace "^#?Port 22$", "Port 2200" | Set-Content $cfg -Encoding ascii
New-NetFirewallRule -Name sshd-alt -DisplayName "OpenSSH 2200" -Enabled True `
-Direction Inbound -Protocol TCP -Action Allow -LocalPort 2200
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell `
-Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -PropertyType String -Force
Restart-Service sshd
Get-NetTCPConnection -LocalPort 2200 -State Listen
'
ลำดับที่ควรระวังคือเปิด firewall ให้พอร์ตใหม่ก่อน restart sshd ไม่งั้นถ้า restart แล้วคุยทางพอร์ตใหม่ไม่ได้ ผมยังมี WinRM เป็นทางกลับเข้าไปแก้ ซึ่งเป็นเหตุผลที่ไม่ควรปิด WinRM ก่อนทดสอบ SSH เสร็จ ส่วน shell เริ่มต้นหลังตั้งเสร็จเป็น PowerShell 5.1 ตามที่เห็นตอนล็อกอินจริง
key ของ admin: ไฟล์ต่างจากบัญชีธรรมดา
จุดที่คนเจอบ่อยคือบัญชีที่อยู่กลุ่ม Administrators ไม่ได้อ่าน ~/.ssh/authorized_keys ของตัวเอง แต่ไปอ่าน C:\ProgramData\ssh\administrators_authorized_keys แทน และไฟล์นี้ต้องมีสิทธิ์เฉพาะ Administrators กับ SYSTEM ไม่งั้น sshd ไม่ยอมใช้ ผมไม่เขียนรายละเอียดคำสั่ง icacls ซ้ำที่นี่ เพราะบทความ สั่งงานอัตโนมัติบนเครื่อง Windows ระยะไกลผ่าน SSH + Task Scheduler เขียนครอบคลุมเรื่องนี้ไว้แล้ว
ฝั่งเครื่องเราสร้างคู่ key เฉพาะสำหรับเครื่องนี้ แล้วส่ง public key ขึ้นไปทาง WinRM เหมือนเดิม
ssh-keygen -t ed25519 -f ~/.ssh/host-a_ed25519 -C "host-a admin"
PUB=$(cat ~/.ssh/host-a_ed25519.pub)
python winrun.py "Set-Content -Path C:\ProgramData\ssh\administrators_authorized_keys -Value '$PUB' -Encoding ascii"
# แล้วตั้ง ACL ตามบทความที่ลิงก์ไว้ข้างบน ก่อนทดสอบ
# ~/.ssh/config
Host host-a
HostName 203.0.113.50
Port 2200
User Administrator
IdentityFile ~/.ssh/host-a_ed25519
IdentitiesOnly yes
พอตั้งเสร็จ ssh host-a เข้าได้โดยไม่ถามรหัส และได้ PowerShell 5.1 เป็น shell เริ่มต้นตามที่เห็นจริง จากจุดนี้ผมใช้ SSH เป็นทางหลัก ส่วน WinRM คงไว้เป็นทางสำรองถ้า SSH ล่ม
ตัวบูตสแตรป
- ใช้ได้โดยไม่ต้องมีสิทธิ์ admin ฝั่งเรา
- ไม่มี TLS ในช่องทาง HTTP
- เหมาะส่งคำสั่งเป็นก้อนครั้งเดียว
- เก็บไว้เป็นทางสำรอง
ทางหลักหลังติดตั้ง
- ใช้ key ไม่ต้องพิมพ์รหัส
- ต่อเรียกใช้เครื่องมือเดิมได้ เช่น scp
- ใช้พอร์ตที่กำหนดเองได้ ไม่ต้องใช้ 22
- ต้องดูแล ACL ของไฟล์ key เอง
ก่อนยิงคำสั่งจริง: ตรวจว่า WinRM ตอบก่อน
ผมไม่ลงมือติดตั้งอะไรจนกว่าจะรู้ว่าช่องทางตอบได้จริงและสิทธิ์เพียงพอ ลำดับที่ผมใช้คือสั่งคำสั่งอ่านอย่างเดียวก่อน เช่น ดูชื่อเครื่อง เวอร์ชัน PowerShell และว่ามี service sshd อยู่แล้วหรือยัง ถ้า error เป็น 401 แปลว่ารหัสหรือชนิด transport ไม่ตรง ถ้า timeout แปลว่าพอร์ตถูกบล็อกหรือ IP ต้นทางถูกจำกัด ซึ่งเป็นสองอาการที่แยกกันได้ในไม่กี่วินาที การเริ่มจากคำสั่งอ่านอย่างเดียวยังเป็นข้อตกลงที่ดีกับเจ้าของเครื่องด้วย เพราะเขาเห็นได้ว่าสิ่งแรกที่ผมทำไม่เปลี่ยนอะไรในระบบ
python winrun.py 'Get-Service sshd -ErrorAction SilentlyContinue | Format-List Name,Status; (Get-CimInstance Win32_OperatingSystem).Caption'
ทำไม pywinrm ถึงข้ามเรื่อง TrustedHosts ได้ เหตุผลตามความเข้าใจของผมคือ TrustedHosts เป็นกฎของ PowerShell remoting ฝั่ง client ของ Windows ที่ไม่ยอมคุยกับเครื่องซึ่งยืนยันตัวตนผ่านโดเมนไม่ได้ ส่วน pywinrm เป็นไลบรารี Python ที่พูดโปรโตคอล WinRM เอง เลยไม่ผ่านกฎข้อนั้น ผมไม่ได้อ่านซอร์สเพื่อพิสูจน์ แต่ผลที่เห็นคือสั่งงานได้โดยไม่ต้องแตะการตั้งค่าอะไรบนเครื่องผม
อีกเรื่องที่ควรเตรียมคือทางกลับ ก่อนเปลี่ยนพอร์ตของ sshd หรือเปิด firewall ให้จดไว้ว่าถ้า SSH เข้าไม่ได้จะใช้อะไรแก้ ในเคสนี้คือ WinRM ที่ยังเปิดอยู่ ถ้าเครื่องไม่มีทางสำรองเลย การเปลี่ยนพอร์ตผิดบรรทัดเดียวอาจล็อกตัวเองออกจากเครื่อง และต้องพึ่งคนที่นั่งหน้าจอ
ข้อควรทำด้านความปลอดภัยหลังจบงาน
เมื่อ SSH เป็นทางหลักแล้ว ผมถือว่าทางบูตสแตรปต้องถูกเก็บให้เรียบร้อย และเรื่องพวกนี้เป็นการตัดสินใจของเจ้าของเครื่อง ผมไม่แก้เองถ้าเขาไม่สั่ง ได้แก่ จำกัดพอร์ต RDP กับ WinRM ด้วย IP ต้นทางหรือปิดถ้าไม่ใช้ เปลี่ยนรหัส Administrator ที่ได้รับมาตอนเริ่มงานเป็นรหัสใหม่ที่เจ้าของถือเอง และปิด WinRM แบบ HTTP เพราะไม่มี TLS
การติดตั้ง ลบ รีสตาร์ต หรือแก้ firewall บนเครื่องที่ไม่ใช่ของตัวเองเป็นงานที่เปลี่ยนระบบจริง ต้องได้รับอนุญาตจากเจ้าของก่อนทุกครั้ง ส่วนการอ่านสถานะทำได้ตามสิทธิ์ที่ได้รับ ตัวอย่างในบทความไม่ใช่คู่มือเข้าเครื่องที่ไม่มีสิทธิ์ ถ้าเป็นเครื่องในงานจริงให้ผู้ดูแลระบบที่รับผิดชอบเป็นคนตัดสินใจเรื่องพอร์ตกับรหัส

สิ่งที่ผมไม่รู้ และไม่ได้ลอง
ผมไม่ได้บันทึกเวลาที่ใช้ติดตั้ง ไม่รู้ว่า Windows Update หรือ KB ตัวไหนจำเป็นสำหรับ OpenSSH รุ่นใหม่บน Server 2016 และไม่ได้ลองกับ Server 2016 รุ่นย่อยอื่น รวมถึงไม่ได้เทียบความเร็ว SSH กับ RDP คำสั่งสามกลุ่มข้างบน (MSI, firewall, shell) เป็นรูปแบบทั่วไป ผมเลยไม่อยากให้ใครเชื่อว่าคัดลอกไปรันแล้วผ่านทุกบรรทัด ควรสร้าง Server 2016 VM ทดสอบก่อน บทเรียนที่ผมเก็บไปคือถ้าเครื่องมีแค่ WinRM ก็พอจะบูตสแตรปทางที่ดีกว่าให้ตัวเองได้ และอย่าลืมว่าทางบูตสแตรปนั้นก็ต้องถูกปิดหลังใช้เสร็จ เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ
อยากให้เราทำให้ไหม?
ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า
เคยเจองานแบบนี้ไหม? ติดตรงไหน?
มาแลกวิธีกันต่อในบอร์ด LinuxThai



ความคิดเห็น 0
ยังไม่มีความคิดเห็น — เป็นคนแรกเลย