Setting up Windows Server on a VPS: the first steps

Published on 15 min read

The checklist for the time right after provisioning, in the correct order: check the license, set the password, time zone, updates, firewall, harden RDP, disks and roles. Including the places where it goes wrong.

A freshly provisioned Windows Server looks finished: RDP answers, the desktop appears, Server Manager starts. That is exactly the trap. Between "RDP answers" and "ready for production" there are about twenty minutes of work, and the order of those steps decides whether you can still get back in afterwards.

Here are the steps in the order they have to be done, each one with the command that verifies it and with the error messages quoted word for word.

Sort out the license first, not last

Windows Server is not part of the server price. KernelHost is not a Microsoft SPLA partner and does not rent out Windows licenses. Windows runs here under a Bring Your Own License model: the license is obtained from a certified Microsoft Partner or directly from Microsoft and brought in by the customer.

For testing, evaluation and migration rehearsals there is the free evaluation version from Microsoft with a term of 180 days. It is fully functional, it is only time limited and not meant for production use.

The very first thing to check is what is actually running on the server:

slmgr /dlv
slmgr /xpr

slmgr /dlv shows the edition name. If it says ServerStandardEval or ServerDatacenterEval, an evaluation is running. slmgr /xpr gives you the expiry date in plain text. Both commands open a popup window, which is a nuisance on Server Core. There you use cscript C:\Windows\System32\slmgr.vbs /dlv instead.

What happens when the 180 days run out

The server keeps running, but the Windows License Monitoring Service shuts it down after roughly an hour of uptime, over and over again. A watermark in the bottom right corner states that the Windows license has expired. Anyone who sees this for the first time usually spends hours hunting for a hardware or driver fault, because the pattern looks like a watchdog.

The clean way out is to switch to a real license. That works without reinstalling:

DISM /Online /Set-Edition:ServerStandard /ProductKey:XXXXX-XXXXX-XXXXX-XXXXX-XXXXX /AcceptEula

Three points where this regularly fails:

  • It only goes from Eval to the full version of the same name, or upwards. DISM will not take you from Datacenter Evaluation back down to Standard.
  • The key has to be a retail, MAK or KMS key. If it does not fit, DISM reports Error: 1168, or the licensing service reports 0xC004F050 stating that the product key is invalid. That almost never means "counterfeit key", it means "wrong key type for this path".
  • A domain controller cannot be converted from evaluation to retail. If you have already run Install-ADDSForest on the evaluation, you first have to demote the role again. That is the most expensive mistake in this article, which is why the license question sits right at the top here and not at the end.

A note on obtaining the installation source: you cannot upload your own ISO images at KernelHost, Windows comes from the image catalog in the customer panel. You bring the license, the image is already there.

Change the administrator password without locking yourself out

The password you were shipped is in the provisioning email and should be replaced right away. The usual route via net user Administrator * works, but it has the drawback of returning only a very thin message when a policy is violated. Better:

$pw = Read-Host -AsSecureString -Prompt "New password"
Set-LocalUser -Name "Administrator" -Password $pw

If you get The password does not meet the password policy requirements, the local policy is taking effect: at least eight characters and three of four character classes. You can check the values in force with net accounts.

More useful than renaming the account is a second, named administrator account. It gives you a way back in if the one account gets locked:

$pw = Read-Host -AsSecureString -Prompt "Password for svcadmin"
New-LocalUser -Name "svcadmin" -Password $pw -PasswordNeverExpires
Add-LocalGroupMember -SID "S-1-5-32-544" -Member "svcadmin"

Why the SID instead of the group name? Because the administrators group is called Administratoren on a German system and Administrators on an English one. Scripts that hard-wire the name break at the first language change with The specified local group does not exist. S-1-5-32-544 is the same on every Windows.

The lockout trap that really does exist on Windows Server 2025

Up to and including Windows Server 2019, the built-in administrator account could not be locked out through network logons. Since a cumulative update for Server 2022, and by default on Windows Server 2025, that has changed: account lockout takes effect after five failed attempts, and it catches the built-in administrator as well.

On a VPS with port 3389 open that means something very concrete: the logon attempts of automated scanners lock the account before you get to it yourself. You type a correct password and still get The referenced account is currently locked out and may not be logged on to.

The way out once it has happened:

  1. Sign in through the console in the customer panel instead of using RDP.
  2. Clear the lockout, either by waiting out the lockout duration (net accounts shows it, the default is ten minutes) or directly:
$u = [ADSI]"WinNT://$env:COMPUTERNAME/Administrator,user"
$u.IsAccountLocked = $false
$u.SetInfo()

The permanent solution is not to raise the lockout threshold, it is to limit who can reach RDP at all. That comes further down. Only afterwards can you relax the threshold again, because by then no foreign attempts arrive any more.

Set the time zone and clock before anything gets logged

Windows images are almost always set to UTC or to an American zone. Nobody notices until the first TLS certificate is rejected as "not yet valid", or until event logs can no longer be correlated with those of other systems.

Get-TimeZone
Get-TimeZone -ListAvailable | Where-Object { $_.Id -like "*Europe*" }
Set-TimeZone -Id "W. Europe Standard Time"

"W. Europe Standard Time" covers Vienna, Berlin and Zurich, including the daylight saving change. The time itself comes from the time service, and this is where the real difference between bare metal and a virtual server shows:

w32tm /query /source
w32tm /query /status

If the source reads Local CMOS Clock, the server is not fetching its time from the network at all. On a virtualized system the clock then drifts visibly, sometimes by several seconds a day. Set a real source:

w32tm /config /manualpeerlist:"time.windows.com,0x9 at.pool.ntp.org,0x9" /syncfromflags:manual /update
Set-Service w32time -StartupType Automatic
Restart-Service w32time
w32tm /resync /force

The proof that it has taken hold: w32tm /query /source now names one of the configured servers, and w32tm /query /status shows a Stratum value below 10 plus a plausible time for the last synchronization. If w32tm /resync reports The computer did not resync because no time data was available instead, the firewall is usually blocking outbound UDP port 123, or the service was not running yet.

If the server joins a domain later: the domain controller then takes over time distribution and the manual peer list has to go, otherwise you end up with two competing time sources.

Windows updates, and what to do when they hang

Patch before you install roles, not afterwards. Otherwise you install roles from an old state and then push them through the update loop a second time.

On Server Core, sconfig handles this, menu item 6. With the desktop experience you go through Settings. It becomes scriptable with the PSWindowsUpdate module:

Install-PackageProvider -Name NuGet -Force
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
Install-Module PSWindowsUpdate -Force
Import-Module PSWindowsUpdate
Get-WindowsUpdate

If the download from the PowerShell Gallery fails with No match was found for the specified search criteria or with a dropped connection, on older builds (Server 2016 and 2019) it is almost always TLS. Run this first, in the same session:

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

For the first run it pays off to keep the reboot in your own hands:

Install-WindowsUpdate -AcceptAll -IgnoreReboot
Get-WURebootStatus -Silent

The first patch run on a fresh image takes 30 to 60 minutes depending on the age of that image, and it needs two or three reboots. That is normal. What is not normal is the message Failure configuring Windows updates. Reverting changes during boot. The two most common causes on a VPS: too little free space on C: (below roughly 10 GB it gets tight) or a hard reboot in the middle of the configuration phase. Check the space with Get-Volume and reset the update store: stop the wuauserv and bits services, rename C:\Windows\SoftwareDistribution, start the services again.

On a side note: Windows Server 2025 offers hotpatching, that is security updates without a reboot. Since May 2026 it no longer costs anything, but it does require a connection to Azure Arc. For a single VPS that is usually more effort than benefit.

Firewall: the profile matters more than the rule

The Windows firewall stays on. Always. The usual advice to "just switch it off for a quick test" regularly ends, on a publicly reachable server, with it staying off for good.

Get-NetFirewallProfile | Select-Object Name, Enabled
Get-NetConnectionProfile

By far the most common firewall misunderstanding on Windows: the rule exists, it is enabled, and access is blocked anyway. The cause is almost always that the rule applies to the Private or Domain profile while the network adapter sits in the Public profile. A VPS with a direct public IP frequently lands exactly there after provisioning. Check which profile a rule belongs to explicitly:

Get-NetFirewallRule -Name "RemoteDesktop-UserMode-In-TCP" | Select-Object Name, Enabled, Profile, Action

And take a look at what is allowed inbound in the first place:

Get-NetFirewallRule -Enabled True -Direction Inbound -Action Allow | Select-Object DisplayName, Profile | Sort-Object DisplayName

What has no business on an internet facing VPS is file and printer sharing on TCP 445. If you need SMB, run it through a tunnel instead of exposing it. A WireGuard tunnel is the most straightforward way to do that, and setting up the peer is described in Set up a WireGuard VPN server.

An ICMP echo, on the other hand, is worth allowing, otherwise your monitoring will report the server as dead:

New-NetFirewallRule -DisplayName "ICMPv4 Echo Inbound" -Protocol ICMPv4 -IcmpType 8 -Direction Inbound -Action Allow

Securing RDP does not mean moving the port

A different port reduces log noise and nothing else. If you want to change it anyway, that works without a reboot, and it is described in Change the RDP port without a reboot. The real hardening consists of three other things.

First: enforce Network Level Authentication. Without NLA, Windows sets up the session before anyone has identified themselves. That is exactly the attack surface you do not want.

$rdp = "HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp"
Get-ItemProperty -Path $rdp -Name UserAuthentication, SecurityLayer
Set-ItemProperty -Path $rdp -Name UserAuthentication -Value 1
Set-ItemProperty -Path $rdp -Name SecurityLayer -Value 2

UserAuthentication = 1 means NLA is active, SecurityLayer = 2 enforces TLS for the connection setup.

Second: restrict the source IP. This is the measure with the biggest effect, and it is a one-liner. It cuts off password attacks completely, and with them the account lockout problem from above.

Set-NetFirewallRule -Name "RemoteDesktop-UserMode-In-TCP" -RemoteAddress "203.0.113.10"
Set-NetFirewallRule -Name "RemoteDesktop-UserMode-In-UDP" -RemoteAddress "203.0.113.10"

The rule name is deliberately the internal -Name and not the display group, because that one is called "Remotedesktop" or "Remote Desktop" depending on the language. Several addresses or a whole network work just as well, for example -RemoteAddress @("203.0.113.10","198.51.100.0/24").

And here is the important precaution: run this from an existing RDP session and keep a second session open in parallel. If you mistype your own IP, the connection is gone instantly. The way back in is then the console in the customer panel. If your IP at home changes, skip this step and make RDP reachable over VPN only instead.

Third: set the lockout threshold sensibly, once the firewall is closed:

net accounts /lockoutthreshold:10 /lockoutduration:15 /lockoutwindow:15

The three RDP errors people really search for

  • An authentication error has occurred. The function requested is not supported. This is the CredSSP case from CVE-2018-0886: the client is patched, the server is not. The correct solution is the patch level on the server. The registry workaround doing the rounds, AllowEncryptionOracle = 2 on the client, reopens exactly that hole and should, if at all, only be set until the patch run is finished and then removed again.
  • The remote computer requires Network Level Authentication, which your computer does not support. This shows up with very old clients. The server is configured correctly here, so update the client instead of switching NLA off.
  • Remote Desktop can't connect to the remote computer for one of these reasons: ... This message is a catch-all. Narrow it down on the server by checking whether anything is listening at all:
Get-Service TermService
Get-NetTCPConnection -LocalPort 3389 -State Listen

If a line with 0.0.0.0:3389 comes back, the service is fine and the fault is in the firewall or somewhere on the way there. If nothing comes back, it is the service or a changed port. If the server stops answering entirely under load and ICMP goes silent as well, it is worth looking in the direction of an attack, and Protecting servers against DDoS attacks fits that case.

Adding extra disks

A second disk does not appear in Explorer by itself once it has been assigned. It is raw and has to be initialized.

Get-Disk
Get-Disk | Where-Object PartitionStyle -eq 'RAW' | Initialize-Disk -PartitionStyle GPT
New-Partition -DiskNumber 1 -UseMaximumSize -DriveLetter D | Format-Volume -FileSystem NTFS -NewFileSystemLabel "Daten" -Confirm:$false
Get-Volume

GPT instead of MBR, always. MBR runs out at 2 TB, and nobody enjoys making the switch later. Four things to trip over:

  • Disk offline or write protected. Initialize-Disk then reports The disk is offline or The disk is write protected. Fix: Set-Disk -Number 1 -IsOffline $false and Set-Disk -Number 1 -IsReadOnly $false.
  • Letter D: is taken. On many images the virtual drive with the installation source is mounted there. New-Partition aborts with The specified drive letter is not available. Check with Get-Volume and move to E: or relocate the virtual drive beforehand.
  • Wrong number. -DiskNumber 1 is not guaranteed to be the new disk. Compare size and PartitionStyle in Get-Disk before you format. A format on number 0 hits the system disk.
  • C: does not grow along after a storage upgrade. The extra space sits unpartitioned behind it:
Update-HostStorageCache
$max = (Get-PartitionSupportedSize -DriveLetter C).SizeMax
Resize-Partition -DriveLetter C -Size $max

If Resize-Partition reports The specified size is not supported or Size Not Supported here, there is a recovery partition sitting between C: and the free space. It has to be removed or moved to the end first, that does not happen in a single command and should not happen without a backup.

Installing roles and features

Only now, after the patch level and the firewall, do the roles come in. A look at the current state:

Get-WindowsFeature | Where-Object Installed | Select-Object Name, DisplayName

A web server is a one-liner:

Install-WindowsFeature -Name Web-Server -IncludeManagementTools
Get-Service W3SVC
Invoke-WebRequest http://localhost -UseBasicParsing | Select-Object StatusCode

If StatusCode 200 comes back, IIS is running locally. That does not yet mean it is reachable from outside, which needs the matching firewall rule and a look from a second system.

The classic among the failures is .NET Framework 3.5. It is not part of the running system, it has to be pulled in from the installation source. Without a mounted image you get:

Fehler: 0x800f081f
Die Quelldateien wurden nicht gefunden.

On a VPS without your own ISO you have two options: set the policy so that Windows may fetch the files directly from Windows Update, or copy the sources\sxs folder from an image with the same build number onto the server and point -Source at it. A different build number does not work, even when it belongs to the same Windows version.

Two more points about roles: Uninstall-WindowsFeature -Name Web-Server -Remove removes not only the role but its files from the disk as well. That saves space, but it makes a later reinstall dependent on a source again, with the same 0x800f081f. And anyone installing Active Directory should scroll back up first: on an evaluation license that is a dead end.

How you can tell that everything is in place

To finish, a round of verification commands. Each of them has an expected answer, and if one of them does not match, the server is not ready yet.

slmgr /xpr
Get-TimeZone
w32tm /query /source
Get-HotFix | Sort-Object InstalledOn -Descending | Select-Object -First 5
Get-NetFirewallProfile | Select-Object Name, Enabled
Get-NetFirewallRule -Name "RemoteDesktop-UserMode-In-TCP" | Get-NetFirewallAddressFilter
Get-LocalUser | Select-Object Name, Enabled, PasswordLastSet
Get-Volume | Where-Object DriveLetter
Get-WindowsFeature | Where-Object Installed | Measure-Object

What you expect to see: an expiry date or "permanently activated", the European time zone, a real time source instead of the local clock, updates from today or yesterday, all three firewall profiles on True, a RemoteAddress restriction instead of Any, a recent password date on the administrator and every volume with a drive letter.

After that, one last and often forgotten step pays off: do not simply drop the session by closing the window, sign out properly. Disconnected sessions stay around with their memory usage, and on a small VPS that adds up over weeks to the point where no new login is possible.

If you are setting up a Linux server in parallel, Setting up a new root server: the checklist covers the same topics for the other side, and Securing SSH and setting up key login is the counterpart there to the section on RDP.

Frequently asked questions

Is a Windows Server license included in the server price?
No. KernelHost is not a Microsoft SPLA partner and does not rent out Windows licenses. Windows is operated under Bring Your Own License: the license is obtained from a certified Microsoft Partner or directly from Microsoft and brought in by the customer. For testing, the free evaluation version from Microsoft with a term of 180 days is available.
What happens when the 180 days of the evaluation version have expired?
The server still starts, but the Windows License Monitoring Service shuts it down after roughly an hour of uptime, and it keeps doing so. A watermark on the desktop points to the expired license. With a regular license the installation can be converted without reinstalling: DISM /Online /Set-Edition:ServerStandard /ProductKey:... /AcceptEula.
Why do I keep getting locked out of my own server?
On Windows Server 2025, account lockout is active by default after five failed logons and it also covers the built-in administrator account. With port 3389 open, automated logon attempts lock the account continuously. The solution is not a higher lockout threshold but restricting the RDP rule to your own source IP with Set-NetFirewallRule -Name RemoteDesktop-UserMode-In-TCP -RemoteAddress.
How do I add a second disk on Windows Server?
Get-Disk shows it with PartitionStyle RAW. Then Initialize-Disk -PartitionStyle GPT, followed by New-Partition -DiskNumber 1 -UseMaximumSize -DriveLetter D and Format-Volume -FileSystem NTFS. If Windows reports an offline state or write protection, Set-Disk -IsOffline $false and Set-Disk -IsReadOnly $false help. If D: is already taken, move to a different drive letter.
What does the message that the requested function is not supported mean?
That is the CredSSP error from CVE-2018-0886. The client has the security update, the server does not have it yet. The correct solution is the patch level on the server, not the widespread registry workaround with AllowEncryptionOracle = 2 on the client, because that reopens the hole.
Why does the Windows firewall block even though the rule exists?
Because firewall rules apply per network profile. If the rule is set to Private or Domain while the network adapter is on Public, it does not take effect. A VPS with a public IP frequently ends up in the Public profile after provisioning. Get-NetConnectionProfile shows the assignment, and Get-NetFirewallRule with the Profile column shows the reach of the rule.

Windows Server VPS RDP PowerShell Server administration Security