Azure Sentinel Integration

Azure Sentinel Integration

Azure Sentinel Integration Guide

Forward Keycloak authentication events and logs to Microsoft Azure Sentinel for cloud-native security monitoring, AI-powered threat detection, and seamless Microsoft ecosystem integration.

Overview

This guide walks you through configuring Skycloak to send security events to Azure Sentinel using HTTP webhooks and the Azure Monitor HTTP Data Collector API. Azure Sentinel is ideal for organizations using Microsoft 365, Azure AD, and Azure cloud services.

What You’ll Achieve

  • Real-time authentication event streaming to Azure Sentinel
  • Integration with Azure’s AI-powered threat detection
  • Unified security monitoring across Microsoft services
  • KQL-based queries and custom workbooks

Prerequisites

Before starting, ensure you have:

  1. Skycloak Requirements

    • Enterprise plan with SIEM Integration add-on
    • Workspace Owner or Admin permissions
  2. Azure Requirements

    • Active Azure subscription
    • Azure Sentinel workspace (Log Analytics workspace with Sentinel enabled)
    • Permission to create Data Connectors and access workspace keys

Step 1: Set Up Azure Sentinel Workspace

Create Log Analytics Workspace (if needed)

  1. Sign in to Azure Portal

  2. Search for Log Analytics workspaces

  3. Click CreateLog Analytics workspace

  4. Configure:

    • Subscription: Select your subscription
    • Resource Group: Create or select existing
    • Name: skycloak-sentinel-workspace
    • Region: Choose region closest to your Skycloak deployment
    • Pricing Tier: Pay-as-you-go (default)
  5. Click Review + CreateCreate

Enable Azure Sentinel

  1. Search for Microsoft Sentinel
  2. Click CreateAdd Microsoft Sentinel to a workspace
  3. Select your Log Analytics workspace
  4. Click Add

Step 2: Create Custom Data Connector

Get Workspace Credentials

  1. Navigate to your Log Analytics workspace
  2. Click Agents (under Settings)
  3. Copy and save:
    • Workspace ID: Long alphanumeric string (e.g., a1b2c3d4-...)
    • Primary Key: Base64-encoded key

Determine Data Collector API Endpoint

The endpoint format is:

https://<WorkspaceID>.ods.opinsights.azure.com/api/logs?api-version=2016-04-01

Replace <WorkspaceID> with your Workspace ID.

Example:

https://a1b2c3d4-e5f6-7890-abcd-ef1234567890.ods.opinsights.azure.com/api/logs?api-version=2016-04-01

Step 3: Create the Skycloak Destination

Skycloak signs each request with Azure’s SharedKey scheme natively, so no relay or intermediate function is needed.

  1. In Skycloak, go to SIEM Integration and click Create Destination.
  2. Basic Information:
    • Destination Name: Azure Sentinel
    • Destination Type: HTTP Webhook
  3. Destination Configuration:
    • Webhook URL: the Data Collector endpoint from Step 2, for example https://a1b2c3d4-e5f6-7890-abcd-ef1234567890.ods.opinsights.azure.com/api/logs?api-version=2016-04-01
    • Authentication Type: Azure Shared Key (Sentinel)
    • Azure Workspace ID: the Workspace ID from Step 2
    • Azure Shared Key: the Primary Key from Step 2 (the Secondary Key works too)
    • Custom Headers: leave empty unless you want a different table name (see below)
  4. Event Filtering: select the events to forward, for example LOGIN, LOGIN_ERROR, LOGOUT, UPDATE_PASSWORD, UPDATE_EMAIL, REGISTER, CODE_TO_TOKEN, REFRESH_TOKEN.
  5. Batching: batch size 500 and batch interval 120 seconds are reasonable starting points.
  6. Save the destination. Skycloak starts forwarding from the moment it is created, not retroactively.

What Skycloak sends

For each batch, Skycloak posts a JSON array of events to your endpoint with:

  • Authorization: SharedKey <WorkspaceID>:<signature>, where the signature is an HMAC-SHA256 hash computed per request from the request’s method, content length, content type, date and resource path, keyed by your shared key
  • x-ms-date: the request timestamp used in that signature
  • Log-Type: the custom table name, which is what makes Azure create KeycloakEvents_CL
  • time-generated-field: timestamp, so Azure uses the event’s own timestamp rather than ingestion time

Your shared key is stored as a credential: it is never returned by the Skycloak API and never appears in logs. Rotating the key in Azure means editing the destination and pasting the new key.

Each record is one event in Skycloak’s standard export format (type, timestamp, realm, user, auth, session, geo, and so on, the same shape used by webhooks and every other SIEM destination). Azure adds a type suffix to each column when it builds the table, so type becomes type_s. Run the | getschema query in Step 4 once your first batch lands and adjust the column names in the sample queries below to match what your workspace actually created.

Table naming

The Log-Type header decides the Azure custom table name (Azure appends _CL). Skycloak picks a default from the data you selected:

Selection in Skycloak Log-Type Azure table
Keycloak events KeycloakEvents KeycloakEvents_CL
Application logs SkycloakApplicationLogs SkycloakApplicationLogs_CL
Security logs SkycloakSecurityLogs SkycloakSecurityLogs_CL
Skycloak audit logs SkycloakAuditLogs SkycloakAuditLogs_CL

To use your own table name, add a custom header such as {"Log-Type": "MyKeycloakTable"} on the destination. Azure only accepts letters and digits in this value.

The queries in the rest of this guide assume the KeycloakEvents_CL default.

Step 4: Verify Data Flow

Check Azure Monitor Logs

  1. Navigate to your Log Analytics workspace
  2. Click Logs (under General)
  3. Run this KQL query:
KeycloakEvents_CL
| take 10

Note: It may take 5-15 minutes for the custom table to appear after first data ingestion.

Verify Table Schema

Check the automatically created table schema:

KeycloakEvents_CL
| getschema

Test with Recent Login Event

  1. Log in to one of your Keycloak realms
  2. Wait 3-5 minutes for batch processing and ingestion
  3. Run this query:
KeycloakEvents_CL
| where event_type_s == "LOGIN"
| order by TimeGenerated desc
| take 1

Note: Azure automatically adds _s suffix to string fields, _d to numbers, _b to booleans.

Step 5: Create Analytics Rules

Example: Detect Brute Force Attacks

  1. Navigate to Microsoft SentinelAnalytics
  2. Click CreateScheduled query rule
  3. Configure:

General:

  • Name: Keycloak Brute Force Attempt
  • Description: Detects multiple failed login attempts from same IP
  • Severity: High
  • MITRE ATT&CK: Credential Access → Brute Force (T1110)

Rule Query:

KeycloakEvents_CL
| where event_type_s == "LOGIN_ERROR"
| where TimeGenerated > ago(5m)
| summarize FailedAttempts=count() by ip_address_s, username_s, bin(TimeGenerated, 5m)
| where FailedAttempts >= 5
| project TimeGenerated, username_s, ip_address_s, FailedAttempts

Query Scheduling:

  • Run query every: 5 minutes
  • Lookup data from the last: 5 minutes

Alert Threshold: Greater than 0 results

Entity Mapping:

  • Account: username_s
  • IP: ip_address_s
  1. Click Review and CreateCreate

Example: Detect Suspicious Login Locations

Alert on logins from unusual geographic locations:

let UserLoginLocations = KeycloakEvents_CL
| where event_type_s == "LOGIN"
| where TimeGenerated > ago(30d)
| extend Location = geo_info_from_ip_address(ip_address_s)
| summarize KnownCountries=make_set(Location.country) by username_s;
KeycloakEvents_CL
| where event_type_s == "LOGIN"
| where TimeGenerated > ago(5m)
| extend Location = geo_info_from_ip_address(ip_address_s)
| join kind=inner UserLoginLocations on username_s
| where not(Location.country in (KnownCountries))
| project TimeGenerated, username_s, ip_address_s, NewCountry=Location.country

Example: Privileged Account Changes

Alert on password changes for admin accounts:

KeycloakEvents_CL
| where event_type_s == "UPDATE_PASSWORD"
| where username_s contains "admin" or username_s contains "root"
| project TimeGenerated, username_s, ip_address_s, realm_s

Step 6: Create Custom Workbooks

Example: Authentication Monitoring Workbook

  1. Navigate to Microsoft SentinelWorkbooks
  2. Click Add workbookEdit
  3. Add visualizations:

Query 1: Login Success Rate (Donut Chart)

KeycloakEvents_CL
| where event_type_s in ("LOGIN", "LOGIN_ERROR")
| where TimeGenerated > ago(24h)
| summarize Count=count() by event_type_s
| render piechart

Query 2: Failed Logins Timeline (Line Chart)

KeycloakEvents_CL
| where event_type_s == "LOGIN_ERROR"
| where TimeGenerated > ago(7d)
| summarize FailedLogins=count() by bin(TimeGenerated, 1h)
| render timechart

Query 3: Top Failed Login Users (Bar Chart)

KeycloakEvents_CL
| where event_type_s == "LOGIN_ERROR"
| where TimeGenerated > ago(24h)
| summarize Failures=count() by username_s
| top 10 by Failures desc
| render barchart

Query 4: Login Activity Map

KeycloakEvents_CL
| where event_type_s == "LOGIN"
| where TimeGenerated > ago(24h)
| extend Location = geo_info_from_ip_address(ip_address_s)
| summarize Logins=count() by Location.country, Location.latitude, Location.longitude
| render scatterchart with (kind=map)
  1. Click Done EditingSave As
  2. Name: Keycloak Authentication Monitoring

Useful KQL Queries

Recent Failed Logins

KeycloakEvents_CL
| where event_type_s == "LOGIN_ERROR"
| where TimeGenerated > ago(1h)
| project TimeGenerated, username_s, ip_address_s, realm_s
| order by TimeGenerated desc

User Login History

KeycloakEvents_CL
| where event_type_s == "LOGIN"
| where username_s == "john.doe"
| where TimeGenerated > ago(30d)
| project TimeGenerated, ip_address_s, realm_s, client_id=details_s.client_id
| order by TimeGenerated desc

Login Activity by Realm

KeycloakEvents_CL
| where event_type_s == "LOGIN"
| where TimeGenerated > ago(7d)
| summarize LoginCount=count() by realm_s
| order by LoginCount desc

Account Changes Summary

KeycloakEvents_CL
| where event_type_s in ("UPDATE_PASSWORD", "UPDATE_EMAIL", "REGISTER", "DELETE_ACCOUNT")
| where TimeGenerated > ago(24h)
| summarize Count=count() by event_type_s, username_s
| order by Count desc

Concurrent Login Detection

KeycloakEvents_CL
| where event_type_s == "LOGIN"
| where TimeGenerated > ago(1h)
| summarize UniqueIPs=dcount(ip_address_s) by username_s, bin(TimeGenerated, 5m)
| where UniqueIPs > 2
| project TimeGenerated, username_s, UniqueIPs

Integration with Microsoft Services

Azure AD Integration

Correlate Keycloak events with Azure AD sign-ins:

KeycloakEvents_CL
| where event_type_s == "LOGIN"
| join kind=inner (
    SigninLogs
    | where TimeGenerated > ago(1h)
) on $left.username_s == $right.UserPrincipalName
| project KeycloakLogin=TimeGenerated, AzureADLogin=TimeGenerated1, username_s, ip_address_s

Microsoft 365 Correlation

Find users with suspicious activity in both Keycloak and M365:

let SuspiciousKeycloakUsers = KeycloakEvents_CL
| where event_type_s == "LOGIN_ERROR"
| where TimeGenerated > ago(1h)
| summarize FailedAttempts=count() by username_s
| where FailedAttempts > 5;
OfficeActivity
| where TimeGenerated > ago(1h)
| join kind=inner SuspiciousKeycloakUsers on $left.UserId == $right.username_s
| project TimeGenerated, Operation, UserId, ClientIP

Troubleshooting

No Data Appearing in Sentinel

  1. Check Skycloak destination status:

    • Navigate to SIEM Integration page
    • Verify “Healthy” status
    • Check “Events Sent” metric is increasing
  2. Verify Workspace ID and Key:

    • Ensure Workspace ID matches your Log Analytics workspace
    • Verify Primary Key is correct (not Secondary Key)
    • Check for trailing spaces in credentials
  3. Check the destination’s last error: an Azure rejection is reported on the destination card in Skycloak. 403 InvalidAuthorization almost always means the Workspace ID and the shared key do not belong to the same Log Analytics workspace, or the key was rotated in Azure and not updated in Skycloak.

    Testing this endpoint by hand needs a signing script, since the Authorization header changes on every request. Microsoft publishes ready-made samples in the Data Collector API reference.

  4. Check Azure Monitor Logs ingestion:

    • Navigate to your workspace → Usage and estimated costs
    • Verify data is being ingested

Custom Table Not Appearing

  • Wait 5-15 minutes: Azure takes time to create custom tables on first ingestion
  • Check the table name: Skycloak sets Log-Type for you (see Table naming); if you overrode it with a custom header, query that table instead
  • Verify data format: Azure expects JSON array with timestamp field

Query Performance Issues

  • Use time filters: Always filter by TimeGenerated for better performance
    KeycloakEvents_CL
    | where TimeGenerated > ago(1h)  // Always add this
  • Limit result sets: Use take or top to limit results
  • Optimize joins: Join on indexed fields when possible

High Ingestion Costs

  1. Filter events: Only forward critical event types
  2. Reduce batch frequency: Increase batch interval to 300-600 seconds
  3. Disable server logs: Turn off “Include Logs” if not needed
  4. Set retention policies: Configure data retention to manage costs
    • Navigate to workspace → Usage and estimated costsData Retention

Best Practices

  1. Use Managed Identity: For production, use Azure Managed Identity instead of shared keys (requires Azure-hosted connector)
  2. Enable Diagnostic Logging: Turn on Azure Monitor diagnostic logs for troubleshooting
  3. Create Automation Playbooks: Use Azure Logic Apps to automate incident response
  4. Set Up Notifications: Configure email/SMS alerts for critical incidents
  5. Regular Rule Tuning: Review and adjust analytics rules based on false positives
  6. Data Retention: Balance retention needs with cost (default: 90 days)
  7. Use Watchlists: Create watchlists for VIP users, known good IPs, etc.

Next Steps

  • Create custom analytics rules for your security requirements
  • Build comprehensive workbooks for monitoring
  • Set up automation playbooks for incident response
  • Integrate with Azure Security Center for unified security posture
  • Configure data retention policies based on compliance needs
  • Explore Microsoft Sentinel content hub for pre-built solutions

Support

For assistance:

Last updated on