Get Alerts for Specific SQL Server Login Failed Events

By:   |   Comments (15)   |   Related: > Security


Problem

In a recent MSSQLTips.com question, a user asked how they could get an alert whenever a login failed due to the account being locked out. I thought this was an interesting problem, and immediately suggested some off-the-cuff ideas. Following through on my ideas, however, was not a simple one-step process, as each idea is a little involved.

Solution

As with many problems, there are several ways to solve this issue. My initial suggestions were to set up an alert with SQL Server Agent, set up an audit specification, or to manually consume the error log periodically. I quickly realized that the audit solution was not practical, both because there is no inherent alerting there, and because it requires Enterprise Edition. So my suggestions have been slightly revised to:

  • A SQL Server Agent alert, using the WMI event AUDIT_LOGIN_FAILED
  • Event Notifications, also using AUDIT_LOGIN_FAILED
  • Manually consuming the error log

All three solutions will use SMTP e-mail, via database mail, as the alert mechanism. So, it is assumed that you already have database mail set up, with an active profile and a valid SMTP server configured. If you don't already have database mail configured, please review this previous tip, "Setting up Database Mail for SQL 2005" - in spite of the title, the steps remain unchanged in SQL Server 2008, 2008 R2 and 2012. Note that in all cases it may take a few minutes before the e-mail arrives in your inbox.

All three solutions will also demonstrate capturing state 5 events (which occur when an attempt from the local machine ). State 10 (account lock-out) is a much tougher one to reproduce, especially in a stand-alone virtual machine. But you should just be able to swap out the state values and otherwise utilize the same solution - no matter which login failed event(s) you're trying to capture (for a list of login states and what they usually mean, see my blog post, "Troubleshooting Error 18456").

SQL Server Agent

In order to use an alert that sends an e-mail with accurate information, SQL Server Agent must be running, a mail profile must be enabled, and tokens replacements must be toggled on. Tokens are used to substitute information in the body of a job step, so that - for example - an e-mail alert can contain information such as the server name and the actual error message. You can set these properties (highlighted below) using Management Studio by right-clicking SQL Server Agent, selecting Properties, and moving to the Alert System screen:

SQL Server Agent > Properties > Alert System

You can create an alert that monitors for the WMI event AUDIT_LOGIN_FAILED, and I will show two ways to send an e-mail in response to this event (but only if the state is 5). One is to notify an operator, the other is to create a job.

For the first case, you need to create an operator, then set up the alert, then set up the notification. You can do this with the following script:

USE [msdb];
GO

EXEC msdb.dbo.sp_add_operator
  
@name = N'Operator1',
  
@enabled = 1,
  
@pager_days = 0,
  
@email_address = N'[email protected]',
  
@category_name = N'[Uncategorized]';

DECLARE @namespace NVARCHAR(255)
  
= N'\\.\root\Microsoft\SqlServer\ServerEvents\' + COALESCE
  
(
      
CONVERT(NVARCHAR(32), SERVERPROPERTY('InstanceName')),
      
N'MSSQLSERVER'
  
);

EXEC msdb.dbo.sp_add_alert
  
@name = N'Login Failed : State 5',
  
@message_id = 0,
  
@severity = 0,
  
@enabled = 1,
  
@delay_between_responses = 0,
  
@include_event_description_in = 1,
  
@category_name = N'[Uncategorized]',
  
@wmi_namespace = @namespace,
  
@wmi_query = N'SELECT * FROM AUDIT_LOGIN_FAILED WHERE State = 5';

EXEC msdb.dbo.sp_add_notification
  
@alert_name = N'Login Failed : State 5',
  
@operator_name = N'Operator1',
  
@notification_method = 1;
GO

Note that if you want to monitor multiple states with the same alert, you can change the @wmi_query to:

@wmi_query = N'SELECT * FROM AUDIT_LOGIN_FAILED WHERE State = 5 OR State = 6'

Now if you attempt to log in with an invalid username, you will receive this e-mail:

E-mail resulting from an alert to an operator

The reason I would prefer to create a job in this case is that I can have much more control over the content of the e-mail. If you notice above, the e-mail tells us that a login failed, but doesn't tell us anything about the actual error message (e.g. the login name, the type of authentication, or the host where the login attempt originated). For more control, you can create a job that uses tokens to translate WMI and other server-level data into an e-mail. You can drop the existing alert as follows:

USE [msdb];
GO
EXEC msdb.dbo.sp_delete_alert
  
@name = N'Login failed : State 5';

Now here is a sample script to create a job, and then re-create the alert so that it points at the job:

USE [msdb];
GO

DECLARE
  
@job_id BINARY(16);

EXEC msdb.dbo.sp_add_job
  
@job_name = N'Mail on login failed : State 5',
  
@enabled = 1,
  
@description = N'Send e-mail on WMI event',
  
@category_name = N'[Uncategorized (Local)]',
  
@owner_login_name = N'sa',
  
@job_id = @job_id OUTPUT;

-- WMI exposes several tokens we can take advantage of:
DECLARE @cmd NVARCHAR(MAX) = N'DECLARE @msg NVARCHAR(MAX) = '
  
+ '''From job: Login failed for $(ESCAPE_SQUOTE(WMI(LoginName)))'
  
+ '. Full error message follows:' + CHAR(13) + CHAR(10)
   +
'$(ESCAPE_SQUOTE(WMI(TextData)))'';

EXEC msdb.dbo.sp_send_dbmail
@recipients = ''[email protected]'',
@profile_name = ''default'',
@body = @msg,
@subject = ''There was a login failed event '
          
+ 'on $(ESCAPE_SQUOTE(A-SVR)).'';';

-- msdb is used as the database for the job step; this prevents
-- any cross-database issues with executing sp_send_dbmail.
EXEC msdb.dbo.sp_add_jobstep
  
@job_id = @job_id,
  
@step_name = N'Step 1 - send e-mail',
  
@step_id = 1,
  
@on_success_action = 1,
  
@on_fail_action = 2,
  
@subsystem = N'TSQL',
  
@database_name = N'msdb',
  
@command = @cmd;

EXEC msdb.dbo.sp_update_job
  
@job_id = @job_id,
  
@start_step_id = 1;

EXEC msdb.dbo.sp_add_jobserver
  
@job_id = @job_id,
  
@server_name = N'(local)';

DECLARE @namespace NVARCHAR(255)
  
= N'\\.\root\Microsoft\SqlServer\ServerEvents\' + COALESCE
  
(
      
CONVERT(NVARCHAR(32), SERVERPROPERTY('InstanceName')),
      
N'MSSQLSERVER'
  
);

EXEC msdb.dbo.sp_add_alert
  
@name = N'Login failed : State 5',
  
@enabled = 1,
  
@category_name = N'[Uncategorized]',
  
@wmi_namespace = @namespace,
  
@wmi_query = N'SELECT * FROM AUDIT_LOGIN_FAILED WHERE State = 5',
  
@job_id = @job_id;

Once the job and alert are set up, you should once again be able to trigger an e-mail by attempting to connect to your SQL Server instance using a login that doesn't exist. Here is the new e-mail with much more complete information:

E-mail resulting from an alert to a job

In either of these cases, if you don't get the e-mail after a few minutes, it could be for several reasons. To troubleshoot, check the following things:

  • Double-click the alert in Object Explorer, and check the History tab. If the Event Count is 0, the alert may not be enabled correctly, or something else might be going on. Check the SQL Server error log - if nothing shows up there, then consider trying again after running a profiler trace watching for the Exception event.
  • Check that the mail profile is sending standard e-mails correctly, to the same address as indicated in the operator or the job. You can do this by right-clicking Database Mail in Object Explorer, choosing "Send Test E-Mail...", and sending an e-mail to the correct address using the same profile. It could be an issue with the SMTP server, the database mail subsystem, or the recipient's mailbox. Try a different recipient if none of the other avenues pan out.
  • For the job solution, check that SQL Server Agent is enabled (if the server has restarted, Agent may be set to start manually), that the job is enabled, and view the job history - the job may have started, but failed for other reasons.

Event Notifications

My good friend Jonathan Kehayias (@SQLPoolBoy) helped out a great deal in my understanding of Event Notifications, and helped me work through this example. At a high level, Event Notifications is a lightweight architecture allowing you to respond to certain DDL and trace events using Service Broker. Since it uses the same underlying architecture as the WMI alerts, we can use the same AUDIT_LOGIN_FAILED event to capture these events as they happen. First we need to set up a queue, a service, and the event notification itself:

USE [msdb];
GO

CREATE QUEUE FailedLoginNotificationQueue;
GO

CREATE SERVICE FailedLoginNotificationService
    
ON QUEUE FailedLoginNotificationQueue
  
([http://schemas.microsoft.com/SQL/Notifications/PostEventNotification]);
GO

CREATE EVENT NOTIFICATION FailedLoginNotification
    
ON SERVER WITH FAN_IN
    FOR
AUDIT_LOGIN_FAILED
    
TO SERVICE 'FailedLoginNotificationService', 'current database';
GO

Now we can create the activation procedure that will get called whenever one of these AUDIT_LOGIN_FAILED events occurs. We have to use RECEIVE to pull any new events off the queue, and some XQuery magic to parse the XML event data, but otherwise the logic is pretty straightforward. Again we're going to use msdb to avoid cross-database issues with calling sp_send_dbmail:

USE [msdb];
GO

CREATE PROCEDURE [dbo].[ProcessFailedLoginEvents]
WITH EXECUTE AS OWNER
AS
BEGIN
   SET NOCOUNT ON
;

  
DECLARE
      
@message_body XML,
      
@message NVARCHAR(MAX),
      
@subject NVARCHAR(255) = 'There was a login failed event on ' + @@SERVERNAME;

  
WHILE (1 = 1)
  
BEGIN
       WAITFOR
      
(
          
RECEIVE TOP(1) @message_body = message_body
              
FROM dbo.FailedLoginNotificationQueue
      
), TIMEOUT 1000;

      
IF (@@ROWCOUNT = 1)
      
BEGIN
           IF
(@message_body.value('(/EVENT_INSTANCE/State)[1]', 'int') = 5)
          
BEGIN
               SELECT
@message = 'From Event Notification: Login failed for user '
                  
+ @message_body.value('(/EVENT_INSTANCE/LoginName)[1]', 'varchar(128)' )
                   +
'. Full error message follows:' + CHAR(13) + CHAR(10)
                   +
@message_body.value('(/EVENT_INSTANCE/TextData)[1]', 'varchar(4000)' );

              
EXEC msdb.dbo.sp_send_dbmail
                  
@profile_name = 'default',
                  
@recipients = '[email protected]',
                  
@subject = @subject,
                  
@body = @message;
          
END
       END
   END
END
GO

ALTER QUEUE FailedLoginNotificationQueue
WITH ACTIVATION
(
  
STATUS = ON,
  
PROCEDURE_NAME = [dbo].[ProcessFailedLoginEvents],
  
MAX_QUEUE_READERS = 1,
  
EXECUTE AS OWNER
);
GO

Note that if you want to capture multiple events, you can just change the IF conditional as follows:

 IF (@message_body.value('(/EVENT_INSTANCE/State)[1]', 'int') IN (5, 6))

With the queue activated, you can again generate an e-mail by attempting to login with invalid credentials. You should see the following e-mail:

E-mail resulting from an Event Notification

Like above, if you don't get the e-mail after a few minutes, it may be because there is a problem with database mail or the SMTP server, but it may also be that the activation procedure is not firing. One step to diagnose this would be to add simple logging to the activation procedure. Remus Rusanu also has some troubleshooting tips.

Parsing the SQL Server error log

This is my least favorite solution, but it's probably the easiest among these three to grasp and to troubleshoot. Basically we're just going to look at the error log every n minutes, check to see if there are any new login failed / state 5 events since the last time we checked, and send an e-mail. First we need a simple table to log the current date/time (we'll start with it being 1900-01-01 so that we capture all login failed events on first run):

USE [msdb];
GO
CREATE TABLE dbo.LastCheck
(
  
[Date] SMALLDATETIME
);
GO
INSERT dbo.LastCheck SELECT '19000101';
GO

Now the following stored procedure first determines when this check was last made, then dumps the current error log into a #temp table, and removes any rows from before the last check. If any remain, it joins the two parts of the error message for each login event (we know that they get recorded with the same timestamp, and the possibility of other collisions are highly unlikely), assembles an e-mail message, sends it off, then updates the last check.

USE [msdb];
GO
ALTER PROCEDURE dbo.CheckForFailedLoginEvents
WITH EXECUTE AS OWNER
AS
BEGIN
   SET NOCOUNT ON
;

  
DECLARE
      
@LastCheck DATETIME,
      
@now       DATETIME = SYSDATETIME(),
      
@message   NVARCHAR(MAX) = N'From manual labor: Login failed event(s).',
      
@subject   NVARCHAR(255) = N'Login failed event(s) on ' + @@SERVERNAME;

  
SELECT
      
@LastCheck = [Date] FROM dbo.LastCheck;


  
CREATE TABLE #t
  
(
      
LogDate DATETIME,
      
ProcessInfo VARCHAR(50),
      
[Text] NVARCHAR(4000)
   );

  
INSERT INTO #t(LogDate, ProcessInfo, [Text])
      
EXEC MASTER..xp_readerrorlog  0, 1, N'State: 5';

  
INSERT INTO #t(LogDate, ProcessInfo, [Text])
      
EXEC MASTER..xp_readerrorlog  0, 1, N'Login Failed';

  
DELETE #t WHERE [LogDate] < @LastCheck;

  
IF EXISTS (SELECT 1 FROM #t)
  
BEGIN
       SELECT
@now = DATEADD(SECOND, 1, MAX([LogDate])) FROM #t;

      
SELECT
          
@message += CHAR(13) + CHAR(10) + CHAR(13) + CHAR(10)
           +
CONVERT(CHAR(10), e.LogDate, 120) + ' '
          
+ CONVERT(CHAR(8), e.LogDate, 108) + CHAR(13) + CHAR(10)
           +
e.[Text] + CHAR(13) + CHAR(10) + x.[Text]
      
FROM #t AS e INNER JOIN #t AS x
      
ON e.LogDate = x.LogDate
      
WHERE e.[Text] LIKE 'Error%' AND x.[Text] LIKE 'Login%'
      
ORDER BY e.LogDate, e.[Text];

      
EXEC msdb.dbo.sp_send_dbmail
          
@profile_name = 'default',
          
@recipients = '[email protected]',
          
@subject = @subject,
          
@body = @message;
  
END

   DROP TABLE
#t;

  
UPDATE dbo.LastCheck SET [Date] = @now;
END
GO

EXEC dbo.CheckForFailedLoginEvents;
GO

A simple execution yields the following e-mail:

E-mail resulting from manual stored procedure call

Since the dbo.LastCheck table was updated with the last instance that was reported, executing the stored procedure again immediately should not yield another e-mail (unless your server is being hammered with invalid login requests).

Now, you can schedule this stored procedure as a separate job, as part of some other maintenance job, or just run it manually - and you can run it as frequently or infrequently as you like.

Conclusion

I've shown three ways to receive an e-mail alert when logins fail with a certain state (or states). I think they each have their merits, depending on the practices and policies already in place in your environment, and your familiarity with the different technologies used. You should be able to expand any of these solutions to cover other login failed events, in addition to any WMI or DDL events, or events that appear in the SQL Server error log. There are also 3rd party products out there that can help with this; for example, Jason Hall blogged about how SQL Sentry Event Manager can be used to capture and display WMI event alerts.


Next Steps


sql server categories

sql server webinars

subscribe to mssqltips

sql server tutorials

sql server white papers

next tip



About the author
MSSQLTips author Aaron Bertrand Aaron Bertrand (@AaronBertrand) is a passionate technologist with industry experience dating back to Classic ASP and SQL Server 6.5. He is editor-in-chief of the performance-related blog, SQLPerformance.com, and also blogs at sqlblog.org.

This author pledges the content of this article is based on professional experience and not AI generated.

View all my tips



Comments For This Article




Sunday, February 18, 2018 - 12:59:03 PM - Raphael Ferreira Back To Top (75236)

First of all, THANK you so much for all these hints and sample codes. I fully appreciate it. I am having a problem though. We have about 20 to 30 SQL servers. For some servers the code I pasted below works perfectly. But for others, the A-SVR token does not work and I end up having an e-mail where the subject does not contain the SQL Server instance name. Any ideas why that would be the case? I mean, it works on several of our SQL servers, but not in others.

We have an alert set to detect failed login attempts. That alert, when triggered, executes a SQL Server Agent job that is meant to send some users an alert e-mail. We have about 20 to 30 servers. The exact same code works perfectly in some servers (returning the server instance name via the A-SVR token), but it returns an empty string in other servers. What am I missing here guys? A configuration somewhere. Most puzzling, in the same exact code, the other token, $(ESCAPE_SQUOTE(WMI(LoginName))) works perfectly. It is only the $(ESCAPE_SQUOTE(A-SVR)) token that returns a blank result.

Job step code:

DECLARE @msg NVARCHAR(MAX) = 'From job: Login failed for $(ESCAPE_SQUOTE(WMI(LoginName))). Full error message follows:
$(ESCAPE_SQUOTE(WMI(TextData)))';

 EXEC msdb.dbo.sp_send_dbmail
  @recipients = '',
  @profile_name = 'sqlnotify',
  @body = @msg,
  @subject = 'There was a login failed event on $(ESCAPE_SQUOTE(A-SVR)).';

Notice the resulting e-mail, missing the server instance name in the subject line:

Here it is, from another server, and in this server the same exact code is working with no issues:

Help much appreciated. TIA, Raphael

P.S. Yes, we do have token replacement correctly setup as far as I can tell:

 


Wednesday, March 11, 2015 - 9:44:39 AM - Aaron Bertrand Back To Top (36486)

@Suresh Just use state 8 instead of state 5.


Tuesday, March 10, 2015 - 5:19:05 PM - Suresh Back To Top (36481)

if i want to get the alert for specific user where the user is put wrong password then how can change the code ? could you help me out


Thursday, September 20, 2012 - 2:08:24 AM - Dinesh Back To Top (19583)

Alter system => Mail session=>Enble profile=>

     I had select my mail profile.

     I want test mail but test button is inactive..

 


Friday, May 18, 2012 - 3:26:10 PM - Aaron Bertrand Back To Top (17540)
Hugo, great, glad that worked. Definitely cleaner than the back-up plan I offered.

Friday, May 18, 2012 - 12:48:43 PM - Hugo Haeck Back To Top (17534)

Hi again Aaron,

Actually I tried the exclude_query_output statement again and then I re-ran the Activation statement and the 'Mail queued' message is now gone!.  So the solution works, simply add @exclude_query_output = 1 to the sp_send_dbmail statement:

EXEC msdb.dbo.sp_send_dbmail
 @profile_name = 'default',
 @recipients = '[email protected]',
 @subject = @subject,
 @body = @message,
 @exclude_query_output = 1;

But don't forget to re-run the ALTER QUEUE statement afterwards (like I did):

ALTER QUEUE FailedLoginNotificationQueue
WITH ACTIVATION
(
 STATUS = ON,
 PROCEDURE_NAME = [dbo].[ProcessFailedLoginEvents],
 MAX_QUEUE_READERS = 1,
 EXECUTE AS OWNER
);
GO

Thanks again for your input...

 


Friday, May 18, 2012 - 11:32:18 AM - Hugo Haeck Back To Top (17532)

Hi Aaron,

Thanks for you replies, yes I have tried exclude_query_output yesterday before posting, but it seems to only affect query results and, unfortunately, not the 'Mail queued' witch remains...

I will try your 2nd suggestion and, if I have any luck using a table as a queue and then using a trigger to send the mail messages in a 2nd step, I'll post the code later on today...

If I can't get it to work I'll either ask that the SCOM alerts be overriden leaving only the informational messages OR revert to using a SQL Server Agent alert like your first solution details.

 

 


Thursday, May 17, 2012 - 5:33:49 PM - Aaron Bertrand Back To Top (17512)

Hugo, if that doesn't help, I'm afraid you're out of luck. This has been a long-running complaint:

http://connect.microsoft.com/SQLServer/feedback/details/126118/suppress-mail-queued-message-when-using-sp-send-dbmail

That connect item has a pitiful closure reason, and it doesn't really explain why they can't add an option to override the default behavior (which wouldn't break anybody unless they willfully used the override).

So seems like a pretty silly thing, but to avoid the 'Mail queued' message from entering the event log due to activation, you could change the activation procedure to simply stuff the message details into a user table that acts as a queue, and have a different background process sweep around and send the e-mails.


Thursday, May 17, 2012 - 5:31:47 PM - Aaron Bertrand Back To Top (17511)

Hugo,

Did you try with @exclude_query_output = 1?

According to http://msdn.microsoft.com/en-us/library/ms190307.aspx:

Specifies whether to return the output of the query execution in the e-mail message. exclude_query_output is bit, with a default of 0. When this parameter is 0, the execution of the sp_send_dbmail stored procedure prints the message returned as the result of the query execution on the console. When this parameter is 1, the execution of the sp_send_dbmail stored procedure does not print any of the query execution messages on the console

 


Thursday, May 17, 2012 - 4:46:32 PM - Hugo Haeck Back To Top (17510)

Nice article, I tried to implement the scenario using Event Notifications and it works well, but it also raises an Informational Message in the Event Viewer:

Event ID: 9724 --> The activated proc [dbo].[ProcessFailedLoginEvents] running on queue msdb.dbo.FailedLoginNotificationQueue output the following:  'Mail queued.'

Witch in turn raises an alert in SCOM "A SQL Server Service Broker procedure output results"...  All of this seemingly because sp_send_dbmail ouputs 'Mail queued'.  This is the only information I found on the topic on the web: http://mpwiki.viacode.com/default.aspx?g=posts&t=19225

Does anyone know how I could disable such an output from sp_send_mail, therefore preventing the informational message and the alert??


Thursday, February 9, 2012 - 12:18:41 PM - pl80 Back To Top (15963)

Chuck,

What host name do you see in the Profiler trace?  What is the application name? Are these columns empty?  Are they selected?


Wednesday, February 1, 2012 - 10:10:00 AM - Chuck Humphrey Back To Top (15865)

I am trying to track down an Error: 18456, Severity: 14, State: 5. Login failed for user 'InternetAdmin'. [CLIENT: named pipe] that I am receiving numerous times (inconsistenently) during the day since I have taken over as a new DBA on this server.  I have tried a trace but it doesn't give me any information to determine where this login attempt is coming from.  I believe a database was dropped before I took over that it is attempting to access.  Can you offer any suggestions on what I might be able to do to help determine where this attempt is coming from so I can have the attempted login stopped?


Tuesday, January 17, 2012 - 7:02:28 PM - Aaron Bertrand Back To Top (15690)

Just a couple of follow-up comments that were brought up to me by SQL Sentry's founder, President and CEO, Greg Gonzalez (and also my boss :-)):

(1) you could also accomplish something similar with a basic alert for Severity 14 (Insufficient Permission) instead of focusing on the error number, since there are a few obscure cases where a different error number may be raised. Though like an alert for error number 18456, this will also include some events that you might consider false positives.

(2) do not take lightly the potential impact on performance of reading the error log - that one is definitely a last resort option, and if you are going to proceed with that option, you should definitely schedule it at a far lower frequency (once an hour, or less, where tolerable). xp_readerrorlog can have a significant impact on CPU particularly on larger systems with significant uptime and/or large logs.


Tuesday, January 17, 2012 - 4:05:07 PM - Jeremy Kadlec Back To Top (15688)

+1


Tuesday, January 17, 2012 - 8:46:16 AM - Roland Back To Top (15679)

Nice idea and a good description :-)

 















get free sql tips
agree to terms