Chapter 24 · Security, Raw SQL, Secrets, Authorization Boundaries, and Abuse Resistance
Least-Privilege Database Accounts, Migration Permissions, Row-Level Security, and Tenant Defense in Depth
Combine runtime least privilege, separate migration ownership, ServiceHub tenant constraints, EF Core query filters, application authorization, and optional database row-level security so bypassing one layer does not automatically expose or mutate another tenant’s data.
Learning outcomes
Separate runtime DML authority from migration/DDL authority and explain why the application rarely needs schema-owner privileges.
Combine application authorization, EF Core tenant filters, tenant-aware constraints, and database least privilege as independent controls.
Describe SQL Server/Azure SQL row-level security filter and block predicates without presenting RLS as portable EF behavior.
Restrict administrative and raw-SQL paths through explicit authorization and separate operational identities.
Test bypass scenarios such as IgnoreQueryFilters, direct SQL, wrong-tenant writes, and migration credential misuse.
Build a production permission/runbook model that records grants, migration ownership, tenant context, audit evidence, and rollback safety.
1. The database account defines the blast radius after an application bug
Suppose a ServiceHub endpoint accidentally calls
IgnoreQueryFilters() or a raw SQL statement omits
the tenant predicate. If the runtime database identity can read
every tenant table, alter schema, disable security policies,
create users and drop databases, a single application defect
inherits that entire blast radius.
Least privilege means granting an identity only the operations and objects needed for its role. Runtime data access usually needs a narrower set of Data Manipulation Language (DML) permissions than schema deployment, which uses Data Definition Language (DDL).
2. Separate runtime and migration identities
| Identity | Typical authority | Should not need |
|---|---|---|
| ServiceHub runtime | SELECT/INSERT/UPDATE/DELETE on required objects; execute selected routines | CREATE/ALTER/DROP schema, create users, disable RLS, broad admin roles |
| Migration/deployment owner | reviewed DDL needed to advance schema/migrations | application request traffic |
| Read-only support/reporting | approved SELECT/views | writes, schema changes, broad raw access |
| Break-glass admin | time-bound emergency authority | normal application traffic |
The exact statements/roles vary by SQL Server, PostgreSQL, MySQL/MariaDB, Oracle and SQLite. The principle is portable; the grants are not. For SQLite, operating-system file permissions are a major part of access control because there is no server login/role model.
3. Keep the Chapter 22 tenant controls layered
ServiceHub already has EF Core named tenant/soft-delete/active filters and tenant-aware composite relationship constraints. Each protects a different failure mode:
| Layer | Stops | Does not stop |
|---|---|---|
| Application authorization | caller choosing an unauthorized tenant/admin operation | a database client bypassing the app |
| EF tenant filter | ordinary queries accidentally omitting tenant predicate | IgnoreQueryFilters/raw DB access/misconfiguration |
| Tenant-aware unique/FK constraints | cross-tenant relationship mistakes | reading another tenant row |
| Runtime DB permissions | application account performing ungranted operations | bad reads/writes that are still granted |
| Database RLS where supported | row access from any query using that DB identity/context | misconfigured security context/policy or privileged bypass paths |
Defense in depth is not duplication: each layer changes the failure mode and provides an independent test/operational control.
4. SQL Server/Azure SQL RLS: filter predicates and block predicates
SQL Server row-level security (RLS) uses a security policy backed by inline table-valued predicate functions. Filter predicates restrict visible rows for reads and applicable update/delete operations. Block predicates can reject writes that would violate the policy. This is database behavior, not an EF feature, and must be tested against SQL Server/Azure SQL rather than SQLite.
CREATE SCHEMA Security;GOCREATE FUNCTION Security.fn_tenant_access(@tenant_id nvarchar(64))RETURNS TABLEWITH SCHEMABINDINGASRETURN SELECT 1 AS allowedWHERE @tenant_id = CAST(SESSION_CONTEXT(N'TenantId') AS nvarchar(64));GOCREATE SECURITY POLICY Security.WorkOrderTenantPolicyADD FILTER PREDICATE Security.fn_tenant_access(tenant_id) ON dbo.work_orders,ADD BLOCK PREDICATE Security.fn_tenant_access(tenant_id) ON dbo.work_orders AFTER INSERT,ADD BLOCK PREDICATE Security.fn_tenant_access(tenant_id) ON dbo.work_orders AFTER UPDATEWITH (STATE = ON);GO
The application must set and verify the per-connection/session
tenant context correctly. Pooled physical connections make
session state security-sensitive; clear/reset or
deterministically overwrite tenant context whenever a connection
is checked out. Never assume EF DbContext pooling
and SQL connection pooling reset arbitrary server session
context for you.
5. Deliberate failure: use one db_owner-style credential everywhere
web requests ─────┐background jobs ───┼── same schema-owner credential ── databasemigration job ─────┤support script ────┘
This collapses all trust boundaries. A SQL injection bug can become DDL; a compromised background worker can alter security policy; an accidental migration call from every replica can race schema changes. Repair it with separate identities, narrowly scoped grants, one coordinated migration owner, and explicit admin pathways.
6. Raw SQL/admin paths need authorization before database execution
An internal endpoint that executes a reviewed maintenance query
is still an administrative capability. Protect it with
authentication, authorization policy, audited intent, bounded
query shapes, and a database identity appropriate to that
operation. The example uses a deliberately small
ITenantAuditAuthorizer seam so the security
decision is explicit and independently testable rather than
coupled to a particular web framework. Never let an ordinary
user toggle IgnoreQueryFilters() through a generic
flag such as ?includeAllTenants=true.
public interface ITenantAuditAuthorizer{ Task<bool> CanReadDeletedAsync( ClaimsPrincipal user, string tenantId, CancellationToken ct);}public sealed record AuditRow(int Id, string WorkOrderNumber);public sealed class TenantAuditReader( IDbContextFactory<ServiceHubContext> contextFactory, ITenantAuditAuthorizer authorization){ public async Task<IReadOnlyList<AuditRow>> ReadDeletedAsync( ClaimsPrincipal user, string tenantId, CancellationToken ct) { if (!await authorization.CanReadDeletedAsync(user, tenantId, ct)) throw new UnauthorizedAccessException(); await using var db = await contextFactory.CreateDbContextAsync(ct); db.AssignTenant(tenantId); return await db.WorkOrders .IgnoreQueryFilters("SoftDeleteFilter") .Where(w => w.TenantId == tenantId && w.IsDeleted) .Select(w => new AuditRow(w.Id, w.WorkOrderNumber)) .ToListAsync(ct); }}
Selective filter disabling is paired with an explicit tenant predicate and authorization check. The database account should still be unable to alter security policy or schema.
7. Verify privileges and RLS independently of EF
Integration tests should connect using the actual runtime test identity, not a schema owner. Attempt an operation the runtime should not have—such as creating a table or selecting a restricted admin object—and assert denial. For RLS, execute the same SQL under two tenant contexts and verify the database changes result visibility even if EF filters are absent.
runtime identity: SELECT/INSERT/UPDATE/DELETE approved ServiceHub objects: allowed CREATE TABLE / ALTER SECURITY POLICY: denied cross-tenant FK mutation: denied by constraint/policy IgnoreQueryFilters ordinary endpoint: not reachable by authorization SQL Server RLS test (optional): tenant-a session cannot read tenant-b rowsmigration identity: reviewed schema migration: allowed application request path: not configured with this credential
8. Mandatory lab: prove defense in depth on free SQLite, then add optional RLS
- Use Chapter 22’s SQLite tenant schema and composite tenant-aware relationship constraint.
- Write an adversarial test that calls an internal query with filters disabled but still requires an explicit authorized tenant.
- Attempt a cross-tenant Technician/WorkOrder relationship and verify the database rejects it.
- Document the SQLite file/OS permission boundary and why it differs from a server database role model.
- For an available SQL Server/Azure SQL lab, create a low-privileged test runtime user and optional RLS policy; verify read filtering and a blocked wrong-tenant write.
- Use a separate migration credential to apply schema changes, then confirm the runtime identity cannot execute equivalent DDL.
- Clean up test users/policies/database resources.
9. Production judgment and bridge
EF Core is not a database firewall. Secure production access combines caller authorization, safe query construction, tenant-aware modeling, least-privileged runtime credentials, coordinated migration ownership, provider/database controls such as RLS where justified, and adversarial tests using the same identities. Lesson 5 finishes the chapter by protecting the diagnostic and backup artifacts created when these systems fail.
Check your understanding
- Why should runtime and migration credentials differ?
- What is the difference between an EF query filter and SQL Server RLS?
- What does a SQL Server RLS block predicate do?
- Why is connection pooling relevant to RLS session context?
- Should an ordinary request be able to request IgnoreQueryFilters?
- How do you prove least privilege?
Review the answers
1. Runtime usually needs narrow DML while migrations need DDL; separation limits the blast radius of application bugs/compromise and prevents every app instance from becoming a schema owner.
2. An EF filter is application-generated query policy and can be bypassed/misconfigured; RLS is enforced by the SQL Server database policy for queries using that database context/identity.
3. It rejects writes that violate the security predicate for configured insert/update/delete operations.
4. Physical sessions can be reused, so tenant session state must be set/reset deterministically to prevent cross-request leakage.
5. No. Filter bypass is an administrative/data-lifecycle capability and must sit behind explicit authorization and constrained code paths.
6. Connect with the runtime identity and assert required operations succeed while prohibited DDL/admin/cross-tenant operations fail.
Authoritative references
- SQL Server row-level security — filter/block predicates, permissions and security-policy behavior
- SQL Server database permissions — database-engine permission model for least-privilege design
- EF Core global query filters — tenant filters, named filters and bypass semantics
- OWASP SQL Injection Prevention Cheat Sheet — least privilege as injection defense in depth
- PostgreSQL row security policies — provider-specific alternative illustrating that RLS semantics are database-owned
- SQLite provider limitations — mandatory local provider boundary and absence of server-role/RLS semantics