SQL Authentication. It's the dread of auditors, and therefore the nightmare of the DBA. Connection strings sitting in the open on vulnerable servers, with something like this:
Server=I_Am_So_Vulnerable;Database=FreeForAll;User Id=Chump;
Password=ItDoesNotMatterHowComplexItIs;
Even worse, you have SA enabled, and all of your servers may just have the same SA password (at least not something like "P@ssW0rd" - I'll pause here to give you time to change it). What if someone with SA access quits? Do you know for sure that this person has no access into your network after separation? If this disgruntled ex-employee were to infiltrate and log in as SA, even the best auditing tool would likely not identify who just stole your credit card table, and who may just have a copy of the encryption key for that card-number column (assuming that you encrypt it as PCI-DSS expects)!
Now, imagine a world where you deactivate SQL logins and force everyone to use integrated security. No more application login to which everyone knows the password. No more SQL logins at all. You stop tying the 13-loop noose in that length of rope and take a few moments to once again savor the possibility of that new Tesla. Here is the look of your new connection string:
Server=No_Password_Here_Sorry;Integrated Security=true;
But then your dreams are shattered into a million pieces as you realize that there are 5500 users in the company's domain, and you imagine the list of logins that you would now spend 27 hours per day, nine days per week, maintaining. Every security request would have to come through your hands. But that is not the case! There is a cure that will make administering SQL Server security much easier than ever before, while also making it much more secure than ever before.
The answer lies in the effective use of Active Directory groups. Here are the benefits:
- AD Groups can be assigned as SQL logins and given specific permissions to databases and specific roles. This includes, but is not limited to, granting and/or denying permissions at the object level.
- The moment a user is disabled in AD, his or her access is instantly ended for every single database in every AD group in which he or she is a member.
- The group membership is used as a path to access; all activity is logged against the actual user's account.
sp_who2
and every auditing tool will show logins based on the actual end user; and DDL/DML changes will be audited to that user. No more guessing who used the application's SQL login - It's gone! For applications that run as Windows services, the service account under which the application runs is made a member of the necessary groups. - AD groups can be nested, which means that DBA sets up logins and access to just one set of AD Groups and then (In theory - thank you Mr. Spock) has no further interaction with day-to-day security management.
Effectively using AD Security to manage access to your SQL Server Databases and Servers
For our example here, I'm going to assume that we will have two levels of AD groups between the end users and the database server.
- Level One: Groups of logins to each database with different access permissions, as well as groups with specialized server-level permissions; and
- Level Two: Groups which represent job roles and server roles for those applications that have service accounts; and finally;
- End users and service accounts.
Generally, only the groups in level one need logins to SQL Server(s), and therefore DBA attention. The Level Two are members of the Level One group(s) necessary to give the correct level of access. Of course, you will need your database server to be a member of the same domain as the end users, or that trusts the domain(s) to which your end users belong.
Here's an example diagram to show what such a security topology could look like:
The diagram above seems to be complicated, but all you as DBA are responsible for are the logins to the databases and the SQL Server. In my shop, we actually have a SQL script that generates dsadd statements for the domain admins to add the groups for any database to Active directory, and a second script that adds the logins, database users and permissions to the database.
Here is the script we use to generate the DSADDs in our shop, amended to remove sensitive data.
select 'dsadd group cn=' + [name] + '_RO,ou=SQL_Groups,ou=User_Groups,dc=int,dc=SampleCompanyinc,dc=com -secgrp yes dsadd group cn=' + [name] + '_RW,ou=SQL_Groups,ou=User_Groups,dc=int,dc=SampleCompanyinc,dc=com -secgrp yes dsadd group cn=' + [name] + '_RWX,ou=SQL_Groups,ou=User_Groups,dc=int,dc=SampleCompanyinc,dc=com -secgrp yes dsadd group cn=' + [name] + '_DBO,ou=SQL_Groups,ou=User_Groups,dc=int,dc=SampleCompanyinc,dc=com -secgrp yes' from sysdatabases where not exists (select 1 from sys.server_principals where [name] like 'SampleCompany\' + sysdatabases.[name] + '_RO') and name not in ('master','model','tempdb','msdb','distribution','litespeedlocal','ReportServer','ReportserverTempDB');
The script is simply run in management studio in text mode and it detects any database where the groups do not exist as logins, and excludes system databases and common tools in our shop.
We get back a pile of commands that look like this:
dsadd group cn=SC_1_RO,ou=SQL_Groups,ou=User_Groups,dc=int,dc=SampleCompanyinc,dc=com -secgrp yes
dsadd group cn=SC_1_RW,ou=SQL_Groups,ou=User_Groups,dc=int,dc=SampleCompanyinc,dc=com -secgrp yes
dsadd group cn=SC_1_RWX,ou=SQL_Groups,ou=User_Groups,dc=int,dc=SampleCompanyinc,dc=com -secgrp yes
dsadd group cn=SC_1_DBO,ou=SQL_Groups,ou=User_Groups,dc=int,dc=SampleCompanyinc,dc=com -secgrp yes
dsadd group cn=SC_Web_RO,ou=SQL_Groups,ou=User_Groups,dc=int,dc=SampleCompanyinc,dc=com -secgrp yes
dsadd group cn=SC_Web_RW,ou=SQL_Groups,ou=User_Groups,dc=int,dc=SampleCompanyinc,dc=com -secgrp yes
dsadd group cn=SC_Web_RWX,ou=SQL_Groups,ou=User_Groups,dc=int,dc=SampleCompanyinc,dc=com -secgrp yes
dsadd group cn=SC_Web_DBO,ou=SQL_Groups,ou=User_Groups,dc=int,dc=SampleCompanyinc,dc=com -secgrp yes
dsadd group cn=Tinker1_RO,ou=SQL_Groups,ou=User_Groups,dc=int,dc=SampleCompanyinc,dc=com -secgrp yes
dsadd group cn=Tinker1_RW,ou=SQL_Groups,ou=User_Groups,dc=int,dc=SampleCompanyinc,dc=com -secgrp yes
dsadd group cn=Tinker1_RWX,ou=SQL_Groups,ou=User_Groups,dc=int,dc=SampleCompanyinc,dc=com -secgrp yes
dsadd group cn=Tinker1_DBO,ou=SQL_Groups,ou=User_Groups,dc=int,dc=SampleCompanyinc,dc=com -secgrp yes
dsadd group cn=tinker2_RO,ou=SQL_Groups,ou=User_Groups,dc=int,dc=SampleCompanyinc,dc=com -secgrp yes
dsadd group cn=tinker2_RW,ou=SQL_Groups,ou=User_Groups,dc=int,dc=SampleCompanyinc,dc=com -secgrp yes
dsadd group cn=tinker2_RWX,ou=SQL_Groups,ou=User_Groups,dc=int,dc=SampleCompanyinc,dc=com -secgrp yes
dsadd group cn=tinker2_DBO,ou=SQL_Groups,ou=User_Groups,dc=int,dc=SampleCompanyinc,dc=com -secgrp yes
What you see above is the output for four databases: Tinker1, Tinker2, SC_1 and SC_Web. We simply paste these lines into a service request to our network group (we are an ITIL-compliant shop), and the groups are created for us. When completed, we have the script that we run to make logins and database users.
NOTE: In our shop we have a group with _RWX after the database name, that gets db_datareader, db_datawriter, and an in house role called db_executor, which is granted execute to all non-sensitive stored procedures. Your customs may vary. You may need specialized permissions for sensitive data such as payroll (you don't want to be around to see some "intelligent" end user tweet your boss' salary).
Our login creation script generates a SQL script that we run on the server and the heavy-lifting is done for us. Here is that script:
SET NOCOUNT ON; go use master go select 'if exists(select 1 from sys.database_principals where type_desc = ''DATABASE_ROLE'' and [name] = ''db_executor'') print ''db_executor role already exists in '' + db_name() else print ''CREATE ROLE [db_executor] AUTHORIZATION [dbo]'' CREATE ROLE [db_executor] AUTHORIZATION [dbo] GO CREATE LOGIN [SampleCompany\' + [name] + '_DBO] FROM WINDOWS WITH DEFAULT_DATABASE=[tempdb] GO USE [' + [name] + '] GO CREATE USER [SampleCompany\' + [name] + '_DBO] FOR LOGIN [SampleCompany\' + [name] + '_DBO] GO USE [' + [name] + '] GO EXEC sp_addrolemember N''db_datareader'', N''SampleCompany\' + [name] + '_DBO'' GO USE [' + [name] + '] GO EXEC sp_addrolemember N''db_datawriter'', N''SampleCompany\' + [name] + '_DBO'' GO USE [' + [name] + '] GO EXEC sp_addrolemember N''db_executor'', N''SampleCompany\' + [name] + '_DBO'' GO USE [' + [name] + '] GO EXEC sp_addrolemember N''db_owner'', N''SampleCompany\' + [name] + '_DBO'' GO USE [master] GO CREATE LOGIN [SampleCompany\' + [name] + '_RWX] FROM WINDOWS WITH DEFAULT_DATABASE=[tempdb] GO USE [' + [name] + '] GO CREATE USER [SampleCompany\' + [name] + '_RWX] FOR LOGIN [SampleCompany\' + [name] + '_RWX] GO USE [' + [name] + '] GO EXEC sp_addrolemember N''db_datareader'', N''SampleCompany\' + [name] + '_RWX'' GO USE [' + [name] + '] GO EXEC sp_addrolemember N''db_datawriter'', N''SampleCompany\' + [name] + '_RWX'' GO USE [' + [name] + '] GO EXEC sp_addrolemember N''db_executor'', N''SampleCompany\' + [name] + '_RWX'' GO USE [master] GO CREATE LOGIN [SampleCompany\' + [name] + '_RW] FROM WINDOWS WITH DEFAULT_DATABASE=[tempdb] GO USE [' + [name] + '] GO CREATE USER [SampleCompany\' + [name] + '_RW] FOR LOGIN [SampleCompany\' + [name] + '_RW] GO USE [' + [name] + '] GO EXEC sp_addrolemember N''db_datareader'', N''SampleCompany\' + [name] + '_RW'' GO USE [' + [name] + '] GO EXEC sp_addrolemember N''db_datawriter'', N''SampleCompany\' + [name] + '_RW'' GO USE [master] GO CREATE LOGIN [SampleCompany\' + [name] + '_RO] FROM WINDOWS WITH DEFAULT_DATABASE=[tempdb] GO USE [' + [name] + '] GO CREATE USER [SampleCompany\' + [name] + '_RO] FOR LOGIN [SampleCompany\' + [name] + '_RO] GO USE [' + [name] + '] GO EXEC sp_addrolemember N''db_datareader'', N''SampleCompany\' + [name] + '_RO'' GO' from sysdatabases where not exists (select 1 from sys.server_principals where [name] like 'SampleCompany\' + sysdatabases.[name] + '_RO') and name not in ('master','model','tempdb','msdb','litespeedlocal','distribution','ReportServer','ReportserverTempDB');
We run the script in text mode, and we get a SQL Script that we paste into a new Management studio window and fire off. The generated script:
if exists(select 1 from sys.database_principals where type_desc = 'DATABASE_ROLE' and [name] = 'db_executor') print 'db_executor role already exists in ' + db_name() else print 'CREATE ROLE [db_executor] AUTHORIZATION [dbo]' CREATE ROLE [db_executor] AUTHORIZATION [dbo] GO CREATE LOGIN [SampleCompany\SC_1_DBO] FROM WINDOWS WITH DEFAULT_DATABASE=[tempdb] GO USE [SC_1] GO CREATE USER [SampleCompany\SC_1_DBO] FOR LOGIN [SampleCompany\SC_1_DBO] GO USE [SC_1] GO EXEC sp_addrolemember N'db_datareader', N'SampleCompany\SC_1_DBO' GO USE [SC_1] GO EXEC sp_addrolemember N'db_datawriter', N'SampleCompany\SC_1_DBO' GO USE [SC_1] GO EXEC sp_addrolemember N'db_executor', N'SampleCompany\SC_1_DBO' GO USE [SC_1] GO EXEC sp_addrolemember N'db_owner', N'SampleCompany\SC_1_DBO' GO USE [master] GO CREATE LOGIN [SampleCompany\SC_1_RWX] FROM WINDOWS WITH DEFAULT_DATABASE=[tempdb] GO USE [SC_1] GO CREATE USER [SampleCompany\SC_1_RWX] FOR LOGIN [SampleCompany\SC_1_RWX] GO USE [SC_1] GO EXEC sp_addrolemember N'db_datareader', N'SampleCompany\SC_1_RWX' GO USE [SC_1] GO EXEC sp_addrolemember N'db_datawriter', N'SampleCompany\SC_1_RWX' GO USE [SC_1] GO EXEC sp_addrolemember N'db_executor', N'SampleCompany\SC_1_RWX' GO USE [master] GO CREATE LOGIN [SampleCompany\SC_1_RW] FROM WINDOWS WITH DEFAULT_DATABASE=[tempdb] GO USE [SC_1] GO CREATE USER [SampleCompany\SC_1_RW] FOR LOGIN [SampleCompany\SC_1_RW] GO USE [SC_1] GO EXEC sp_addrolemember N'db_datareader', N'SampleCompany\SC_1_RW' GO USE [SC_1] GO EXEC sp_addrolemember N'db_datawriter', N'SampleCompany\SC_1_RW' GO USE [master] GO CREATE LOGIN [SampleCompany\SC_1_RO] FROM WINDOWS WITH DEFAULT_DATABASE=[tempdb] GO USE [SC_1] GO CREATE USER [SampleCompany\SC_1_RO] FOR LOGIN [SampleCompany\SC_1_RO] GO USE [SC_1] GO EXEC sp_addrolemember N'db_datareader', N'SampleCompany\SC_1_RO' GO if exists(select 1 from sys.database_principals where type_desc = 'DATABASE_ROLE' and [name] = 'db_executor') print 'db_executor role already exists in ' + db_name() else print 'CREATE ROLE [db_executor] AUTHORIZATION [dbo]' CREATE ROLE [db_executor] AUTHORIZATION [dbo] GO CREATE LOGIN [SampleCompany\SC_Web_DBO] FROM WINDOWS WITH DEFAULT_DATABASE=[tempdb] GO USE [SC_Web] GO CREATE USER [SampleCompany\SC_Web_DBO] FOR LOGIN [SampleCompany\SC_Web_DBO] GO USE [SC_Web] GO EXEC sp_addrolemember N'db_datareader', N'SampleCompany\SC_Web_DBO' GO USE [SC_Web] GO EXEC sp_addrolemember N'db_datawriter', N'SampleCompany\SC_Web_DBO' GO USE [SC_Web] GO EXEC sp_addrolemember N'db_executor', N'SampleCompany\SC_Web_DBO' GO USE [SC_Web] GO EXEC sp_addrolemember N'db_owner', N'SampleCompany\SC_Web_DBO' GO USE [master] GO CREATE LOGIN [SampleCompany\SC_Web_RWX] FROM WINDOWS WITH DEFAULT_DATABASE=[tempdb] GO USE [SC_Web] GO CREATE USER [SampleCompany\SC_Web_RWX] FOR LOGIN [SampleCompany\SC_Web_RWX] GO USE [SC_Web] GO EXEC sp_addrolemember N'db_datareader', N'SampleCompany\SC_Web_RWX' GO USE [SC_Web] GO EXEC sp_addrolemember N'db_datawriter', N'SampleCompany\SC_Web_RWX' GO USE [SC_Web] GO EXEC sp_addrolemember N'db_executor', N'SampleCompany\SC_Web_RWX' GO USE [master] GO CREATE LOGIN [SampleCompany\SC_Web_RW] FROM WINDOWS WITH DEFAULT_DATABASE=[tempdb] GO USE [SC_Web] GO CREATE USER [SampleCompany\SC_Web_RW] FOR LOGIN [SampleCompany\SC_Web_RW] GO USE [SC_Web] GO EXEC sp_addrolemember N'db_datareader', N'SampleCompany\SC_Web_RW' GO USE [SC_Web] GO EXEC sp_addrolemember N'db_datawriter', N'SampleCompany\SC_Web_RW' GO USE [master] GO CREATE LOGIN [SampleCompany\SC_Web_RO] FROM WINDOWS WITH DEFAULT_DATABASE=[tempdb] GO USE [SC_Web] GO CREATE USER [SampleCompany\SC_Web_RO] FOR LOGIN [SampleCompany\SC_Web_RO] GO USE [SC_Web] GO EXEC sp_addrolemember N'db_datareader', N'SampleCompany\SC_Web_RO' GO if exists(select 1 from sys.database_principals where type_desc = 'DATABASE_ROLE' and [name] = 'db_executor') print 'db_executor role already exists in ' + db_name() else print 'CREATE ROLE [db_executor] AUTHORIZATION [dbo]' CREATE ROLE [db_executor] AUTHORIZATION [dbo] GO CREATE LOGIN [SampleCompany\Tinker1_DBO] FROM WINDOWS WITH DEFAULT_DATABASE=[tempdb] GO USE [Tinker1] GO CREATE USER [SampleCompany\Tinker1_DBO] FOR LOGIN [SampleCompany\Tinker1_DBO] GO USE [Tinker1] GO EXEC sp_addrolemember N'db_datareader', N'SampleCompany\Tinker1_DBO' GO USE [Tinker1] GO EXEC sp_addrolemember N'db_datawriter', N'SampleCompany\Tinker1_DBO' GO USE [Tinker1] GO EXEC sp_addrolemember N'db_executor', N'SampleCompany\Tinker1_DBO' GO USE [Tinker1] GO EXEC sp_addrolemember N'db_owner', N'SampleCompany\Tinker1_DBO' GO USE [master] GO CREATE LOGIN [SampleCompany\Tinker1_RWX] FROM WINDOWS WITH DEFAULT_DATABASE=[tempdb] GO USE [Tinker1] GO CREATE USER [SampleCompany\Tinker1_RWX] FOR LOGIN [SampleCompany\Tinker1_RWX] GO USE [Tinker1] GO EXEC sp_addrolemember N'db_datareader', N'SampleCompany\Tinker1_RWX' GO USE [Tinker1] GO EXEC sp_addrolemember N'db_datawriter', N'SampleCompany\Tinker1_RWX' GO USE [Tinker1] GO EXEC sp_addrolemember N'db_executor', N'SampleCompany\Tinker1_RWX' GO USE [master] GO CREATE LOGIN [SampleCompany\Tinker1_RW] FROM WINDOWS WITH DEFAULT_DATABASE=[tempdb] GO USE [Tinker1] GO CREATE USER [SampleCompany\Tinker1_RW] FOR LOGIN [SampleCompany\Tinker1_RW] GO USE [Tinker1] GO EXEC sp_addrolemember N'db_datareader', N'SampleCompany\Tinker1_RW' GO USE [Tinker1] GO EXEC sp_addrolemember N'db_datawriter', N'SampleCompany\Tinker1_RW' GO USE [master] GO CREATE LOGIN [SampleCompany\Tinker1_RO] FROM WINDOWS WITH DEFAULT_DATABASE=[tempdb] GO USE [Tinker1] GO CREATE USER [SampleCompany\Tinker1_RO] FOR LOGIN [SampleCompany\Tinker1_RO] GO USE [Tinker1] GO EXEC sp_addrolemember N'db_datareader', N'SampleCompany\Tinker1_RO' GO if exists(select 1 from sys.database_principals where type_desc = 'DATABASE_ROLE' and [name] = 'db_executor') print 'db_executor role already exists in ' + db_name() else print 'CREATE ROLE [db_executor] AUTHORIZATION [dbo]' CREATE ROLE [db_executor] AUTHORIZATION [dbo] GO CREATE LOGIN [SampleCompany\tinker2_DBO] FROM WINDOWS WITH DEFAULT_DATABASE=[tempdb] GO USE [tinker2] GO CREATE USER [SampleCompany\tinker2_DBO] FOR LOGIN [SampleCompany\tinker2_DBO] GO USE [tinker2] GO EXEC sp_addrolemember N'db_datareader', N'SampleCompany\tinker2_DBO' GO USE [tinker2] GO EXEC sp_addrolemember N'db_datawriter', N'SampleCompany\tinker2_DBO' GO USE [tinker2] GO EXEC sp_addrolemember N'db_executor', N'SampleCompany\tinker2_DBO' GO USE [tinker2] GO EXEC sp_addrolemember N'db_owner', N'SampleCompany\tinker2_DBO' GO USE [master] GO CREATE LOGIN [SampleCompany\tinker2_RWX] FROM WINDOWS WITH DEFAULT_DATABASE=[tempdb] GO USE [tinker2] GO CREATE USER [SampleCompany\tinker2_RWX] FOR LOGIN [SampleCompany\tinker2_RWX] GO USE [tinker2] GO EXEC sp_addrolemember N'db_datareader', N'SampleCompany\tinker2_RWX' GO USE [tinker2] GO EXEC sp_addrolemember N'db_datawriter', N'SampleCompany\tinker2_RWX' GO USE [tinker2] GO EXEC sp_addrolemember N'db_executor', N'SampleCompany\tinker2_RWX' GO USE [master] GO CREATE LOGIN [SampleCompany\tinker2_RW] FROM WINDOWS WITH DEFAULT_DATABASE=[tempdb] GO USE [tinker2] GO CREATE USER [SampleCompany\tinker2_RW] FOR LOGIN [SampleCompany\tinker2_RW] GO USE [tinker2] GO EXEC sp_addrolemember N'db_datareader', N'SampleCompany\tinker2_RW' GO USE [tinker2] GO EXEC sp_addrolemember N'db_datawriter', N'SampleCompany\tinker2_RW' GO USE [master] GO CREATE LOGIN [SampleCompany\tinker2_RO] FROM WINDOWS WITH DEFAULT_DATABASE=[tempdb] GO USE [tinker2] GO CREATE USER [SampleCompany\tinker2_RO] FOR LOGIN [SampleCompany\tinker2_RO] GO USE [tinker2] GO EXEC sp_addrolemember N'db_datareader', N'SampleCompany\tinker2_RO' GO
And, yes, I know - way too many USE statements. An artefact of copy-and-paste to build the script generator. If it ain't broke, don't fix it. If you feel that you can knock off the rough edges and make a better Mona Lisa, feel free. I've attached both scripts to this article.
Reap the Benefits After the Groups are In Place
Once the AD groups are in place for permitted operations by database, then it is now the job of the domain admins -- not you -- with input and guidance from business stakeholders, to determine what roles have what access. By setting up groups of people with a specific role, the domain admins do for themselves what you've done for DBA: they reduce the amount of login work necessary to provision any end-user. Once you bring up a database with the AD groups suggested in this article, you usually have no further work on access security. If some user needs read-only permission to database XYZ on server ABC, that is now handled by the domain admins (who may be secretly remaking the 13-loop noose they found in your trash can after you drive home in your new Tesla, as auditors throw rose petals in front of your car).
In the end, eliminating SQL logins and going with AD groups buys the DBA (and his or her company) the following major benefits:
- Security - No more open passwords to get into SQL server; connection strings reveal nothing; and
- Efficiency - Terminated users lose access as soon as the AD profile is disabled; and
- Auditability - No matter how many levels of AD Group abstraction exist between the end user and the database, SQL server sees the actual end-user's login for every operation. If you have SQL Audit properly enabled (2008+) or if you have an auditing tool, every operation should be captured by the login that executed it. Of course, users who leave their passwords on sticky notes on the fronts of their monitors are (hopefully) not your problem.
- Convenience - Easy scripted setup of security for database; access past that level determined by sysadmins and business stakeholders.
Conclusion
Make your life easier and your sleep sounder. Do something that will make your CIO smile and remember you when it's time to give out annual pay increases. This simple measure can be easily planned and implemented shop-wide in phases, and allow you to work towards the ultimate goal: deactivating SQL authentication on every production server. It's a big dream, but dream big to accomplish big results.
-----
John F. Tamburo is the Chief Database Administrator for Landauer, Inc., the world's leading authority on radiation measurement, physics and education. John can be found at @SQLBlimp on Twitter. John also blogs at www.sqlblimp.com