What db_owner Actually Grants
db_owner is not a bundle of convenient permissions. It is functionally equivalent to CONTROL on the database, and its members operate with the same rights as the database owner (dbo) itself. That includes, among other things:
- Creating, altering, and dropping any table, view, procedure, function, or trigger
- Granting and revoking permissions to other principals, including granting db_owner to someone else
- Creating objects with
EXECUTE AS OWNER, so the code runs asdbo, not as the caller - Dropping or modifying audit-relevant objects, including the DDL triggers meant to catch this kind of activity
- Changing database-level settings, including
TRUSTWORTHY
Because db_owner is a fixed, all-encompassing role rather than a set of individually revocable grants, restricting it after the fact does not work. A db_owner member can simply re-grant whatever you took away, or route around a DENY by creating a new object owned by dbo that bypasses the caller's own permission set entirely.
The Real Problem: Ownership Chaining and EXECUTE AS OWNER
SQL Server's ownership chain means that when object A calls object B and both are owned by the same principal, permission checks on B are skipped. A db_owner member can create a procedure or trigger that explicitly runs as the owner:
CREATE PROCEDURE dbo.usp_DoSomethingHarmless
WITH EXECUTE AS OWNER
AS
BEGIN
-- anything in here runs as dbo, not as the caller
...
END;
On its own this is a normal, documented feature used for legitimate impersonation patterns. The problem is who dbo actually is. In most environments, databases are created by a setup script, a migration tool, or an admin running as themselves, which means the database owner is frequently a login that already holds sysadmin at the server level, not a scoped, database-only account.
TRUSTWORTHY set to ON and the database owner maps to a sysadmin login, any db_owner member can create an object with EXECUTE AS OWNER that reaches outside the database entirely, for example adding themselves to the sysadmin server role. The impersonation that was meant to stay inside one database crosses the trust boundary because TRUSTWORTHY tells SQL Server to honor it at the instance level too.
What This Looks Like in Practice
-- Check the actual exposure on an instance
SELECT
d.name AS database_name,
d.is_trustworthy_on,
SUSER_SNAME(d.owner_sid) AS db_owner_login,
IS_SRVROLEMEMBER('sysadmin', SUSER_SNAME(d.owner_sid)) AS owner_is_sysadmin
FROM sys.databases d
WHERE d.database_id > 4
ORDER BY d.is_trustworthy_on DESC, owner_is_sysadmin DESC;
Any row where is_trustworthy_on = 1 and owner_is_sysadmin = 1 is a database where every db_owner member is, in effect, one CREATE PROCEDURE ... WITH EXECUTE AS OWNER statement away from full instance control.
Triggers Make It Worse, Not Better
A malicious or careless db_owner member does not even need to call a procedure explicitly. A DML or DDL trigger with EXECUTE AS OWNER fires automatically on ordinary application activity, which means the elevated code path executes silently as part of routine INSERT, UPDATE, or schema-change traffic. It requires no special privilege on the caller's side to trigger, it leaves no obvious "someone ran a suspicious procedure" trail, and it survives long after whoever created it has moved on. This is precisely why db_owner is a poor fit for anyone other than the small number of people who are supposed to be able to redesign the schema: the same rights that let a legitimate developer ship a migration also let any db_owner member, malicious or compromised, plant a durable backdoor.
Why DENY and Auditing Do Not Save You Here
Because db_owner includes the right to manage permissions on the database, a member can:
- Grant themselves or anyone else additional access, including re-adding db_owner if it were ever removed
- Drop or disable DDL audit triggers that would otherwise log
CREATE PROCEDUREorCREATE TRIGGERevents - Alter or drop server audit specifications scoped to that database, where permitted
- Set
TRUSTWORTHYon a database they own, if they also holdCONTROL SERVERor the database is later moved to an instance where that combination is not locked down
Least privilege only works when the role you hand out cannot rewrite the rules it is being judged against. db_owner can.
The Fix: db_datareader, db_datawriter, and a Custom db_execute Role
Almost nobody actually needs db_owner. Applications need to read and write data and call the procedures they were built against. Developers, in the overwhelming majority of day-to-day work, need the same thing. Schema changes should go through a controlled deployment path, not through a role that is active every time the application connects.
| Role | Grants | Can create triggers/procedures? |
|---|---|---|
db_owner |
Everything (equivalent to CONTROL on the database) | Yes, and can run them as dbo |
db_datareader |
SELECT on all tables and views | No |
db_datawriter |
INSERT, UPDATE, DELETE on all tables | No |
db_execute (custom) |
EXECUTE on procedures/functions in a defined schema | No, membership alone grants no DDL rights |
db_execute is not a built-in SQL Server role. You create it once per database and grant it exactly what your application actually calls:
CREATE ROLE db_execute AUTHORIZATION dbo;
GRANT EXECUTE ON SCHEMA::dbo TO db_execute;
-- add the application login / service account
ALTER ROLE db_datareader ADD MEMBER app_service_account;
ALTER ROLE db_datawriter ADD MEMBER app_service_account;
ALTER ROLE db_execute ADD MEMBER app_service_account;
-- remove the shortcut
ALTER ROLE db_owner DROP MEMBER app_service_account;
TRUSTWORTHY or server-level roles. There is no EXECUTE AS OWNER object for a db_datareader/db_datawriter/db_execute member to create in the first place.
If a schema for a single procedure needs finer control than "everything in the schema," grant EXECUTE object by object instead of at the schema level, or move genuinely sensitive procedures into a separate schema that db_execute is not granted on.
Where Real DDL Rights Still Belong
Someone still has to deploy schema changes. That does not mean the application account, or every developer's personal login, needs standing db_owner. Keep DDL rights on a small number of scoped deployment identities, grant them only for the duration of a release where that is practical, and prefer signed procedures or a certificate-based impersonation pattern over blanket ownership when a specific operation genuinely needs elevated rights inside the database. The goal is the same one behind any least-privilege model: the permission should exist only when it is being used, not permanently, "just in case."
Migration Checklist
- Inventory current db_owner members per database with
sys.database_role_membersorsp_helprolemember 'db_owner' - Run the
is_trustworthy_on/owner_is_sysadminquery above across the instance and treat any positive hit as a priority - Create the
db_executerole per database and grant it against the schema(s) the application actually calls - Add affected logins to db_datareader, db_datawriter, and db_execute; test the application against this set in a non-production environment first
- Watch for DDL hidden inside application code paths (auto-migrations, "create table if not exists" logic); these need a separate, scoped deployment identity, not a return to db_owner
- Remove db_owner membership once testing confirms the application has no remaining dependency on it
- Set
TRUSTWORTHY OFFunless there is a documented, still-necessary reason for it being on
Integration with Control Matrix
Our Control Matrix covers least-privilege access control across SQL Server infrastructure in more depth. See also GRANT, DENY, REVOKE: Understanding SQL Server Permissions for how permission inheritance and precedence work at the object level.
The Bottom Line
db_owner is not "read/write plus a bit extra." It is full control over the database, including the ability to create code that runs as the database owner and, depending on TRUSTWORTHY and who that owner actually is, potentially as sysadmin on the instance. It cannot be safely restricted after the fact because it includes the right to undo any restriction placed on it.
For the vast majority of application accounts and day-to-day development work, db_datareader, db_datawriter, and a purpose-built db_execute role provide everything that is actually needed, and none of what makes db_owner dangerous.