Skip to main content
Version: Next

Kerberos / Integrated Authentication

End-user guide for connecting to Active-Directory-joined SQL Server via Kerberos (POSIX) or SSPI (Windows). Spec 042. Mirrors the structure of AZURE.md.

Table of Contents​

Quick Start​

On a POSIX host with a valid Kerberos ticket:

kinit alice@CORP.EXAMPLE.COM # acquire TGT (password prompt)
klist # verify

Then in DuckDB:

LOAD mssql;
ATTACH 'Server=sqlhost.corp.example.com,1433;Database=AdventureWorks;Trusted_Connection=yes;Encrypt=yes;TrustServerCertificate=yes'
AS adw (TYPE mssql);

SELECT TOP 5 name FROM adw.sys.tables;

That's it. The extension picks up your kinit ticket from the credential cache, negotiates SPNEGO with the SQL Server's SPN, and connects.

Prerequisites​

POSIX (Linux, macOS)​

StepLinux (Debian / Ubuntu)Linux (RHEL / Fedora)macOS
Install client toolssudo apt install krb5-user libgssapi-krb5-2sudo dnf install krb5-workstationbuilt into the OS
Build-time dep (only if you build the extension yourself)sudo apt install libkrb5-devsudo dnf install krb5-develbuilt into the OS
Configure /etc/krb5.confyes — point at your AD KDCsyesyes
Acquire ticketkinit user@REALMkinit user@REALMkinit user@REALM
Verifyklistklistklist

The Kerberos runtime is loaded on demand (spec 053 / issue #161). The extension itself loads with no dependency on libgssapi_krb5.so.2 / libkrb5.so.3 — a minimal image without Kerberos installed can LOAD mssql and use SQL or Azure AD authentication fine. The runtime is dlopened only the first time you select authenticator=krb5 (or Trusted_Connection=yes). If it is missing at that point you get a clear error naming the package to install (libgssapi-krb5-2 on Debian/Ubuntu, krb5-libs on RHEL/Fedora), not a load failure. macOS uses the always-present GSS system framework, so nothing extra is needed there.

Minimal /etc/krb5.conf:

[libdefaults]
default_realm = CORP.EXAMPLE.COM
dns_lookup_kdc = true
forwardable = true
rdns = false

[realms]
CORP.EXAMPLE.COM = {
kdc = dc01.corp.example.com:88
kdc = dc02.corp.example.com:88
}

[domain_realm]
.corp.example.com = CORP.EXAMPLE.COM

Windows​

Native Windows SSPI via secur32.dll's Negotiate package. Uses the current Windows logon session — no kinit needed. Same connection-string surface as POSIX:

ATTACH 'Server=sqlhost.corp.example.com,1433;Database=YourDB;Trusted_Connection=yes;Encrypt=yes;TrustServerCertificate=yes'
AS db (TYPE mssql);

Or the explicit form:

ATTACH 'Server=sqlhost.corp.example.com,1433;Database=YourDB;authenticator=winsspi;Encrypt=yes;TrustServerCertificate=yes'
AS db (TYPE mssql);

Trusted_Connection=yes and Integrated Security=SSPI both resolve to authenticator=winsspi on Windows; on POSIX they resolve to authenticator=krb5. The behavior is the same — your machine's existing authentication credentials are used.

Prerequisites:

  • Domain-joined Windows host (or sufficient privileges to acquire a Kerberos TGT for the target realm).
  • SQL Server's SPN registered in AD with the canonical MSSQLSvc/<fqdn>:<port> form. Verify with setspn -L DOMAIN\sqlservice from an admin PowerShell.
  • No build-time dependencies — secur32.lib is part of the Windows SDK.

Limitations:

  • Only credential-cache mode (current logon session). Keytab and raw-credentials modes are POSIX-only.
  • Negotiate falls back to NTLM transparently when the KDC isn't reachable for the target SPN. If you require Kerberos-only authentication, ensure the SPN is registered correctly so the fallback is never triggered.

WSL2 (Ubuntu under Windows)​

WSL2 is a fully supported testing path — it's a real Linux kernel with MIT Kerberos.

# 1. Install Kerberos client + build deps (one-time)
sudo apt update
sudo apt install -y krb5-user libgssapi-krb5-2 libssl3 chrony

# 2. Configure /etc/krb5.conf (same as Linux above)
sudo nano /etc/krb5.conf

# 3. Acquire ticket
kinit alice@CORP.EXAMPLE.COM
klist

# 4. Use the extension
duckdb -c "
INSTALL mssql FROM community;
LOAD mssql;
ATTACH 'Server=sqlhost.corp.example.com;Database=YourDB;Trusted_Connection=yes;Encrypt=yes;TrustServerCertificate=yes' AS p (TYPE mssql);
SELECT TOP 5 name FROM p.sys.tables;
"

WSL2-specific gotchas:

  1. No SSO with Windows logon. WSL2's Linux ccache lives at /tmp/krb5cc_$UID, separate from Windows' credential cache. You must kinit inside WSL2 even if your Windows session already has a TGT.

  2. Clock drift after laptop sleep. WSL2's clock can drift far enough to trigger KRB5KRB_AP_ERR_SKEW. Fix:

    sudo apt install chrony # persistent
    sudo hwclock -s # one-shot sync from BIOS
  3. DNS to corporate DCs. If your DCs aren't resolvable from WSL2 by default, add to /etc/wsl.conf:

    [network]
    generateResolvConf = false

    then write your own /etc/resolv.conf, or hardcode KDCs in krb5.conf.

  4. SPN verification from WSL2. setspn -L is Windows-only, but you can ask the KDC directly:

    kvno -S MSSQLSvc sqlhost.corp.example.com # asks for a service ticket

    If this succeeds, the SPN is registered correctly.

Credential Modes​

Three modes are supported on POSIX. The mode is selected by which connection-string keys (or secret fields) are populated.

Default mode when you have a kinit ticket. The extension uses GSS_C_NO_CREDENTIAL so GSSAPI picks up the ticket from KRB5CCNAME or /tmp/krb5cc_<uid>.

ATTACH 'Server=sqlhost;Database=db;Trusted_Connection=yes' AS k (TYPE mssql);

Works on Linux + macOS.

Provide a keytab file containing the service account's long-term keys. GSSAPI acquires a TGT internally — no kinit needed.

ATTACH 'Server=sqlhost;Database=db;authenticator=krb5;krb5-keytabfile=/etc/keytabs/svc.keytab;User Id=svc@CORP.EXAMPLE.COM'
AS k (TYPE mssql);

Linux only — macOS's bundled GSS framework lacks the required MIT extensions (gss_acquire_cred_from).

3. Raw Credentials (SECRET-ONLY)​

Domain user + password, no kinit required. Available only via CREATE SECRET — Password in a connection string is unconditionally rejected to keep cleartext passwords out of logs.

CREATE SECRET kerb_raw (
TYPE mssql,
host 'sqlhost.corp.example.com',
port 1433,
database 'AdventureWorks',
authenticator 'krb5',
user 'alice',
password 'her-password',
krb5_realm 'CORP.EXAMPLE.COM',
use_encrypt true
);

ATTACH '' AS adw (TYPE mssql, SECRET kerb_raw);

Linux only. macOS lacks krb5_get_init_creds_password in the public SDK.

Connection Examples​

ADO.NET style​

-- Simplest (uses defaults; pyodbc-compatible)
ATTACH 'Server=sqlhost;Database=db;Trusted_Connection=yes' AS k (TYPE mssql);

-- ADO.NET canonical alias
ATTACH 'Server=sqlhost;Database=db;Integrated Security=SSPI' AS k (TYPE mssql);
ATTACH 'Server=sqlhost;Database=db;Integrated Security=true' AS k (TYPE mssql);

-- Explicit go-mssqldb form
ATTACH 'Server=sqlhost;Database=db;authenticator=krb5' AS k (TYPE mssql);

-- Keytab mode (service account)
ATTACH 'Server=sqlhost;Database=db;authenticator=krb5;krb5-keytabfile=/etc/keytabs/svc.keytab;User Id=svc@CORP.EXAMPLE.COM'
AS k (TYPE mssql);

-- Override SPN when DNS doesn't match what's registered in AD
ATTACH 'Server=10.0.0.5;Database=db;Trusted_Connection=yes;service_principal_name=MSSQLSvc/sqlhost.corp.example.com:1433'
AS k (TYPE mssql);

-- Per-connection krb5.conf override (Linux only)
ATTACH 'Server=sqlhost;Database=db;Trusted_Connection=yes;krb5-configfile=/opt/myapp/krb5.conf'
AS k (TYPE mssql);

URI style​

ATTACH 'mssql://sqlhost/db?authenticator=krb5' AS k (TYPE mssql);

ATTACH 'mssql://sqlhost/db?authenticator=krb5&krb5-keytabfile=/etc/keytabs/svc.keytab&krb5-realm=CORP.EXAMPLE.COM'
AS k (TYPE mssql);

MSSQL Secret​

CREATE SECRET kerb (
TYPE mssql,
host 'sqlhost.corp.example.com',
port 1433,
database 'AdventureWorks',
authenticator 'krb5',
use_encrypt true
);

ATTACH '' AS adw (TYPE mssql, SECRET kerb);

service_principal_name override​

When the host you Server= to is an IP address, alias, or doesn't match the registered SPN's hostname:

ATTACH 'Server=sql-internal.local,1433;Database=db;Trusted_Connection=yes;service_principal_name=MSSQLSvc/sqlhost.corp.example.com:1433'
AS k (TYPE mssql);

Accepts either the canonical Kerberos principal form (MSSQLSvc/host:port) or the hostbased-service form (MSSQLSvc@host).

Using MSSQL Secrets​

Use a secret when you want to:

  • Persist Kerberos config across DuckDB sessions
  • Use raw credentials (the only path that accepts a Password)
  • Keep connection strings clean
-- Credential cache (kinit ticket) -- minimal secret
CREATE SECRET kerb_ccache (
TYPE mssql,
host 'sqlhost.corp.example.com',
port 1433,
database 'AdventureWorks',
authenticator 'krb5'
);

-- Keytab for a service account
CREATE SECRET kerb_keytab (
TYPE mssql,
host 'sqlhost.corp.example.com',
port 1433,
database 'AdventureWorks',
authenticator 'krb5',
user 'svc-etl@CORP.EXAMPLE.COM',
krb5_keytabfile '/etc/keytabs/svc-etl.keytab',
use_encrypt true
);

-- Raw credentials -- ONLY available via secret
CREATE SECRET kerb_raw (
TYPE mssql,
host 'sqlhost.corp.example.com',
port 1433,
database 'AdventureWorks',
authenticator 'krb5',
user 'alice',
password 'her-password',
krb5_realm 'CORP.EXAMPLE.COM'
);

-- All Kerberos secret fields:
-- authenticator -- 'krb5' (or 'winsspi' on Windows)
-- krb5_configfile -- /etc/krb5.conf override (Linux only)
-- krb5_keytabfile -- selects keytab mode
-- krb5_credcachefile -- ccache path override (Linux only)
-- krb5_realm -- AD realm (UPPERCASE)
-- service_principal_name -- SPN override

Note the underscore convention in secret fields vs hyphens in connection strings — this matches the project's existing convention (schema_filter in secrets, schema-filter in connection strings).

Diagnostic function: mssql_kerberos_auth_test​

Test the Kerberos auth path without actually connecting to SQL Server. Parallels mssql_azure_auth_test for Azure AD. Useful for confirming that /etc/krb5.conf is reachable from the DuckDB process, the ccache has a valid TGT, the SPN resolves through the KDC, and gss_init_sec_context produces a non-empty SPNEGO blob — none of which kinit + klist alone verify.

Three overloads:

-- 1. Smoke test against a host, default port 1433, default ccache.
-- Derives SPN = MSSQLSvc/<host>:1433.
SELECT mssql_kerberos_auth_test('sqlhost.corp.example.com');

-- 2. Explicit port (named-instance / non-1433 deployments).
-- Derives SPN = MSSQLSvc/<host>:<port>.
SELECT mssql_kerberos_auth_test('sqlhost.corp.example.com', 1433);

-- 3. Against a configured MSSQL secret -- exercises the FULL Krb5Config
-- path (honors keytab, krb5-realm, service_principal_name override,
-- etc.) exactly the way an ATTACH against the same secret would.
SELECT mssql_kerberos_auth_test_secret('my_kerb_secret');

Success output​

One-line status string:

OK: principal=alice@CORP.EXAMPLE.COM, spn=MSSQLSvc/sqlhost.corp.example.com:1433, mech=SPNEGO, token_size=1834 bytes

The values reveal the same things an experienced SRE would check manually:

  • principal=... — what klist reports as the default principal in the ccache (or <no ticket> if the ccache is empty). On macOS this shows <macOS: run klist> because GSS.framework doesn't export the krb5_cc_* extensions; run klist from the shell to see the principal there.
  • spn=... — the exact service principal name the KDC was asked for. Compare to setspn -L DOMAIN\sqlservice on a Windows admin host.
  • mech=SPNEGO — confirms the mechanism is right (SQL Server expects SPNEGO, not raw Kerberos).
  • token_size=... — sanity check that the blob isn't suspiciously small (a 100-byte token usually means SPNEGO couldn't establish the underlying Kerberos mech).

Failure output​

The function returns the verbatim error message — same wording that ATTACH-time validation would produce, including the actionable hint from the GSSAPI status taxonomy. Examples:

-- No kinit yet
MSSQL Kerberos auth failed: gss_acquire_cred: ... No credentials cache file found.
(Hint: no credentials cache. Run 'kinit <user>@<REALM>' first.)

-- SPN missing from AD
MSSQL Kerberos auth failed: gss_init_sec_context: ... Server not found in Kerberos database
(Hint: server SPN not registered. Verify with 'setspn -L <account>' on Windows admin host.)

-- Clock skew
MSSQL Kerberos auth failed: gss_init_sec_context: ... Clock skew too great
(Hint: clock skew between client and KDC exceeds 5 minutes. Sync system clock via ntp/chrony.)

-- Wrong secret type
MSSQL Kerberos auth test: secret 'sql_pwd' is not configured for Kerberos (authenticator != 'krb5').
Add authenticator 'krb5' to the secret.

-- Build without Kerberos support
MSSQL Kerberos auth test: this build of the mssql extension was compiled without
Kerberos support (MSSQL_ENABLE_KRB5 was not defined). Rebuild with -DENABLE_KRB5=ON.

When to use each variant​

GoalFunction
Quick "can I auth at all?" checkmssql_kerberos_auth_test('host')
Verifying a non-default port resolves the right SPNmssql_kerberos_auth_test('host', 12345)
Validating a keytab / SPN override before ATTACHmssql_kerberos_auth_test_secret('secret_name')
Pre-flighting a CI pipeline (service account + keytab)mssql_kerberos_auth_test_secret(...) then ATTACH

Limitations​

  • Tests the client-side GSSAPI flow only. It does NOT confirm that SQL Server itself will accept the ticket (the server-side mapping EXAMPLE.COM\testuser → SQL login needs setspn-style work that's outside the extension's scope).
  • A pass here + ATTACH failure → almost always a server-side login mapping issue (run init.sql-style CREATE LOGIN [REALM\user] FROM WINDOWS on the SQL Server side).
  • A fail here + working kinit → mssql_kerberos_auth_test exposes a bug in your config that kinit didn't catch (wrong SPN, wrong realm resolution, etc.).

Diagnostic function: mssql_winsspi_auth_test (Windows)​

The Windows SSPI peer of mssql_kerberos_auth_test. Same idea: exercises the client-side handshake (AcquireCredentialsHandleW + InitializeSecurityContextW, Negotiate package) without connecting to SQL Server. Useful for confirming the current logon session has a valid ticket, the SPN resolves, and Negotiate produces a non-empty token.

Three overloads:

-- 1. Smoke test against a host, default port 1433.
SELECT mssql_winsspi_auth_test('sqlhost.corp.example.com');

-- 2. Explicit port.
SELECT mssql_winsspi_auth_test('sqlhost.corp.example.com', 1433);

-- 3. Explicit SPN -- override default MSSQLSvc/<host>:<port> derivation.
SELECT mssql_winsspi_auth_test_spn('MSSQLSvc/sqlcluster.corp.example.com:1433');

Success output​

OK: principal=alice@CORP.EXAMPLE.COM, spn=MSSQLSvc/sqlhost.corp.example.com:1433, mech=Negotiate, token_size=1652 bytes

principal here is the current logon session's UPN (via GetUserNameEx(NameUserPrincipal)); falls back to DOMAIN\user if the UPN isn't set on the account. mech=Negotiate is the SSPI package name — Negotiate transparently selects Kerberos when the SPN resolves through AD and falls back to NTLM otherwise, which is why we report it explicitly.

Failure output​

The function returns the verbatim error message from winsspi_authenticator.cpp, wrapping FormatMessageW plus the SSPI status code. Common cases:

-- SPN not registered for this server
MSSQL Kerberos auth failed: InitializeSecurityContext failed:
sspi_status=0x80090303, The specified target is unknown or unreachable.
(Hint: SPN not found in Active Directory. Verify with 'setspn -L domain\sqlsvc'.)

-- No domain credentials in the current session
MSSQL Kerberos auth failed: InitializeSecurityContext failed:
sspi_status=0x8009030E, No credentials are available in the security package.
(Hint: log in as a domain user, or run 'runas /netonly' against domain creds.)

-- Clock skew
MSSQL Kerberos auth failed: InitializeSecurityContext failed:
sspi_status=0x80090324, The clocks on the client and server machines are skewed.
(Hint: sync system clock; typical tolerance is ±5 minutes.)

Function availability​

  • On Windows builds: real implementation, exercises SSPI Negotiate.
  • On Linux / macOS builds: returns a clear "wrong platform — use mssql_kerberos_auth_test on POSIX" message. Registered unconditionally so the function exists on every platform and gives consistent diagnostics rather than "function does not exist".

Troubleshooting​

Common Errors​

Every error from the Kerberos path is prefixed with MSSQL Kerberos auth failed: and includes the verbatim GSSAPI status text plus an actionable hint.

Error containsCauseFix
no credentials cacheNever ran kinit (or kdestroy cleared it)kinit user@REALM
ticket expiredTicket lifetime elapsed (typically 8-24 h)kinit again
Server principal '...' not registered in KDCSPN missing from ADsetspn -L <account> on Windows admin host; ask AD admin to register if missing
Clock skew between client and KDC exceeds 5 minutesSystem clock driftedsudo chronyc tracking / sudo hwclock -s; install chrony for persistence
KDC unreachableDNS / network / firewallVerify /etc/krb5.conf, try kinit -V user@REALM for verbose output
preauthentication failed -- wrong passwordBad password in raw modeCheck secret password
no matching key in keytabKeytab doesn't have the requested principalklist -k /path/to.keytab to inspect; re-export from KDC if stale
'Password' cannot be combined with 'Trusted_Connection' / 'authenticator=krb5'Tried raw credentials via connection stringUse a CREATE SECRET instead
'User Id' with authenticator=krb5 requires either 'krb5-keytabfile' or a Password supplied via MSSQL secretUser Id with no keytab / password / ticket-mode infoDrop User Id (CredCache mode picks it up from kinit), or add a keytab
extension was compiled without Kerberos supportBuilt with -DENABLE_KRB5=OFFRebuild with -DENABLE_KRB5=ON (default), or use SQL auth

Verifying the SPN​

The SQL Server must have an SPN registered in AD with name MSSQLSvc/<fqdn>:<port> (or MSSQLSvc/<fqdn>:<instance> for named instances).

From a Windows admin host:

setspn -L DOMAIN\sqlservice
# Should include: MSSQLSvc/sqlhost.corp.example.com:1433

From any POSIX host (asks the KDC for a service ticket):

kvno -S MSSQLSvc sqlhost.corp.example.com
# kvno = N indicates the SPN is registered and reachable

If the SPN is missing, an AD admin must register it:

setspn -A MSSQLSvc/sqlhost.corp.example.com:1433 DOMAIN\sqlservice

Combined TLS + auth failures​

If the server requires encryption AND auth fails, you'll see both errors. Read them in order:

MSSQL connection validation failed: MSSQL Kerberos auth failed: ...

The TLS handshake (Encrypt=yes) happens before LOGIN7, so a TLS error is reported separately. Use MSSQL_DEBUG=1 to see the PRELOGIN response and TLS handshake details on stderr.

RC4 deprecation (CVE-2026-20833)​

Microsoft is removing the KDC's willingness to issue RC4-encrypted service tickets. This is an Active Directory / KDC behavior change, not a SQL Server or extension change, but it can surface here as a sudden Cannot generate SSPI context or "Server not found in Kerberos database"-style failure on a connection that worked yesterday.

Does it affect this extension? No — not the code. The extension never selects an encryption type. It hands the whole flow to the system GSSAPI / MIT-krb5 (or macOS GSS.framework) libraries via gss_init_sec_context with the SPNEGO mech; the ticket enctype is negotiated between your krb5 library, your /etc/krb5.conf, and the KDC. Modern MIT krb5 and Heimdal already default to AES (AES256/AES128-SHA1), which is exactly what the hardened KDC keeps issuing. So a correctly configured client keeps working unchanged across the rollout.

Rollout timeline (per the CVE guidance):

DatePhaseEffect
2026-01-13AuditKDC logs warning events when RC4 is used; no behavior change
2026-04-14Enforced (revertible)Default flips to AES-SHA1 only; admins can roll back via registry
2026-07 (July)FinalRegistry rollback removed; AES mandatory unless the account explicitly opts back into RC4

Where it bites in practice — all of these are environment/AD fixes, identical for any Kerberos client (pyodbc, JDBC, go-mssqldb, us):

  1. SQL Server service account is RC4-only. If the account backing the SPN has msDS-SupportedEncryptionTypes unset or set to RC4 only, the KDC will stop issuing it tickets after enforcement. Fix on the AD side: set AES on the account (0x18 = AES128+AES256; 0x1C/0x24 keep RC4 alongside AES only if you genuinely still need it).
  2. Keytab generated with RC4 keys. A keytab made with ktpass ... /crypto RC4-HMAC-NT will stop authenticating. Regenerate with /crypto AES256-SHA1 (or /crypto all) and confirm with klist -k /path/to.keytab that AES entries (aes256-cts-hmac-sha1-96) are present. This is the one case the CVE guidance explicitly calls out for third-party / keytab-based clients.
  3. Client pinned to RC4 in krb5.conf. Remove any default_tkt_enctypes/default_tgs_enctypes/permitted_enctypes lines that force rc4-hmac (arcfour-hmac). Leaving them unset lets the library use its AES defaults — which is what you want.

Diagnosing it: mssql_kerberos_auth_test('host') reproduces the client-side GSSAPI flow without touching SQL Server, so it isolates a KDC/enctype failure from a SQL login-mapping failure. On the host, klist -e after a kinit shows the actual enctype of your tickets — if you see arcfour-hmac where you expect aes256-cts-hmac-sha1-96, the account or keytab still needs AES.

Sources:

WSL2 specifics​

See the WSL2 prerequisites section above. The most common trip-ups on WSL2 are clock skew (after laptop sleep) and missing ticket (because WSL2's ccache is separate from Windows logon).

Running the test stack locally​

The repo ships a self-contained Kerberos test stack at test/kerberos/ — a docker-compose with a KDC, a SQL Server, and a test client. No real Active Directory required.

cd test/kerberos
docker compose up -d --build # multi-stage build, ~5 min first time
docker compose exec test-client /run-tests.sh
docker compose down -v # -v clears the keytabs volume

The stack provides:

  • kdc — MIT Kerberos KDC, realm EXAMPLE.COM. Test principal testuser@EXAMPLE.COM (password testpass), service principal MSSQLSvc/sql.example.com:1433.
  • sql — SQL Server 2022 joined-equivalent via the shared service keytab. init.sql creates TestDB and the EXAMPLE.COM\testuser login mapping.
  • test-client — Ubuntu + krb5-user + the built mssql.duckdb_extension (built in a multi-stage Linux container so the stack works on macOS hosts).

For an interactive session inside the test client:

docker compose exec test-client bash

# Inside the container:
kinit testuser@EXAMPLE.COM # password: testpass
klist
duckdb --unsigned
> LOAD '/home/tester/mssql.duckdb_extension';
> ATTACH 'Server=sql.example.com,1433;Database=TestDB;Trusted_Connection=yes;Encrypt=yes;TrustServerCertificate=yes' AS k (TYPE mssql);
> SELECT * FROM k.dbo.test;

See test/kerberos/README.md for the full layout and troubleshooting.

Running the Windows SSPI test suite​

The POSIX test/kerberos/ docker-compose stack has no Windows analog — SSPI requires a real Windows logon session against a real KDC, which a container can't provide. The Windows test surface is a sqllogictest file that runs manually on a domain-joined Windows host:

Suite: test/sql/integrated_auth/winsspi_basic.test — four cases covering Trusted_Connection=yes ATTACH, catalog SELECT, raw mssql_scan, and the explicit authenticator=winsspi form.

Gated on three env vars so it's a no-op in normal CI:

Env varPurpose
MSSQL_WINSSPI_TEST=1Opt-in switch
MSSQL_TEST_HOSTSQL Server hostname (FQDN)
MSSQL_TEST_DBDatabase to ATTACH (any DB you can read works)

Run via the helper script (recommended):

.\scripts\ci\winsspi_test.ps1 -SqlHost sqlhost.corp.example.com -Database master

The script verifies klist shows a TGT, sets the env vars, and runs unittest.exe against the suite. The unittest.exe either comes from a local make build (build\release\test\unittest.exe) or from the unittest-windows_amd64 artifact of a green CI run.

Or directly:

$env:MSSQL_WINSSPI_TEST = "1"
$env:MSSQL_TEST_HOST = "sqlhost.corp.example.com"
$env:MSSQL_TEST_DB = "master"

.\build\release\test\unittest.exe test\sql\integrated_auth\winsspi_basic.test

Pre-flight with the diagnostic function first. Cheaper to debug a SPN or credential issue via mssql_winsspi_auth_test than via a failing sqllogictest:

duckdb.exe -c "
INSTALL mssql FROM community;
LOAD mssql;
SELECT mssql_winsspi_auth_test('sqlhost.corp.example.com');
"

If that returns OK: ..., the SSPI handshake works and any sqllogictest failure is on the SQL Server side (login mapping, permissions). If it returns an error, fix the client-side problem first.

Why this isn't automated in CI. GitHub-hosted Windows runners aren't domain-joined and can't be made to be. A self-hosted Windows runner joined to an Entra Domain Services managed domain would work but adds ~$140/mo Azure cost and a maintenance surface that only pays off once there's a second Windows contributor. Until then: maintainer manual smoke test before merging Windows-touching PRs.

Reference​

Supported credential modes​

ModeTriggerPOSIX LinuxPOSIX macOSNotes
CredCache(default — no keytab, no password)yesyesUses kinit ticket from KRB5CCNAME
Keytabkrb5-keytabfile=/path + User Id=svc@REALMyesrejected at ATTACHService-account / CI pattern
Rawsecret with user + password + krb5_realmyesrejected at ATTACHSecret-only — Password in connection string is rejected

Connection-string keys​

Names verbatim from microsoft/go-mssqldb's integratedauth/ package.

KeyAliasesPurpose
authenticator—krb5 (POSIX) or winsspi (Windows; pending)
Trusted_ConnectionTrusted Connection, TrustedConnectionpyodbc alias — yes/true/SSPI/1 resolves to platform default
Integrated SecurityIntegratedSecurity, Integrated_SecurityADO.NET alias — same resolution
krb5-configfilekrb5_configfile/etc/krb5.conf override (Linux only)
krb5-keytabfilekrb5_keytabfileSelects keytab mode
krb5-credcachefilekrb5_credcachefileccache path override (Linux only)
krb5-realmkrb5_realmAD realm (UPPERCASE)
service_principal_nameservice-principal-name, serviceprincipalnameOverride default SPN derivation

Platform matrix​

PlatformCredCacheKeytabRawStatus
Linux x86_64 / ARM64yesyesyes (secret only)Phase 3 shipped
macOS ARM64yesrejectedrejectedPhase 3 shipped
Windows x64yes (logon session)n/a (use logon session)n/a (use logon session)Phase 4 shipped
WSL2 Ubuntuyesyesyes (secret only)Same as Linux

Security notes​

  • Cleartext passwords are never accepted from a connection string when Kerberos is in use. Use CREATE SECRET for raw-credentials mode.
  • Per-connection overrides (krb5-configfile, krb5-credcachefile) are applied through gss_acquire_cred_from cred_store elements, NOT through setenv(). Thread-safe and per-instance.
  • The raw-mode MEMORY ccache is destroyed after gss_acquire_cred_from has its own internal copy, so cleartext credentials don't linger in the process-global MIT ccache registry.
  • Kerberos itself provides mutual authentication: the server identity is cryptographically verified by the KDC. TrustServerCertificate=yes only affects TLS certificate validation — it does not weaken Kerberos.

Operational security checklist​

ItemWhyHow to verify
Credential cache file is 0600The TGT in /tmp/krb5cc_<uid> is a bearer credential for your AD principal — anyone who can read the file can impersonate you for the TGT's lifetime.ls -la /tmp/krb5cc_$UID (MIT default is 0600; verify after any custom KRB5CCNAME config). For DIR: / KEYRING:persistent: ccache types, check the equivalent ACL.
Keytab files are 0600 and owned by the service accountA keytab is the password equivalent — anyone who can read it can impersonate the principal until the keys are rotated.find / -name '*.keytab' 2>/dev/null -exec ls -la {} \;
KRB5_TRACE is not set in productionMIT Kerberos writes ticket-acquisition trace (principal names, key versions, ticket flags) to the path in KRB5_TRACE. Useful for debugging, dangerous if forgotten.env | grep KRB5_TRACE — should be empty
MSSQL_DEBUG is unset (or 0) in productionOur extension logs auth flow timing, SPN, host, and pool factory errors to stderr at level 1–3. Sizes and metadata only (no token contents — verified by audit), but reveals operational signal.env | grep MSSQL_DEBUG — should be empty
Coredumps disabled for the DuckDB processIf the process crashes, the coredump contains in-memory cleartext tokens, ccache contents, and TGS session keys.ulimit -c 0 before launching DuckDB, or prlimit --core=0 -p <pid> on a running process. For systemd-managed services: LimitCORE=0 in the unit file.
Connection strings with passwords don't reach shell historyduckdb -c "ATTACH '...;Password=...' ..." writes the password to ~/.bash_history and exposes it in ps auxw to other users for the duration of the process.Use CREATE SECRET + ATTACH '' AS db (TYPE mssql, SECRET <name>) instead, or wrap the ATTACH in a heredoc (duckdb <<'EOF'\n…\nEOF). For Kerberos this is moot — Trusted_Connection=yes carries no secret.
duckdb_secrets() does not expose the passwordThe password field is registered as a redacted secret key in DuckDB's SecretManager and shows as *** instead of the cleartext.SELECT name, secret_string FROM duckdb_secrets() WHERE type='mssql';

See Also​