SQL Server failed login storm: brute force, credential drift, and service-account failures
A failed-login storm fills ERRORLOG with “Login failed for user” entries (Error 18456). Counting them is the first instinct and the wrong one. Aggregate rate cannot tell brute force from a batch job running on rotated secrets, a service account whose password just expired, an application pool recycling, or a SQL Server 2025 replication secondary emitting benign noise every few minutes.
Two pieces of context SQL Server records but does not surface in any performance counter do the work: the source IP (or hostname) and the target principal. With those, the picture collapses into one of a handful of patterns, each with a different severity and a different fix.
There is no Failed Logins/sec counter in sys.dm_os_performance_counters. The Logins/sec counter under General Statistics counts successful logins only. To see failures, parse the error log, or run SQL Server Audit (FAILED_LOGIN_GROUP) or an Extended Events session on error_reported. This article assumes at least one of those capture paths is available.
Error 18456 state codes
Error 18456 is the canonical login-failure event. The message returned to the client is deliberately unhelpful: state is always reported as 1 to avoid leaking which step of authentication failed. The real state code and reason are written only to the SQL Server error log.
The state code is the fastest triage signal.
| State | Meaning |
|---|---|
| 1 | Client-visible state (always). Real reason hidden. |
| 2 / 5 | Login does not exist (5 = local Windows auth, 2 = remote). |
| 7 | Login is disabled AND the password supplied is wrong. |
| 8 | Password mismatch. The most common state in any storm. |
| 58 | SQL auth attempted against a server in Windows-only mode. |
| 146-149 (SQL Server 2016+) | CONNECT SQL or CONNECT ENDPOINT permission missing. Replaced old states 11/12. |
State 8 dominates most storms. To distinguish “wrong password” from “locked account” from “real brute force”, you need source IP and target principal.
Ticket on a sustained flood from one source, on service-account failures, or on a rate more than 5x the hourly baseline. Page only with source-aware evidence: privileged-target spray from unexpected hosts, or service-account failures causing an active application outage. Any sa attempt from a non-trusted source is notable on its own.
Common causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| Brute force / password spray | Many failures from one or few source IPs, often targeting sa, admin, or common names; varied targets (spray) or one target (guess) | Source IP distribution and target principal list |
| Credential drift | Sustained failures from many legitimate app hosts against one service account, starting at a secret-rotation window | Deployment and secret-rotation timeline |
| Service-account lockout or expiry | Every connection from one app fails simultaneously; LOGINPROPERTY('IsLocked') returns 1; may coincide with SQL service account lockout on AD | Account lockout state and AD health |
| Restart storm / pool recycle | Brief burst matching app pool recycle or service restart timestamps; resolves when pool stabilizes | App pool recycle events and service restart logs |
| Replication noise | Recurring failures every few minutes on a transactional replication secondary with a fixed state code | Replication topology and build level |
Quick checks
All read-only. Run on the instance being flooded.
-- Read the most recent Login failed entries (newest first)
EXEC xp_readerrorlog 0, 1, 'Login failed', NULL, NULL, NULL, 'DESC';
-- Filter specifically for the 18456 line so you can read the real state code
EXEC xp_readerrorlog 0, 1, '18456';
-- Any sa attempts in the current log? Notable on its own.
EXEC xp_readerrorlog 0, 1, 'Login failed', 'sa';
There is no built-in DMV that returns parsed error-log rows. Stream xp_readerrorlog output into a temp table to bucket by source IP and target principal, or use SQL Server Audit / Extended Events for structured capture.
-- Account lockout state for a suspect login (requires CHECK_POLICY = ON)
SELECT name,
LOGINPROPERTY(name, 'IsLocked') AS is_locked,
LOGINPROPERTY(name, 'BadPasswordCount') AS bad_password_count,
LOGINPROPERTY(name, 'BadPasswordTime') AS bad_password_time,
LOGINPROPERTY(name, 'LockoutTime') AS lockout_time,
LOGINPROPERTY(name, 'PasswordLastSet') AS password_last_set,
is_policy_checked,
is_expiration_checked
FROM sys.sql_logins
WHERE name IN ('<suspect_login>', 'sa');
-- Confirm who currently holds sysadmin (post-incident check after a spray)
SELECT sp.name, sp.type_desc, sp.create_date, sp.modify_date
FROM sys.server_principals sp
JOIN sys.server_role_members srm ON sp.principal_id = srm.member_principal_id
JOIN sys.server_principals rp ON srm.role_principal_id = rp.principal_id
WHERE rp.name = 'sysadmin';
# Confirm the SQL Server service account state on the host (Windows).
# MSSQLSERVER is the default instance; named instances use MSSQL$<InstanceName>.
Get-WmiObject Win32_Service -Filter "Name='MSSQLSERVER'" |
Select-Object Name, StartName, State
# On Linux hosts, confirm the SQL service is up
systemctl status mssql-server
How to diagnose it
- Confirm the flood is real, not a log-recycle artifact. Cycles of
sp_cycle_errorlogand high-volume auditing can make a normal background rate look alarming. Compare current failure count against your hourly baseline. - Bucket failures by source IP and target principal. SQL Server Audit’s
FAILED_LOGIN_GROUPand an Extended Events session onsqlserver.error_reported(filter[severity]=(14) AND [error_number]=(18456)) both give youclient_host_nameandnt_username. Audit alone does not exposeclient_app_name; pair the two if you need it. - Apply the decision tree below.
- For service-account patterns, check
LOGINPROPERTY(name, 'IsLocked'). If locked, find out why before unlocking. A locked SQL login withCHECK_POLICY = ONfollows Windows lockout policy: threshold, duration, and reset counter come from the local security policy or the domain GPO. - Verify the SQL Server service account itself. If the service account is locked or disabled on the domain controller, every SQL authenticated login with
CHECK_POLICY = ONfails until the service account is unlocked. This is one of the most confusing failure modes: the entire instance appears to reject all credentials, but the engine is fine.
flowchart TD
A[Error 18456 flood] --> B{Single source IP or few?}
B -- Yes --> C{Targeting sa or privileged?}
B -- No, many sources --> D{Same target login?}
C -- Yes --> E[Brute force or spray - PAGE if non-trusted]
C -- No --> F[Stale app secret - TICKET]
D -- Service account --> G[Lockout, expiry, or AD unreachable]
D -- Many logins --> H[Restart storm or replication noise]
G --> I{SQL service account locked?}
I -- Yes --> J[Global auth failure - unlock service account]
I -- No --> K[Rotate secret or fix SPN]Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
| Error 18456 rate (parsed from log) | Only direct measure of failed logins | Sustained rate > 5x hourly baseline |
| Source IP cardinality | Separates spray from drift | Single IP dominating, or IPs outside expected subnets |
| Target principal distribution | Distinguishes privileged-target attack from app drift | sa or sysadmin members in the target list |
| State code distribution | Reveals lockout vs wrong-password vs missing-login | State 7 spike, state 146-149 (permission), state 58 (auth mode mismatch) |
LOGINPROPERTY('IsLocked') | Definitive lockout indicator | Any value of 1 on a service account |
| SQL Server service account state (AD) | Hidden global auth failure cause | Service account locked or disabled on DC |
| Application login success rate | Confirms user-visible impact | Success rate collapsing alongside failure spike |
| Audit and XE session health | Capture path itself can break | system_health or custom XE session stopped |
Fixes
Brute force or password spray
Short term: block the source IPs at the network layer (firewall, host-level Windows Firewall, cloud NSG). Do not rely on SQL Server to rate-limit: there is no native brute-force detection feature. Disabling the targeted login usually makes things worse, because the attacker has now achieved a denial of service for you.
Medium term: enforce Windows account lockout policy via CHECK_POLICY = ON on SQL logins so repeated bad passwords auto-lock the account. Disable or rename sa. ALTER LOGIN [sa] DISABLE and ALTER LOGIN [sa] WITH NAME = [dbo_sa] are both safe operations: the engine tracks the sa principal by its SID (0x01), not by the name. Coordinate any rename with app owners first, and note that both are disruptive while they propagate.
Confirm whether the attacker ever succeeded. Check sysadmin role membership and pull security-audit events from the default trace.
Credential drift
Short term: identify the secret that changed and either roll forward the connection strings to the new secret, or roll back the secret to the previous value if the rotation was premature.
Medium term: alert on the start of every secret rotation so on-call can watch the failure rate during the window. The pattern is distinctive once you have source-aware capture: many legitimate hosts, one principal, starting at a rotation timestamp.
Service-account failures
Distinguish two failure classes.
The service account that runs the app is locked or expired. Unlock on AD (Unlock-ADAccount), reset the password, update connection strings. Check whether CHECK_EXPIRATION = ON triggered it.
The SQL Server service account itself is locked. Every SQL login with CHECK_POLICY = ON fails until you unlock it. The engine looks healthy, the network is fine, and yet every connection rejects. This is a known support scenario . Unlock the service account on the DC and SQL auth resumes immediately.
For gMSA-managed service accounts on AG listeners, also check SPN registration against the listener name. A missing listener SPN can force NTLM fallback and cause intermittent authentication failures during password rotation .
Prevention
- Enable SQL Server Audit with a server audit specification capturing
FAILED_LOGIN_GROUP. Server-level specs work in all editions. On SQL Server 2022+, readers ofsys.fn_get_audit_fileneedVIEW SERVER SECURITY AUDITinstead ofCONTROL SERVER. - Pair audit with Extended Events on
sqlserver.error_reportedfiltered to severity 14 and error 18456 if you needclient_app_nameandclient_host_name. Audit alone does not surface those. - Enforce
CHECK_POLICY = ON(andCHECK_EXPIRATION = ONwhere appropriate) on every SQL login. Lockout threshold, duration, and reset counter come from the Windows account policy: set them deliberately via secpol.msc or GPO, not by accident. - Disable or rename
sa. Track anysaattempt from non-trusted sources as a notable event regardless of volume. - Maintain a source-IP allowlist baseline per principal. Anomaly detection against that baseline is what turns raw failure counts into actionable signal.
- Validate the SQL Server service account state in your host health checks, not just your SQL health checks. A locked service account is invisible to DMVs.
- Document build-specific false positives. Some SQL Server 2025 builds emit benign recurring 18456-class audit events on transactional replication secondaries every few minutes. Record the pattern when you see it so future on-call engineers do not page on it.
How Netdata helps
- Per-second metric collection catches the failure-rate slope early, before the error log buries you. Correlate the failure spike with batch requests/sec and user connections to confirm whether the storm is impacting real workload.
- ML-based anomaly detection on login-related log events and connection counts surfaces the deviation from baseline without hand-tuned thresholds.
- Correlated dashboards let you overlay the failure spike against SQL service account state, AG replica health, and application login success rate, so you can tell a global auth failure from an isolated spray in seconds.
- Audit-log and error-log parsing surface the source IP and target principal that aggregate counters cannot, which is exactly the evidence the playbook requires before paging.
- Anomaly retention across restarts preserves the failure pattern even when the engine restarts, which matters for post-incident forensics on credential drift and service-account lockouts.
Netdata’s Microsoft SQL Server monitoring with Netdata brings these signals together with per-second metrics and ML anomaly detection.
Related guides
- SQL Server AlwaysOn failover readiness: quorum, health checks, and the failover you assume works
- SQL Server Availability Group not synchronizing: NOT_HEALTHY replicas and failover risk
- SQL Server AG send and redo queues growing: replication lag and failover RTO
- SQL Server backup freshness: the recovery point you only discover you lack during an incident
- SQL Server blocking chains: finding the head blocker before workers run out
- SQL Server buffer cache hit ratio low: when the working set no longer fits in memory
- SQL Server user connections climbing: connection pool leaks and retry storms
- SQL Server CPU utilization high: telling query load apart from a bad plan
- SQL Server CXPACKET and CXCONSUMER waits: parallelism, MAXDOP, and what is actually wrong
- SQL Server database in SUSPECT or RECOVERY_PENDING: an offline database and how to recover it
- SQL Server Error 1205: transaction was deadlocked and chosen as the deadlock victim
- SQL Server Error 701: there is insufficient system memory to run this query






