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.