Configuring Sentinel Protected Tables with az rest
This post only covers the configuration side of protected tables. It doesn’t get into the broader why, or how you actually roll it out across a real environment - configuring something is easy, implementing it into an organization so it works is another beast entirely. I enjoyed reading Sandor Tokesi’s write-up on protected tables, and it inspired a good chunk of this one.

Quick info
- Protected tables gate row access behind an explicit ABAC (attribute-based access control) condition instead of the implicit data-read that control-plane roles grant.
- Currently in public preview. (docs)
- This feature applies to all Azure Monitor/LAW tables using a
protectionLevelproperty on the table. Values areGeneral(default) andProtected. (docs) - Unauthorized queries return zero rows with no error. The schema and columns stay visible while rows are protected. (docs)
The access decision, before any CLI:
flowchart TD
Q[User query] --> T{Table protectionLevel?}
T -->|General| RP[Normal read permission]
RP --> OK[Rows returned]
T -->|Protected| RN[Read permission necessary<br/>but not sufficient]
RN --> G{Explicit grant?}
G -->|Privileged Monitoring<br/>Data Reader / ABAC| OK
G -->|None| ZERO[Zero rows, no error]
DAO[DataActionsOnly mode] -.removes implicit<br/>Reader / Monitoring Reader path.-> RP
classDef allow fill:#dcfce7,stroke:#16a34a,color:#14532d;
classDef deny fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;
classDef gate fill:#e0f2fe,stroke:#0284c7,color:#0c4a6e;
class OK allow;
class ZERO deny;
class T,G,RN,DAO gate;
Setting protectionLevel without the portal
The permission required to change a table’s protection level is Microsoft.OperationalInsights/workspaces/tables/protectionLevel/write. That action is held by Owner and Log Analytics Contributor. (docs)
As there is no Bicep/ARM property surfaced for this yet we can use az rest with a PATCH-method. Set a table to Protected:
az rest --method patch \
--url "https://management.azure.com/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.OperationalInsights/workspaces/<ws>/tables/<Table>?api-version=2026-03-01" \
--body '{"properties": {"protectionLevel": "Protected"}}'
The CLI examples in Microsoft’s docs hardcode 2025-02-01, but that version accepts the PATCH with a 200 and silently drops protectionLevel - a follow-up GET shows it still null. 2026-03-01 applies protectionLevel properly.
Granting read on protected tables
Grant read at a scope with the built-in Privileged Monitoring Data Reader role. It carries the DataAction Microsoft.OperationalInsights/workspaces/tables/data/read with the built-in ABAC condition, so it reads all protected tables at the assigned scope. (docs)
az role assignment create \
--assignee "<objectId>" \
--role "Privileged Monitoring Data Reader" \
--scope "/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.OperationalInsights/workspaces/<ws>"
Operational gotchas
- Scheduled rules lose access. A scheduled rule that queries a table stops seeing rows once that table is protected. The fix is to give the rule a managed identity and grant that identity the built-in Privileged Monitoring Data Reader role. There’s some nuance around which ABAC conditions actually work here, and Sandor’s write-up covers it well.
- Cross-workspace queries aren’t supported. The
app()andworkspace()functions for cross-workspace queries don’t yet support protected tables. (docs) - Granularity is per-workspace. Protection is scoped to the table in one workspace, not federated across workspaces. (docs)
Removing implicit data access
Protecting a table only stops implicit data-read for that table. Roles like Reader and Monitoring Reader still grant implicit data-read on everything else.
DataActionsOnly is a workspace access mode set through the dataAuthorizationMode property. In the default mode, control-plane roles that carry a read action (Reader, Monitoring Reader, or a custom role using control-plane actions) imply data-read on the workspace’s tables. In DataActionsOnly mode, only DataActions grant data access, which closes the path where a control-plane role could read protected data. (docs)
Enable it with a PATCH:
az rest --method patch \
--url "https://management.azure.com/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.OperationalInsights/workspaces/<ws>?api-version=2026-03-01" \
--body '{"properties": {"features": {"dataAuthorizationMode": "DataActionsOnly"}}}'
See Enable DataActionsOnly mode for the full steps.
Auditing
Protection changes land in the Activity Log under the Update Table operation (Microsoft.OperationalInsights/workspaces/tables/write). Note protectionLevel/write is the RBAC action you need to change protection, not the operation name that gets logged.
AzureActivity
| where OperationNameValue endswith "TABLES/WRITE"
| where ActivityStatusValue == "Success"
| extend Table = tostring(Properties_d.entity)
| project TimeGenerated, Caller, CallerIpAddress, Table, OperationNameValue
| order by TimeGenerated desc
Toggling Heartbeat protection on and off in my own tenant produced four Success rows, one per write. Actual output, with Caller and CallerIpAddress anonymized and the table path trimmed (OperationNameValue is MICROSOFT.OPERATIONALINSIGHTS/WORKSPACES/TABLES/WRITE on every row):
TimeGenerated Caller CallerIpAddress Table
2026-07-14T08:44:45.881939Z user@contoso.com 203.0.113.24 .../tables/Heartbeat
2026-07-14T08:43:23.880427Z user@contoso.com 203.0.113.24 .../tables/Heartbeat
2026-07-14T08:42:44.803406Z user@contoso.com 203.0.113.24 .../tables/Heartbeat
2026-07-14T08:42:36.063499Z user@contoso.com 203.0.113.24 .../tables/Heartbeat
This catches every table write, not just protection changes. Neither the Start nor the Success row carries the request body, so you get who touched which table and when - Caller, CallerIpAddress, Properties_d.entity - but not whether it went to Protected or General. Read the table’s current state for that.
Final thoughts
Protected tables are a property flip plus a role grant, with DataActionsOnly as the workspace-wide switch. It’s public preview, so the api-version behavior and the limitations above can still change. Test the toggle and the read path in your own tenant before you rely on them.