/*============================================================================== Ensure-SysadminLogin.sql ---------------------------------------------------------------------------- Idempotently ensures a SQL Server *login* exists, has a known password, is enabled and unlocked, and is a member of the sysadmin fixed server role. Target: SQL Server 2022 (16.x). Also correct on 2012+; the only 2022-specific content is the granular-role note at the end of the file. ON THE NAME "SA GROUP" ---------------------- SQL Server has no group called "SA". `sa` is a single built-in *login*. The privilege people mean by "sa rights" comes from the **sysadmin fixed server role**, of which `sa` is merely one member. This script therefore adds the login to `sysadmin`. * sysadmin bypasses ALL permission checks. Members can do anything on the instance, including dropping databases and reading every credential the service account can decrypt. Grant it deliberately. * ALTER SERVER ROLE is the supported syntax. sp_addsrvrolemember is deprecated and is not used here. * SQL Server 2022 adds granular fixed server roles that cover most reasons people reach for sysadmin. See the note at the end of this file. USAGE ----- SSMS / Azure Data Studio / sqlcmd -i : edit the PARAMETERS block below and run. Interactively with no password on disk or in shell history: pwsh ./Ensure-SysadminLogin.ps1 -ServerInstance wonder-sql-vd03 -LoginName svc_example The .ps1 wrapper discards everything above the --#PARAMS-END sentinel and runs the remainder through sp_executesql with real SqlParameters, so the password never appears in the command line, the SQL text, a temp file, or a trace. Do not remove that sentinel, and do not declare the parameter variables below it -- either change silently breaks the wrapper. REQUIRES -------- A caller holding ALTER ANY LOGIN and CONTROL SERVER (in practice, an existing sysadmin). Anything less cannot grant sysadmin membership. SAFETY ------ * Wrapped in a transaction with XACT_ABORT; rolls back on any error. * Refuses to touch a principal that is not a SQL login. A Windows login or group of the same name has no password to set, and silently skipping that would be worse than failing. * The password is escaped via QUOTENAME rather than concatenated raw, and is never printed by this script. ==============================================================================*/ /*------------------------------------------------------------------------------ PARAMETERS -- edit these two (three if the password policy is in the way). A single quote in the password must be DOUBLED here, because you are editing a T-SQL string literal: pa'ss -> N'pa''ss' Get that wrong and the literal ends early: the password silently becomes the truncated prefix, and the remainder of your text is parsed as SQL and executed. The .ps1 wrapper binds the password as a parameter and so has no such rule -- another reason to prefer it for anything but a quick manual run. ------------------------------------------------------------------------------*/ DECLARE @LoginName sysname = N'wwadmin'; DECLARE @Password nvarchar(128) = N'SomeStr0ng!Passw0rd'; /* CHECK_POLICY = 1 enforces the Windows password policy (complexity, history, lockout) against this login. Leave it on unless a policy you do not control is blocking a service account. Turning it OFF also forces CHECK_EXPIRATION off, and toggling it OFF then ON later clears the password history. */ DECLARE @CheckPolicy bit = 1; /* Set to 0 to create/fix the login but NOT grant sysadmin -- useful when only the password-reset half is wanted. */ DECLARE @GrantSysadmin bit = 1; DECLARE @DefaultDatabase sysname = N'master'; --#PARAMS-END /*============================================================================== No editing required below this line. ==============================================================================*/ SET NOCOUNT ON; SET XACT_ABORT ON; DECLARE @sql nvarchar(max); DECLARE @msg nvarchar(400); DECLARE @type char(1); DECLARE @isDisabled bit; DECLARE @created bit = 0; DECLARE @roleAdded bit = 0; ------------------------------------------------------------------------------ -- 0. Validate inputs before touching anything. ------------------------------------------------------------------------------ IF @LoginName IS NULL OR LTRIM(RTRIM(@LoginName)) = N'' OR @LoginName = N'CHANGE_ME' THROW 50001, 'LoginName was not supplied. Edit the PARAMETERS block, or use the .ps1 wrapper.', 1; IF @Password IS NULL OR @Password = N'' OR @Password = N'CHANGE_ME' THROW 50002, 'Password was not supplied. Edit the PARAMETERS block, or use the .ps1 wrapper.', 1; -- A backslash means a Windows principal (DOMAIN\user). Those have no SQL -- password, so the caller has almost certainly confused the two login types. IF CHARINDEX(N'\', @LoginName) > 0 THROW 50003, 'LoginName looks like a Windows principal (contains a backslash). This script manages SQL Authentication logins only.', 1; ------------------------------------------------------------------------------ -- 1. Can the instance even serve a SQL login? -- IsIntegratedSecurityOnly = 1 means Windows Authentication mode. The login -- can still be created, but nobody will ever connect with it -- so say so -- rather than reporting a success the caller cannot use. ------------------------------------------------------------------------------ IF CONVERT(int, SERVERPROPERTY('IsIntegratedSecurityOnly')) = 1 RAISERROR('WARNING: instance is in Windows Authentication mode. The SQL login will be created but CANNOT be used to connect until Mixed Mode is enabled and the instance is restarted.', 10, 1) WITH NOWAIT; ------------------------------------------------------------------------------ -- 2. Inspect any existing principal of this name. ------------------------------------------------------------------------------ SELECT @type = sp.type, @isDisabled = sp.is_disabled FROM sys.server_principals AS sp WHERE sp.name = @LoginName; IF @type IS NOT NULL AND @type <> 'S' BEGIN SET @msg = FORMATMESSAGE( 'A server principal named "%s" already exists but is of type "%s" (S=SQL login, U=Windows login, G=Windows group, R=server role, C=certificate-mapped, K=asymmetric-key-mapped). Refusing to proceed: only a SQL login has a password this script can set.', @LoginName, @type); THROW 50004, @msg, 1; END BEGIN TRANSACTION; ------------------------------------------------------------------------------ -- 3. Create, or reset the password. -- QUOTENAME with a doubled-quote delimiter is what makes the password safe -- to embed: a password containing a single quote would otherwise terminate -- the literal and inject SQL. ------------------------------------------------------------------------------ IF @type IS NULL BEGIN SET @sql = N'CREATE LOGIN ' + QUOTENAME(@LoginName) + N' WITH PASSWORD = ' + QUOTENAME(@Password, '''') + N', DEFAULT_DATABASE = ' + QUOTENAME(@DefaultDatabase) + N', CHECK_POLICY = ' + CASE WHEN @CheckPolicy = 1 THEN N'ON' ELSE N'OFF' END + CASE WHEN @CheckPolicy = 1 THEN N', CHECK_EXPIRATION = OFF' ELSE N'' END + N';'; EXEC sys.sp_executesql @sql; SET @created = 1; RAISERROR('Created SQL login.', 10, 1) WITH NOWAIT; END ELSE BEGIN -- Policy first: a password that satisfies the new policy may violate the -- old one, so the flag has to move before the password does. SET @sql = N'ALTER LOGIN ' + QUOTENAME(@LoginName) + N' WITH CHECK_POLICY = ' + CASE WHEN @CheckPolicy = 1 THEN N'ON' ELSE N'OFF' END + N';'; EXEC sys.sp_executesql @sql; -- UNLOCK clears an account locked out by failed logins. It is only legal -- alongside a password change, which is exactly what is happening here, -- and only meaningful while CHECK_POLICY is ON. SET @sql = N'ALTER LOGIN ' + QUOTENAME(@LoginName) + N' WITH PASSWORD = ' + QUOTENAME(@Password, '''') + CASE WHEN @CheckPolicy = 1 THEN N' UNLOCK' ELSE N'' END + N';'; EXEC sys.sp_executesql @sql; SET @sql = N'ALTER LOGIN ' + QUOTENAME(@LoginName) + N' WITH DEFAULT_DATABASE = ' + QUOTENAME(@DefaultDatabase) + N';'; EXEC sys.sp_executesql @sql; RAISERROR('Login already existed; password reset and unlocked.', 10, 1) WITH NOWAIT; END ------------------------------------------------------------------------------ -- 4. Enable it. A disabled login will not connect no matter how good its -- password or how high its role membership. ------------------------------------------------------------------------------ IF @created = 0 AND @isDisabled = 1 BEGIN SET @sql = N'ALTER LOGIN ' + QUOTENAME(@LoginName) + N' ENABLE;'; EXEC sys.sp_executesql @sql; RAISERROR('Login was disabled; re-enabled.', 10, 1) WITH NOWAIT; END ------------------------------------------------------------------------------ -- 5. sysadmin membership -- the thing "SA group" actually means. -- ADD MEMBER is already idempotent; the explicit check exists so the summary -- below can distinguish "granted now" from "already had it". ------------------------------------------------------------------------------ IF @GrantSysadmin = 1 BEGIN IF NOT EXISTS ( SELECT 1 FROM sys.server_role_members AS srm JOIN sys.server_principals AS r ON r.principal_id = srm.role_principal_id JOIN sys.server_principals AS m ON m.principal_id = srm.member_principal_id WHERE r.name = N'sysadmin' AND m.name = @LoginName) BEGIN SET @sql = N'ALTER SERVER ROLE [sysadmin] ADD MEMBER ' + QUOTENAME(@LoginName) + N';'; EXEC sys.sp_executesql @sql; SET @roleAdded = 1; RAISERROR('Added to the sysadmin fixed server role.', 10, 1) WITH NOWAIT; END ELSE RAISERROR('Already a member of sysadmin; no change.', 10, 1) WITH NOWAIT; END COMMIT TRANSACTION; ------------------------------------------------------------------------------ -- 6. Verify from the catalog, not from what the script believes it just did. ------------------------------------------------------------------------------ SELECT LoginName = sp.name, LoginType = sp.type_desc, Enabled = CASE WHEN sp.is_disabled = 1 THEN 'NO' ELSE 'YES' END, IsSysadmin = CASE WHEN IS_SRVROLEMEMBER(N'sysadmin', sp.name) = 1 THEN 'YES' ELSE 'NO' END, PolicyChecked = CASE WHEN sl.is_policy_checked = 1 THEN 'YES' ELSE 'NO' END, ExpirationChecked = CASE WHEN sl.is_expiration_checked = 1 THEN 'YES' ELSE 'NO' END, LockedOut = CASE WHEN LOGINPROPERTY(sp.name, 'IsLocked') = 1 THEN 'YES' ELSE 'NO' END, MustChange = CASE WHEN LOGINPROPERTY(sp.name, 'IsMustChange') = 1 THEN 'YES' ELSE 'NO' END, DefaultDatabase = sp.default_database_name, PasswordLastSet = LOGINPROPERTY(sp.name, 'PasswordLastSetTime'), Action = CASE WHEN @created = 1 THEN 'CREATED' ELSE 'UPDATED' END + CASE WHEN @roleAdded = 1 THEN ' +SYSADMIN' ELSE '' END FROM sys.server_principals AS sp LEFT JOIN sys.sql_logins AS sl ON sl.principal_id = sp.principal_id WHERE sp.name = @LoginName; /*============================================================================== IS SYSADMIN ACTUALLY WHAT YOU WANT? On SQL Server 2022 these fixed server roles cover most reasons people reach for sysadmin, without granting the ability to destroy the instance: ##MS_ServerStateReader## read DMVs / performance state ##MS_ServerPerformanceStateReader## the performance subset of the above ##MS_DatabaseConnector## CONNECT to every database ##MS_DefinitionReader## read metadata / object definitions ##MS_DatabaseManager## create and drop databases ##MS_LoginManager## create and drop logins ##MS_SecurityDefinitionReader## read security metadata Same syntax: ALTER SERVER ROLE [##MS_DatabaseConnector##] ADD MEMBER [x]; A monitoring, reporting, or read-only application account almost never needs sysadmin. An application that owns exactly one database wants db_owner in that database instead -- a server-level role is the wrong tool for it. ==============================================================================*/