How to connect to Azure Service Bus using role-based access control in C#
There are two different ways to authorize access to your Azure Service Bus. Role-based access control (RBAC) is the cleanest and the most secure. It uses Azure Active Directory (AAD) to check who or what is trying to access your resource and whether they have permission.
There are three build-in roles for Service Bus that you can assign individually or in combination: Service Bus Data Sender, Service Bus Data Receiver, and Service Bus Data Owner. The first two make it possible to send or receive messages, the third allows full control (send, receive, and more).
We found the setup a bit fiddly, so we wanted to share our solution.
1. Create managed identities for your app services
Our Service Bus ‘users’ are all Azure Functions and WebJobs, so the first thing we did was to make sure the app services each had a managed identity. This is the identity that represents them in AAD.
There are lots of ways to create a managed identity for an app service. We just added this snippet to our ARM templates inside the definition of the app service at the same level as the resource “type”.
"type": "Microsoft.Web/sites","identity": { "type": "SystemAssigned"}
2. Assign Service Bus roles to your managed identities
You can assign roles to your managed identities directly, but we chose to do it by assigning them to an AAD group instead.
We did this because there is a limit to the number of role-assignments per subscription in Azure and, as a large organisation with a lot of cloud infrastructure, we bump up against that limit quite often. Assigning roles individually consumes one role assignment for each user. Assigning them to an AAD group consumes one role assignment for the group. It also makes it easier to keep track of who has permission to do what.
We added our managed identities to our AAD group during app deployment, which for us is an automated process in an Azure DevOps pipeline.
We used an Azure CLI task to get the identity of the app, check if it’s already a member of the group, and add it if not.
$managedIdentity = az functionapp identity show --resource-group myresourcegroup --name myapp --query principalId -o tsv$isMember = az ad group member check --group mygroup --member-id $managedIdentity | ConvertFrom-Jsonif($isMember.Value -eq $False){ az ad group member add --group mygroup --member-id $managedIdentity}
3. Store your Service Bus Namespace in application settings
Next, the apps need the details of the Service Bus they want to access. The most important one is the fully qualified namespace, which follows this naming convention:
myservicebusnamespace.servicebus.windows.net
There are lots of ways to supply it, but we chose to add ours to the app settings in our deployment pipeline, again using the Azure CLI.
az functionapp config appsettings set --name myapp --resource-group myresourcegroup --settings "ServiceBusConnection__fullyQualifiedNamespace =yourservicebusnamespace.servicebus.windows.net"
You’ll notice that the app setting contains a double underscore: “ServiceBusConnection__fullyQualifiedNamespace”. This is the fiddly bit, and we’ll explain why it needs to be that way in the next section.
4. Connect to Service Bus with triggers and bindings
Azure Functions and Azure WebJobs both have built-in triggers and bindings in C# that can connect to Service Bus. They look a bit like this.
[ServiceBusTrigger("myqueue", Connection = "ServiceBusConnection")]
The part labelled “Connection”, is the reference to the app setting that holds the fully qualified namespace of the Service Bus.
What’s not immediately obvious is that if you just add an app setting called “ServiceBusConnection”, the triggers and bindings will not be able to use it.
Instead, you need to add an app setting called “ServiceBusConnection__fullyQualifiedNamespace”.
The double underscores represent a nested app setting. They look like this if you define them in JSON.
"ServiceBusConnection": { "fullyQualifiedNamespace": "myservicebusnamespace.servicebus.windows.net"}
Behind the scenes, triggers and bindings will unpack your ServiceBusConnection app setting, and they will be looking for that nested fullyQualifiedNamespace.
5. Link the rest of your bindings to your application settings
The other details the triggers and bindings will look for are the names of the Service Bus queues, topics, and subscriptions you want your app to be able to access. You can type these directly into your code or, if they’re likely to change, you can save them as app settings.
To do this, you need to wrap the names in % characters. This tells the binding not to read the values as strings but to look for an app setting of the same name instead.
[ServiceBusTrigger("%MyQueue%", Connection = "ServiceBusConnection")]
6. Connect to Service Bus with a client
You don’t have to use triggers and bindings to connect to Service Bus. You can also send and receive messages using a Service Bus Client.
The app settings you need are almost, but not completely, identical.
You can make a client directly, or you can use the Azure Client extension for the service collection in your Startup class. We chose the latter because it allows us to make our clients available for dependency injection.
builder.Services.AddAzureClients(builder =>{
builder.AddServiceBusClientWithNamespace(fullyQualifiedNamespaceName).WithCredential(new DefaultAzureCredential());});
Either way, you need the fully qualified namespace of the Service Bus you want the client to connect to, and a credential to allow AAD to check that you have the required permission.
This setup differs from triggers and bindings in that it doesn’t look directly at your app settings. You have to pull the fully qualified namespace out yourself by getting hold of your configuration.
We did this in Startup just before adding our clients:
IConfiguration config = builder.Services.BuildServiceProvider().GetService<IConfiguration>();
If you’re using triggers and bindings, you can access the nested setting you already have like this.
fullyQualifiedNamespaceName = config["ServiceBusConnection:fullyQualifiedNamespace"]
If you’re only using clients, you can use a standard app setting without nesting, and you can call it whatever you like.
fullyQualifiedNamespaceName = config["FullyQualifiedNamespace"]
7. Authenticate with the Default Azure Credential
The last step in client setup is to provide a credential to allow AAD to check that your app has permission to use the Service Bus. This happens behind the scenes in the triggers and bindings. You need to supply it explicitly to use a Service Bus client.
We chose the Default Azure Credential. It’s a system that works down through a hierarchy of Azure credentials until it finds an identity it can assume.
It works well for us because it will use the developer’s own credential in Visual Studio for local testing, the service principal’s credential in a DevOps pipeline for remote testing, and the managed identity’s credential in the deployed app to access the Service Bus for real.