powershelldba.de

go-sqlcmd and "ANONYMOUS LOGON": Why sqlcmd Stopped Using Windows Auth

A scheduled job that has called sqlcmd -S SERVER -q "..." unchanged for years suddenly fails with Login failed for user 'NT AUTHORITY\ANONYMOUS LOGON', right after SQL Server Tools or Azure Data Studio got updated. Nothing changed on the SQL Server side. Nothing changed in the script. sqlcmd itself changed underneath it.

The Symptom

A call that used to just work:

sqlcmd -S SQL01 -q "SELECT @@VERSION"

...now fails with an anonymous-logon error, even though the account running it is the same domain user that always worked, with the same rights on the same instance. Re-running it with an explicit domain login makes no difference. The account was never the problem.

What Actually Changed

Microsoft replaced the classic ODBC-based sqlcmd.exe with a full rewrite in Go ("go-sqlcmd"). It ships automatically with newer SQL Server Tools installs and with Azure Data Studio. Installing either one silently puts the new binary ahead of the old one on PATH.

Aspect Old sqlcmd (ODBC) New go-sqlcmd
Call with no auth parameters Falls back to Windows Authentication (SSPI) automatically Sends no credentials, connects as NT AUTHORITY\ANONYMOUS LOGON
Result Connects with the current Windows identity SQL Server rejects the anonymous connection
Fix Not needed Pass -E explicitly to request Windows Authentication

The old binary's implicit SSPI/NTLM fallback is simply gone in the rewrite. It isn't a bug in the SQL Server engine, a permission change, or a Kerberos regression; it's a behavior change in the client tool itself, and it hits hardest exactly where nobody is watching: unattended jobs, scheduled tasks, and internal tooling that call bare sqlcmd without ever having needed an auth flag before.

Don't confuse this with a Kerberos/SPN problem. NT AUTHORITY\ANONYMOUS LOGON looks exactly like what you'd see if a Service Principal Name were missing, but it isn't the same failure. The quick way to tell them apart: run the exact same call once with -E added. If -E makes it work immediately, it's this client-side regression, not a directory-level SPN gap. Real background on the SPN side of Windows Authentication is in Kerberos and SPN: Windows Authentication Deep Dive, including Get-sqmSPNReport for checking whether SPNs are actually the issue.

The One-Line Fix (and Why It Doesn't Scale)

Adding -E to any bare call resolves it immediately:

sqlcmd -S SQL01 -E -q "SELECT @@VERSION"

That's the whole fix, technically. The problem is where that call happens: a scheduling tool, a deployment pipeline, dozens of ad-hoc scripts written over years, all calling plain sqlcmd because that always worked. Editing every call site is a real project, and every call site you miss keeps failing the same way after the next SQL Server Tools or Azure Data Studio update.

A Transparent Wrapper Instead

Rather than touching every caller, a small wrapper can sit in front of the real sqlcmd.exe on the system PATH: it inspects the arguments it was called with, adds -E only when nothing else already authenticates the call, and passes everything through untouched otherwise. Callers keep invoking sqlcmd exactly as before; they never know the wrapper is there.

Two files make up the wrapper:

@echo off
REM Transparent wrapper for go-sqlcmd. Adds -E (Windows Auth) when needed.
setlocal enabledelayedexpansion
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0sqlcmd_wrapper.ps1" %*
exit /b !ERRORLEVEL!

The decision itself is a short check: does the call already carry a SQL login (-U) or any explicit auth flag (-E, -A, -N, -P)? If yes, pass everything through unchanged: the caller knew what it wanted. If neither is present, insert -E:

function Add-WindowsAuthIfNeeded {
    if ($hasUserLogin)  { return @($Arguments) }   # -U present: leave alone
    if ($hasAuthFlag)   { return @($Arguments) }   # -E/-A/-N/-P present: leave alone
    return @("-E") + @($Arguments)                 # neither: add Windows Auth
}

The trickier part is finding the real sqlcmd.exe without the wrapper calling itself in a loop. It checks, in order: the system PATH (skipping its own install folder), a list of known install locations for SQL Server Tools 2016โ€“2022 and Azure Data Studio, and finally where.exe as a last resort filtered to exclude the wrapper's own directory.

Installing It

๐Ÿ“ฆ Download the ready-to-run installer

A small ZIP with the wrapper plus a one-click installer that does the whole setup below automatically, including the PATH change:

โฌ‡ Download sqlcmd-wrapper-setup.zip

~6 KB ยท sqlcmd.bat, sqlcmd_wrapper.ps1, Install-/Uninstall-SqlcmdWrapper (.bat + .ps1), README.txt: plain-text scripts, nothing compiled or obfuscated.

Extract it anywhere and double-click Install-SqlcmdWrapper.bat. It asks for the one Administrator (UAC) prompt it needs (writing to the machine-wide PATH requires elevation), then copies the two wrapper files to C:\Tools\sqlcmd-wrapper and prepends that folder to the system PATH (not the user PATH). Open a new terminal afterward (existing shells keep the old PATH cached) and verify:

where sqlcmd
sqlcmd -S SERVERNAME -q "SELECT @@VERSION"

The first line from where sqlcmd must now be C:\Tools\sqlcmd-wrapper\sqlcmd.bat, and the query should succeed with Windows Authentication, no flag needed. To remove everything again, double-click Uninstall-SqlcmdWrapper.bat from the same ZIP. It reverses both steps and deletes the folder.

Read before running any downloaded installer, including this one. The elevation request is a standard Windows UAC prompt, not something the script bypasses; you can decline it and nothing changes. Both .ps1 files are plain, readable PowerShell (no compiled binary, no obfuscation); open them in a text editor first if you want to see exactly what runs before you approve the prompt.

Doing it by hand instead

The installer only automates these five steps, worth knowing if you'd rather run them yourself, or need a different target folder:

๐Ÿ”ง Five steps, fully reversible:
  1. Create C:\Tools\sqlcmd-wrapper and copy sqlcmd.bat + sqlcmd_wrapper.ps1 into it (both files must be in the same folder).
  2. Prepend that folder to the system PATH (not the user PATH): it has to come before the real sqlcmd.exe's location.
  3. Open a new terminal. Existing shells keep the old PATH cached and won't see the change.
  4. Verify with where sqlcmd: the first line returned must be C:\Tools\sqlcmd-wrapper\sqlcmd.bat.
  5. Test: sqlcmd -S SERVERNAME -q "SELECT @@VERSION" should now succeed with Windows Authentication, no flag needed.

To remove it by hand, drop the folder from the system PATH, open a new terminal, and the real sqlcmd.exe is called directly again: nothing else was ever changed.

Debugging a Bad Install

If it's still not behaving, turn on the wrapper's own verbose logging before touching anything else:

$env:SQLCMD_WRAPPER_DEBUG = "1"
sqlcmd -S SERVERNAME -q "SELECT @@VERSION"

This prints which real sqlcmd.exe the wrapper resolved to and whether it decided to insert -E, almost always enough to spot a stale PATH entry or a terminal that was never reopened after the install.

Best Practices

The Bottom Line

This isn't a SQL Server problem, and it isn't a Kerberos problem; it's a client tool that quietly dropped a fallback it used to have, surfacing as an access-denied error that looks exactly like a permissions issue. Adding -E fixes any single call instantly; a transparent wrapper on PATH fixes every call that would otherwise need it, without opening up every script that calls sqlcmd to make the same one-character edit.