Search code examples
sqlcertificatesql-agent-job

SQL Certificate Permissions to Run Jobs


We have a need where a user with low permissions can kick off 1 job, through a proc. So, we:

  1. created a proc (in master db) to kick off the job
  2. created a certificate and signed the proc with the certificate
  3. created a login from the certificate and gave higher level access to that login
  4. created a role in the master db and granted EXECUTE to that one proc for this role
  5. put the initial user with low permissions into this new role

My question is why will this not work? We use something similar to allow a user to restore a database on one of our servers. The only way to get this to work for me right now is to give the low permissions user higher permissions in msdb db. This obviously isn't ideal and undoes exactly what we're trying to avoid.

The sample permissions code is below and then the proc code is below that.

USE [master];
GO

CREATE CERTIFICATE [TestCert]
    ENCRYPTION BY PASSWORD = N'ComplicatedPassword'
    WITH SUBJECT = N'Certificate to allow user to kick off one job';
GO

ADD SIGNATURE
    TO [dbo].[ProcThatCallsJob] 
    BY CERTIFICATE [TestCert]
    WITH PASSWORD = N'ComplicatedPassword';
GO

CREATE LOGIN [TestLoginFromCert] FROM CERTIFICATE [TestCert]
GO

ALTER SERVER ROLE [sysadmin] ADD MEMBER [TestLoginFromCert]
GO

CREATE ROLE [RoleToExecuteProc]

GO

GRANT EXECUTE ON OBJECT::[dbo].[ProcThatCallsJob]
    TO [RoleToExecuteProc];
GO

CREATE USER [TestUser_LowPermissions]  
FOR LOGIN [TestUser]
GO

ALTER ROLE [RoleToExecuteProc] ADD MEMBER [TestUser_LowPermissions]
GO

USE [msdb]
GO

CREATE USER [TestUserFromCert]  
FOR LOGIN [TestLoginFromCert]
GO  

ALTER ROLE [db_datareader] ADD MEMBER [BDWReportUserRunJobs]
GO
ALTER ROLE [SQLAgentOperatorRole] ADD MEMBER [BDWReportUserRunJobs]
GO

The proc, [dbo].[ProcThatCallsJob]:

USE [master]
GO

CREATE PROCEDURE [dbo].[ProcThatCallsJob]
AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;

    DECLARE @sql NVARCHAR(MAX);

    SET @sql = N'USE msdb
                 EXEC msdb.dbo.sp_start_job @job_name = ''10171_TestJob''
                '
    EXECUTE sp_executesql @sql;

END

Is the issue in any way related to the fact that the proc is signed with a certificate in master db (associated with a login with sysadmin privileges)? I can put the certificate in msdb db, but I wouldn't have anything to sign it to there.

EDIT - I created the proc and certificate in msdb db as a test of my hypothesis that change databases was causing some concern. No luck. The [TestUser_LowPermissions] still gets an error that they cannot execute sp_start_job. So, I'm still at a loss as to why the signed proc is not inheriting the [TestLoginFromCert] permissions of sysadmin.


Solution

  • From MSDN forum:

    Everything you did so far was correct in itself. The problem lies within the nested system stored procedures themselves.

    The thing is: sp_start_job is doing another permission/role check, hard-coded:

    -- Check permissions beyond what's checked by the sysjobs_view -- SQLAgentReader role can see all jobs but -- cannot start/stop jobs they do not own IF (@job_owner_sid <> SUSER_SID() -- does not own the job AND (ISNULL(IS_SRVROLEMEMBER(N'sysadmin'), 0) = 0) -- is not sysadmin AND (ISNULL(IS_MEMBER(N'SQLAgentOperatorRole'), 0) = 0)) -- is not SQLAgentOperatorRole BEGIN RAISERROR(14393, -1, -1);
    RETURN(1) -- Failure END

    Since you don’t want to mess with system code, there are not many options left: 1. Work with Impersonation to sa: highly risky 2. Use a Trustworthy database – I personally dislike openly saying that a lot, because it is hard enough to educate the average users NOT to use this concept. But you can secure this if it is your very own code-only database 3. Use a combination of [SQLAgentOperatorRole] and your own procedure. Meaning: Make the user member of that role and then deny execute on sp_startjob (and other procs). This way you can grant the execute via your own proc and circumvent the hard-coded check by giving him the [SQLAgentOperatorRole].