Skip to content

Commit 355b121

Browse files
curdbeckerneilpang
authored andcommitted
Add deployment plugin for Windows RDP via OpenSSH (#6925)
* Add deployment plugin for Windows RDP via OpenSSH
1 parent 3230d00 commit 355b121

2 files changed

Lines changed: 177 additions & 0 deletions

File tree

acme.sh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,6 +1057,25 @@ _digest() {
10571057

10581058
}
10591059

1060+
#Usage: certpath hashalg
1061+
#Output certificate fingerprint without colons
1062+
_fingerprint() {
1063+
cert="$1"
1064+
alg="$2"
1065+
if [ -z "$alg" ]; then
1066+
_usage "Usage: _fingerprint certpath hashalg"
1067+
return 1
1068+
fi
1069+
1070+
if [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ] || [ "$alg" = "md5" ]; then
1071+
# openssl prints "SHA1 Fingerprint=AA:BB:CC:..."; strip prefix and colons.
1072+
${ACME_OPENSSL_BIN:-openssl} x509 -in "$cert" -noout -fingerprint -"$alg" | sed 's/.*=//; s/://g'
1073+
else
1074+
_err "$alg is not supported yet"
1075+
return 1
1076+
fi
1077+
}
1078+
10601079
#Usage: hashalg secret_hex [outputhex]
10611080
#Output binary hmac
10621081
_hmac() {

deploy/windows_rdp.sh

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
#!/usr/bin/env sh
2+
3+
# install a certificate on a Windows host over OpenSSH and bind it to the Remote
4+
# Desktop listener (RDP-Tcp).
5+
#
6+
# One ssh invocation does the whole job:
7+
# * the PFX is built locally, base64'd, and embedded as a string literal
8+
# inside a generated PowerShell script;
9+
# * the script is piped to `powershell.exe -Command -` over ssh. No scp,
10+
# no temp files on the Windows host.
11+
#
12+
# First run:
13+
# export DEPLOY_WIN_RDP_HOST=winserver.example.com
14+
# acme.sh --deploy -d winserver.example.com --deploy-hook windows_rdp
15+
#
16+
# Available variables:
17+
# DEPLOY_WIN_RDP_HOST required SSH host
18+
# DEPLOY_WIN_RDP_USER optional SSH user, must be a local administrator (can also by set via ssh_config)
19+
# DEPLOY_WIN_RDP_PORT optional SSH port, default 22
20+
# DEPLOY_WIN_RDP_SSH_OPTS optional extra ssh options, e.g.
21+
# "-i /root/.ssh/win_id_ed25519 -o StrictHostKeyChecking=yes"
22+
# DEPLOY_WIN_RDP_LISTENER optional RDP listener name, default RDP-Tcp
23+
# DEPLOY_WIN_RDP_RESTART optional "1" to restart TermService after install.
24+
# Active RDP sessions will drop!
25+
26+
windows_rdp_deploy() {
27+
_cdomain="$1"
28+
_ckey="$2"
29+
_ccert="$3"
30+
_cca="$4"
31+
_cfullchain="$5"
32+
33+
_debug _cdomain "$_cdomain"
34+
_debug _ckey "$_ckey"
35+
_debug _ccert "$_ccert"
36+
_debug _cca "$_cca"
37+
_debug _cfullchain "$_cfullchain"
38+
39+
if ! _exists "ssh"; then
40+
_err "ssh is required but was not found in PATH."
41+
return 1
42+
fi
43+
44+
# ---- configuration ------------------------------------------------------
45+
_getdeployconf DEPLOY_WIN_RDP_HOST
46+
_getdeployconf DEPLOY_WIN_RDP_USER
47+
_getdeployconf DEPLOY_WIN_RDP_PORT
48+
_getdeployconf DEPLOY_WIN_RDP_SSH_OPTS
49+
_getdeployconf DEPLOY_WIN_RDP_LISTENER
50+
_getdeployconf DEPLOY_WIN_RDP_RESTART
51+
52+
if [ -z "$DEPLOY_WIN_RDP_HOST" ]; then
53+
_err "DEPLOY_WIN_RDP_HOST must be set."
54+
return 1
55+
fi
56+
57+
_savedeployconf DEPLOY_WIN_RDP_HOST "$DEPLOY_WIN_RDP_HOST"
58+
[ -n "$DEPLOY_WIN_RDP_USER" ] && _savedeployconf DEPLOY_WIN_RDP_USER "$DEPLOY_WIN_RDP_USER"
59+
[ -n "$DEPLOY_WIN_RDP_PORT" ] && _savedeployconf DEPLOY_WIN_RDP_PORT "$DEPLOY_WIN_RDP_PORT"
60+
[ -n "$DEPLOY_WIN_RDP_SSH_OPTS" ] && _savedeployconf DEPLOY_WIN_RDP_SSH_OPTS "$DEPLOY_WIN_RDP_SSH_OPTS"
61+
[ -n "$DEPLOY_WIN_RDP_LISTENER" ] && _savedeployconf DEPLOY_WIN_RDP_LISTENER "$DEPLOY_WIN_RDP_LISTENER"
62+
[ -n "$DEPLOY_WIN_RDP_RESTART" ] && _savedeployconf DEPLOY_WIN_RDP_RESTART "$DEPLOY_WIN_RDP_RESTART"
63+
64+
_port="${DEPLOY_WIN_RDP_PORT:-22}"
65+
_listener="${DEPLOY_WIN_RDP_LISTENER:-RDP-Tcp}"
66+
if [ -n "$DEPLOY_WIN_RDP_USER" ]; then
67+
_target="$DEPLOY_WIN_RDP_USER@$DEPLOY_WIN_RDP_HOST"
68+
else
69+
_target="$DEPLOY_WIN_RDP_HOST"
70+
fi
71+
_pfx_pass="acme"
72+
73+
# ---- build thumbprint + PFX locally ------------------------------------
74+
_thumb="$(_fingerprint "$_ccert" 'sha1')"
75+
if [ -z "$_thumb" ]; then
76+
_err "Failed to compute certificate thumbprint."
77+
return 1
78+
fi
79+
_debug "Thumbprint: $_thumb"
80+
81+
_debug "Building PFX at $_pfx_file"
82+
_pfx_file="$(_mktemp)"
83+
if ! _toPkcs "$_pfx_file" "$_ckey" "$_ccert" "$_cca" "$_pfx_pass"; then
84+
_err "Failed to build PFX archive."
85+
rm -f "$_pfx_file"
86+
return 1
87+
fi
88+
_pfx_b64=$(_base64 "multiline" <"$_pfx_file")
89+
rm -f "$_pfx_file"
90+
91+
# ---- build installer script --------------------------------------------
92+
if [ "$DEPLOY_WIN_RDP_RESTART" = "1" ]; then
93+
_restart_ps='Restart-Service -Name TermService -Force'
94+
else
95+
_restart_ps='# New RdP connections will pick up the new cert automatically.'
96+
fi
97+
98+
# Escape every literal `$` with `\$` so the shell does not expand it.
99+
# Values substituted from shell: $_pfx_b64, $_pfx_pass, $_thumb, $_listener.
100+
_ps1=$(
101+
cat <<PSEOF
102+
103+
\$ErrorActionPreference = 'Stop'
104+
105+
\$pfxBytes = [Convert]::FromBase64String('${_pfx_b64}')
106+
107+
# Note: It is quite important to use a X509Certificate2Collection here in any case, since we otherwise
108+
# could run into quite a lot of trouble when importing the certificate including its entire chain
109+
# and its private key. Windows might behave arbitrarily and not consistently import the certificate
110+
# at all - unless "Exportable" is included in the storage flags. However, then the certificate seems
111+
# unaccessible to TermService for some weird reasons despite all permissions being set (at least on my
112+
# Win 11 lab machine). This might be some security setting that prevents TermService from working with
113+
# exportable keys? I don't know - importing the entire collection including chain or not always fixes
114+
# the issues.
115+
#
116+
# Note2: If you should have kicked yourself out for some reason, then deleting the certificate will make
117+
# TermService restore the original, self-signed certificate after at least after the second login attempt.
118+
# Deleting the certificate can be easily accomplished via the Powershell, since SSH access will still be
119+
# present in any case - the following command should get you out of trouble:
120+
# \$cert = Get-ChildItem -Path 'Cert:\LocalMachine\My\\${_thumb}' | Select-Object -First 1 | Remove-Item
121+
122+
\$flags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]'MachineKeySet,PersistKeySet'
123+
\$certs = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
124+
\$certs.Import(\$pfxBytes, '${_pfx_pass}', \$flags)
125+
126+
\$store = [System.Security.Cryptography.X509Certificates.X509Store]::new('My', 'LocalMachine')
127+
\$store.Open('ReadWrite')
128+
\$store.AddRange(\$certs)
129+
\$store.Close()
130+
Write-Host "Installed certs into LocalMachine\\My"
131+
132+
\$ts = Get-CimInstance -Namespace root/cimv2/terminalservices -ClassName Win32_TSGeneralSetting -Filter "TerminalName='${_listener}'"
133+
if (-not \$ts) { throw "Listener '${_listener}' not found." }
134+
Set-CimInstance -InputObject \$ts -Property @{SSLCertificateSHA1Hash="${_thumb}"}
135+
Write-Host "Listener ${_listener} now uses ${_thumb}"
136+
137+
${_restart_ps}
138+
PSEOF
139+
)
140+
_debug "Powershell script:${_ps1}"
141+
142+
# ---- run over a single ssh connection ----------------------------------
143+
_ssh_opts="-o BatchMode=yes -p $_port"
144+
if [ -n "$DEPLOY_WIN_RDP_SSH_OPTS" ]; then
145+
_ssh_opts="$_ssh_opts $DEPLOY_WIN_RDP_SSH_OPTS"
146+
fi
147+
148+
_info "Deploying to $DEPLOY_WIN_RDP_HOST ..."
149+
# shellcheck disable=SC2086
150+
if ! printf '%s\n' "$_ps1" | ssh $_ssh_opts "$_target" \
151+
'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command -'; then
152+
_err "Remote install failed. Re-run acme.sh with --debug to see the PowerShell output."
153+
return 1
154+
fi
155+
156+
_info "Certificate for $_cdomain deployed and bound to $_listener on $DEPLOY_WIN_RDP_HOST."
157+
return 0
158+
}

0 commit comments

Comments
 (0)