- 16 minutes to read

NTLM vs Kerberos Authentication

:When Nodinite Web Client and Web API are accessed using Windows Authentication, the browser must use Kerberos for good performance. If the browser silently falls back to NTLM, every HTTP request triggers hundreds of LSA SID lookup calls against the Domain Controller, causing 4+ second response times and DC flooding.

Important

Typical symptom: curl is fast, browsers and Swagger UI are extremely slow (4-30 seconds per page). Adding the site to the Local Intranet Zone immediately fixes the problem. This confirms NTLM fallback as the root cause.

Root Cause

Why NTLM Falls Back from Kerberos

Client Default Authentication Behaviour
curl Negotiate → Kerberos ✅ Fast — single Kerberos ticket exchange
Microsoft Edge / Chrome Kerberos only for Intranet Zone sites ❌ Falls back to NTLM for Internet Zone sites
PowerShell Invoke-WebRequest Negotiate / NTLM ❌ May use NTLM if Kerberos ticket unavailable
ASP.NET Core (Server) Supports both via SPNEGO Correctly handles both — not the problem

Modern browsers only send Kerberos tickets to sites in the Local Intranet Zone. If the Nodinite URL is not in that zone, the browser falls back to NTLM, causing the LSA flood.

Why NTLM Causes LSA Floods

Each NTLM-authenticated HTTP request requires the server to resolve the user's full Windows security token:

  1. Browser sends NTLM challenge/response (3+ round-trips per request)
  2. IIS worker (w3wp.exe) receives the authenticated identity
  3. Nodinite authorisation code resolves group names: WindowsIdentity.Groups.Select(s => s.Translate(typeof(NTAccount)))
  4. Each Translate() call triggers a LookupSids request to the Domain Controller
  5. A user in a large Active Directory may have 50-200+ group SIDs in their token
  6. This means 50-200+ DC round-trips per page load

Evidence in Windows Security Logs

When the LSA flood is occurring, the Windows Security log and LSA diagnostic log show entries like:

[ 2/20 22:14:00] LspDsLookup - LookupSids request for 1 SIDs ...
    Sids[ 0 ] = S-1-5-21-2138058708-701738657-3842216330-3208
    Requestor details: Local Machine, Process ID = 13328,
                       Process Name = C:\Windows\System32\inetsrv\w3wp.exe

The w3wp.exe process resolving individual SIDs one by one, repeatedly, within the same second.

Why Kerberos Does Not Have This Problem

With Kerberos:

  • Authentication uses a service ticket from the KDC — single round-trip, cached by the client
  • Subsequent requests reuse the cached ticket with no re-authentication overhead
  • Group SID resolution still occurs, but Kerberos connections are persistent and overall auth overhead is minimal

Diagnostic Script

Use the following PowerShell 7 script to identify whether the performance issue is caused by NTLM fallback. Save as kerberoscheck.ps1 and run from the client workstation experiencing slow performance (not the server).

#Requires -Version 7.0
<#
.SYNOPSIS
    Kerberos vs NTLM Authentication Diagnostic Script for Nodinite Web API / Web Client.

.DESCRIPTION
    This script verifies whether a target Nodinite endpoint is using Kerberos or NTLM
    authentication. NTLM fallback causes severe performance degradation due to LSA SID
    lookup floods against the Domain Controller on every HTTP request.

.PARAMETER Url
    The URL of the Nodinite Web API or Web Client endpoint to test.
    Example: https://nodinite.yourdomain.com/api/isalive

.PARAMETER CheckSPN
    If set, checks SPN registration for the target host.

.EXAMPLE
    .\kerberoscheck.ps1 -Url "https://nodinite.yourdomain.com/api/isalive"

.EXAMPLE
    .\kerberoscheck.ps1 -Url "https://nodinite.yourdomain.com/api/isalive" -CheckSPN
#>
[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [string]$Url,
    [switch]$CheckSPN
)

$ErrorActionPreference = 'Stop'

Write-Host "============================================================" -ForegroundColor Cyan
Write-Host " Nodinite Kerberos / NTLM Authentication Diagnostic Tool" -ForegroundColor Cyan
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host ""

# 1. Parse the target host from the URL
$uri = [System.Uri]::new($Url)
$targetHost = $uri.Host
$targetPort = $uri.Port
$scheme = $uri.Scheme

Write-Host "[INFO] Target Host : $targetHost" -ForegroundColor Yellow
Write-Host "[INFO] Target Port : $targetPort" -ForegroundColor Yellow
Write-Host "[INFO] Scheme      : $scheme" -ForegroundColor Yellow
Write-Host ""

# 2. Check current user and domain info
Write-Host "--- Current User & Domain Info ---" -ForegroundColor Green
Write-Host "  Username       : $($env:USERNAME)"
Write-Host "  Domain         : $($env:USERDOMAIN)"
Write-Host "  Computer       : $($env:COMPUTERNAME)"
Write-Host "  DNS Domain     : $($env:USERDNSDOMAIN)"
Write-Host ""

# 3. Check DNS resolution
Write-Host "--- DNS Resolution ---" -ForegroundColor Green
try {
    $dns = Resolve-DnsName -Name $targetHost -ErrorAction Stop
    foreach ($record in $dns) {
        Write-Host "  $($record.Name) -> $($record.IPAddress) ($($record.QueryType))"
    }
}
catch {
    Write-Host "  [ERROR] DNS resolution failed for $targetHost : $_" -ForegroundColor Red
}
Write-Host ""

# 4. Check Kerberos ticket cache (klist)
Write-Host "--- Kerberos Ticket Cache (klist) ---" -ForegroundColor Green
try {
    $klistOutput = & klist 2>&1
    $httpTickets = $klistOutput | Select-String -Pattern "HTTP/" -Context 2, 2
    if ($httpTickets) {
        Write-Host "  [OK] Found HTTP service tickets:" -ForegroundColor Green
        foreach ($ticket in $httpTickets) {
            Write-Host "    $($ticket.Line.Trim())"
        }
    }
    else {
        Write-Host "  [WARN] No HTTP service tickets found in the cache." -ForegroundColor Yellow
        Write-Host "  This may indicate Kerberos is not being used for HTTP services." -ForegroundColor Yellow
        Write-Host ""
        Write-Host "  Full klist output:" -ForegroundColor Yellow
        $klistOutput | ForEach-Object { Write-Host "    $_" }
    }
}
catch {
    Write-Host "  [ERROR] Could not run klist: $_" -ForegroundColor Red
}
Write-Host ""

# 5. Check SPN registration (optional)
if ($CheckSPN) {
    Write-Host "--- SPN Registration Check ---" -ForegroundColor Green
    try {
        $spnOutput = & setspn -Q "HTTP/$targetHost" 2>&1
        Write-Host "  setspn -Q HTTP/$targetHost :" -ForegroundColor Yellow
        $spnOutput | ForEach-Object { Write-Host "    $_" }
        Write-Host ""
        if ($targetHost -notmatch '\.') {
            $fqdn = "$targetHost.$($env:USERDNSDOMAIN)"
            Write-Host "  setspn -Q HTTP/$fqdn :" -ForegroundColor Yellow
            $spnFqdnOutput = & setspn -Q "HTTP/$fqdn" 2>&1
            $spnFqdnOutput | ForEach-Object { Write-Host "    $_" }
        }
    }
    catch {
        Write-Host "  [ERROR] Could not run setspn: $_" -ForegroundColor Red
    }
    Write-Host ""
}

# 6. Test HTTP call with Negotiate auth and detect protocol
Write-Host "--- HTTP Authentication Test ---" -ForegroundColor Green
Write-Host "  Testing: $Url" -ForegroundColor Yellow
Write-Host ""

try {
    # Step 1: Check server authentication challenge (no credentials)
    Write-Host "  Step 1: Checking server authentication challenge (no credentials)..." -ForegroundColor Yellow
    try {
        $challengeResponse = Invoke-WebRequest -Uri $Url -Method Get -UseBasicParsing `
            -ErrorAction SilentlyContinue -SkipHttpErrorCheck
        if ($challengeResponse.StatusCode -eq 401) {
            $wwwAuth = $challengeResponse.Headers['WWW-Authenticate']
            Write-Host "    Server WWW-Authenticate: $wwwAuth"
            if ($wwwAuth -match 'Negotiate') { Write-Host "    [OK] Server supports Negotiate (Kerberos preferred)" -ForegroundColor Green }
            if ($wwwAuth -match 'NTLM') { Write-Host "    [WARN] Server also advertises NTLM - fallback is possible" -ForegroundColor Yellow }
        }
        else {
            Write-Host "    Server returned $($challengeResponse.StatusCode) without requiring auth" -ForegroundColor Yellow
        }
    }
    catch { Write-Host "    Could not get challenge response: $_" -ForegroundColor Yellow }
    Write-Host ""

    # Step 2: Test with Default Credentials (Negotiate)
    Write-Host "  Step 2: Testing with Default Credentials (Negotiate)..." -ForegroundColor Yellow
    $handler = [System.Net.Http.HttpClientHandler]::new()
    $handler.UseDefaultCredentials = $true
    $handler.PreAuthenticate = $true
    $handler.ServerCertificateCustomValidationCallback = [System.Net.Http.HttpClientHandler]::DangerousAcceptAnyServerCertificateValidator
    $client = [System.Net.Http.HttpClient]::new($handler)
    $client.Timeout = [TimeSpan]::FromSeconds(30)

    $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
    $response = $client.GetAsync($Url).GetAwaiter().GetResult()
    $stopwatch.Stop()

    Write-Host "    Status Code    : $($response.StatusCode)" -ForegroundColor $(if ($response.IsSuccessStatusCode) { 'Green' } else { 'Red' })
    Write-Host "    Response Time  : $($stopwatch.ElapsedMilliseconds) ms"
    $client.Dispose(); $handler.Dispose()
    Write-Host ""

    # Step 3: Check for Kerberos ticket after request
    Write-Host "  Step 3: Checking for Kerberos ticket after request..." -ForegroundColor Yellow
    $klistAfter = & klist 2>&1
    $httpTicketsAfter = $klistAfter | Select-String -Pattern "HTTP/$targetHost" -Context 1, 1
    if ($httpTicketsAfter) {
        Write-Host "    [OK] Kerberos ticket found for HTTP/$targetHost — KERBEROS IS BEING USED" -ForegroundColor Green
        foreach ($ticket in $httpTicketsAfter) { Write-Host "      $($ticket.Line.Trim())" }
    }
    else {
        Write-Host "    [CRITICAL] No Kerberos ticket for HTTP/$targetHost" -ForegroundColor Red
        Write-Host "    Authentication likely fell back to NTLM!" -ForegroundColor Red
        Write-Host "    This causes LSA SID lookup floods on every request," -ForegroundColor Red
        Write-Host "    resulting in 4+ second response times." -ForegroundColor Red
    }
}
catch {
    Write-Host "  [ERROR] HTTP test failed: $_" -ForegroundColor Red
}
Write-Host ""

# 7. Check Browser Intranet Zone settings
Write-Host "--- Browser Intranet Zone Check ---" -ForegroundColor Green
Write-Host "  Checking if $targetHost is in the Local Intranet Zone..." -ForegroundColor Yellow

$intranetZoneKeys = @(
    "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\Domains\$targetHost",
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\Domains\$targetHost",
    "HKCU:\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\Domains\$targetHost",
    "HKLM:\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\Domains\$targetHost"
)

$hostParts = $targetHost.Split('.')
if ($hostParts.Length -ge 2) {
    $domain = ($hostParts | Select-Object -Skip 1) -join '.'
    $intranetZoneKeys += @(
        "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\Domains\$domain",
        "HKLM:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\Domains\$domain",
        "HKCU:\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\Domains\$domain",
        "HKLM:\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\Domains\$domain"
    )
}

$foundInZone = $false
foreach ($key in $intranetZoneKeys) {
    if (Test-Path $key) {
        $props = Get-ItemProperty -Path $key -ErrorAction SilentlyContinue
        Write-Host "    Found zone entry: $key" -ForegroundColor Yellow
        $props.PSObject.Properties | Where-Object { $_.Name -notlike 'PS*' } | ForEach-Object {
            $zoneName = switch ($_.Value) { 1 { "Intranet" } 2 { "Trusted" } 3 { "Internet" } 4 { "Restricted" } default { "Unknown" } }
            Write-Host "      $($_.Name) = $($_.Value) ($zoneName)"
            if ($_.Value -eq 1) { $foundInZone = $true }
        }
    }
}

$autoDetectKey = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap"
if (Test-Path $autoDetectKey) {
    $autoDetect = Get-ItemProperty -Path $autoDetectKey -ErrorAction SilentlyContinue
    Write-Host "    Auto-detect intranet: IntranetName=$($autoDetect.IntranetName), ProxyOverride=$($autoDetect.ProxyOverride)"
}

if (-not $foundInZone) {
    Write-Host "    [WARN] $targetHost is NOT explicitly in the Local Intranet Zone." -ForegroundColor Yellow
    Write-Host "    Edge/Chrome will use NTLM unless the site is in the Intranet Zone." -ForegroundColor Yellow
    Write-Host ""
    Write-Host "    REMEDIATION:" -ForegroundColor Cyan
    Write-Host "    Option A: Group Policy > Site to Zone Assignment List" -ForegroundColor Cyan
    Write-Host "      Add: $($uri.Scheme)://$targetHost = 1 (Intranet)" -ForegroundColor Cyan
    Write-Host "    Option B: Registry: HKLM:\Software\...\ZoneMap\Domains\$domain" -ForegroundColor Cyan
    Write-Host "    Option C: Manual: Internet Options > Security > Local Intranet > Sites > Advanced" -ForegroundColor Cyan
}
else {
    Write-Host "    [OK] $targetHost is in the Local Intranet Zone." -ForegroundColor Green
}
Write-Host ""

# 8. Check Edge/Chrome auth policies
Write-Host "--- Edge/Chrome Authentication Policies ---" -ForegroundColor Green
$edgePolicies = @(
    @{ Path = "HKLM:\SOFTWARE\Policies\Microsoft\Edge"; Name = "AuthServerAllowlist" },
    @{ Path = "HKLM:\SOFTWARE\Policies\Microsoft\Edge"; Name = "AuthNegotiateDelegateAllowlist" },
    @{ Path = "HKLM:\SOFTWARE\Policies\Microsoft\Edge"; Name = "AuthSchemes" },
    @{ Path = "HKLM:\SOFTWARE\Policies\Google\Chrome"; Name = "AuthServerAllowlist" },
    @{ Path = "HKLM:\SOFTWARE\Policies\Google\Chrome"; Name = "AuthNegotiateDelegateAllowlist" },
    @{ Path = "HKLM:\SOFTWARE\Policies\Google\Chrome"; Name = "AuthSchemes" }
)
foreach ($policy in $edgePolicies) {
    if (Test-Path $policy.Path) {
        $val = (Get-ItemProperty -Path $policy.Path -Name $policy.Name -ErrorAction SilentlyContinue).$($policy.Name)
        if ($val) { Write-Host "  $($policy.Path)\$($policy.Name) = $val" -ForegroundColor Yellow }
    }
}
$authServerAllowlist = $null
if (Test-Path "HKLM:\SOFTWARE\Policies\Microsoft\Edge") {
    $authServerAllowlist = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Edge" `
        -Name "AuthServerAllowlist" -ErrorAction SilentlyContinue).AuthServerAllowlist
}
if (-not $authServerAllowlist) {
    Write-Host "  [INFO] Edge AuthServerAllowlist policy is not configured." -ForegroundColor Yellow
    Write-Host "  Consider setting it to '*.$($env:USERDNSDOMAIN)' to enable Negotiate/Kerberos." -ForegroundColor Yellow
}
Write-Host ""

# 9. Performance test (5 sequential requests)
Write-Host "--- Performance Test (5 sequential requests) ---" -ForegroundColor Green
$handler2 = [System.Net.Http.HttpClientHandler]::new()
$handler2.UseDefaultCredentials = $true
$handler2.PreAuthenticate = $true
$handler2.ServerCertificateCustomValidationCallback = [System.Net.Http.HttpClientHandler]::DangerousAcceptAnyServerCertificateValidator
$client2 = [System.Net.Http.HttpClient]::new($handler2)
$client2.Timeout = [TimeSpan]::FromSeconds(60)

$times = @()
for ($i = 1; $i -le 5; $i++) {
    try {
        $sw = [System.Diagnostics.Stopwatch]::StartNew()
        $resp = $client2.GetAsync($Url).GetAwaiter().GetResult()
        $sw.Stop()
        $times += $sw.ElapsedMilliseconds
        Write-Host "  Request $i : $($sw.ElapsedMilliseconds) ms (Status: $($resp.StatusCode))"
    }
    catch { Write-Host "  Request $i : FAILED - $_" -ForegroundColor Red }
}
$client2.Dispose(); $handler2.Dispose()

if ($times.Count -gt 0) {
    $avg = ($times | Measure-Object -Average).Average
    $max = ($times | Measure-Object -Maximum).Maximum
    Write-Host ""
    Write-Host "  Average: $([math]::Round($avg, 0)) ms, Max: $max ms"
    if ($avg -gt 2000) { Write-Host "  [CRITICAL] Average > 2s — likely NTLM with LSA flood." -ForegroundColor Red }
    elseif ($avg -gt 500) { Write-Host "  [WARN] Average > 500ms — may indicate auth overhead." -ForegroundColor Yellow }
    else { Write-Host "  [OK] Response times look healthy." -ForegroundColor Green }
}
Write-Host ""

# 10. Summary and recommendations
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host " SUMMARY & RECOMMENDATIONS" -ForegroundColor Cyan
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "If authentication is falling back to NTLM:" -ForegroundColor Yellow
Write-Host ""
Write-Host "1. Add the Nodinite URL to the Local Intranet Zone (immediate fix):" -ForegroundColor White
Write-Host "   Group Policy > Site to Zone Assignment List" -ForegroundColor White
Write-Host "   Value: $($uri.Scheme)://$targetHost = 1" -ForegroundColor White
Write-Host ""
Write-Host "2. Verify SPNs are registered for the IIS service account:" -ForegroundColor White
Write-Host "   setspn -S HTTP/$targetHost <ServiceAccountName>" -ForegroundColor White
Write-Host "   setspn -S HTTP/$($targetHost):$targetPort <ServiceAccountName>" -ForegroundColor White
Write-Host ""
Write-Host "3. For Edge, configure AuthServerAllowlist policy:" -ForegroundColor White
Write-Host "   HKLM\SOFTWARE\Policies\Microsoft\Edge\AuthServerAllowlist = '*.$($env:USERDNSDOMAIN)'" -ForegroundColor White
Write-Host ""
Write-Host "4. Ensure Kerberos delegation is configured if Web API impersonates" -ForegroundColor White
Write-Host "   users to backend services (SQL Server, etc.)." -ForegroundColor White
Write-Host ""
Write-Host "5. Consider upgrading to OpenID Connect / SSO to eliminate" -ForegroundColor White
Write-Host "   Windows Authentication overhead entirely." -ForegroundColor White
Write-Host "============================================================" -ForegroundColor Cyan

Script Usage

# Basic check - run from the workstation experiencing slow performance
.\kerberoscheck.ps1 -Url "https://nodinite.yourdomain.com/api/isalive"

# Include SPN registration verification (requires domain tools)
.\kerberoscheck.ps1 -Url "https://nodinite.yourdomain.com/api/isalive" -CheckSPN

What the Script Checks

Check Purpose
DNS resolution Verifies the hostname resolves correctly
klist ticket cache Checks if Kerberos tickets exist for the target
SPN registration Verifies HTTP/<hostname> SPN is registered (with -CheckSPN)
Server challenge Confirms server advertises Negotiate
HTTP auth test Makes a real HTTP call and detects auth protocol used
Intranet Zone registry Confirms site is in any of the four zone map registry locations
Edge/Chrome auth policies Checks AuthServerAllowlist and AuthSchemes
Performance test Measures 5 sequential requests to quantify degradation

Diagnose First: Two Independent Gates

Kerberos requires both gates to be open. The diagnostic shortcut is:

Test Result Meaning
curl --negotiate -u : https://nodinite.yourdomain.com/api/isalive is fast Gate 2 open SPN is registered correctly — do not touch SPNs
Browser is slow but curl is fast Gate 1 closed Browser is not attempting Kerberos — fix zone/policy
Both curl and browser are slow Both gates closed Fix zone first, then check SPN
Browser fast after adding to Intranet Zone Root cause confirmed Make zone setting permanent via GPO

Important

If curl --negotiate already works, the SPN is already registered correctly. Do not register new SPNs — you will waste time and risk duplicates. The only fix needed is Gate 1: the browser zone or policy.

SPN registration is a separate concern covered in Service Principal Names (SPN).


Resolution Steps

1. Add Site to Local Intranet Zone (Immediate Fix)

This is the highest-impact, lowest-risk fix. It tells the browser to use Negotiate/Kerberos instead of NTLM.

  1. Open Group Policy Management Console
  2. Navigate to: Computer Configuration > Administrative Templates > Windows Components > Internet Explorer > Internet Control Panel > Security Page
  3. Open Site to Zone Assignment List → Enable
  4. Add entries (Value 1 = Local Intranet):
Setting Value
https://nodinite.yourdomain.com 1
https://nodinite.yourdomain.com:40001 1
  1. Run gpupdate /force on affected machines

Via Registry (Per-Machine, No Reboot Required)

# Run as Administrator on the client workstation
$domain = "nodinite.yourdomain.com"  # Replace with your hostname
$regPath = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\Domains\$domain"
New-Item -Path $regPath -Force
Set-ItemProperty -Path $regPath -Name "https" -Value 1 -Type DWord
Write-Host "✓ $domain added to Local Intranet Zone (Value 1)" -ForegroundColor Green

Manual (Per-User, for Testing Only)

  1. Open inetcpl.cpl (Internet Options)
  2. Security tab → Local IntranetSitesAdvanced
  3. Add: https://nodinite.yourdomain.com
  4. Click CloseOK
  5. Restart browser — Kerberos should now be used

2. Configure Edge/Chrome Authentication Policies

For Edge (Chromium-based), set the AuthServerAllowlist policy to allow Negotiate for your domain:

# Set via PowerShell (Run as Administrator)
New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Edge" -Force
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Edge" `
    -Name "AuthServerAllowlist" -Value "*.yourdomain.com"  # Replace with your domain

# Verify
Get-ItemProperty "HKLM:\SOFTWARE\Policies\Microsoft\Edge" -Name "AuthServerAllowlist"

Or deploy via Group Policy:

Computer Configuration > Administrative Templates > Microsoft Edge > HTTP authentication
  Policy: Authentication server allowlist
  Value: *.yourdomain.com

3. SPN Missing? (Only If Both curl and Browser Are Slow)

If curl --negotiate is also slow or returns a 401, the HTTP SPN for the IIS application pool service account may not be registered. See Service Principal Names (SPN) for the complete guide covering default instances, named instances, clusters, and app pool accounts.

Tip

The SPN guide includes an IIS application pool section with setspn commands for HTTP SPNs. The same tool — Microsoft Kerberos Configuration Manager — detects missing SPNs automatically.

4. Verify Kerberos Delegation (If Web API Accesses Backend Services)

If the Nodinite Web API impersonates users to access SQL Server or other backend services:

  1. Open Active Directory Users and Computers
  2. Find the service account → PropertiesDelegation tab
  3. Select: Trust this user for delegation to specified services only
  4. Select: Use any authentication protocol (or Kerberos only if all SPNs are registered)
  5. Add the backend service SPNs (e.g., MSSQLSvc/sqlserver.yourdomain.com:1433)

See Trusted for delegation for the full guide.

5. Long-Term: Migrate to OpenID Connect / SSO

The permanent solution is to move from Windows Authentication to OpenID Connect with an identity provider (Microsoft Entra ID, Keycloak, AD FS). This eliminates:

  • NTLM/Kerberos negotiation issues and required zone configuration
  • LSA SID lookup overhead on every request
  • Kerberos delegation complexity
  • SPN management burden
  • Dependency on Active Directory for authentication

See Install Nodinite v7 - OpenID for setup instructions.


Verification

After applying the fix, verify Kerberos is being used:

# 1. Open a new browser window and navigate to the Nodinite URL
# 2. Check klist for an HTTP service ticket
klist

# Look for a ticket like:
#   Server: HTTP/nodinite.yourdomain.com @ YOURDOMAIN.COM
#   KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96

# 3. Measure request time (should be <500ms with Kerberos)
Measure-Command {
    Invoke-WebRequest -Uri "https://nodinite.yourdomain.com/api/isalive" -UseDefaultCredentials
}

Quick Reference

Symptom Cause Fix
4+ second API response times NTLM causing LSA SID flood Add site to Intranet Zone
Hundreds of LookupSids in logs SID→Name translation per request Add site to Intranet Zone
No HTTP ticket in klist after browser request Browser not attempting Kerberos Intranet Zone + Edge policy
curl fast, browser slow curl uses Negotiate, browser falls back to NTLM Fix zone/policy only — SPN is fine
Both curl and browser slow SPN likely missing Check SPN registration — see SPN guide
Works after adding to Intranet Zone Confirms NTLM was the root cause Make zone setting permanent via GPO

Next Step

How to perform hardening on your Nodinite installation