Skip to main content

Automate Azure Bastion with Drasi Realtime RBAC Monitoring

· 46 min read

Drasi (named after the Greek word for 'Action') is a change data processing platform that automates real-time detection, evaluation, and meaningful reaction to events in complex, event-driven systems, created as part of the Azure Incubation Teams, Drasi was accepted in to the Cloud Native Computing Foundation, at the Sandbox Maturity level in January of 2025.

I was fortunate enough to witness a demo of this in action and wondered how I might learn to use Drasi with something I am familiar with - the Microsoft Azure ecosystem. The Azure Role Assignment Monitor with Drasi was born.

🚀 Introduction to Drasi

info

Drasi is a Data Change Processing platform that makes it easier to build dynamic solutions that detect and react to data changes that occur in existing databases and software systems (i.e., not only new systems built using Drasi). Drasi’s change detection capabilities extend beyond simply reporting add, update, and delete operations, as typically provided by database transaction/change logs and message-based change notification solutions. Instead, Drasi’s low-code query-based approach enables you to write rich graph queries through which you can express sophisticated rules describing the types of changes you want to detect and the data you want to distribute about those changes to downstream systems.

Here are some examples of scenarios where Drasi’s functionality can be applied to existing systems to detect and react to changing data:

  • Observing data from building sensors and automatically adjusting HVAC settings to maintain a comfortable environment for building occupants.
  • Risk management through early awareness of company employees, facilities, and assets that are at risk due to emerging incidents occurring in their current location.
  • Optimizing the delivery of orders to customers when they arrive in a curbside pickup zone.
  • Improving infrastructure threat detection by raising alerts when a container with known security threats is deployed to a Kubernetes Cluster.

📋 Understanding Drasi Components

The power of Drasi is the components:

Drasi components

  • Sources
  • Continuous queries
  • Reactions

You can have multiple sources, which are the data sources that Drasi monitors for changes. These can be databases, message queues, or any other system that can provide change data. A number of provided sources exist out of the box - CosmosDB, MySQL, PostgreSQL, Event Hub, SQL Server, Kubernetes, Dataverse.

These sources are monitored for changes, and when a change is detected, it is processed by a continuous query. Sources act like skilled translators. They watch a system’s native log or feed, convert inserts/updates/deletes into a consistent graph form, and push only the meaningful deltas to the engine. A continuous query is a query that runs continuously and processes the changes detected by the source. The continuous query can be used to filter, transform, and aggregate the data from the source. Drasi leverages a subset of the Cypher graph query language because it excels at expressing patterns—entities and relationships—across multiple data sources. You write a one-time MATCH/WHERE/RETURN query that continually runs under the covers. Reactions are the actions that are taken when a change is detected and processed by the continuous query. Reactions can be used to send notifications, update other systems, or trigger other actions. Drasi provides a number of reactions out of the box - Azure Functions, Logic Apps, Webhooks, and more. Reactions subscribe to one or more Continuous Queries and trigger real-world actions when those queries’ result sets change. Because Reactions consume the structured “Query Result Change” payload—detailing exactly which nodes or relationships were added, updated, or removed—they can make fully informed decisions without re-polling underlying systems.

Imagine a smart-building scenario:

  • Source: Drasi watches Azure Cosmos DB for room-temperature updates and PostgreSQL for occupancy schedules.
  • Continuous Query: A single Cypher query joins rooms, sensors, and schedules to detect “occupied room < 18°C” conditions.
  • Reaction: When that condition appears in the CQ’s result set, a Reaction module fires a heating command via MQTT; when it no longer applies, another Reaction module stops the heater.

Because Sources supply clean graph events, the CQ keeps an always-correct view without heavy polling, and Reactions act on precise change notifications, the entire pipeline runs at low latency and minimal operational overhead. No custom glue code. No brittle cron jobs.

🎯 Our Azure Bastion Automation Scenario

In my scenario, I wanted to monitor the Azure Role Assignments in my Azure Subscription, my example is the use of the Azure Bastion Service, this services allows secure RDP and SSH connectivity to virtual machines in your Azure Virtual Network without the need for a public IP address on the virtual machine. This is a great service, it does require a role assignment to be able to use it, and its a pay as you go service - so I may not necessarily need it running all the time, wasting cost. Because I might require this service very ad-hoc, I couldn't rely on a schedule-type system I'd usually use (ie, tag the resource with the schedule, have an Azure runbook create and delete the resource). There are other ways to monitor Azure Role Assignments, but I wanted to use Drasi to monitor the changes in real-time and react to them, in a way that could be expanded.

So my scenario is:

When Sarah from Marketing needs access to a VM:

  • An admin assigns her "VM Administrator Login" role (manually or via a PIM assignment)
  • Automatically, this system detects the change
  • Automatically, it creates a secure Bastion host
  • Sarah can now securely connect to the VM
  • When her access is revoked, the Bastion is automatically cleaned up

📋 Azure Activity Logs → 📨 Event Hub → 🔍 Drasi → 📧 Event Grid → ⚡ Azure Function → 🛡️ Create/Delete Bastion (or Ignore)

Azure Activity Logs: Every action in Azure (like assigning roles) gets logged Event Hub: Collects these logs in real-time Drasi Source: Reads events from the Event Hub Drasi Continuous Query: Filters for role assignment events we care about Drasi Reaction: Sends notifications to Event Grid when matches are found Azure Function: Receives the notification and takes action, adds additional logic as needed to filter results and take action Bastion Management: Creates or removes Azure Bastion hosts as needed

Drasi

warning

There is a delay between the diagnostic logs being written to the Event Hub; it can take a few minutes for the role assignment changes to be logged. This is not a Drasi issue, but an Azure Event Hub/Azure Monitor export issue, it can take up to 5 minutes for the logs to be written to the Event Hub (but once they are, Drasi picks it up within seconds). In my testing, I was able to fast-track my testing by publishing the Event Hub message directly into the Event Hub data explorer, without needing to go and manually wait for the role assignment to be created or deleted.

🛠️ Setting Up Drasi Environment

So, first things first, let's get Drasi up and running! To do this, I am going to use a GitHub Codespace - you can find my devcontainer setup here - this will allow me to run Drasi in a container via docker if I want, but the main part is - it will already install the latest Drasi CLI, and have all the dependencies I need to deploy Drasi to my Azure Kubernetes Service, and manage it - such as the Drasi Visual Studio Code extension - all the code in this example is in this repository as well (ie Drasi query, Function App).

To do this, I am going to deploy to an Azure Kubernetes Service cluster. The size of the cluster will depend on your use case. For my purposes, I am going with the following resources:

🏗️ Azure Resources Required

Resource TypeNameRegionConfigurationPurpose
AKS Clusterdrasi-aksNew Zealand North• Standard Cluster • System Pool: Standard_D4ds_v5 • User Pool: Standard_D4s_v5 • Workload Identity enabled • Namespace: drasi-systemHosts Drasi and runs query host for Continuous Queries
User Assigned Managed Identitydrasi-miNew Zealand North• Role: Azure Event Hubs Data Receiver • OIDC connection to AKSAuthentication for Drasi to access Event Hub
Event Hub NamespaceazroleNew Zealand North• Standard tier • 1 throughput unit • Hub name: drasieventhub1Drasi source - collects Azure Activity Logs
Event Grid Topicdrasi-eventgrid-topicNew Zealand North• Basic tier • Schema: CloudEvents v1.0 • Azure Function subscriptionDrasi reaction - sends notifications when changes detected
Azure Function Appdrasi-function-appAustralia East*• Linux Flex Consumption plan • PowerShell Core runtimeCreates/deletes Azure Bastion based on role assignments

* Australia East region used as this SKU is not available in New Zealand North

Important Notes

  • AKS VM Sizing: If you encounter errors with the Drasi API and query host starting, check your User pool VM size. Standard_D4s_v5 works well, but smaller sizes may cause issues with Drasi and Dapr runtime.
  • Workload Identity: The AKS cluster must have Workload identity enabled for proper authentication.

I won't go through the creation of these resources individually; instead, I will focus on how they are used in relation to our Drasi Azure Role Assignment Monitor setup.

🚀 Installing Drasi on AKS

So we have our Azure resources created, and we can now deploy Drasi to our AKS cluster. To do this, we will use the Drasi CLI, which is installed in the devcontainer I mentioned earlier. Alternatively, you can install it locally if you prefer.

First, we will connect to our AKS cluster and confirm we can see the namespace before running the drasi init command to deploy Drasi to our AKS cluster:

# Install kubectl if not already installed
sudo az aks install-cli

# Login to Azure and get cluster credentials
az login
az aks get-credentials --resource-group <your-resource-group> --name <your-cluster-name>

# Check Drasi version
drasi version

# Initialize Drasi on Kubernetes
drasi init --namespace drasi-system --version 0.3.4

# Verify installation
kubectl get pods -n drasi-system

Drasi pods running

📊 Configuring Azure Activity Logs

Now that we have Drasi running in our AKS cluster, we can start the additional configuration. Let us configure our Azure Activity Logs to be sent to our Event Hub, so that Drasi can monitor them for changes. To do this, we will need to create a diagnostic setting on our subscription and configure it to send the Activity Logs to our Event Hub.

Role-based assignments are an Administrative category, so we will need to select the Administrative category when creating the diagnostic setting. You can do this via the Azure Portal by navigating to your subscription, selecting Activity Log, select Export Activity Logs, and then selecting the Event Hub you created earlier.

Azure Activity Log Diagnostic Setting

Once you have created the diagnostic setting, you can verify that the logs are being sent to the Event Hub by using the Azure Event Hub Data Explorer. It can take a few minutes for the logs to start appearing.

🔐 Setting Up Authentication with Workload Identity

Next, it's time to configure our Drasi source to read from the Event Hub. We are going to use Entra ID authentication to authenticate Drasi to the Event Hub, and this is where the Workload Identity we created earlier comes in. We will need to create a User Assigned Managed Identity and assign it the Azure Event Hubs Data Receiver role on the Event Hub namespace.

User Assigned Managed Identity

Now we need to grab the Issuer URL and Client ID of the User Assigned Managed Identity, we will need these to configure our federated credential, and configure out federated credential. Make sure to update for your own environment, the source name needs to align with your source name, and the service account to your namespace.

# Variables
AKS_NAME="drasi-azroleassignment-aks"
RG_NAME="drasi-azrolemonitor-rg"
MI_NAME="drasi-mi"
FC_NAME="drasi-eventhub"
SUBJECT="system:serviceaccount:drasi-system:source.azure-role-eventhub-source"
AUDIENCE="api://AzureADTokenExchange"

# Get the OIDC issuer URL for the AKS cluster
ISSUER_URL=$(az aks show --name "$AKS_NAME" --resource-group "$RG_NAME" --query "oidcIssuerProfile.issuerUrl" -o tsv)

# Create federated credential
az identity federated-credential create \
--name "$FC_NAME" \
--identity-name "$MI_NAME" \
--resource-group "$RG_NAME" \
--issuer "$ISSUER_URL" \
--subject "$SUBJECT" \
--audience "$AUDIENCE"

Create Federated Credential

📡 Creating Drasi Event Hub Source

Now we can create our Drasi source:

eventhubsource.yaml
kind: Source
apiVersion: v1
name: azure-role-eventhub-source
spec:
kind: EventHub
identity:
kind: MicrosoftEntraWorkloadID
clientId: 50421fa8-277f-4c22-a085-d5880422dc52
properties:
host: azrole.servicebus.windows.net
eventHubs:
- drasieventhub1
bootstrapWindow: 5

Update the clientId with your User Assigned Managed Identity Client ID, and the host with your Event Hub namespace. You can then apply this source to your Drasi cluster:

drasi apply -f eventhubsource.yaml

Then check the status of the source:

drasi list source
# Output should show the source is Available.

Drasi Source

🔍 Building the Continuous Query

Now that we have our source configured. It's time to create our Continuous Query. This is the query that will filter the Azure Activity Logs for the role assignment events we care about. We will use the Cypher query language to do this. It's worth noting that Drasi uses a subset of the Cypher query language (https://drasi.io/reference/query-language/), so not all Cypher queries will work.

Make sure to update to the source name to match your source name, and the label to match the label (ie, the name of the Event Hub (the hub, not the namespace)) you want to use for the role assignment events.

azure-role-change-vmadminlogin.yaml
kind: ContinuousQuery
apiVersion: v1
name: azure-role-change-vmadminlogin
spec:
mode: query
sources:
subscriptions:
- id: azure-role-eventhub-source
nodes:
- sourceLabel: drasieventhub1
pipeline:
- extract-role-assignments
middleware:
- name: extract-role-assignments
kind: unwind
drasieventhub1:
- selector: $.records[?(@.operationName == 'MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE' || @.operationName == 'MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/DELETE')]
label: RoleAssignment
key: $.time
properties:
time: $.time
resourceId: $.resourceId
operationName: $.operationName
operationType: $.resultType
category: $.category
level: $.level
correlationId: $.correlationId
caller: $.identity.claims.name
callerIpAddress: $.callerIpAddress
tenantId: $.tenantId
subscriptionId: $.identity.authorization.scope
status: $.resultSignature
subStatus: $.resultType
durationMs: $.durationMs
properties: $.properties
entity: $.properties.entity
requestBody: $.properties.requestbody
resourceType: "Microsoft.Authorization/roleAssignments"
resourceProviderName: "Microsoft.Authorization"
query: |
MATCH (r:RoleAssignment)
RETURN r.correlationId AS correlationId,
r.time AS timestamp,
r.resourceId AS resourceId,
r.operationName AS operationName,
r.operationType AS operationType,
r.category AS category,
r.level AS level,
r.callerIpAddress AS callerIpAddress,
r.caller AS caller,
r.tenantId AS tenantId,
r.subscriptionId AS subscriptionId,
r.status AS status,
r.subStatus AS subStatus,
r.durationMs AS durationMs,
r.properties AS properties,
r.entity AS entity,
r.resourceType AS resourceType,
r.resourceProviderName AS resourceProviderName,
r.requestBody AS requestBody

🧠 Understanding the Continuous Query Logic

This configuration leverages Drasi's event-driven streaming capabilities to track and analyze Azure role assignment changes in real time. Let's walk through what's happening:

The file starts by declaring a ContinuousQuery named azure-role-change-vmadminlogin. This tells Drasi to continuously process incoming data streams for relevant events. It then connects to an Azure Event Hub source (drasieventhub1) that streams Azure activity logs. The pipeline extracts only those records where the operation is either a role assignment creation (WRITE) or deletion (DELETE).

A middleware step called extract-role-assignments uses JSONPath selectors to:

  • Filter for role assignment events.
  • Extract key properties such as timestamp, resource ID, operation details, caller identity, IP address, tenant and subscription IDs, and more.
  • Label each event as a RoleAssignment node for downstream querying.

The core query than matches all RoleAssignment nodes and returns a rich set of fields for each event, including:

  • Correlation and resource IDs
  • Operation details (name, type, status)
  • Caller and network information
  • Entity and resource metadata
  • Raw request body for further analysis

It is worth calling out - that the key: $.time line under the extract-role-assignments middleware specifies that the event's timestamp (from the $.time field in each Azure log record) is used as the unique key for each extracted RoleAssignment node. Using the time as a key helps Drasi maintain a consistent view of the data stream, ensuring that each event is processed only once - the problem with using the correlationId as the key is that it is not unique, and can be reused for multiple events, ie the status of a Role back assignment goes from Created, Started, Success and each of these events share the same correlationId, which overwrites the previous event in the graph - and not all statuses have the same properties (ie role definition Id which we need as part of our filtering in the Function App later on).

drasi apply -f azure-role-change-vmadminlogin.yaml

Then check the status of the query:

drasi list query
# Output should show the query is Running. Any errors, such as syntax in the query, will be displayed here in the Error Message column.

Drasi Continuous Query

⚡ Creating the Event Grid Reaction

Now that we have our source and continuous query configured, we can create our Drasi reaction. This is the action that will be taken when a role assignment event is detected. In this case, we will use an Azure Event Grid, which will send a notification to our Azure Function when a role assignment event is detected that matches our query. To do this, we will create a Drasi reaction that subscribes to our continuous query, and sends a notification to our Event Grid topic. Make sure that the query matches the name of the Continous query you want it to respond to and the Event Grid URI and Key are updated to match your Event Grid topic.

azure-role-assignment-eventgrid-reaction.yaml
kind: Reaction
apiVersion: v1
name: my-reactionvmlogin
spec:
kind: EventGrid
queries:
azure-role-change-vmadminlogin:
properties:
eventGridUri: https://drasi-eventgrid-topic.newzealandnorth-1.eventgrid.azure.net/api/events
eventGridKey: 95zOYGFPN8rl3XlbgN00YFPSSl4wcM6FN0z9ootjSlYmUFIzKvkoJQQJ99BFACkRTxVXJ3w3AAABAZEGPUfe
format: unpacked

You can also use Entra ID authenticate to connect to the Event Grid topic, but for simplicity (and I was also testing this in a docker build of Drasi, so its good to highlight different methods to connect), I am using a shared access key in this example.

drasi apply -f azure-role-assignment-eventgrid-reaction.yaml

Then check the status of the query:

drasi list reaction
# Output should show the query is Available.

Drasi Reaction

🔧 Building the Azure Function App

Now we have our Drasi source, continuous query, and reaction configured, it's time to move to our Azure Function. This function will be triggered by the Event Grid notification when a role assignment event is detected that matches our query. The function will then create or delete the Azure Bastion host based on the role assignment event. The Azure Function is written in PowerShell and uses the Azure PowerShell module to create and delete the Azure Bastion host. The function will check the operation type of the role assignment event, and if it is a WRITE operation, it will create the Azure Bastion host. If it is a DELETE operation, it will delete the Azure Bastion host. You can add some additional logic here around additional checks, such as checking if the role assignment is for a specific user, or adding additional roles.

The Function App code is configured to use the system-managed identity of the Function App, which is assigned the Contributor role on the Subscription where the Azure Bastion host will be created. This allows the function to create and delete the Azure Bastion host without needing to store any credentials in the function code.

The Function App is configured to be expandable:

📝 Function App Architecture

  • The entry point is run.ps1, receives Event Grid results, and orchestrates the response.
  • It then parses it to an EventProcessor.ps1, which then handles the event processing logic.
  • After the logic matches the event, it calls ActionHandlers.ps1 to create or delete the Azure Bastion host.

With a config.json file to store the configuration settings for the Function App, such as the Azure Bastion host name, resource group name, location, and subscription to target, these values will be used by the ActionHandlers to create/delete the Bastion resource in the specific subscription/resource group you want.

info

You can find the complete code for the Function App and Drasi queries here: lukemurraynz/Drasi-RoleAssignmentMonitor.

config.json
{
"global": {
"enableLogging": true,
"defaultSubscriptionId": "6bca53bc-98d9-4cd3-92e7-0364c7bffac4",
"defaultResourceGroupName": "rg-11B74992",
"tags": {
"CreatedBy": "Drasi-AutoBastion",
"Purpose": "Automated-RBAC-Response"
}
},
"actions": {
"CreateBastion": {
"enabled": true,
"parameters": {
"bastionNamePrefix": "bastion-auto",
"subnetAddressPrefix": "10.0.1.0/26",
"publicIpNamePrefix": "pip-bastion-auto",
"subscriptionId": "<your-subscription-id>",
"resourceGroupName": "<your-resource-group>",
"bastionName": "<your-bastion-name>"
}
},
"CleanupBastion": {
"enabled": true,
"parameters": {
"preserveIfOtherAssignments": true,
"gracePeriodMinutes": 5,
"subscriptionId": "<your-subscription-id>",
"resourceGroupName": "<your-resource-group>"
}
}
},
"roleMappings": {
"/providers/Microsoft.Authorization/roleDefinitions/1c0163c0-47e6-4577-8991-ea5c82e286e4": "Virtual Machine Administrator Login",
"/providers/Microsoft.Authorization/roleDefinitions/fb879df8-f326-4884-b1cf-06f3ad86be52": "Virtual Machine User Login"
}
}

🚀 Deploying the Function App

Now let's deploy our Function App to Azure. I am just going to publish directly from VS Code.

Push Azure Function App

Once pushed, we need to make sure the Trigger is configured to the Event Grid topic by creating a Subscription. I will do this in the Azure Portal.

Create Event Grid Subscription

warning

If you get [Error] ERROR: The member 'FormatsToProcess' in the module manifest is not valid. I have found republishing the module to the Function App fixes this in the second attempt.

🧪 Testing the Complete System

Now - lets test it, by assigning myself the Virtual Machine Administrator Login role, which will then create an Azure Activity Log entry, which will get pushed to the Event Hub, which will then be picked up by Drasi, processed and flicked through to Event Grid for my Function App to process. Make sure you set the RBAC assignment to the Subscription scope that is directing its logs to the Event Hub, or it will never be picked up, you can have multiple subscriptions sending logs to the same Event Hub _(and the Region doesn't matter). Remember, there may be a delay before the event is added to the Event Hub, so please be patient. I monitor the Event Hub in the Azure Portal to track when the event arrives, using the Azure Event Hub Data Explorer.

You can always push through an example log entry through the Event Hub directly, especially if you have a previous WRITE or DELETE event you can copy and resend to fast-track your testing. It's worth noting that Bastion can take a few minutes to create itself, be patient, but you can check its status in the Azure Portal. You could trigger a Teams notification from the Azure Function once it's completed.

Assign VM Admin Role

!Azure Function Created Bastion

Now, if we remove the old, the DELETE event will be picked up by Drasi, and the Function App will process it, removing the Bastion host and Public IP address.

Azure Function Removed Bastion