Today’s post is brought to you by the letter A
Auditors.
Aversion.
Alcohol.
Ammunition.
All things that start with the letter “A” and coincidentally all seemingly related.
This past week I’ve found myself hip deep in the Auditing pool. Justifying such things as the need for Database Administrators to have sysadmin SQL Server role rights and why we can’t drop the Public role from all the SQL Server databases because too many users have rights to it.
The Orphans Must Die
For all there is to grumble about within the auditing process there are some good checks that I’ve ignored for many years that I’m now directed into looking at by these faceless and formless outside forces. Periodically checking such things as orphaned logins and orphaned users is a solid DBA responsibility. Simply telling the Auditor that these entities are safe (because in the case of the orphan login, the associated Active Directory account doesn’t exist or in the case of the orphan user there is no associated SQL Server login to map to and therefore your security chain is missing a critical link) is insufficient for them. They expect you to do something about it.
Existence is opportunity for exploitation. The orphans must die.
These first script identifies those users in each database on the instance without a corresponding login on the instance:
DECLARE @SQL nvarchar(2000) DECLARE @name nvarchar(128) DECLARE @database_id int SET NOCOUNT ON; IF NOT EXISTS (SELECT name FROM tempdb.sys.tables WHERE name like '%#orphan_users%') BEGIN CREATE TABLE #orphan_users ( database_name nvarchar(128) NOT NULL, [user_name] nvarchar(128) NOT NULL, drop_command_text nvarchar(200) NOT NULL ) END CREATE TABLE #databases (database_id int NOT NULL, database_name nvarchar(128) NOT NULL, processed bit NOT NULL) INSERT INTO #databases (database_id, database_name, processed) SELECT database_id, name, 0 FROM master.sys.databases WHERE name NOT IN ('master', 'tempdb', 'msdb', 'distribution', 'model') WHILE (SELECT COUNT(processed) FROM #databases WHERE processed = 0) > 0 BEGIN SELECT TOP 1 @name = database_name, @database_id = database_id FROM #databases WHERE processed = 0 ORDER BY database_id SELECT @SQL = 'USE [' + @name + ']; INSERT INTO #orphan_users (database_name, user_name, drop_command_text) SELECT DB_NAME(), u.name, ' + '''' + 'USE [' + @name + ']; DROP USER [' + '''' + ' + u.name + ' + '''' + '] ' + '''' + 'FROM master..syslogins l RIGHT JOIN sysusers u ON l.sid = u.sid WHERE l.sid IS NULL AND issqlrole <> 1 AND isapprole <> 1 AND ( u.name <> ' + '''' + 'INFORMATION_SCHEMA' + '''' + ' AND u.name <> ' + '''' + 'guest' + '''' + ' AND u.name <> ' + '''' + 'dbo' + '''' + ' AND u.name <> ' + '''' + 'sys' + '''' + ' AND u.name <> ' + '''' + 'system_function_schema' + '''' + ')' PRINT @SQL; EXEC sys.sp_executesql @SQL UPDATE #databases SET processed = 1 WHERE database_id = @database_id; END SELECT database_name, [user_name], drop_command_text FROM #orphan_users ORDER BY [database_name], [user_name]; DROP TABLE #databases; DROP TABLE #orphan_users; SET NOCOUNT OFF;
Slam the Door on Most of Your Guests
Likewise, there is the guest account in SQL Server. I’m not going to delve into what the guest account is. There is a fair description of it in Books Online. The important thing to note is that it is a vulnerability, but it is still utilized in master and tempdb. Our Auditors want it gone and I agree with them.
This script is quite simple in that it loops through your user databases (and hits the model database as well so you’ll not have to deal with this again in the future for new databases created on the instance while we’re at it) and creates ad-hoc SQL commands you can then run against your instance to revoke connect permissions to the guest account:
SET NOCOUNT ON; DECLARE @SQL nvarchar(2000) DECLARE @name nvarchar(128) DECLARE @database_id int CREATE TABLE #databases (database_id int NOT NULL, databasename nvarchar(128) NOT NULL, processed bit NOT NULL) INSERT INTO #databases (database_id, databasename, processed) SELECT database_id, name, 0 FROM master.sys.databases WHERE name NOT IN ('master', 'tempdb', 'msdb', 'distribution') WHILE (SELECT COUNT(processed) FROM #databases WHERE processed = 0) > 0 BEGIN SELECT TOP 1 @name = databasename, @database_id = database_id FROM #databases WHERE processed = 0 ORDER BY database_id SELECT @SQL = 'USE [' + @name + ']; REVOKE CONNECT TO [GUEST];' PRINT @SQL; UPDATE #databases SET processed = 1 WHERE database_id = @database_id; END DROP TABLE #databases; SET NOCOUNT OFF;
All Your Default Databases are Belong to Mine
Another item that I needed to address concerned logins that didn’t have an associated default database. This typically happens when the database that was assigned as a login’s default database was dropped at some point. If cleanup wasn’t done on the logins associated with the database or if the login has a corresponding user in multiple databases you’re left in this situation. This does not mean that the login has lost connectivity to the instance. If the login has rights to another database it can still connect so long as the database is explicitly set in the connection method used to tap into SQL Server’s nougatty center. What I’ve moved towards doing now when creating logins is setting their default database to tempdb and granting public rights to the login for tempdb. You will never drop tempdb. I considered this more of a housekeeping mechanism more that an audit point/security issue. What is more of an issue, but was not addressed, is the cleanup of logins that have no associated database rights.
This script identifies those logins that have no set default database. Thanks to dynamic SQL it gives you a command to run against the instance that will set tempdb as the default database for the login.
SELECT SL.name, SL.dbname, 'USE [tempdb]; ALTER LOGIN [' + SL.name + '] WITH DEFAULT_DATABASE=[tempdb]; CREATE USER [' + SL.name + '] FOR LOGIN [' + SL.name + '] WITH DEFAULT_SCHEMA = [dbo];' AS SQL_command FROM sys.[syslogins] SL LEFT JOIN sys.[databases] SD ON SL.[dbname] = SD.[name] WHERE SD.name IS NULL ORDER BY SL.[name], SL.[dbname];
A Different sa Login: The Same but Different, Like New Coke or Gallagher 2
Another audit point I agree with entirely, though it just masks the issue, is the existence of the sa login. The auditors wanted this uber-privileged account renamed. I always take it a step further. I advocate renaming it and never using it again for anything evermore no longer. Is that clear enough for you? The process for doing so is really quite simple. Elegantly so. It’s a simple ALTER LOGIN command against the sa login. Something like:
ALTER LOGIN [sa] WITH NAME = [somethingobnoxiouslyhardtofigureout_dontuse_sa2]
Role Memberships are for Closers!
The last thing I looked at that I’m divulging here is the prevalent expectation that many Independent Software Vendors (ISVs) have regarding database rights. It’s frustrating as a Database Administrator to be told we need to grant severely elevated rights such as sysadmin (allowing rights to do absolutely anything to the SQL Server instance including activities legal only in the state of Nevada) or dbcreator rights allowing the login to create and drop databases with abandon to a login so that someone can install or upgrade a product purchased from an ISV. It’s extremely cringe-worthy when the diligent DBA does some digging in the code only to find out there is a single line of code in the script to check for the rights assignment and then the script NEVER MAKES USE OF THE RIGHTS AFFORDED BY THE ROLE MEMBERSHIP?
Well, there is definitely an education opportunity there with your ISVs. One I’m not going to be able to solve for you. Likewise, just like most of you out there I find the majority of the of under my support require db_owner, db_datareader, or db_datawriter role membership for all their users. The role db_owner provides far too many rights for any non-Database Administrator. I can be more forgiving of the other two database roles mentioned in that yes, it allows users to read and alter data (respectively) for each table or view in the database when it’s probably not required. In many cases the application limits the rights to table and view access far more stringently than the database permissions do in this model.
The point the auditors and I both have is to limit the rights as much as possible. I put the onus on the Application Analysts (working with the ISVs) to justify the rights to role membership. The scripts you see below simply identify those logins (server role permissions) and users (database role permissions) with these elevated rights so I can send the information off in an Excel spreadsheet for review by those ultimately responsible for justification.
This first query identifies server role memberships:
SELECT SP_L.name AS Login_Name, SP_R.name AS Server_Role FROM master.sys.server_principals SP_L INNER JOIN master.sys.server_role_members SRM ON SP_L.principal_id = SRM.member_principal_id INNER JOIN master.sys.server_principals SP_R ON SRM.role_principal_id = SP_R.principal_id WHERE SP_R.type_desc = 'SERVER_ROLE' ORDER BY SP_R.name, SP_L.name;
This second query lists all users with role memberships in the database roles that ship with Microsoft SQL Server. It ignores user-defined database roles because those are preferable for restricting access to SQL Server databases:
DECLARE @SQL nvarchar(2000) DECLARE @name nvarchar(128) DECLARE @database_id int SET NOCOUNT ON; IF NOT EXISTS (SELECT name FROM tempdb.sys.tables WHERE name like '%#elevated_users%') BEGIN CREATE TABLE #elevated_users ( database_name nvarchar(128) NOT NULL, [user_name] nvarchar(128) NOT NULL, [database_role_name] nvarchar(128) NOT NULL ) END CREATE TABLE #databases (database_id int NOT NULL, database_name nvarchar(128) NOT NULL, processed bit NOT NULL) INSERT INTO #databases (database_id, database_name, processed) SELECT database_id, name, 0 FROM master.sys.databases WHILE (SELECT COUNT(processed) FROM #databases WHERE processed = 0) > 0 BEGIN SELECT TOP 1 @name = database_name, @database_id = database_id FROM #databases WHERE processed = 0 ORDER BY database_id SELECT @SQL = 'USE [' + @name + ']; INSERT INTO #elevated_users (database_name, user_name, database_role_name) SELECT DB_NAME(), SP_L.name AS user_name, SP_R.name AS database_role_name FROM sys.database_principals SP_L INNER JOIN sys.database_role_members SRM ON SP_L.principal_id = SRM.member_principal_id INNER JOIN sys.database_principals SP_R ON SRM.role_principal_id = SP_R.principal_id WHERE SP_R.type_desc = ' + '''' + 'DATABASE_ROLE' + '''' + ' AND SP_L.name <> ' + '''' + 'dbo' + '''' + ' AND SP_R.is_fixed_role = 1;' --PRINT @SQL; EXEC sys.sp_executesql @SQL UPDATE #databases SET processed = 1 WHERE database_id = @database_id; END SELECT database_name, [user_name], database_role_name FROM #elevated_users ORDER BY [database_name], database_role_name, [user_name]; DROP TABLE #databases; DROP TABLE #elevated_users; SET NOCOUNT OFF;
Providing Audit Pain Relief: It’s What We Can Do.
While not saving you from the painful burning sensation you get while undergoing an audit, hopefully I was able to give you some treatment here in Urgent Care that you can use immediately. I also hope you were able to see what a little bit of knowledge about the structural architecture of the SQL Server system tables and a dose of dynamic T/SQL can do to automate some of the tasks associated with the audit process.