EST. 2026 — บันทึกงานเซิร์ฟเวอร์จริง SYSADMIN · SECURITY · AI · DIRECTADMIN เล่มที่ 01
THE SYSADMIN ALMANAC
หน้าหลัก / Linux / ได้ Windows Server 2016 มาแต่ RDP: ลง OpenSSH ผ่าน WinRM ไม่ต้องเข้าจอ
Linux

ได้ Windows Server 2016 มาแต่ RDP: ลง OpenSSH ผ่าน WinRM ไม่ต้องเข้าจอ

โดย Ton-Or ·6 พ.ย. 2026·อ่าน 10 นาที
ได้ 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 บนเครื่องผมเอง ซึ่งเครื่องที่ผมใช้ทำงานอยู่ไม่ให้

ทางเข้าที่ใช้ได้ กับทางตันโน้ตบุ๊กไม่มีสิทธิ์ adminServer 2016host-a1 pywinrm, ntlmhttp :5985/wsman2Add-WindowsCapabilityไม่มีใน Server 2016ต้องลง MSI เองEnter-PSSessionติด TrustedHostsต้อง admin ฝั่ง client
ทางที่ใช้ได้คือเส้นทึบ ส่วนเส้นประไปจบที่กล่องแดง ซึ่งเป็นสองทางลัดที่ตันบนสถานการณ์นี้

ทางที่ใช้ได้: 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 เลย

ช่องทางนี้ไม่เข้ารหัสด้วย TLS

WinRM ที่พอร์ต 5985 เป็น HTTP ธรรมดา ไม่มี TLS ห่อ ผมใช้มันเป็นแค่ตัวบูตสแตรปและเก็บไว้เป็นทางสำรอง เมื่อ SSH ใช้ได้แล้วควรปิดหรือจำกัดพอร์ตนี้ด้วย IP ต้นทาง รายละเอียดอยู่ท้ายบทความ

เครื่อง Windows Server รุ่นเก่ามีกุญแจทองเหลืองแขวนไว้ข้างปุ่มเปิดเครื่อง พร้อมป้ายบอกว่าไม่มี OpenSSH ในตัว
Server 2016 ไม่มี OpenSSH ติดมาให้ กุญแจอยู่ข้างเครื่องแต่ต้องหยิบมาลงเอง

ลง OpenSSH ทั้งกระบวนการผ่าน WinRM

ขั้นตอนทั้งหมดผมสั่งผ่านสคริปต์ด้านบน โดยไม่เปิดจอ RDP เลย ลำดับคือ

  1. ดาวน์โหลดไฟล์ MSI ของ Win32-OpenSSH จากหน้า release ของ Microsoft บน GitHub ลงเครื่องปลายทาง
  2. ติดตั้งแบบเงียบด้วย msiexec
  3. ตั้งให้ sshd เริ่มเองตอนบูต แล้วกำหนดพอร์ตเป็นเลขอื่นที่ไม่ใช่ 22 ใน sshd_config
  4. เปิด firewall ของ Windows ให้พอร์ตนั้น
  5. วาง public key ใน administrators_authorized_keys เพราะบัญชีเป็น Administrator
  6. เพิ่ม 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 ตามที่เห็นตอนล็อกอินจริง

หกขั้นบนเซิร์ฟเวอร์ ทั้งหมดสั่งผ่าน WinRM1 โหลด MSIจาก GitHub2 msiexecติดตั้งเงียบ3 sshd + พอร์ตAutomatic, Port 22004 firewallเปิดพอร์ต 22005 วาง public keyACL Admin + SYSTEM6 ssh host-aได้ PowerShell 5.1WinRM เก็บไว้เป็นทางสำรองควรทำ: เปลี่ยนรหัส Administratorจำกัด IP ของ RDP และ WinRM
ขั้น 1-6 ทำเสร็จทางไกล ส่วนกล่องแดงคือสิ่งที่ควรทำต่อและเป็นการตัดสินใจของเจ้าของเครื่อง

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 ล่ม

WinRM + pywinrm

ตัวบูตสแตรป

  • ใช้ได้โดยไม่ต้องมีสิทธิ์ admin ฝั่งเรา
  • ไม่มี TLS ในช่องทาง HTTP
  • เหมาะส่งคำสั่งเป็นก้อนครั้งเดียว
  • เก็บไว้เป็นทางสำรอง
OpenSSH

ทางหลักหลังติดตั้ง

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

ซองจดหมายป้าย MSI ถูกสอดใต้ประตู มีสายเคเบิลป้าย WinRM วิ่งใต้ประตูแทนการเข้าทางประตูหน้าป้าย RDP
ไม่ต้องเข้าทางประตูหน้า (RDP) ส่งของใต้ประตูผ่านสาย WinRM แล้ววางกุญแจ SSH ไว้ข้างใน
0ครั้งที่ต้องเปิดจอ RDP
6ขั้นติดตั้งทางไกล
5.1PowerShell ของ shell เริ่มต้น

สิ่งที่ผมไม่รู้ และไม่ได้ลอง

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

อยากให้เราทำให้ไหม?

ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า

คุยเรื่องงานกับเรา

เคยเจองานแบบนี้ไหม? ติดตรงไหน?

มาแลกวิธีกันต่อในบอร์ด LinuxThai

→ คุยต่อในบอร์ด

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

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