Skip to content

feat(windows): ship templates with RDP enabled - #33

Merged
ericwang401 merged 1 commit into
ConvoyPanel:mainfrom
gabbelitoV2:feat/windows-rdp-enabled
Sep 4, 2026
Merged

feat(windows): ship templates with RDP enabled#33
ericwang401 merged 1 commit into
ConvoyPanel:mainfrom
gabbelitoV2:feat/windows-rdp-enabled

Conversation

@gabbelitoV2

Copy link
Copy Markdown
Contributor

Windows Server ships with RDP off and nothing in the build changed that, so a clone's only first contact was the noVNC console. That path breaks in practice: the console types against the guest's en-US layout, so a --cipassword containing symbols typed on a non-US client keyboard arrives as different characters. Observed live 2026-08-18 on a 2025 clone: '=' typed on a Swedish layout never matched, the Security log filled with 0xC000006A (wrong password), while the very same string passed an in-guest LogonUser type 2. Convoy provisions clones with only --cipassword, so RDP is the expected first door in.

Finalize.ps1 sets fDenyTSConnections=0 and enables the inbox Remote Desktop firewall group in the post-generalize block -- the same slot as the WU-policy restore, for the same reason: registry writes after /quit land in the sealed image, and the Remote Desktop rules are disjoint from the WinRM rules the teardown removes, so packer's session survives. The group is matched by its locale-independent id (@FirewallAPI.dll,-28752), not the DisplayGroup string. NLA stays at its Server default (required), so nothing is reachable pre-auth.

The tradeoff is deliberate and documented: every clone exposes 3389 on whatever network it lands on, including public ones, guarded by NLA plus the strength of --cipassword. The new rdp-enabled verify check asserts enabled + listening on 3389 + NLA on for every clone, so a regression in either direction fails verify by name.

Verified live on a 2025 clone (runtime enablement: listener up instantly, external connect to 3389 OK); untested on a full build as of 2026-08-19 -- recorded as such in docs/windows.md.

Windows Server ships with RDP off and nothing in the build changed that,
so a clone's only first contact was the noVNC console. That path breaks
in practice: the console types against the guest's en-US layout, so a
--cipassword containing symbols typed on a non-US client keyboard
arrives as different characters. Observed live 2026-08-18 on a 2025
clone: '=' typed on a Swedish layout never matched, the Security log
filled with 0xC000006A (wrong password), while the very same string
passed an in-guest LogonUser type 2. Convoy provisions clones with only
--cipassword, so RDP is the expected first door in.

Finalize.ps1 sets fDenyTSConnections=0 and enables the inbox Remote
Desktop firewall group in the post-generalize block -- the same slot as
the WU-policy restore, for the same reason: registry writes after /quit
land in the sealed image, and the Remote Desktop rules are disjoint from
the WinRM rules the teardown removes, so packer's session survives. The
group is matched by its locale-independent id (@FirewallAPI.dll,-28752),
not the DisplayGroup string. NLA stays at its Server default (required),
so nothing is reachable pre-auth.

The tradeoff is deliberate and documented: every clone exposes 3389 on
whatever network it lands on, including public ones, guarded by NLA plus
the strength of --cipassword. The new rdp-enabled verify check asserts
enabled + listening on 3389 + NLA on for every clone, so a regression in
either direction fails verify by name.

Verified live on a 2025 clone (runtime enablement: listener up
instantly, external connect to 3389 OK); untested on a full build as of
2026-08-19 -- recorded as such in docs/windows.md.
@akshtt-dev

Copy link
Copy Markdown

you are a saver, I wasn't able to login via noVNC.

ericwang401 added a commit that referenced this pull request Sep 4, 2026
Two defects that both shipped bad templates without failing a build, on top
of the RDP change from #33.

#32 — winget absent from 2025 clones. 2025 ships two coexisting
DesktopAppInstaller versions; sysprep rejects the per-user-registered one and
removing it fails 0x80070032 until the SIBLING version's provisioned entry is
dropped. Provisioning is what registers an app into each new user profile, and
remove-build-profile.ps1 deletes the build profile, so every clone's first
logon creates a fresh one with no winget. Every build passed; the loss was
only findable by preserving a VM and looking inside.

Finalize now re-provisions any family whose provisioning the cleanup dropped,
from the payload Server keeps on disk (InboxApps, then WindowsApps) — no
network. It runs after the blocking version is gone, so it cannot resurrect
what sysprep objected to, and failures are logged rather than fatal: a
template without winget is a defect, one that never generalizes is useless.
Generic rather than winget-specific — anything the cleanup drops is something
a clone was supposed to have.

The new winget-present check asserts winget.exe RESOLVES on the clone rather
than reading the provisioned-package list, since a package whose provisioning
survived but never registered for the user is the same defect from the
clone's point of view. Scoped to windows-server-2025 via RECIPE_OVERRIDES —
winget is not inbox on 2019 or 2022, so asserting it there would fail
templates that are behaving correctly.

#31 — templates that failed smoke tests were published anyway. The upload is
a side effect of the build: the node-side post-processor runs CF_UPLOAD_CMD
as soon as the artifact is hashed, which is before `cf verify` runs. Since
`cf publish --r2` advertises the newest sidecar per template, a failed
artifact could supersede a good one — which is why a bucket held two
windows-server-2025 templates after one successful run.

build-one.yml now builds with --skip-upload, verifies, then uploads with
`cf upload --remote`. Steps stop on first failure, so reaching the upload IS
the verify pass. publish.yml is left alone deliberately: it aggregates
whatever is already in R2 and has no idea what passed, so the gate belongs at
the point of upload.

Both fixes are untested on a real build (a Windows build is 1-3h and its
provisioners run only at the end); Finalize.ps1 is parse-checked with pwsh
and the winget change is recorded in docs/windows.md as untested per
AGENTS.md.
@ericwang401
ericwang401 merged commit 517d0f7 into ConvoyPanel:main Sep 4, 2026
2 checks passed
ericwang401 added a commit that referenced this pull request Sep 4, 2026
Lands PR #33 (RDP enabled on Windows templates) together with fixes for
#32 (winget stripped from 2025 clones by the Appx cleanup) and #31
(templates that failed smoke tests were published anyway).

All three are Windows/publish-pipeline concerns that touch the same files,
so they land as one merge rather than three racing branches.
ericwang401 added a commit that referenced this pull request Sep 4, 2026
Check scripts are TypeScript template literals, where `\S` is not a
recognized escape and silently becomes `S`. The rdp-enabled check added in
#33 wrote its registry paths with single backslashes, so it compiled, it
reviewed fine, and it reached the guest as:

    HKLM:SYSTEMCurrentControlSetControlTerminal Server

Every Windows verify then failed ~17 minutes in with "Cannot find path ...
because it does not exist" — reported as a failing template rather than a
broken check. windows-server-2019 failed exactly this way after a clean
4-hour build.

Every other check in the file already used `\\`; only the new one did not.

Added tests that render each check's script and assert no `HK*:` is followed
straight by a key name, and that every hive path keeps more than one
separator. Confirmed they fail with the bug reintroduced and pass with it
fixed — a regression test for this class is worth more than the fix, since
the defect is invisible in review and only shows up hours into a build.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants