TCP Provider: Error code 0x102 is what every remote client gets from a fresh SQL Server 2022 Express or Developer install. The full message adds “A network-related or instance-specific error has occurred”, yet nothing is broken. Setup leaves TCP/IP switched off on those editions and puts every edition in Windows-only authentication, so nothing listens on port 1433 and SQL logins cannot be used, and Windows Firewall drops the port even after you turn TCP on. SQL-login connections need all three fixed, and the error never tells you which one is left.
This guide makes those changes from PowerShell, opens port 1433 to your LAN only, creates a dedicated SQL login instead of reusing sa, and tests it from Linux with sqlcmd. Every command below was run in October 2026 on Windows Server 2025 with SQL Server 2022 Express as the default instance and a Debian 13 client.
Prerequisites
You need an installed SQL Server 2022 instance and an elevated PowerShell session on the server, because every change here writes to HKLM or the firewall. If you still have to install the engine, our guide to installing SQL Server on Windows Server covers the setup wizard. The lab used these machines:
- Windows Server 2025 Datacenter, hostname WIN2025-SRV, IP 10.0.1.60
- SQL Server 2022 Express, default instance
MSSQLSERVER - A Debian 13 client at 10.0.1.50 on the 10.0.1.0/24 LAN
- TCP 1433 reachable between them, with no other firewall in the path
Named instances, the SQL Server Browser service (UDP 1434), Developer and Standard editions, and SQL Server Management Studio were not part of this test. The registry paths below are for the default instance MSSQLSERVER, which is what our unattended install created. The Express GUI installer defaults to a named instance called SQLEXPRESS instead, so its ID, its service name (MSSQL$SQLEXPRESS) and its port all differ, and the next section covers what to change for it.
If you manage the server from SSMS on a Windows workstation, you enter the same host, login and password in its connection dialog, and our SSMS install guide covers the client itself. Current SSMS releases default to mandatory encryption, so against the self-signed certificate described at the end you need to tick Trust server certificate. We did not test SSMS against this server.
Error: “There was an error generating the XML document”
You will only meet this one if you install SQL Server unattended over SSH, the way we did. Setup failed after 14 seconds with exit code -2068774911, and Detail.txt showed the inner exception System.Security.Cryptography.CryptographicException: Access is denied. That is DPAPI refusing to work in a non-interactive SSH logon, so the fix is to run the same setup.exe as SYSTEM through a scheduled task. This is the script that passed, in 4.4 minutes:
$cmd = 'C:\sqlsetup\expr\setup.exe'
$arg = '/Q /ACTION=Install /FEATURES=SQLEngine /INSTANCENAME=MSSQLSERVER /SQLSYSADMINACCOUNTS="BUILTIN\Administrators" /IACCEPTSQLSERVERLICENSETERMS /UPDATEENABLED=False'
$a = New-ScheduledTaskAction -Execute $cmd -Argument $arg
Register-ScheduledTask -TaskName SqlSetup -Action $a -User 'SYSTEM' -RunLevel Highest -Force | Out-Null
$start = Get-Date
Start-ScheduledTask -TaskName SqlSetup
do { Start-Sleep 10; $s = (Get-ScheduledTask -TaskName SqlSetup).State } while ($s -eq 'Running')
"task finished, last result: " + (Get-ScheduledTaskInfo -TaskName SqlSetup).LastTaskResult + " minutes: " + [math]::Round(((Get-Date) - $start).TotalMinutes, 1)
Unregister-ScheduledTask -TaskName SqlSetup -Confirm:$false
The script expects the Express package already extracted to C:\sqlsetup\expr (we used SQLEXPR_x64_ENU.exe /q /x:C:\sqlsetup\expr). The SQL2022-SSEI-Dev.exe web bootstrapper was no help here: over SSH it sat for 10 minutes without downloading anything. Installing from the GUI on the console should not hit the DPAPI error, although we did not run that path for this refresh. If the server itself is still bare, the Windows Server 2025 installation guide gets it to the point where this one starts.
Check the instance and its defaults
Before changing anything, read what setup actually wrote, because the registry path carries the instance ID and you need it in every later command. SQL Server 2022 uses MSSQL16 followed by the instance name, so a default instance lives under MSSQL16.MSSQLSERVER. List the installed instances and resolve the ID:
Get-ItemProperty -Path 'HKLM:\Software\Microsoft\Microsoft SQL Server' | Out-String -Stream | Select-String 'InstalledInstances'
(Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL').MSSQLSERVER
One instance, and its ID confirms the path the rest of the guide uses:
InstalledInstances : {MSSQLSERVER}
MSSQL16.MSSQLSERVER
On a named instance, drop the .MSSQLSERVER property from the second command to list every name under Instance Names\SQL, and use the ID it prints in $base below. Named instances also listen on a dynamic port by default, so remote clients need either a fixed port (set TcpPort under IPAll and clear TcpDynamicPorts) or the SQL Server Browser service on UDP 1434, neither of which we tested.
Store the instance’s registry key in $base so you do not retype it, then read the authentication mode, the TCP switch and the port. If you open a new PowerShell window later, run the $base line again first:
$base = 'HKLM:\Software\Microsoft\Microsoft SQL Server\MSSQL16.MSSQLSERVER\MSSQLServer'
"LoginMode = " + (Get-ItemProperty -Path $base -Name LoginMode).LoginMode
"Tcp Enabled = " + (Get-ItemProperty -Path "$base\SuperSocketNetLib\Tcp" -Name Enabled).Enabled
Get-ItemProperty -Path "$base\SuperSocketNetLib\Tcp\IPAll" | Select-Object TcpPort, TcpDynamicPorts | Format-List
The capture from the lab server shows both blockers at once.

LoginMode = 1 means Windows authentication only, so a SQL login such as sa is rejected even when its password is right. Tcp Enabled = 0 means the engine is not listening on the network at all, although IPAll already carries port 1433 with no dynamic port, which is what you want for a default instance. A LoginMode of 2 or a TCP value of 1 means someone has already done part of this work, so skip the matching step.
Enable mixed mode and TCP/IP for SQL Server remote connections
Both settings are plain registry values, and neither takes effect until the service restarts, so set them together and restart once. LoginMode 2 is mixed mode (Windows and SQL Server authentication), which you only need for SQL logins: domain-joined Windows clients using integrated authentication need TCP and the firewall rule only. It is the same switch Microsoft documents for changing the server authentication mode in SSMS. The restart drops every open connection, so do it in a quiet window on a server that’s already in use. -Force also stops dependent services, which on editions other than Express includes SQL Server Agent (untested here, since Express ships Agent disabled):
Set-ItemProperty -Path $base -Name LoginMode -Value 2
Set-ItemProperty -Path "$base\SuperSocketNetLib\Tcp" -Name Enabled -Value 1
Restart-Service MSSQLSERVER -Force
Read both values back and check which addresses sqlservr is listening on now:
Get-ItemProperty -Path $base -Name LoginMode | Select-Object LoginMode, PSChildName | Format-List
Get-ItemProperty -Path "$base\SuperSocketNetLib\Tcp" -Name Enabled | Select-Object Enabled | Format-List
Get-NetTCPConnection -State Listen | Where-Object { $_.OwningProcess -eq (Get-Process sqlservr).Id } | Select-Object LocalAddress, LocalPort | Format-Table -AutoSize
Port 1433 is now open on every IPv4 and IPv6 address, which is the result you want before going near the firewall.
LoginMode : 2
PSChildName : MSSQLServer
Enabled : 1
LocalAddress LocalPort
------------ ---------
:: 1433
0.0.0.0 1433
The engine writes the same facts to its ERRORLOG at startup, and that file is where you’ll read the real reason behind every failed login later. Filter it for the three line types that matter here:
Get-Content 'C:\Program Files\Microsoft SQL Server\MSSQL16.MSSQLSERVER\MSSQL\Log\ERRORLOG' | Select-String 'Login failed|Server is listening|Authentication mode' | ForEach-Object { $_.Line }
A healthy restart logs mixed mode and both listeners.
2026-10-20 16:38:50.13 Server Authentication mode is MIXED.
2026-10-20 16:38:50.61 spid28s Server is listening on [ 'any' <ipv6> 1433] accept sockets 1.
2026-10-20 16:38:50.61 spid28s Server is listening on [ 'any' <ipv4> 1433] accept sockets 1.
Don’t test from another machine yet. We did, and sqlcmd on the Debian client still failed with the same Login timeout expired and Error code 0x102 as before, because Windows Firewall had all three profiles enabled and nothing allowed 1433 in.
Open port 1433 in Windows Firewall
Windows Server 2025 keeps the Domain, Private and Public firewall profiles on by default, and SQL Server setup does not add a rule for the engine. Create an inbound rule for TCP 1433 on all three profiles, then confirm which profiles are active:
New-NetFirewallRule -Name "MSSQL-1433" -DisplayName "SQL Server (TCP 1433)" -Direction Inbound -Protocol TCP -LocalPort 1433 -Action Allow -Profile Domain,Private,Public | Select-Object Name, Enabled, Direction, Action | Format-List
Get-NetFirewallProfile | Select-Object Name, Enabled | Format-Table -AutoSize
The rule is enabled, and so is every profile it has to cover.
Name : MSSQL-1433
Enabled : True
Direction : Inbound
Action : Allow
Name Enabled
---- -------
Domain True
Private True
Public True
A rule that accepts any source address leaves port 1433 open to anything that can route to the server, and SQL Server logins are a favourite brute-force target. Limit the rule to the network your clients sit on. We scoped it to the lab LAN and the remote client kept connecting:
Set-NetFirewallRule -Name MSSQL-1433 -RemoteAddress 10.0.1.0/24
Get-NetFirewallRule -Name MSSQL-1433 | Get-NetFirewallAddressFilter | Select-Object LocalAddress, RemoteAddress | Format-List
Windows stores the prefix as a subnet mask, which is the same range written differently.
LocalAddress : Any
RemoteAddress : 10.0.1.0/255.255.255.0
For a single application server, put its one IP in -RemoteAddress instead of a /24. If you also manage other ports on the same box, our notes on opening a port in Windows Server firewall cover the GUI side of the same rules.
Create a dedicated SQL login
Give each application its own login and database instead of handing it sa. A leaked application password then exposes one database rather than the whole instance, and you can disable or rotate it without breaking anything else. Put the statements in a script file, because the GO separators matter here (the error below shows why):
notepad C:\Users\Administrator\appuser.sql
Paste the following and swap Str0ng-Passw0rd! for your own password. CHECK_POLICY = ON holds the SQL login to the Windows password policy, so a weak password is refused at creation time:
CREATE DATABASE appdb;
GO
CREATE LOGIN appuser WITH PASSWORD = N'Str0ng-Passw0rd!', CHECK_POLICY = ON;
GO
USE appdb;
GO
CREATE USER appuser FOR LOGIN appuser;
ALTER ROLE db_owner ADD MEMBER appuser;
GO
Run it with Windows authentication. Setup made BUILTIN\Administrators a sysadmin, so -E works from an elevated session, and -b makes sqlcmd exit non-zero if any batch fails:
sqlcmd -S localhost -E -b -i C:\Users\Administrator\appuser.sql
The only output is the context switch from the USE line, and the exit code was 0.
Changed database context to 'appdb'.
db_owner is broad inside appdb but grants nothing on other databases or the server. If the application only reads and writes rows, db_datareader and db_datawriter are the tighter choice, and the guide to managing SQL Server users and permissions goes through the role options.
Msg 911: Database ‘appdb’ does not exist
Our first version ran the same statements as one batch, without the GO lines, and sqlcmd stopped with this:
Msg 911, Level 16, State 1, Server WIN2025-SRV, Line 1
Database 'appdb' does not exist. Make sure that the name is entered correctly.
sqlcmd sends everything up to a GO as one batch, and SQL Server checks USE appdb before the CREATE DATABASE in the same batch has run. The whole batch failed, so nothing was created: the login did not exist afterwards and the fixed file ran cleanly on the second try. Split database creation, login creation and the USE into separate batches, as in the file above.
Keep the sa login disabled
SQL Server 2022 setup leaves sa disabled when you install in Windows-only mode, and switching to mixed mode does not enable it. Leave it that way. Every scanner on the internet knows the name sa, so a disabled sa plus a named application login means an attacker has to guess the login name as well as the password. Check its state:
sqlcmd -S localhost -E -W -Q "SET NOCOUNT ON; SELECT name, is_disabled FROM sys.sql_logins WHERE name = 'sa'"
An is_disabled of 1 is what you want to see.
name is_disabled
---- -----------
sa 1
Some vendor installers insist on sa during setup. In that case set a password and enable it for the install only:
sqlcmd -S localhost -E -b -Q "ALTER LOGIN sa WITH PASSWORD = N'An0ther-Str0ng-Pass!'; ALTER LOGIN sa ENABLE;"
Disable it again as soon as the installer finishes, then re-run the check above to confirm is_disabled is back to 1:
sqlcmd -S localhost -E -b -Q "ALTER LOGIN sa DISABLE;"
Disabling sa does not affect Windows authentication, so you can still administer the server locally with sqlcmd -S localhost -E as an administrator.
Test from a Linux client with sqlcmd
A Linux box is the strictest client you can test from: it cannot fall back to Windows authentication, and Microsoft’s ODBC Driver 18 encrypts by default, so it catches every problem a Windows application server would hit later. The same sqlcmd build is what you get alongside SQL Server on Ubuntu. On Debian 13, add Microsoft’s package repository first:
cd /tmp
curl -fsSLO https://packages.microsoft.com/config/debian/13/packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
sudo apt-get update
The repository package sets itself up for Debian 13.
Setting up packages-microsoft-prod (1.1-debian13) ...
Install mssql-tools18, which pulls in the msodbcsql18 driver. ACCEPT_EULA=Y accepts Microsoft’s licence for both packages so apt doesn’t stop at a prompt. Do not reach for the newer Go-based sqlcmd package on this release: apt-get install sqlcmd exited with code 100 because the Debian 13 repository does not carry it.
sudo ACCEPT_EULA=Y apt-get install -y mssql-tools18 unixodbc-dev
The tools land in /opt/mssql-tools18/bin, which is not on your PATH, so call sqlcmd by its full path to check the version:
/opt/mssql-tools18/bin/sqlcmd -? | head -3
The banner confirms the Linux build.
Microsoft (R) SQL Server Command Line Tool
Version 18.7.0001.1 Linux
Copyright (C) 2017 Microsoft Corporation. All rights reserved.
Export the server address and the login details once, so every test below uses the same values. Single quotes around the password stop bash from expanding the !:
export HOST_IP="10.0.1.60"
export DB_NAME="appdb"
export DB_USER="appuser"
export DB_PASS='Str0ng-Passw0rd!'
Connect as the new login and ask the server who you are. -C trusts the server’s certificate (explained below), -l 8 sets the login timeout to 8 seconds (already the default, kept here as we ran it), and -W trims column padding:
/opt/mssql-tools18/bin/sqlcmd -S "${HOST_IP}" -U "${DB_USER}" -P "${DB_PASS}" -d "${DB_NAME}" -C -l 8 -W -Q "SET NOCOUNT ON; SELECT DB_NAME() AS db, SUSER_SNAME() AS login"
Seeing appdb and appuser proves the whole chain: TCP listener, firewall rule, mixed mode and the login’s default database:
db login
-- -----
appdb appuser
The capture below repeats that test with the port written out (-S "${HOST_IP},1433", the syntax you need for a non-default port), then tries sa after it was disabled, with SA_PASS exported as the sa password from the previous section:

Everything we hit on the way there is below, with the server-side reason for each one. Errors the client prints are deliberately vague, so keep the ERRORLOG command from the mixed-mode step handy when a test fails.
Error: “TCP Provider: Error code 0x102” and “Login timeout expired”
This is what the Debian client printed before any change, and again after TCP was on but the firewall wasn’t:
Sqlcmd: Error: Microsoft ODBC Driver 18 for SQL Server : Login timeout expired.
Sqlcmd: Error: Microsoft ODBC Driver 18 for SQL Server : TCP Provider: Error code 0x102.
Sqlcmd: Error: Microsoft ODBC Driver 18 for SQL Server : A network-related or instance-specific error has occurred while establishing a connection to 10.0.1.60. Server is not found or not accessible. Check if instance name is correct and if SQL Server is configured to allow remote connections. For more information see SQL Server Books Online..
0x102 is a timeout, so the client never reached SQL Server at all and no login was attempted. The cause is always on the network side, and in our lab it was one of these:
- TCP/IP still disabled on the instance
- the service not restarted after the registry change
- no inbound firewall rule for TCP 1433
- a rule whose
-RemoteAddressexcludes the client
Test the port on its own before you touch credentials, because a password change cannot fix any of them. Install netcat first if the client lacks it (sudo apt-get install -y netcat-openbsd), then probe the port:
nc -zv -w 3 10.0.1.60 1433
A blocked port times out like this, while an open one prints succeeded. If it times out, run the Get-NetTCPConnection check on the server: no 1433 listener means TCP is still off or the service wasn’t restarted, and a listener that’s there means the firewall is dropping you.
nc: connect to 10.0.1.60 port 1433 (tcp) timed out: Operation now in progress
Once the port answers, any remaining error comes from SQL Server itself, which narrows the search to TLS or the login.
Error: “certificate verify failed:self-signed certificate”
Drop -C from the sqlcmd command and ODBC Driver 18 refuses to connect:
Sqlcmd: Error: Microsoft ODBC Driver 18 for SQL Server : SSL Provider: [error:0A000086:SSL routines::certificate verify failed:self-signed certificate].
Sqlcmd: Error: Microsoft ODBC Driver 18 for SQL Server : Client unable to establish connection. For solutions related to encryption errors, see https://go.microsoft.com/fwlink/?linkid=2226722.
Driver 18 encrypts every connection by default and validates the server certificate. SQL Server, though, generates a self-signed certificate at startup when none is configured. -C keeps the encryption and skips the validation: with it, sys.dm_exec_connections reported encrypt_option TRUE, auth_scheme SQL and net_transport TCP for our session. That’s fine on a LAN you control. Across untrusted networks, install a certificate from your own CA on SQL Server so clients can verify it without -C; we did not test that part. The sqlcmd utility reference lists the related encryption switches.
Error: “Login failed for user”
The client usually prints only Login failed for user 'appuser'.. and keeps the reason to itself. The ERRORLOG on the server holds the real cause, and each one we triggered has a different fix:
| Client message | ERRORLOG reason | Fix |
|---|---|---|
| Login failed for user ‘appuser’. | An attempt to login using SQL authentication failed. Server is configured for Integrated authentication only. | Set LoginMode to 2 and restart the service |
| Login failed for user ‘appuser’. | Could not find a login matching the name provided. | Create the login, and check the name for typos |
| Login failed for user ‘appuser’. | Password did not match that for the login provided. | Correct the password, or reset it with ALTER LOGIN |
| Login failed for user ‘sa’. | An error occurred while evaluating the password. | sa is disabled and still has the random password setup gave it. Use a dedicated login, or run ALTER LOGIN sa WITH PASSWORD then ALTER LOGIN sa ENABLE |
| Login failed for user ‘sa’. Reason: The account is disabled. | The account is disabled. | Expected: use a dedicated login instead |
Three rows look identical from the client, which is why guessing wastes time: a Windows-only server, a missing login and a wrong password all print the same line. Read the log first. After a service restart, the entries from before the restart move to ERRORLOG.1 in the same folder, so check that file too if the failure happened before your last restart. With the client returning appdb appuser, mixed mode on, TCP 1433 limited to your LAN and sa disabled, the server is ready for an application connection string that uses the same host, login and database.