AlwaysOn Availability Groups are a reasonably simple way to set up disaster recovery (DR) for your SQL Server environment, and with fairly little effort, you can get a bit of high availability (HA) from it as well. But there are a few gotchas, the most obvious of them being that Availability Groups only synchronize specific user-databases, not the entire server setup.
Things that are not included in AGs include logins, SQL Server Agent jobs, SSIS packages stored in SQL Server, linked servers and server settings. You could sync these manually (as is often the case), but wouldn’t you just love to have an automated process do all this for you?
In this post, we’ll look at logins. For the sake of simplicity, I’ll assume that you have a primary replica with a single AG and any number of secondary replicas. The logic holds true if you have multiple AGs, it just gets trickier.
EDIT: Since I wrote this post, quite a few years ago, I learned about dbaTools, which is an amazing collection of Powershell commandlets for DBAs. One of those allows you to synchronize not only logins, but tons of other things that are not covered in this post.
See Copy-DbaLogin, and this post on how to use it.
If you can, I would advise you to use dbaTools to synchronize your stuff instead of this script. However, some companies can’t or won’t use dbaTools, so I’ll leave this post up for posterity.
Quick recap on logins and users
There are two types of so-called security principals you need to keep track of. The first one is the login, which is the server-level principal, the one that your clients and apps log on with. Each login can map to any number of users, one for each database. Users, thus, are referred to as database principals. The login connects to the user with a SID, a security identifier, which is a unique binary string.
For Windows authenticated logins, the SID is created from the Windows user or group, whereas SQL Server authenticated accounts cannot inherit a SID, so a new one is created in SQL Server.
Server-level principals (logins) can be members of server roles and have server-level permissions. Database principals (users), on the other hand, can be members of database roles and have permissions to database objects.
For a more detailed discussion on the SQL Server security model, read some of my earlier posts on principals, securables, permissions and execution context.
Why sync logins?
If you fail over your Availability Group, whether planned or unplanned, you’ll probably want to be as close to “business as usual” as possible after the failover, meaning that users can still log on to their databases or applications as usual.
Whether things are on fire or you’re just applying a service pack, this should be one less issue for you to deal with.
What do I need to sync?
So we have:
- Server-level principals (logins)
- Server roles
- Server role memberships
- Server-level permissions
- Database-level principals (users)
- Database roles
- Database role memberships
- Database object permissions
- Contained users (special users without logins)
Users, database roles as well as their respective memberships and permissions in the databases are all contained in each respective database. This means that if you make any change in the database, it’s already replicated across the Availability Group.
This means that you’ll only need to take care of the server-level stuff, the first four things on the list.
Windows logins and groups
As I mentioned above, Windows/AD logins and groups already come with a SID (which is what connects logins to users). This means that you can create a login for a given Windows account on any number of servers, and all those logins will get the same SID.
Another advantage with Windows logins are that you don’t have to know the password because Windows or Active Directory manages that for you.
This query will show you how to generate CREATE LOGIN statements for all Windows users and groups:
SELECT N'CREATE LOGIN ['+sp.[name]+'] FROM WINDOWS;' FROM master.sys.server_principals AS sp WHERE sp.[type] IN ('U', 'G') AND sp.is_disabled=0 AND sp.[name] NOT LIKE 'NT [AS]%\%';
SQL Server logins
SQL Server logins generate a new SID when they are created, so if you just create a login on two servers, they’ll end up with different SIDs. To manage this, you can actually manually specify the SID of a SQL Server login when you create it:
CREATE LOGIN [login_name] WITH PASSWORD='Great password', SID=0x02963F...18A3DECEBE;
You can read the SID from the primary replica’s sys.server_principals DMV and then explicitly state that SID when you’re creating the same login on all your secondary replicas.
While you’re at it, read the hashed password from sys.sql_logins. That way, you don’t have to know or write down the passwords – you can just synchronize the hashed passwords between replicas.
CREATE LOGIN [login_name] WITH PASSWORD=0x0200A6860363412...478650 HASHED, CHECK_POLICY=OFF, SID=0x02963F...18A3DECEBE;
Here’s how to get the information you need from the primary replica:
SELECT N'CREATE LOGIN ['+sp.[name]+'] WITH PASSWORD=0x'+ CONVERT(nvarchar(max), l.password_hash, 2)+N' HASHED, CHECK_POLICY=OFF, '+ N'SID=0x'+CONVERT(nvarchar(max), sp.[sid], 2)+N';' FROM master.sys.server_principals AS sp INNER JOIN master.sys.sql_logins AS l ON sp.[sid]=l.[sid] WHERE sp.[type]='S' AND sp.is_disabled=0;
Server roles
If you use user-defined server roles, you may want to set them up on the secondary replicas as well. Here’s a list of your user-defined server roles from the primary replica:
SELECT N'CREATE SERVER ROLE ['+sp.[name]+N'];' FROM sys.server_principals AS sp WHERE sp.principal_id>=100 AND sp.[type]='R';
Permissions
And here are the server-level permissions for all types of server-level principals:
--- GRANT/DENY (permission) TO principal
SELECT p.state_desc+N' '+p.[permission_name]+N' TO ['+sp.[name] COLLATE database_default+N'];'
FROM sys.server_permissions AS p
INNER JOIN sys.server_principals AS sp ON
p.grantee_principal_id=sp.principal_id;
Putting it all together
I thought you’d never ask. I’ve created a stored procedure that will fetch all logins, server roles and server role memberships from a primary replica and sync those to the local (secondary) replica.
Head on over to the Downloads page and get SyncLogins.
Prerequisites
You’ll need a linked server from your secondary replica to the primary replica. This linked server should feature the absolute bare-minimum of permissions, preferably with only the “be made using the login’s current security context” selected.
The account running the procedure (or the mapped login in the linked server) will need SELECT access to the following DMVs on the remote (primary) server:
- master.sys.server_principals
- master.sys.sql_logins
- master.sys.server_role_members
- master.sys.server_permissions
The local account obviously also needs permissions to add/drop logins and assign server role memberships.
Running the sync procedure
The stored procedure runs on the secondary replica and accepts three (optional) parameters:
- @primary_replica: a linked server that points to a SQL Server instance from which you want to read logins and server roles. If left blank, the procedure will try to select the primary replica from an Availability Group. This will fail if there is no Availability Group or if there is more than one.
- @allow_drop_logins: set to 1 if you trust the procedure to drop existing logins on the local instance. Note: if an login is dropped and recreated on the primary, you’ll run into problems if you can’t drop the local login.
- @print_only: set to 1 to only output all changes as T-SQL code instead of executing the changes.
Todo
Some features are yet-to-do:
- Support for credential-based logins
- Setting the correct owning principal when creating a login
- Altering permissions to endpoints
EDIT: As of November 2018, this script is on GitHub, so you can fork your own copy, make improvements to it, and send me a pull request!
Hi , I was testing this, but it fails (error below) specifically on the code below as the password_hash on sql for user type S is NULL (what do you recommend?)
Code:
INSERT INTO @queue ([sql])
SELECT N’
CREATE LOGIN [‘+p.[name]+’] ‘+(CASE
WHEN p.[type]=’S’
THEN N’WITH PASSWORD=0x’+CONVERT(nvarchar(max), p.password_hash, 2)+N’ HASHED, CHECK_POLICY=OFF, SID=0x’+CONVERT(nvarchar(max), p.[sid], 2)+N’, ‘
WHEN p.[type] IN (‘U’, ‘G’)
THEN N’FROM WINDOWS WITH ‘ END)+
N’DEFAULT_DATABASE=[‘+p.default_database_name+N’]’+
ISNULL(N’, DEFAULT_LANGUAGE=’+p.default_language_name, N”)+N’;’
FROM @primaryLogins AS p
WHERE p.[sid] NOT IN (SELECT [sid] FROM master.sys.server_principals) AND
p.[type] IN (‘U’, ‘G’, ‘S’);
Error:
Msg 515, Level 16, State 2, Procedure LMC_SyncLogins, Line 166
Cannot insert the value NULL into column ‘sql’, table ‘@queue’; column does not allow nulls. INSERT fails.
Ah, do you by any chance have users without logins? I suppose you could filter the query to exclude users where p.password_hash is null..
Thanks for your feedback!
Hi Daniel,
Thanks for your prompt response , all the sql_logins have password_has NULL, this is SQL Server 2014 Enterprise, I can’t exclude them because I need to recreate them on the secondary AG, any advise ?
Thanks again
My question was if you’re using contained database users (i.e. users that are local to the database and don’t have a login)? Or, maybe, it’s a permission issue that the security context that you’re using doesn’t have sufficient permissions?
Hi Daniel,
You are right, it was the security context, once I gave sysadmin to the Linked server user then I see the password Hash. Now in order to restrict the permissions to this user, before assigning the sysadmin role, this user has securityadmin, serveradmin, db_owner over master system database, but it was not enough to see the password_hash, any advise on restricting security rights to be able to see the hash? besides the initial rights
Thanks again
The procedure does not transfer any database-level permissions, it only creates server-level logins – totally useless
It’s not supposed to. I’m not sure what you were expecting, but in an AG, the database-level permissions are synced by the AG. The procedure transfers *server-level* principals.
Hi,
How far are you with the other articles?
“logins, SQL Server Agent jobs, SSIS packages stored in SQL Server, linked servers and server settings”
I could use all them currently 🙂
So glad you like it! This was the tricky part, and the other ones are kind of still on my to-do list. 🙂
All right, I will hang on tight and keep an eye on your posts.
Thanks for the tip. Just a note that there’s a typo in your code – you need to add an “=” after the WITH PASSWORD clause in your section titled “Here’s how to get the information you need from the primary replica:”.
Oh, thanks! 🙂
Excellent reference. Thank you!
Thanks! Glad I could help. 🙂
I get the same error that the first response mentions. However, this is a brand new AG and all I have added was one SQL login and a windows login besides the Admin accounts.
I receive this error:
Msg 515, Level 16, State 2, Procedure SyncLogins, Line 175
Cannot insert the value NULL into column ‘sql’, table ‘@queue’; column does not allow nulls. INSERT fails.
The statement has been terminated.
Did you check if it could the same permission issue with the linked server account?
Hi, we’ve written a free (for 2 instances) tool for comparing servers which you may find useful after you’ve used these scripts. We compare a long list of configuration settings and even fix the orphaned users in the Advise module (free for 30 days).
You can download it from http://www.aireforge.com
Hi Daniel – I’m getting the following errors when running the stored procedure. I’ve attempted to turn the CHECK_POLICY off on the three users that are throwing the error but am still receiving the error. Any ideas?
Msg 15025, Level 16, State 1, Line 5
The server principal ‘##MS_PolicyEventProcessingLogin##’ already exists.
Msg 33020, Level 16, State 1, Line 7
A HASHED password cannot be set for a login that has CHECK_POLICY turned on.
Msg 33020, Level 16, State 1, Line 8
A HASHED password cannot be set for a login that has CHECK_POLICY turned on.
Msg 33020, Level 16, State 1, Line 9
A HASHED password cannot be set for a login that has CHECK_POLICY turned on.
Huh, that’s odd. Haven’t seen that before. I would try googling the error message and see if that leads you anywhere.
Did you turn off CHECK_POLICY on both/all servers?
Well look at that… that did the trick. 🙂 Thanks for being a second set of neurons Daniel!
Hi Daniel,
here in script you are fatching the name of primary replica first and then using it all over in script so it automatic fetch detail from primary replica so in here what is the use Linked server if i am executing this on local or secondry replica?
The reason I’m using a linked server to access the primary replica is because the primary replica is the “master” record for the logins. If the Availability Group fails over to another replica, that server will now be the primary – thus the need to set up the linked servers on all replicas.
Was that what you meant by your question?
Thanks Daniel ,
I got the use of linked server but one small issue I am facing at moment once i run the job which execute this procedure will move logins correct and those login have correct db level access to secondary replica but its not moving fix server role access . Please let me know in case I am missing anything in that ..I just modified your code as below -:
USE [master]
GO
create PROCEDURE [dbo].[SyncLogins_test]
@primary_replica sysname=NULL,
— @allow_drop_logins bit=0,
@print_only bit=0
AS
SET NOCOUNT ON;
DECLARE @sql nvarchar(max), @msg nvarchar(max);
IF (@primary_replica IS NULL) BEGIN;
SELECT @primary_replica=primary_replica
FROM sys.dm_hadr_availability_group_states;
IF (@@ROWCOUNT>1) BEGIN;
THROW 50000, N’More than one availability group exists on server, please specify @primary_replica.’, 16;
RETURN;
END;
IF (@primary_replica IS NULL) BEGIN;
THROW 50000, N’No availability group found, please specify @primary_replica.’, 16;
RETURN;
END;
END;
IF (@primary_replica=@@SERVERNAME) BEGIN;
PRINT N’This server is the primary replica. No changes will be made.’;
RETURN;
END;
— These are the logins (Windows user and groups, SQL logins) from
— the primary replica.
DECLARE @primaryLogins TABLE (
[name] sysname NOT NULL,
[sid] varbinary(85) NOT NULL,
[type] char(1) NOT NULL,
is_disabled bit NULL,
default_database_name sysname NULL,
default_language_name sysname NULL,
is_policy_checked bit NULL,
is_expiration_checked bit NULL,
password_hash varbinary(256) NULL,
PRIMARY KEY CLUSTERED ([sid])
);
SET @sql=N’
SELECT sp.[name], sp.[sid], sp.[type], sp.is_disabled, sp.default_database_name,
sp.default_language_name, l.is_policy_checked, l.is_expiration_checked, l.password_hash
FROM [‘+@primary_replica+’].master.sys.server_principals AS sp
LEFT JOIN [‘+@primary_replica+’].master.sys.sql_logins AS l ON sp.[sid]=l.[sid]
WHERE sp.[type] IN (”U”, ”G”, ”S”) AND
sp.[name] NOT LIKE ”NT Service\%” AND
sp.[name] NOT IN (”NT AUTHORITY\SYSTEM”) AND SP.NAME NOT IN(SELECT SP2.NAME FROM master.sys.server_principals AS sp2 )’;
INSERT INTO @primaryLogins
EXECUTE master.sys.sp_executesql @sql;
— These are the server roles on the primary replica.
DECLARE @primaryRoles TABLE (
[sid] varbinary(85) NOT NULL,
[name] sysname NOT NULL,
PRIMARY KEY CLUSTERED ([sid])
);
SET @sql=N’
SELECT sr.[sid], sr.[name]
FROM [‘+@primary_replica+’].master.sys.server_principals AS sr
WHERE sr.is_fixed_role=0 AND
sr.[type]=”R”’ ;
INSERT INTO @primaryRoles
EXECUTE master.sys.sp_executesql @sql;
— These are the role members of the server roles on
— the primary replica.
DECLARE @primaryMembers TABLE (
role_sid varbinary(85) NOT NULL,
member_sid varbinary(85) NOT NULL,
PRIMARY KEY CLUSTERED (role_sid, member_sid)
);
SET @sql=N’
SELECT r.[sid], m.[sid]
FROM [‘+@primary_replica+N’].master.sys.server_principals AS r
INNER JOIN [‘+@primary_replica+N’].master.sys.server_role_members AS rm ON r.principal_id=rm.role_principal_id
INNER JOIN [‘+@primary_replica+N’].master.sys.server_principals AS m ON rm.member_principal_id=m.principal_id’;
INSERT INTO @primaryMembers
EXECUTE master.sys.sp_executesql @sql;
— These are the server-level permissions on the
— primary replica.
DECLARE @primaryPermissions TABLE (
state_desc nvarchar(120) NOT NULL,
[permission_name] nvarchar(256) NOT NULL,
principal_name sysname NOT NULL,
PRIMARY KEY CLUSTERED ([permission_name], principal_name)
);
SET @sql=N’
SELECT p.state_desc, p.[permission_name], sp.[name]
FROM [‘+@primary_replica+’].master.sys.server_permissions AS p
INNER JOIN [‘+@primary_replica+’].master.sys.server_principals AS sp ON p.grantee_principal_id=sp.principal_id
WHERE p.class=100 AND SP.NAME NOT IN(SELECT SP2.NAME FROM master.sys.server_principals AS sp2 )’;
INSERT INTO @primaryPermissions
EXECUTE master.sys.sp_executesql @sql;
— This table variable contains the “run queue” of all commands
— we want to execute on the local (secondary) replica, ordered
— by “seq”.
DECLARE @queue TABLE (
seq int IDENTITY(1, 1) NOT NULL,
[sql] nvarchar(max) NOT NULL,
PRIMARY KEY CLUSTERED (seq)
);
— Login doesn’t exist on the secondary – CREATE.
INSERT INTO @queue ([sql])
SELECT N’
CREATE LOGIN [‘+p.[name]+’] ‘+(CASE
WHEN p.[type]=’S’
THEN N’WITH PASSWORD=0x’+CONVERT(nvarchar(max), p.password_hash, 2)+N’ HASHED, CHECK_POLICY=OFF, SID=0x’+CONVERT(nvarchar(max), p.[sid], 2)+N’, ‘
WHEN p.[type] IN (‘U’, ‘G’)
THEN N’FROM WINDOWS WITH ‘ END)+
N’DEFAULT_DATABASE=[‘+p.default_database_name+N’]’+
ISNULL(N’, DEFAULT_LANGUAGE=’+p.default_language_name, N”)+N’;’
FROM @primaryLogins AS p
WHERE p.[sid] NOT IN (SELECT [sid] FROM master.sys.server_principals) AND
p.[type] IN (‘U’, ‘G’, ‘S’); –and p.password_hash is not null;
— Login exists but has been enabled/disabled – ALTER.
INSERT INTO @queue ([sql])
SELECT N’
ALTER LOGIN [‘+ISNULL(sp.[name], x.[name])+’]’+
(CASE WHEN x.is_disabled=0 AND sp.is_disabled=1 THEN N’ ENABLE’
WHEN x.is_disabled=1 AND (sp.is_disabled=0 OR sp.[sid] IS NULL) THEN N’ DISABLE’ END)+N’;’
FROM @primaryLogins AS x
LEFT JOIN master.sys.server_principals AS sp ON x.[sid]=sp.[sid]
WHERE x.is_disabled!=sp.is_disabled OR
x.is_disabled=1 AND sp.[sid] IS NULL;
— Login exists but has changed in some respect – ALTER.
INSERT INTO @queue ([sql])
SELECT N’
ALTER LOGIN [‘+sp.[name]+’] WITH ‘+
SUBSTRING(
(CASE WHEN x.password_hash!=l.password_hash
THEN N’, PASSWORD=0x’+CONVERT(nvarchar(max), x.password_hash, 2)+N’ HASHED’
ELSE N” END)+
(CASE WHEN ISNULL(x.default_database_name, N’master’)!=ISNULL(sp.default_database_name, N’master’)
THEN ‘, DEFAULT_DATABASE=[‘+x.default_database_name+N’]’
ELSE N” END)+
(CASE WHEN x.default_language_name!=sp.default_language_name
THEN ‘, DEFAULT_LANGUAGE=’+x.default_language_name
ELSE N” END)+
(CASE WHEN x.[name]!=sp.[name]
THEN ‘, NAME=[‘+x.[name]+N’]’
ELSE N” END)+
(CASE WHEN x.is_policy_checked!=l.is_policy_checked
THEN ‘, CHECK_POLICY=’+(CASE x.is_policy_checked WHEN 1 THEN N’ON’ ELSE N’OFF’ END)
ELSE N” END)+
(CASE WHEN x.is_expiration_checked!=l.is_expiration_checked
THEN ‘, CHECK_EXPIRATION=’+(CASE x.is_expiration_checked WHEN 1 THEN N’ON’ ELSE N’OFF’ END)
ELSE N” END), 3, 10000)+N’;’
FROM @primaryLogins AS x
INNER JOIN master.sys.server_principals AS sp ON x.[sid]=sp.[sid]
LEFT JOIN master.sys.sql_logins AS l ON sp.[sid]=l.[sid]
WHERE x.password_hash!=l.password_hash OR
ISNULL(x.default_database_name, N’master’)!=ISNULL(sp.default_database_name, N’master’) OR
ISNULL(x.default_language_name, N’us_english’)!=ISNULL(sp.default_language_name, N’us_english’) OR
x.[name]!=sp.[name] OR
ISNULL(x.is_policy_checked, 0)!=ISNULL(l.is_policy_checked, 0) OR
ISNULL(x.is_expiration_checked, 0)!=ISNULL(l.is_expiration_checked, 0);
— Roles that don’t exist on the secondary – CREATE.
INSERT INTO @queue ([sql])
SELECT N’
CREATE SERVER ROLE [‘+r.[name]+N’];’
FROM @primaryRoles AS r
WHERE [sid] NOT IN (
SELECT [sid]
FROM sys.server_principals
WHERE is_fixed_role=0 AND
[type]=’R’);
— Add server role memberships:
INSERT INTO @queue ([sql])
SELECT N’
ALTER SERVER ROLE [‘+pr.[name]+N’] ADD MEMBER [‘+pl.[name]+N’];’
FROM @primaryMembers AS pm
INNER JOIN @primaryLogins AS pl ON pm.member_sid=pl.[sid]
INNER JOIN @primaryRoles AS pr ON pm.role_sid=pr.[sid]
LEFT JOIN sys.server_principals AS r ON pm.role_sid=r.[sid] AND r.[type]=’R’
LEFT JOIN sys.server_principals AS m ON pm.member_sid=m.[sid]
LEFT JOIN sys.server_role_members AS rm ON r.principal_id=rm.role_principal_id AND m.principal_id=rm.member_principal_id
WHERE rm.role_principal_id IS NULL;
——————————————————————————-
— GRANT/DENY server-level permissions:
INSERT INTO @queue ([sql])
SELECT N’
‘+pp.state_desc+N’ ‘+pp.[permission_name]+N’ TO [‘+pp.principal_name+’];’
FROM @primaryPermissions AS pp
INNER JOIN sys.server_principals AS sp ON pp.principal_name=sp.[name]
LEFT JOIN sys.server_permissions AS p ON
p.grantee_principal_id=sp.principal_id AND
p.[permission_name] COLLATE database_default=pp.[permission_name] AND
p.class=100
WHERE pp.state_desc!=p.state_desc COLLATE database_default;
——————————————————————————-
— Ready to roll:
SET @sql=N”;
SELECT @sql=@sql+[sql] FROM @queue ORDER BY seq;
— @print_only=1: PRINT the queue.
WHILE (@print_only=1 AND @sql!=N”) BEGIN;
PRINT LEFT(@sql, CHARINDEX(CHAR(13), @sql+CHAR(13))-1);
SET @sql=SUBSTRING(@sql, CHARINDEX(CHAR(13), @sql+CHAR(13))+2, LEN(@sql));
END;
— @print_only=0: Execute the queue.
IF (@print_only=0)
EXECUTE master.sys.sp_executesql @sql;
GO
Hi Daniel,
When we create a server role ‘A’ on Primary replica and create the same on the secondary replica , they both will have different SID. so when I run the synclogin proc on the secondary replica ,It gives me CREATE role A even though the role exists on the secondary replica because its comparing SIDs.
That’s a very good point – CREATE SERVER ROLE does not support the WITH SID clause to my knowledge, so there’s really no easy way to fix this.
Also when we change a server role of login the primary replica , the procedure on the secondary replica doesn’t identify that change. Where does SQL store the login and the sever role link information?
The sys.server_role_members DMV connects logins to roles.
Hi Daniel,
I did a few changes to the code as follows. Hope it helps. I am basically not using SID and using the name instead. Also in this code I am comparing the secondary node server role access with primary nodes sever role access.
——————————————————————————-
— Roles that don’t exist on the primary – DROP.
INSERT INTO @queue ([sql])
SELECT N’
DROP ROLE [‘+sp.[name]+N’];’
FROM master.sys.server_principals AS sp
WHERE is_fixed_role=0 AND
sp.[type]=’R’ AND
sp.[name] NOT IN (SELECT [name] FROM @primaryRoles);
— Roles that don’t exist on the secondary – CREATE.
INSERT INTO @queue ([sql])
SELECT N’
CREATE SERVER ROLE [‘+r.[name]+N’];’
FROM @primaryRoles AS r
WHERE [name] NOT IN (
SELECT [name]
FROM sys.server_principals
WHERE is_fixed_role=0 AND
[type]=’R’);
——————————————————————————-
— Add missing role membership
DECLARE @missingrolemembership TABLE (
[accname] sysname NOT NULL,
[rolename] sysname NOT NULL
);
SET @sql=N’SELECT sp1.name,spr1.name from [‘+@primary_replica+’].master.sys.server_principals sp1
JOIN [‘+@primary_replica+’].master.sys.server_role_members srm1 ON (sp1.principal_id = srm1.member_principal_id)
JOIN [‘+@primary_replica+’].master.sys.server_principals spr1 on (spr1.principal_id = srm1.role_principal_id)
WHERE NOT EXISTS
(
SELECT sp.name,spr.name from master.sys.server_principals sp
JOIN master.sys.server_role_members srm ON (sp.principal_id = srm.member_principal_id)
JOIN master.sys.server_principals spr on (spr.principal_id = srm.role_principal_id)
)’
INSERT INTO @missingrolemembership
EXECUTE master.sys.sp_executesql @sql;
INSERT INTO @queue ([sql])
SELECT N’
ALTER SERVER ROLE [‘+[rolename]+N’] ADD MEMBER [‘+[accname]+N’];’
FROM @missingrolemembership
——————————————————————————-
— revoke role membership that exist on secondary and not in primary
DECLARE @revokerolemembership TABLE (
[accname] sysname NOT NULL,
[rolename] sysname NOT NULL
);
SET @sql=N’SELECT sp.name,spr.name from master.sys.server_principals sp
JOIN master.sys.server_role_members srm ON (sp.principal_id = srm.member_principal_id)
JOIN master.sys.server_principals spr on (spr.principal_id = srm.role_principal_id)
WHERE NOT EXISTS
(
SELECT sp1.name,spr1.name from [‘+@primary_replica+’].master.sys.server_principals sp1
JOIN [‘+@primary_replica+’].master.sys.server_role_members srm1 ON (sp1.principal_id = srm1.member_principal_id)
JOIN [‘+@primary_replica+’].master.sys.server_principals spr1 on (spr1.principal_id = srm1.role_principal_id)
)’
INSERT INTO @revokerolemembership
EXECUTE master.sys.sp_executesql @sql;
INSERT INTO @queue ([sql])
SELECT N’
ALTER SERVER ROLE [‘+[rolename]+N’] DROP MEMBER [‘+[accname]+N’];’
FROM @revokerolemembership
— missing Server level role permission
DECLARE @missingserverrolepermission TABLE (
state_desc nvarchar(120) NOT NULL,
[permission_name] nvarchar(256) NOT NULL,
principal_name sysname NOT NULL,
PRIMARY KEY CLUSTERED ([permission_name], principal_name)
);
SET @sql=N’SELECT p.state_desc, p.[permission_name], sp.[name]
FROM [‘+@primary_replica+’].master.sys.server_permissions AS p
INNER JOIN [‘+@primary_replica+’].master.sys.server_principals AS sp ON p.grantee_principal_id=sp.principal_id
WHERE p.class=100 and sp.type =”R”
AND NOT EXISTS (
SELECT p1.state_desc, p1.[permission_name], sp1.[name]
FROM master.sys.server_permissions AS p1
INNER JOIN .master.sys.server_principals AS sp1 ON p1.grantee_principal_id=sp1.principal_id
WHERE p1.class=100 and sp1.type =”R”
AND sp1.[name] = sp.[name]
)’
—- Revoke server role permission that exist on secondary and not in primary
DECLARE @revokeserverrolepermission TABLE (
state_desc nvarchar(120) NOT NULL,
[permission_name] nvarchar(256) NOT NULL,
principal_name sysname NOT NULL,
PRIMARY KEY CLUSTERED ([permission_name], principal_name)
);
SET @sql=N’SELECT CASE WHEN p.state_desc =”GRANT” THEN ”DENY” ELSE ”GRANT” END, p.[permission_name], sp.[name]
FROM master.sys.server_permissions AS p
INNER JOIN .master.sys.server_principals AS sp ON p.grantee_principal_id=sp.principal_id
WHERE p.class=100 and sp.type =”R”
AND NOT EXISTS (
SELECT p1.state_desc, p1.[permission_name], sp1.[name]
FROM [‘+@primary_replica+’].master.sys.server_permissions AS p1
INNER JOIN [‘+@primary_replica+’].master.sys.server_principals AS sp1 ON p1.grantee_principal_id=sp1.principal_id
WHERE p1.class=100 and sp1.type =”R”
AND sp1.[name] = sp.[name]
AND p1.[permission_name] = p.permission_name
)’
INSERT INTO @revokeserverrolepermission
EXECUTE master.sys.sp_executesql @sql;
Hi Daniel.
This is a great post identifying all elements required to sync from primary to secondary.
This has been on my to-do list for a few months now.
My gotya is that we load balance across the two nodes so a password that has been changed (new password or user) is on which node? OK obviously the primary node of that Availability Group. But when you have 8 Availability Groups that are load balanced between nodes…
I now have to get down to the databases at AG Primary Node level.
I’ll post what I come up with to get feedback and at least share with you.
Thank you Omar. the inclusion of permission level grants is important as they are often missed.
Oh, yeah I haven’t had access to a load-balanced environment. I’d be very curious to see if it can be solved!
Have a nice one!
Hi Daniel – I just ran across your script. Very nicely done, however I needed to make a couple of change to support the granting/revoke of all server roles.
1. I removed the reference to is_fixed_role so that it would replicate all roles granted to a user.
2. In order for proc to be able to revoke the server roles, the drop login code needed to be at the end of the script so that it is the last thing executed – otherwise you get an error when it tries to revoke server role permissions because the login was already gone.
3. I removed the ability to copy/drop sa and any ##%## logins
Again, nicely done sir.
Thanks a lot, Patrick! I appreciate your input.
I’ll try to incorporate your additions in my code as soon as I have the time.
Hi Daniel,
Thanks for this code!
Like Patrick, I also think it is important to make sure that the sa and ## users and perhaps NO sysadmins are removed, since running with wrong linked server permissions could be catastrophic and it is one thing to lose your other logins – quite another to lose that and administrative users.
Also, would recommend not logins not be removed from the sysadmin role – I have had cases where a user is in sysadmin on both nodes but the code thinks the primary login is not and would try to remove it from sysadmin on secondary. Perhaps add another parameter @AllowRoleRemoval or have that code block follow the setting of @allow_drop_logins so that if not dropping logins we are also not removing them from roles. If this code gets run from a non sysadmin it wont see all rights and will behave destructively. I would rather know that if I want to remove a user from sysadmin I have to manually do that on both nodes.
I also put code in that checks that it sees more than a few logins before continuing just in case the linked server user permissions have changed and would cause everything to be dropped.
And I had an odd circumstance at the section of code that would print ‘This server is the primary replica. No changes will be made.’ where the @@servername returned NULL which causes the script to continue on the primary (luckily it was being run as print_only). I changed the code that looks for the primary from IF (@primary_replica=@@SERVERNAME) to: IF (select
ars.role_desc
from sys.dm_hadr_availability_replica_states ars
inner join sys.availability_groups ag
on ars.group_id = ag.group_id
where ag.name = (SELECT Name FROM sys.availability_groups)
and ars.is_local = 1) = ‘PRIMARY’
which returned the correct value for whether it is running on the primary.
Hope this helps.
Thanks again!
Thanks for your feedback! I’ll try to remember to include it next time I get the time. 🙂
Just trying to understand. What benefit the scripts above in the blog have that are not in the scripts to migrate logins provided by Microsoft? Microsoft has a prepared script here: https://support.microsoft.com/en-us/help/918992/how-to-transfer-logins-and-passwords-between-instances-of-sql-server
Well, the Microsoft script is a bit of a one-off. My solution is aimed at being a continuously running sync, so your AG replicas’ logins are always in sync if and when you need to fail over (planned or not).
Nice Article Daniel.
There is PowerShell script that automatically sync logins from Primary replica to Secondary replica. Please check the link
https://maq.guru/synchronizing-sql-server-logins-in-an-always-on-availability-group/
Congratulations for this code.
I had problems with the policy = OFF.
So, I altered part of your code, to fix the problem:
SELECT N’CREATE LOGIN [‘+sp.[name]+’] WITH PASSWORD=0x’+
CONVERT(nvarchar(max), l.password_hash, 2)+N’ HASHED, CHECK_POLICY=’+
CASE
WHEN l.is_policy_checked = 1
THEN ‘ON, SID=0x’+CONVERT(nvarchar(max), sp.[sid], 2)+’;’
ELSE ‘OFF, SID=0x’+CONVERT(nvarchar(max), sp.[sid], 2)+’;’
END IsPolicyCheck
FROM master.sys.server_principals AS sp
INNER JOIN master.sys.sql_logins AS l ON sp.[sid]=l.[sid]
WHERE sp.[type]=’S’ AND sp.is_disabled=0;
Now, this code get dynamically this configuration.
Awesome, thank you, Fabio!
Daniel thank you for this, I have been testing this on a SQL 2014 AG and noticed that it didn’t seem to clone the server roles in my environment. I went back to check them on the primary node with you code – SELECT N’CREATE SERVER ROLE [‘+sp.[name]+N’];’
FROM sys.server_principals AS sp
WHERE sp.principal_id>=100 AND
sp.[type]=’R’;
But it returns no results, select * FROM sys.server_principals shows no rows with type ‘R’
What am I doing wrong here?
Thanks
Only thing I can think of is if those server roles are default server roles (like sysadmin, public, etc). Those should obviously already exist at the secondary, hence the “WHERE principal_id>=100”. Do you actually have server roles that you’ve created yourself, with a principal_id=100″ to something like “owning_principal_id>1”?
Let me know how that works out!
Ah, I read your question again and realized that if you get no rows from sys.server_principals at all (WHERE type=’R’), maybe you don’t have sufficient permissions to view the server roles?
Like @mateorose I was also getting the error about the ##MS_PolicyEventProcessingLogin## already existing. This was not due to the policy being enabled as I had disabled it on both servers. I ended up just excluding it as shown below.
SET @sql=N’
SELECT sp.[name], sp.[sid], sp.[type], sp.is_disabled, sp.default_database_name,
sp.default_language_name, l.is_policy_checked, l.is_expiration_checked, l.password_hash
FROM [‘+@primary_replica+’].master.sys.server_principals AS sp
LEFT JOIN [‘+@primary_replica+’].master.sys.sql_logins AS l ON sp.[sid]=l.[sid]
WHERE sp.[type] IN (”U”, ”G”, ”S”) AND
UPPER(sp.[name]) NOT LIKE ”NT SERVICE\%” AND
sp.[name] NOT IN (”NT AUTHORITY\SYSTEM”, ”##MS_PolicyEventProcessingLogin##”)’;
Thanks!
Just as a side note, you can create your linked servers as the listener name so you don’t have to have multiple linked servers in scenarios where you have multiple secondaries. You just create it like any normal SQL Server based linked server (and we use Logins Current Security Context), but just make sure that you manually setup SPN’s for the listener name.
For example, if your listener is “LSNR-Example” you would do the following at a command prompt:
setspn -S MSSQLSvc/LSNR-Example.domain.com gmsa-ServiceAccount
setspn -S MSSQLSvc/LSNR-Example.domain.com:1433 gmsa-ServiceAccount
setspn -S MSSQLSvc/LSNR-Example.domain.com:Instance gmsa-ServiceAccount
setspn -S MSSQLSvc/LSNR-Example.domain.com:50456 gmsa-ServiceAccount
The above is an example of default instances and a named instance in case you have a secondary on a named instance (just make sure your port on the named instance is static and the Browser service is turned on).
That’s pretty awesome! Thanks!
Hi Daniel
I have used this a few times and its great. I have made a couple of small changes to work around a few issues
1) separated a password change from the other login changes and added CHECK_POLICY=OFF to this. CHECK_POLICY will be turned back on again with subsequent runs but this gets around errors for a hashed password change on a login with CHECK_POLICY turned on
2) put the “revoke” items before the drop login. This prevents errors when a login is dropped and then roles cant be revoked from a login that has already been dropped. Not a big problem but its nice to have the procedure complete without errors and that way I can use the job notification “when the job fails”
Thanks again
Chris
Awesome, thanks!
Is there an updated version of this that consolidates all these great ideas?
Not really. I’ll try to find the time to update the script soon.
Where is the rest of the series?
Hah, haven’t written it yet. 😀
Darn! Syncing stuff between primary and secondary is the bane of my existence.
Daniel Hi,
Its very useful scripts.
Logins are getting replicated to secondary but Roles are not.
Can you please assist, Thanks in advance
Hi,
I don’t have time to help you with this right now, but if you check the other comments you might get some help there.
Hi Daniel, fantastic script, however I am getting an error using the @exclude_logins, the rest of it works perfectly. Can’t for the life of me see if there is a syntax error when I run the proc
Msg 102, Level 15, State 1, Line 10
Incorrect syntax near ‘sp’.
any ideas?
Thanks
Col
Hi Colin,
I don’t currently have a good test environment with AGs, so this was a bug that I hadn’t found in testing. I’ve pushed an update to the script, so it should work now.
Daniel
@print_only, — 1=only print the T-SQL.
Where is the T-SQL at?
It should print the T-SQL commands at the end of the script execution.
No I do not get the script at the end
Are you checking the messages tab? The script is returned using a PRINT statement, not a SELECT (see lines 335-338 of the script).
You may also want to check that you have the latest version.
Yes I looked in Messages and this is what I see:
Commands completed successfully.
Completion time: 2020-10-20T12:21:19.9487692-07:00
— Nothing else under this
When I run a test Print it works
PRINT ‘Printing a simple Message’
I’m running Windows 2016 with SQL 2016 SP2
Hi Daniel – thanks for the script.
We have a client site using this script, and it generated this (which causes an error) –
ALTER LOGIN [sa] WITH PASSWORD=…… HASHED, CHECK_POLICY=OFF, CHECK_POLICY=OFF;
I believe this can occur from this code snippet –
INSERT INTO @queue ([sql])
SELECT N’
ALTER LOGIN [‘+sp.[name]+’] WITH ‘+
SUBSTRING(
(CASE WHEN x.password_hash!=l.password_hash
THEN N’, PASSWORD=0x’+CONVERT(nvarchar(max), x.password_hash, 2)+N’ HASHED, CHECK_POLICY=OFF’
ELSE N” END)+
(CASE WHEN ISNULL(x.default_database_name, N’master’)!=ISNULL(sp.default_database_name, N’master’)
THEN ‘, DEFAULT_DATABASE=[‘+x.default_database_name+N’]’
ELSE N” END)+
(CASE WHEN x.default_language_name!=sp.default_language_name
THEN ‘, DEFAULT_LANGUAGE=’+x.default_language_name
ELSE N” END)+
(CASE WHEN x.[name]!=sp.[name]
THEN ‘, NAME=[‘+x.[name]+N’]’
ELSE N” END)+
(CASE WHEN x.is_policy_checked!=l.is_policy_checked
THEN ‘, CHECK_POLICY=’+(CASE x.is_policy_checked WHEN 1 THEN N’ON’ ELSE N’OFF’ END)
ELSE N” END)+
(CASE WHEN x.is_expiration_checked!=l.is_expiration_checked
THEN ‘, CHECK_EXPIRATION=’+(CASE x.is_expiration_checked WHEN 1 THEN N’ON’ ELSE N’OFF’ END)
ELSE N” END), 3, 10000)+N’;’
FROM @primaryLogins AS x
INNER JOIN master.sys.server_principals AS sp ON x.[sid]=sp.[sid]
LEFT JOIN master.sys.sql_logins AS l ON sp.[sid]=l.[sid]
WHERE x.password_hash!=l.password_hash OR
ISNULL(x.default_database_name, N’master’)!=ISNULL(sp.default_database_name, N’master’) OR
ISNULL(x.default_language_name, N’us_english’)!=ISNULL(sp.default_language_name, N’us_english’) OR
x.[name]!=sp.[name] OR
ISNULL(x.is_policy_checked, 0)!=ISNULL(l.is_policy_checked, 0) OR
ISNULL(x.is_expiration_checked, 0)!=ISNULL(l.is_expiration_checked, 0);
I’m curious if anyone else has found this issue?
There’s a simple fix by unchecking “Enforce password policy” for the ‘sa’ logon on the secondary. Although it does look to me like there’s a bug in the code.
Thanks! I’ll take a look when I have the time.
In the meantime, if you stumble upon a fix for the code, feel free to create a pull request in the Github repository.
I think I fixed the problem. Update here: https://github.com/sqlsunday/ag-sync/commit/126d4b857b4cd051ba1a1ddf95667d5e0785118e
Thanks very much, we’ll give that a go.
Thanks, the fix on Github worked well. It looks like there may be an issue if logins are enabled on one server and disabled on the secondary. Should the script cater for this, or would I need to ensure logins are enabled on secondaries (if enabled on the primary)?
Great! Without looking at the script (not by my computer), a sync should also sync the ”enabled” switch..
Hi Daniel – Great script but I am unable to get it to work via a SQL Agent job, is this a known issue?
Hi Daniel,
thanks for your handy solution…it works almost perfect for me 😀
Maybe you can help me with a small problem regarding user drops.
Whenever I delete a user on my primary replica and my secondary is synching I get the following error:
Cannot drop the server principal ‘[some_user]’, because it does not exist or you do not have permission.
That is bacause the drop first drops the login and after that the associated server role.
DROP LOGIN [some_user];
ALTER SERVER ROLE [processadmin] DROP MEMBER [some_user];
Because the login is already gone it abviously cannot alter the role afterwards.
What am i missing here?
The query in my SQL job looks like follows:
EXECUTE dbo.SyncLogins
@primary_replica = ‘[TotallySecretSQLServername]\[TotallySecretInstanceName]’,
@allow_drop_logins = 1,
@check_policy = 1,
@exclude_logins = ‘some_users’
Thank you in advance for your reply.
Regard
Dennis
Is it possible to transfer the server level granular permissions like view server state etc.
You should check out the Dbatools commandlets that I mention in the post. Otherwise, you’ll probably have to roll something yourself.