Ship logs to Microsoft Sentinel
Use this guide to stream your Audit Logs to Microsoft Sentinel via the Azure Monitor Logs Ingestion API. Firezone authenticates to your tenant as a Microsoft Entra application using workload identity federation. No client secret is stored, by Firezone or by you.
Step 1: Start creating the Log Sink in Firezone
In the admin portal, go to SettingsLog Sinks, click Add, and select Microsoft Sentinel. The form walks you through the two-stage Azure setup below, so keep it open while you work.
Step 2: Grant admin consent
Enter your Microsoft Entra tenant ID in the form, then have a tenant administrator click Grant admin consent. This adds the Firezone Sentinel Log Ingestion application to your tenant so you can assign it a role in the next step. The application requests no API permissions and cannot read your directory or data. It only becomes a service principal that you grant the Monitoring Metrics Publisher role to, on a single data collection rule. Its access is limited to publishing logs to that one rule.
Step 3: Create the ingestion resources
Create a data collection endpoint, a custom table, and a data collection rule, then grant the Firezone application the Monitoring Metrics Publisher role on the rule. The portal provides the same steps with Firezone's application ID filled in, so prefer copying scripts from there. Choose your tool:
- You need a Log Analytics workspace: use the one Microsoft Sentinel is enabled on. If you don't have one yet, search for Log Analytics workspaces in the Azure portal and create one, then search for Microsoft Sentinel, choose Create, and add it to that workspace.
- Search for Data collection endpoints in the Azure portal and create one in the same region as your workspace. Firezone delivers logs to this endpoint.
- Open your workspace and go to Settings → Tables. Choose Create → New
custom log (DCR-based). Name the table
FirezoneLogs, choose Create a new data collection rule and give it a name such asfirezone-logs, and select the data collection endpoint from the previous step. On the Schema and transformation step, upload the sample file offered in the Firezone form and keep the default transformation, then create the table. The table's columns areTimeGenerated,Message,Stream, andFirezone. - Search for Data collection rules, open the rule you just created, and go to Access control (IAM). Choose Add → Add role assignment, select the Monitoring Metrics Publisher role, then under Members choose User, group, or service principal and select the Firezone Sentinel Log Ingestion application created by the admin consent above, then review and assign. The assignment can take up to 30 minutes to take effect.
- Fill in the fields in the Firezone form. Each field's hint says where to
find its value in the Azure portal. If you followed these steps, the
stream name is
Custom-FirezoneLogs_CL.
Assumes an existing Log Analytics workspace. Set the variables for your
environment, then run the script with the Azure CLI logged into your
subscription. It prints the values for the Firezone form. Replace
<firezone-client-id> with the application ID shown in the portal's version
of this script:
RG="my-resource-group"
WORKSPACE="my-workspace"
LOCATION="eastus"
WORKSPACE_ID=$(az monitor log-analytics workspace show \
-g "$RG" -n "$WORKSPACE" --query id -o tsv)
if ! az monitor log-analytics workspace table show \
-g "$RG" --workspace-name "$WORKSPACE" -n FirezoneLogs_CL > /dev/null 2>&1; then
az monitor log-analytics workspace table create \
-g "$RG" --workspace-name "$WORKSPACE" -n FirezoneLogs_CL \
--columns TimeGenerated=datetime Message=string Stream=string Firezone=dynamic \
--output none
fi
DCE_ID=$(az monitor data-collection endpoint show \
-g "$RG" -n firezone-logs --query id -o tsv 2>/dev/null)
if [ -z "$DCE_ID" ]; then
DCE_ID=$(az monitor data-collection endpoint create \
-g "$RG" -n firezone-logs -l "$LOCATION" \
--public-network-access Enabled --query id -o tsv)
fi
cat > firezone-dcr.json <<EOF
{
"location": "$LOCATION",
"properties": {
"dataCollectionEndpointId": "$DCE_ID",
"streamDeclarations": {
"Custom-FirezoneLogs_CL": {
"columns": [
{ "name": "TimeGenerated", "type": "datetime" },
{ "name": "Message", "type": "string" },
{ "name": "Stream", "type": "string" },
{ "name": "Firezone", "type": "dynamic" }
]
}
},
"destinations": {
"logAnalytics": [
{ "workspaceResourceId": "$WORKSPACE_ID", "name": "firezone" }
]
},
"dataFlows": [
{
"streams": ["Custom-FirezoneLogs_CL"],
"destinations": ["firezone"],
"transformKql": "source",
"outputStream": "Custom-FirezoneLogs_CL"
}
]
}
}
EOF
DCR_ID=$(az monitor data-collection rule show \
-g "$RG" -n firezone-logs --query id -o tsv 2>/dev/null)
if [ -z "$DCR_ID" ]; then
DCR_ID=$(az monitor data-collection rule create \
-g "$RG" -n firezone-logs --rule-file firezone-dcr.json --query id -o tsv)
fi
SP_ID=$(az ad sp show --id <firezone-client-id> --query id -o tsv)
if ! az role assignment list --assignee "$SP_ID" --scope "$DCR_ID" \
--role "Monitoring Metrics Publisher" --query "[0].id" -o tsv 2>/dev/null | grep -q .; then
az role assignment create --assignee-object-id "$SP_ID" \
--assignee-principal-type ServicePrincipal \
--role "Monitoring Metrics Publisher" --scope "$DCR_ID" \
--output none
fi
echo "Enter these in the Firezone form:"
echo " Ingestion Endpoint: $(az monitor data-collection endpoint show \
-g "$RG" -n firezone-logs --query logsIngestion.endpoint -o tsv)"
echo " DCR Immutable ID: $(az monitor data-collection rule show \
-g "$RG" -n firezone-logs --query immutableId -o tsv)"
echo " Stream Name: Custom-FirezoneLogs_CL"
Assumes an existing Log Analytics workspace and requires the azurerm,
azuread, and azapi providers. The outputs are the values for the Firezone
form. Replace <firezone-client-id> with the application ID shown in the
portal's version of this snippet:
locals {
resource_group = "my-resource-group"
workspace_name = "my-workspace"
location = "eastus"
}
data "azurerm_log_analytics_workspace" "this" {
name = local.workspace_name
resource_group_name = local.resource_group
}
resource "azurerm_monitor_data_collection_endpoint" "firezone" {
name = "firezone-logs"
resource_group_name = local.resource_group
location = local.location
}
resource "azapi_resource" "firezone_table" {
type = "Microsoft.OperationalInsights/workspaces/tables@2022-10-01"
name = "FirezoneLogs_CL"
parent_id = data.azurerm_log_analytics_workspace.this.id
body = jsonencode({
properties = {
schema = {
name = "FirezoneLogs_CL"
columns = [
{ name = "TimeGenerated", type = "datetime" },
{ name = "Message", type = "string" },
{ name = "Stream", type = "string" },
{ name = "Firezone", type = "dynamic" }
]
}
}
})
}
resource "azurerm_monitor_data_collection_rule" "firezone" {
name = "firezone-logs"
resource_group_name = local.resource_group
location = local.location
data_collection_endpoint_id = azurerm_monitor_data_collection_endpoint.firezone.id
destinations {
log_analytics {
workspace_resource_id = data.azurerm_log_analytics_workspace.this.id
name = "firezone"
}
}
data_flow {
streams = ["Custom-FirezoneLogs_CL"]
destinations = ["firezone"]
transform_kql = "source"
output_stream = "Custom-FirezoneLogs_CL"
}
stream_declaration {
stream_name = "Custom-FirezoneLogs_CL"
column {
name = "TimeGenerated"
type = "datetime"
}
column {
name = "Message"
type = "string"
}
column {
name = "Stream"
type = "string"
}
column {
name = "Firezone"
type = "dynamic"
}
}
depends_on = [azapi_resource.firezone_table]
}
data "azuread_service_principal" "firezone" {
client_id = "<firezone-client-id>"
}
resource "azurerm_role_assignment" "firezone_ingest" {
scope = azurerm_monitor_data_collection_rule.firezone.id
role_definition_name = "Monitoring Metrics Publisher"
principal_id = data.azuread_service_principal.firezone.object_id
}
output "ingestion_endpoint" {
value = azurerm_monitor_data_collection_endpoint.firezone.logs_ingestion_endpoint
}
output "dcr_immutable_id" {
value = azurerm_monitor_data_collection_rule.firezone.immutable_id
}
Step 4: Configure the sink
Back in the Firezone form, fill in:
| Field | Description |
|---|---|
| Name | A name to identify this log sink. |
| Tenant ID | Your Microsoft Entra tenant ID, also called the directory ID. |
| Ingestion Endpoint | The Logs Ingestion URI shown on your data collection endpoint's Overview page, for example https://my-dce-abcd.eastus-1.ingest.monitor.azure.com. |
| DCR Immutable ID | The Immutable Id shown on the data collection rule's Overview page, also shown in its JSON View, for example dcr-0123456789abcdef0123456789abcdef. |
| Stream Name | The DCR's input stream, listed in its JSON View under streamDeclarations. Defaults to Custom-FirezoneLogs_CL. |
A few validations to be aware of: the ingestion endpoint must be HTTPS and
its host must end in .ingest.monitor.azure.com. This guarantees the token
Firezone mints for your tenant is never sent to a non-Azure host. The DCR
immutable ID must match dcr-<32 hex characters> and the stream name must
start with Custom-.
Then choose which Log streams to deliver, all four of which are enabled by default, and optionally check Deliver existing logs to backfill logs recorded before the sink was created.
Deliver existing logs can only be selected when you create the sink. It can't be turned on later for an existing sink.
Click Create. Deliveries begin within about a minute, though the role assignment from Step 3 can take up to 30 minutes to propagate. Firezone retries automatically in the meantime.
Delivery format
For each delivery, Firezone obtains a short-lived bearer token for your
tenant, then POSTs a JSON array to
<Ingestion Endpoint>/dataCollectionRules/<DCR Immutable ID>/streams/<Stream Name>?api-version=2023-01-01.
Each record maps onto the custom table's four columns. Query the full event
from the Firezone dynamic column in KQL, for example
FirezoneLogs_CL | where Stream == "change".
{
"TimeGenerated": "2026-07-14T09:26:31.114Z",
"Message": "firezone change c0654d20aa1b4a0000000002",
"Stream": "change",
"Firezone": {
"type": "change",
"log_id": "c0654d20aa1b4a0000000002",
"timestamp": "2026-07-14T09:26:31.114213Z",
"object": "policies",
"operation": "update",
"before": { "id": "0a83588e-…", "description": "Engineering → GitLab" },
"after": {
"id": "0a83588e-…",
"description": "Engineering → GitLab (MFA required)"
},
"subject": {
"actor_id": "e2f6a9c4-…",
"actor_name": "Jamie Admin",
"actor_email": "jamie@company.com",
"actor_type": "account_admin_user",
"auth_provider_id": "9d0c2f4e-…",
"ip": "203.0.113.44",
"ip_region": "US",
"ip_city": "San Francisco",
"ip_lat": 37.7749,
"ip_lon": -122.4194,
"user_agent": "Mozilla/5.0 …"
}
}
}
{
"TimeGenerated": "2026-07-14T09:26:31.114Z",
"Message": "firezone session 53f2c8a91b7e4d20a6c19e04",
"Stream": "session",
"Firezone": {
"type": "session",
"log_id": "53f2c8a91b7e4d20a6c19e04",
"timestamp": "2026-07-14T09:26:31.114213Z",
"context": "client",
"subject": {
"actor_id": "7a1d9e3c-…",
"actor_name": "Riley Engineer",
"actor_email": "riley@company.com",
"actor_type": "account_user",
"auth_provider_id": "9d0c2f4e-…",
"device_id": "2b4d6f8a-…",
"token_id": "c4e6a8b0-…",
"ip": "203.0.113.44",
"ip_region": "US",
"ip_city": "San Francisco",
"ip_lat": 37.7749,
"ip_lon": -122.4194,
"user_agent": "Firezone/1.4.0 (macOS 15.5; arm64)"
}
}
}
{
"TimeGenerated": "2026-07-14T09:26:31.114Z",
"Message": "firezone api_request a7b3e91c4d208f5a6e1b2c3d",
"Stream": "api_request",
"Firezone": {
"type": "api_request",
"log_id": "a7b3e91c4d208f5a6e1b2c3d",
"timestamp": "2026-07-14T09:26:31.114213Z",
"actor_id": "b8e0c2a4-…",
"api_token_id": "d0f2a4c6-…",
"method": "GET",
"path": "/policies",
"content_length": 0,
"request_id": "F8nMlbf6MUyJZUUABBzB9-yT",
"user_agent": "terraform-provider-firezone/0.4.1",
"ip": "203.0.113.44",
"ip_region": "US",
"ip_city": "San Francisco",
"ip_lat": 37.7749,
"ip_lon": -122.4194
}
}
Each flow is delivered as two events sharing its log_id. The start event
is suffixed with -s and carries no counters, while the end event is
suffixed with -e and carries the final totals:
{
"TimeGenerated": "2026-07-14T09:26:01.000Z",
"Message": "firezone flow f4e8a2c61b09d735e2a48b17-e",
"Stream": "flow",
"Firezone": {
"type": "flow",
"log_id": "f4e8a2c61b09d735e2a48b17-e",
"timestamp": "2026-07-14T09:26:01.000000Z",
"flow_start": "2026-07-14T09:25:31.000000Z",
"flow_end": "2026-07-14T09:26:01.000000Z",
"last_packet": "2026-07-14T09:26:01.000000Z",
"device_id": "2b4d6f8a-…",
"role": "initiator",
"policy_authorization_id": "6c8e0a2b-…",
"policy_id": "0a83588e-…",
"resource_id": "5c8f6f6e-…",
"resource_name": "GitLab",
"resource_address": "gitlab.company.com",
"actor_id": "7a1d9e3c-…",
"actor_email": "riley@company.com",
"actor_name": "Riley Engineer",
"client_version": "1.4.0",
"device_os_name": "iOS",
"device_os_version": "17.4",
"protocol": "tcp",
"inner_src_ip": "100.64.0.1",
"inner_src_port": 54321,
"inner_dst_ip": "10.0.0.5",
"inner_dst_port": 443,
"outer_src_ip": "203.0.113.10",
"outer_src_port": 51820,
"outer_dst_ip": "198.51.100.5",
"outer_dst_port": 51820,
"domain": "gitlab.company.com",
"rx_packets": 100,
"tx_packets": 80,
"rx_bytes": 102400,
"tx_bytes": 20480
}
}
How Firezone responds to HTTP status codes
| Response | Behavior |
|---|---|
2xx | The batch is accepted and delivery advances. Azure acknowledges successful deliveries with 204. |
403 | Retried, because role assignments can take up to 30 minutes to propagate. Disabled after 24 hours of persistent failure. |
400 for DCR/stream mismatches | Retried as a configuration issue you can fix in place, for example a wrong stream name or DCR. Disabled after 24 hours if unresolved. |
400 with Azure's malformed-request error codes | The batch is bisected to isolate the rejected record. Delivery pauses at it and Firezone engineers are notified automatically. No entries are skipped. |
413 | The batch is split in half and retried to isolate an oversized record. |
429, 5xx | Retried every minute. Disabled after 24 hours of persistent failure. |
401 or an AADSTS token error | Treated as a configuration error, usually missing admin consent or a wrong tenant ID: the sink is disabled immediately. |
Duplicates and redelivery
Firezone guarantees at-least-once delivery, retrying any batch that isn't
acknowledged rather than risk losing data, so Sentinel can occasionally
receive the same record twice. When it does, deduplicate on
Firezone.log_id in KQL: flow start and end events are already suffixed
with -s and -e, which makes the log_id a unique key across all streams.
Recovering a failed sink
A sink showing Warning is retrying automatically, and during initial setup this is often just the role assignment propagating. A sink showing Error has stopped delivering and needs attention: fix the cause, commonly revoked admin consent, a deleted role assignment, or a wrong tenant ID, then open the sink, click Edit, and click Save to re-enable it. Delivery picks up right where it left off, from the last acknowledged entry, and Deliver Now triggers a delivery immediately if you don't want to wait for the next cycle.
Deleting the sink deletes its delivery state but not your Azure resources or ingested data, so re-creating it starts fresh while reusing the same DCE, DCR, and table: the new sink delivers logs recorded from its creation onward, plus a full backfill if Deliver existing logs is selected, which re-sends records Sentinel may already have.
Need help? See all support options.