A SCADA identity should be able to perform its approved process workflow and nothing more. Prefer custom database roles, approved views and stored procedures over db_owner, db_datareader, db_datawriter or direct table-wide permissions.
-
GRANTallows,DENYexplicitly blocks andREVOKEremoves an explicit decision. - Assign policy to roles and membership to users.
- Give Runtime
EXECUTEon controlled write procedures instead of unrestricted table writes. - Test effective permissions as the target user before deployment.
SQL DCL for SCADA and Industrial Automation
This guide covers SQL Server GRANT DENY REVOKE, SCADA database permissions, custom SQL roles, least-privilege Runtime access and a complete DCL security lab.
1. What Is DCL?
DCL means Data Control Language. It manages who can perform an action on a SQL Server securable.
| Statement | Meaning | Automation example |
|---|---|---|
GRANT | Allow a permission | Allow Runtime to execute usp_RecordEvent |
DENY | Explicitly block a permission | Block direct deletion from event tables |
REVOKE | Remove an explicit GRANT or DENY | Return permission evaluation to role inheritance |
CREATE USER, CREATE ROLE and ALTER ROLE prepare security principals and memberships; they are security-management DDL used alongside DCL.
2. Understand the Authorization Model
A permission decision connects three elements:
- Principal: who requests access—login, database user or role.
- Securable: what is protected—server, database, schema, table, view or procedure.
- Permission: which action—
SELECT,INSERT,UPDATE,DELETE,EXECUTEor another permission.
GRANT EXECUTE
ON OBJECT::[dbo].[usp_RecordEvent]
TO [scada_runtime_role];Here the principal is scada_runtime_role, the securable is the procedure and the permission is EXECUTE.
3. Login, Database User and Role
| Principal | Scope | Purpose |
|---|---|---|
| Login | SQL Server instance | Authenticates to the Database Engine |
| Database user | One database | Represents an identity inside that database |
| Database role | One database | Groups users under one permission policy |
| Server role | SQL Server instance | Groups server-level permissions |
For SQL Server, a login is normally mapped to a user in each required database. For the safe lab, use a contained test user without a login:
USE [SQF_DB];
GO
CREATE USER [scada_lab_user] WITHOUT LOGIN;
GO4. Securables and Permission Scope
Permissions form a hierarchy. A grant at a broad scope can flow to contained securables.
| Scope | Example | Risk |
|---|---|---|
| Server | VIEW SERVER STATE | Affects the entire SQL Server instance |
| Database | CONNECT, SELECT ALL USER SECURABLES | Broad database visibility |
| Schema | SELECT ON SCHEMA::reporting | Applies to schema-contained objects |
| Object | SELECT ON dbo.vw_ChargeSummary | Narrow and easy to review |
| Column | SELECT ON dbo.Table(Column) | Fine-grained but complex to maintain |
Start at object scope. Move to schema scope only when every current and future object in that schema is intended for the same audience.
5. GRANT: Allow an Approved Action
GRANT SELECT
ON OBJECT::[dbo].[vw_ChargeSummary]
TO [scada_runtime_role];
GRANT EXECUTE
ON OBJECT::[dbo].[usp_RecordEvent]
TO [scada_runtime_role];Use explicit securable classes such as OBJECT:: and SCHEMA:: in deployment scripts. They make the intended permission scope easier to review.
If Runtime only calls a write procedure, grant EXECUTE on that procedure. Do not also grant direct INSERT, UPDATE and DELETE on the underlying tables without a separate requirement.
6. DENY: Explicitly Block a Permission
DENY DELETE
ON OBJECT::[dbo].[tblEvent]
TO [scada_runtime_role];DENY is useful when a role inherits a broader grant that must be blocked. However, no grant is simpler than a deny when you control the full role design.
DENY versus column-level GRANT has a documented backward-compatibility exception. Avoid complicated mixed-scope rules when a clean custom role can express the policy.Do not casually deny permissions to public; every database user belongs to that role and the effect can be much wider than intended.
7. REVOKE: Remove an Explicit Decision
-- Remove an explicit GRANT or DENY at this scope.
REVOKE DELETE
ON OBJECT::[dbo].[tblEvent]
FROM [scada_runtime_role];REVOKE does not mean “block”. After revocation, the user may still receive the permission through another role or a broader grant. Recalculate effective permissions after every change.
| Current action | Next statement | Result |
|---|---|---|
| Explicit GRANT | REVOKE | Explicit allow removed |
| Explicit DENY | REVOKE | Explicit block removed |
| No explicit permission | None | Role and hierarchy determine access |
8. How SQL Server Calculates Effective Permissions
Effective access can come from the user, one or more roles, broader permission scopes, ownership or execution context. In general, an applicable DENY overrides a GRANT, subject to documented exceptions.
SELECT
HAS_PERMS_BY_NAME(N'dbo.vw_ChargeSummary', N'OBJECT', N'SELECT') AS CanReadSummary,
HAS_PERMS_BY_NAME(N'dbo.usp_RecordEvent', N'OBJECT', N'EXECUTE') AS CanRecordEvent,
HAS_PERMS_BY_NAME(N'dbo.tblEvent', N'OBJECT', N'DELETE') AS CanDeleteEvents;Run this under the target execution context. A result of 1 means permission, 0 means no permission and NULL can indicate an invalid securable/permission context.
9. Create a Custom SCADA Role
CREATE ROLE [scada_runtime_role] AUTHORIZATION [dbo];
GO
ALTER ROLE [scada_runtime_role]
ADD MEMBER [scada_lab_user];
GORoles express job functions: scada_runtime_role, report_reader_role, maintenance_engineer_role and deployment_role. Avoid creating slightly different permissions directly on dozens of individual users.
-- Remove membership during offboarding or role change.
ALTER ROLE [scada_runtime_role]
DROP MEMBER [scada_lab_user];
10. Design Read-Only Reporting Access
Expose approved columns through a view and grant access to the view rather than every base table.
CREATE ROLE [report_reader_role] AUTHORIZATION [dbo];
GRANT SELECT
ON OBJECT::[dbo].[vw_ChargeSummary]
TO [report_reader_role];When the view and referenced objects share an owner, ownership chaining can allow view access without separate base-table permissions. Dynamic SQL and cross-database access can break that simple chain, so test the exact module.
11. Design Execute-Only Runtime Writes
Place validation and DML inside a stored procedure, then grant only EXECUTE.
GRANT EXECUTE
ON OBJECT::[dbo].[usp_RecordEvent]
TO [scada_runtime_role];
-- No direct INSERT/UPDATE/DELETE grants are required for a normal ownership chain.This design limits the write shape, validates equipment and types, returns the created identity and lets the database owner change table internals without granting Runtime broader access.
12. Use Schema-Level Grants Deliberately
CREATE SCHEMA [reporting] AUTHORIZATION [dbo];
GO
GRANT SELECT
ON SCHEMA::[reporting]
TO [report_reader_role];A schema-level grant can simplify administration for a governed reporting schema. It also applies to appropriate future objects added to that schema. Do not use GRANT CONTROL ON SCHEMA merely to provide read access.
13. Why Broad Fixed Roles Are Risky
| Role | Capability | SCADA concern |
|---|---|---|
db_owner | Full database control | Runtime can change security and schema |
db_datareader | Read all user tables/views | Exposes unrelated plant, HR or configuration data |
db_datawriter | Write all user tables | Bypasses stored-procedure validation |
sysadmin | Unrestricted server control | Never appropriate for an ordinary HMI/SCADA service |
Custom roles make the access matrix visible. Fixed roles can be appropriate for DBA operations, but not as a shortcut for normal Runtime connectivity.
14. WITH GRANT OPTION
-- Powerful delegation; normally not for Runtime identities.
GRANT SELECT
ON OBJECT::[dbo].[vw_ChargeSummary]
TO [report_security_admin]
WITH GRANT OPTION;WITH GRANT OPTION lets the grantee grant the same permission to others. Restrict it to controlled security administrators. Revoking a permission that was delegated can require CASCADE; understand the downstream grants before changing it.
15. Test with EXECUTE AS and REVERT
EXECUTE AS USER = N'scada_lab_user';
SELECT USER_NAME() AS [ExecutionUser];
SELECT
HAS_PERMS_BY_NAME(N'dbo.vw_ChargeSummary', N'OBJECT', N'SELECT') AS CanReadSummary,
HAS_PERMS_BY_NAME(N'dbo.usp_RecordEvent', N'OBJECT', N'EXECUTE') AS CanRecordEvent,
HAS_PERMS_BY_NAME(N'dbo.tblEvent', N'OBJECT', N'DELETE') AS CanDeleteEvents;
REVERT;
SELECT USER_NAME() AS [RestoredUser];1; direct delete returns 0; REVERT restores the administrator context.16. Audit Roles and Explicit Permissions
SELECT
rolep.[name] AS [RoleName], memberp.[name] AS [MemberName]
FROM sys.database_role_members AS drm
JOIN sys.database_principals AS rolep
ON rolep.[principal_id] = drm.[role_principal_id]
JOIN sys.database_principals AS memberp
ON memberp.[principal_id] = drm.[member_principal_id]
ORDER BY rolep.[name], memberp.[name];
SELECT
grantee.[name] AS [PrincipalName],
p.[state_desc], p.[permission_name], p.[class_desc],
CASE
WHEN p.[class] = 1 THEN OBJECT_SCHEMA_NAME(p.[major_id])
WHEN p.[class] = 3 THEN SCHEMA_NAME(p.[major_id])
END AS [SecurableSchema],
CASE
WHEN p.[class] = 1 THEN OBJECT_NAME(p.[major_id])
WHEN p.[class] = 3 THEN N'(schema)'
END AS [SecurableName]
FROM sys.database_permissions AS p
JOIN sys.database_principals AS grantee
ON grantee.[principal_id] = p.[grantee_principal_id]
WHERE grantee.[name] IN (N'scada_runtime_role', N'report_reader_role')
ORDER BY grantee.[name], p.[class_desc], p.[permission_name];Catalog views show explicit permissions, not a complete flattened result of every inheritance path or fixed-role capability. Combine catalog review with impersonation and HAS_PERMS_BY_NAME.
17. Protect Automation Credentials
- Prefer Windows or managed identities and dedicated service accounts.
- Do not reuse personal DBA accounts for Runtime.
- Store secrets in an approved secret store, not in HTML, VBS, Python source or HMI text fields.
- Use encrypted connections and validate the SQL Server certificate.
- Rotate credentials under a tested procedure that avoids production downtime.
- Disable or remove accounts immediately during decommissioning.
- Log authentication failures and permission changes without exposing passwords.
18. Separate Runtime, Reporting and Deployment
| Identity | Typical permissions | Must not receive by default |
|---|---|---|
| SCADA Runtime | Execute approved write procedures; select approved live views | DDL, security administration, unrestricted delete |
| Reporting service | Select approved reporting views/procedures | Write permissions |
| Migration/deployment | Time-limited DDL/DCL required by release | Permanent use by Runtime |
| DBA/security administrator | Controlled administrative access | Embedded application use |
This separation limits blast radius and creates a clearer audit trail.
19. Common DCL Errors and Fixes
| Symptom | Likely cause | Check or fix |
|---|---|---|
| Login succeeds but database access fails | No mapped user or CONNECT path | Inspect login/user mapping and database state |
| Permission denied after GRANT | Applicable DENY, wrong database/schema or execution context | Test as user and inspect all role memberships |
| REVOKE did not block access | Permission is inherited elsewhere | Find role/broader-scope grants; use DENY only if policy requires it |
| View works but base table fails | Intended ownership chain | Confirm view is the supported interface |
| Procedure fails inside dynamic SQL | Ownership chain not applied to dynamic batch | Review module signing/execution context and parameterization |
| User appears overprivileged | Fixed-role membership or broad schema/database grant | Remove broad membership and rebuild custom role |
| Cannot drop user/role | Owns objects/schema or still has members | Transfer ownership and remove memberships first |
20. Complete Practical DCL Lab
Hands-on- Use a disposable practice database only.
- Run setup as a test security administrator.
- Do not copy lab principals into production unchanged.
Write the access matrix
| Workflow | Interface | Permission |
|---|---|---|
| Read charge summary | dbo.vw_ChargeSummary | SELECT |
| Record an event | dbo.usp_RecordEvent | EXECUTE |
| Delete event history | Not exposed | None |
| Change schema/security | Not exposed | None |
Create the lab user and role
CREATE USER [scada_lab_user] WITHOUT LOGIN;
CREATE ROLE [scada_runtime_role] AUTHORIZATION [dbo];
ALTER ROLE [scada_runtime_role] ADD MEMBER [scada_lab_user];Grant only approved interfaces
GRANT SELECT ON OBJECT::[dbo].[vw_ChargeSummary] TO [scada_runtime_role];
GRANT EXECUTE ON OBJECT::[dbo].[usp_RecordEvent] TO [scada_runtime_role];Prove allowed and blocked operations
Use EXECUTE AS USER and HAS_PERMS_BY_NAME. Confirm view select and procedure execute are allowed while direct table delete and schema alteration are not.
Inspect the catalog
Query sys.database_role_members and sys.database_permissions. Save the result with the approved access matrix.
Revoke and retest one permission
REVOKE SELECT ON OBJECT::[dbo].[vw_ChargeSummary] FROM [scada_runtime_role];
-- Retest as scada_lab_user: CanReadSummary should now be 0.
GRANT SELECT ON OBJECT::[dbo].[vw_ChargeSummary] TO [scada_runtime_role];You can now create a custom role, grant narrow object permissions, explain DENY versus REVOKE, impersonate a test user and audit explicit access.
DCL Deployment Checklist
- Identify the real service identity.
- List workflows, not guessed permissions.
- Expose approved views and stored procedures.
- Create a custom database role.
- Grant at the narrowest maintainable scope.
- Avoid broad fixed roles and delegation rights.
- Test as the target user.
- Audit membership and explicit permissions.
- Store approval evidence and rollback DCL.
- Review access periodically and on personnel/system changes.
Further Reading
Frequently Asked Questions
What is DCL in SQL Server?
Data Control Language manages authorization through GRANT, DENY and REVOKE.
What is the difference between DENY and REVOKE?
DENY explicitly blocks a permission. REVOKE removes an explicit GRANT or DENY, after which inherited permissions determine access.
Should a SCADA account be db_owner?
Normally no. Grant only the approved view and stored-procedure permissions required by Runtime.
Why use database roles?
Roles centralize policy, simplify membership changes and make security audits easier.
How do I test permissions?
Use EXECUTE AS USER, HAS_PERMS_BY_NAME and catalog views in a test database, then always issue REVERT.
