One SAS authorization rule scoped to an entire Service Bus namespace (
azurerm_servicebus_namespace_authorization_rule), with all three permissions closed by default. Targetshashicorp/azurerm ~> 4.0.
- 🔑 Declares one SAS authorization rule scoped to a whole Service Bus namespace, as a keystone resource named
this. - 🔒 All three permissions default to
false, so the empty call grants nothing and the caller must name the access needed. ⚠️ States the scope plainly: a namespace rule reaches every queue and topic, including ones created later — so a rule written for one queue silently widens as the namespace grows.- 🧭 Points at the better options first: an entity-scoped rule where a client needs one queue or topic, and Entra ID with an RBAC role where the client can use it at all.
- ✅ Mirrors every constraint the provider actually enforces, read from its source: the rule-name pattern, an anchored namespace-ID pattern, at-least-one-permission, and the
manage-requires-listen-and-sendrule — the last two on one variable, to avoid Terraform's validation cycle. - 📤 Emits
listen/send/manageso a security review reads the grant from state;managein particular is meant to be asserted false.
💡 Why it matters:
manageis not "read-write plus" — it permits creating and deleting queues, topics and subscriptions, so a namespace-scoped manage key can destroy the messaging topology rather than merely read or write messages. And every key this module produces is a bearer credential that lands in Terraform state, whether or not anyone reads the output. Both facts argue for the narrowest rule you can get away with, or for skipping SAS entirely.
If this module saves you time, please consider supporting its continued development:
- ⭐ Star the repository on GitHub.
- 🤝 Connect on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
flowchart LR
rg["terraform-azurerm-resource-group"]
kv["terraform-azurerm-key-vault"]
ra["terraform-azurerm-role-assignments: the Entra ID alternative to SAS"]
ns["terraform-azurerm-servicebus-namespace: composite, owns queues topics subscriptions rules"]
nsar["terraform-azurerm-servicebus-namespace-authorization-rule: WHOLE namespace"]
qar["terraform-azurerm-servicebus-queue-authorization-rule: one queue"]
tar["terraform-azurerm-servicebus-topic-authorization-rule: one topic"]
cmk["terraform-azurerm-servicebus-namespace-customer-managed-key: system-assigned identity path"]
dr["terraform-azurerm-servicebus-namespace-disaster-recovery-config: creates the ALIAS"]
pe["terraform-azurerm-private-endpoint: private connectivity to the namespace"]
ns2["a SECOND namespace in another region, Premium and EMPTY"]
clients["clients: prefer the alias endpoint so a failover needs no reconfiguration"]
rg -->|"resource_group_name, location"| ns
kv -->|"key_vault_key_id, BY ID"| cmk
ns -->|"id, BY ID"| nsar
ns -->|"id, BY ID"| cmk
ns -->|"id, BY ID"| pe
ns -->|"id as primary_namespace_id"| dr
ns2 -->|"id as partner_namespace_id, updatable"| dr
ns -->|"queue_ids, BY ID"| qar
ns -->|"topic_ids, BY ID"| tar
nsar -->|"id as alias_authorization_rule_id, else ROOT key is used"| dr
dr -->|"alias connection strings"| clients
qar -->|"least-privilege connection string"| clients
tar -->|"least-privilege connection string"| clients
ra -->|"Data Sender or Data Receiver: no key to leak"| ns
classDef me fill:#0078D4,stroke:#004578,color:#fff;
classDef keystone fill:#004578,stroke:#001f3f,color:#fff;
classDef sib fill:#eef2f7,stroke:#b8c4d0,color:#1b1b1b;
class ns keystone;
class nsar,qar,tar,cmk,dr me;
class rg,kv,ra,ns2,clients,pe sib;
flowchart TB
scope["scope: the WHOLE namespace, including entities created LATER"]
prefer["prefer an entity-scoped rule where a client needs one queue or topic"]
entra["prefer Entra ID plus an RBAC role over SAS entirely: no key exists to leak"]
addr["name and namespace_id: both force-new"]
newkeys["replacing the rule ISSUES NEW KEYS, so every client on the old string breaks"]
listen["listen: default FALSE"]
send["send: default FALSE"]
manage["manage: default FALSE, and it permits DELETING queues topics subscriptions"]
guards["both cross-field checks live on manage: at-least-one, and manage requires listen plus send"]
this["terraform-azurerm-servicebus-namespace-authorization-rule"]
res["azurerm_servicebus_namespace_authorization_rule.this"]
posture["outputs listen, send, manage so a review reads the grant from state"]
creds["6 SENSITIVE outputs: primary and secondary key, connection string, and the alias forms"]
alias["the alias forms are EMPTY unless the namespace is geo-DR paired"]
scope -->|"read this first"| this
prefer -->|"narrower alternative"| scope
entra -->|"stronger alternative"| this
addr -->|"identity"| this
newkeys -->|"cost of replacement"| addr
listen -->|"closed by default"| this
send -->|"closed by default"| this
manage -->|"the dangerous one"| this
guards -->|"enforced at plan"| manage
this -->|"creates"| res
res -->|"emits"| posture
res -->|"emits"| creds
alias -->|"a client on the non-alias string does not survive failover"| creds
classDef me fill:#0078D4,stroke:#004578,color:#fff;
classDef keystone fill:#004578,stroke:#001f3f,color:#fff;
classDef sib fill:#eef2f7,stroke:#b8c4d0,color:#1b1b1b;
class this me;
class res keystone;
class scope,prefer,entra,addr,newkeys,listen,send,manage,guards,posture,creds,alias sib;
Resource inventory
| Resource | Count | Role |
|---|---|---|
azurerm_servicebus_namespace_authorization_rule.this |
1 | The keystone authorization rule, with its optional timeouts block. No tags. |
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/azurerm |
~> 4.0 |
| Provider block | None in this module — the caller configures provider "azurerm" { features {} }, auth, and subscription. |
Schema notes that bite (verified against the live provider schema):
nameandnamespace_idare both force-new. Replacing the rule issues new keys, so every client using the old connection string stops working at that moment.- The provider's
namevalidator is weaker than its own error message. The pattern is^[a-zA-Z0-9][-._a-zA-Z0-9]{0,48}([a-zA-Z0-9])?$, whose trailing alphanumeric group is optional — soa-,x.andx_are all accepted, although the message says a name "must start and end with a letter or number". The true maximum is 50 characters, not "less the 50". This module mirrors the pattern, because a check written from the message would refuse names the provider accepts — and a failedvalidation {}blocksterraform destroyas well as apply. - Do not name this rule
RootManageSharedAccessKey. Azure creates a rule by that name on every namespace automatically, with all three permissions granted. Declaring it here has Terraform adopt or overwrite a rule it did not create, and destroying it removes the namespace's built-in administrative credential. - The geo-DR replication wait is bounded by
timeouts.updateon create, update AND delete. Raisingtimeouts.deletedoes not extend the slowest part of a destroy. That wait happens only when the namespace is Premium and has exactly one disaster-recovery config; otherwise it returns immediately. - This resource carries a state upgrader and its two siblings do not. It has a schema version, and state written by an older provider has its stored ID re-parsed case-insensitively and rewritten in canonical casing. The queue- and topic-scoped rules carry neither, so a mis-cased ID in their state is left as found. It is the one genuine behavioural difference between three otherwise near-identical resources, and it matters only when adopting old state.
- The provider's permission rules live in its
CustomizeDiff, which runs inside the provider during plan and needs credentials; the module's ownvalidation {}blocks move both refusals offline. - All three permission booleans are optional to the provider, so a rule granting nothing is accepted. This module rejects that at plan.
- The service requires
listenandsendwhenevermanageis true; validated at plan here. managepermits creating and deleting queues, topics and subscriptions — not merely message access.- The scope is the whole namespace including entities created later, so the grant widens as the namespace grows without anyone editing the rule.
- The
*_connection_string_aliasoutputs are empty unless the namespace participates in a geo-DR pairing. A client using the non-alias string does not survive a failover. - The namespace is addressed by Resource ID, so the reference creates a real Terraform dependency (unlike the Event Hubs equivalents, which take names). The module's check on it is anchored with
$, which matters more here than on the queue- and topic-scoped siblings: every Service Bus child ID extends the namespace ID, so an unanchored pattern would accept a queue ID, a topic ID or this rule's own ID and silently scope the rule elsewhere. - Deleting the rule invalidates its keys immediately — a client-visible event.
- This resource type has no
tagssurface.
Contributoron the namespace's resource group, or a custom role coveringMicrosoft.ServiceBus/namespaces/authorizationRules/*.Microsoft.ServiceBus/namespaces/authorizationRules/listKeys/actionto read the keys and connection strings this module emits.
⚠️ Note the operational consequence: whoever can apply this configuration can mint a namespace-wide credential — and if they setmanage, one that can delete every queue and topic in the namespace. Scope the role accordingly.
- An existing Service Bus namespace.
- A decision about whether SAS is needed at all. If the client can use Entra ID with an RBAC role (
Azure Service Bus Data Sender/Data Receiver), that is the stronger option and this module is unnecessary. - The caller configures the
provider "azurerm" { features {} }block, auth, and subscription.
terraform-azurerm-servicebus-namespace-authorization-rule/
├── providers.tf # required_version >= 1.12.0; azurerm ~> 4.0; no provider block
├── variables.tf # three permissions closed by default; both guards on `manage`; timeouts tail
├── main.tf # keystone azurerm_servicebus_namespace_authorization_rule.this
├── outputs.tf # id, name, the permission trio, and 6 sensitive credentials
├── README.md # this document
├── SCOPE.md # cross-module contract
├── LICENSE # MIT
└── .gitignore # canonical library ignore set
provider "azurerm" {
features {}
}
# A producer that publishes across the namespace.
module "sb_publisher_rule" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-servicebus-namespace-authorization-rule.git?ref=v1.0.0"
name = "publisher"
namespace_id = module.sb_namespace.id # BY ID — a real dependency
send = true
# listen and manage stay false.
}🔒 Nothing is granted unless you type it. And consider whether this client could use Entra ID instead — see example 3.
ℹ️ The caller owns the provider, its authentication, and the mandatory
features {}block. This module never declares them.
Consumes
| Input | Type | Source module |
|---|---|---|
namespace_id |
string |
terraform-azurerm-servicebus-namespace (id) |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
Rule Resource ID (first) | terraform-azurerm-servicebus-namespace-disaster-recovery-config (alias_authorization_rule_id), audit inventories |
name |
Rule name | operational review |
namespace_id |
The namespace the rule is scoped to | composition wiring |
listen / send |
Whether receive / send access is granted | governance review |
manage |
Whether manage access is granted | security review — assert false |
primary_key / secondary_key |
Sensitive. The SAS key pair | client configuration |
primary_connection_string / secondary_connection_string |
Sensitive. Connection strings, key inline | client configuration |
primary_connection_string_alias / secondary_connection_string_alias |
Sensitive. Alias forms, empty unless geo-DR paired | clients that must survive a failover |
The examples below reference existing resources by ID or name rather than creating them; this module owns only its own resource. Those references are declared inputs:
variable "app_identity_principal_id" {
description = "identity principal id of an existing app that these examples reference but do not create."
type = string
}
variable "orders_app_identity_principal_id" {
description = "identity principal id of an existing orders app that these examples reference but do not create."
type = string
}
variable "sb_namespace_dr_id" {
description = "id of an existing sb namespace dr that these examples reference but do not create."
type = string
}1 · A send-only producer
module "sb_publisher_rule" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-servicebus-namespace-authorization-rule.git?ref=v1.0.0"
name = "publisher"
namespace_id = module.sb_namespace.id
send = true
}🔒 The narrowest useful namespace-scoped rule. Note it still reaches every queue and topic in the namespace — see example 4 for the narrower shape.
2 · A listen-only consumer
module "sb_consumer_rule" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-servicebus-namespace-authorization-rule.git?ref=v1.0.0"
name = "consumer"
namespace_id = module.sb_namespace.id
listen = true
}
⚠️ A namespace-wide listen rule can drain every queue in the namespace, not just the one the client was written against. That is rarely what a single consumer needs.
3 · The option to reach for first
module "sb_data_sender" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-role-assignments.git?ref=v1.0.0"
scope = module.sb_namespace.id
role_assignments = {
app_sender = {
role_definition_name = "Azure Service Bus Data Sender"
principal_id = var.app_identity_principal_id
}
}
}🔒 No key exists to leak, rotate, or land in Terraform state. Where a client can authenticate with a managed identity, an RBAC role is strictly better than SAS. This module exists for clients that cannot — a device, a third party, or a library with no Entra support — and saying so is more useful than presenting the two as equivalent.
4 · Narrower still: scope the rule to one entity
module "orders_queue_rule" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-servicebus-queue-authorization-rule.git?ref=v1.0.0"
name = "orders-consumer"
queue_id = module.sb_namespace.queue_ids["orders"]
listen = true
}💡 If SAS is unavoidable, an entity-scoped rule bounds the blast radius of a leaked key to one queue — and, unlike a namespace rule, it does not widen as the namespace grows. Prefer this shape by default and reserve the namespace-scoped form for clients that genuinely span entities.
5 · A rule granting nothing is rejected
module "sb_rule" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-servicebus-namespace-authorization-rule.git?ref=v1.0.0"
name = "useless"
namespace_id = module.sb_namespace.id
# ❌ no permissions
}Error: Invalid value for variable
At least one of listen, send, or manage must be true — a rule with no permissions is
accepted by neither the service nor any useful client.
💡 The provider treats all three booleans as optional, so a rule granting nothing applies cleanly and produces a connection string that fails on first use. Caught at plan instead.
6 · `manage` requires `listen` and `send`
manage = true # ❌ on its ownError: Invalid value for variable
When manage is true, both listen and send must also be true — the service requires it.
# ✅ The shape the service accepts:
listen = true
send = true
manage = trueℹ️ Both checks live on the
managevariable, readinglistenandsendone-directionally — Terraform rejects validations that reference each other, so splitting them across the three booleans would create a cycle. The same trap appears on the Event Hubs authorization rules and onkusto-script.
7 · What `manage` actually permits
listen = true
send = true
manage = true # 🔒 can DELETE queues, topics and subscriptions🔒 This is the one to be careful with.
manageis not "read-write plus a bit" — a holder can create and destroy the messaging topology across the whole namespace. Grant it only to tooling that genuinely administers the namespace, and prefer Entra ID withAzure Service Bus Data Ownereven then.manageis emitted as an output so a review can assert it is false.
8 · Reviewing the grants from state
output "sb_rule_grants" {
description = "Any manage = true here is a rule that can delete the messaging topology."
value = {
for k, m in module.sb_rules : k => {
listen = m.listen
send = m.send
manage = m.manage
}
}
}💡 The three booleans are emitted precisely so this table can be built from state rather than by reading configuration — the same reasoning this library applies to emitting posture on the NetApp volume and the Logic Apps assembly.
9 · The keys are credentials, and they are in state
# All six are marked sensitive = true.
module.sb_publisher_rule.primary_connection_string
module.sb_publisher_rule.primary_key🔒 Marking them sensitive keeps them out of console output and CI logs; it does not keep them out of Terraform state, where they sit in plaintext. Anyone with read access to the state file holds the credential. That is the honest argument for Entra ID (example 3) rather than a reason to avoid the outputs.
10 · Rotation uses the key pair, not Terraform
primary_key ← in use
secondary_key ← standby
ℹ️ The service maintains two keys so a client can be moved from one to the other while both are valid. Regenerating a key is a data-plane operation (portal, CLI, SDK), not a Terraform one — this module has no "rotate" input, and adding one would misrepresent where the operation lives. What Terraform can do is destroy and recreate the rule, which issues an entirely new pair and breaks every client at once.
11 · The alias outputs are empty unless paired
output "publisher_endpoint" {
# Empty string unless the namespace is in a geo-DR pairing.
value = module.sb_publisher_rule.primary_connection_string_alias
sensitive = true
}ℹ️ An empty credential output looks like a bug and is not: the alias forms only populate once the namespace participates in a geo-DR pairing.
⚠️ The corollary matters more — a client wired to the non-alias string does not survive a failover, so for a paired namespace the alias form is the one to hand out.
12 · Backing a geo-DR alias with a scoped rule
module "sb_dr" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-servicebus-namespace-disaster-recovery-config.git?ref=v1.0.0"
name = "orders-alias"
primary_namespace_id = module.sb_namespace.id
partner_namespace_id = var.sb_namespace_dr_id
alias_authorization_rule_id = module.sb_publisher_rule.id # ← this module's id
}🔒 Supply this. Left null, the alias connection strings carry
RootManageSharedAccessKey— a credential that can delete every entity in the namespace. Pointing the alias at a scoped rule instead is the single highest-value use of this module'sidoutput.
13 · Several rules from a keyed map
locals {
rules = {
publisher = { listen = false, send = true }
consumer = { listen = true, send = false }
}
}
module "sb_rules" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-servicebus-namespace-authorization-rule.git?ref=v1.0.0"
for_each = local.rules
name = each.key
namespace_id = module.sb_namespace.id
listen = each.value.listen
send = each.value.send
}💡 One rule per client role, rather than one shared rule with both permissions — that way revoking a producer does not also revoke every consumer, and a leaked key tells you which client leaked it.
14 · Renaming or deleting breaks clients
name = "publisher-v2" # forces replacement → NEW KEYS
⚠️ Both fields are force-new, and replacing the rule issues a fresh key pair — so every client on the old connection string fails at that moment. Deletion is the same event without the replacement. Neither is a quiet change: treat a plan that replaces an authorization rule as a coordinated client rollout.
15 · 🏗️ End-to-end composition
provider "azurerm" {
features {}
}
# 1 · The resource group.
module "sb_rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-messaging-eastus2"
location = "eastus2"
}
# 2 · The namespace, with its queues. Premium so geo-DR is available later.
module "sb_namespace" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-servicebus-namespace.git?ref=v1.0.0"
name = "sbns-orders-eastus2"
resource_group_name = module.sb_rg.name
location = module.sb_rg.location
sku = "Premium"
queues = {
orders = {
name = "orders"
max_delivery_count = 10
}
}
}
# 3 · The PREFERRED path for anything that can use it: Entra ID, no key at all.
module "sb_app_sender" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-role-assignments.git?ref=v1.0.0"
scope = module.sb_namespace.id
role_assignments = {
app_sender = {
role_definition_name = "Azure Service Bus Data Sender"
principal_id = var.orders_app_identity_principal_id
}
}
}
# 4 · SAS for the client that cannot use Entra ID — scoped to ONE queue, not the namespace.
module "orders_queue_rule" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-servicebus-queue-authorization-rule.git?ref=v1.0.0"
name = "partner-feed"
queue_id = module.sb_namespace.queue_ids["orders"]
send = true
}
# 5 · A namespace-scoped rule — this module — used where it is genuinely warranted:
# backing the geo-DR alias in step 6 with something other than the root manage key.
module "sb_alias_rule" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-servicebus-namespace-authorization-rule.git?ref=v1.0.0"
name = "alias-endpoint"
namespace_id = module.sb_namespace.id
listen = true
send = true
# manage stays FALSE — the alias needs message access, not the power to delete queues.
}
# 6 · The pairing. Without step 5's id, these alias strings would carry the ROOT manage key.
module "sb_dr" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-servicebus-namespace-disaster-recovery-config.git?ref=v1.0.0"
name = "orders-alias"
primary_namespace_id = module.sb_namespace.id
partner_namespace_id = var.sb_namespace_dr_id
alias_authorization_rule_id = module.sb_alias_rule.id
}
output "messaging_access_review" {
description = "Every SAS grant in one place. manage should be false throughout."
value = {
alias_rule = {
listen = module.sb_alias_rule.listen
send = module.sb_alias_rule.send
manage = module.sb_alias_rule.manage # expect false
}
alias_uses_root_key = module.sb_dr.uses_root_manage_key # expect false
}
}💡 The ordering here is an argument, not just wiring. Step 3 is the preferred mechanism and needs no key; step 4 uses SAS but bounds it to one queue; and step 5 is the only place a namespace-scoped rule is genuinely warranted — giving the geo-DR alias a scoped credential instead of the root manage key.
manageis false throughout, and both review outputs are expected false. Output names on sibling modules are illustrative; match them to the versions you pin.
Required (both force-new): name, namespace_id.
Permissions — all default false, at least one required: listen, send, manage.
Universal tail: timeouts. This resource type does not support tags.
Full object() schemas
variable "name" {
# force-new. Replacing the rule ISSUES NEW KEYS, so every client on the old connection
# string stops working at that moment.
type = string
}
variable "namespace_id" {
# force-new. The namespace's RESOURCE ID — so the reference creates a real Terraform
# dependency (unlike the Event Hubs equivalents, which take names).
type = string
}
variable "listen" {
# Receive access across the WHOLE namespace, including entities created later.
type = bool
default = false
}
variable "send" {
# Send access across the WHOLE namespace, including entities created later.
type = bool
default = false
}
variable "manage" {
# 🔒 NOT "read-write plus": manage permits creating and DELETING queues, topics and
# subscriptions. A namespace-scoped manage key can destroy the messaging topology.
#
# BOTH cross-field checks live HERE, reading listen/send one-directionally, because
# Terraform rejects validations that reference each other:
# - at least one of listen / send / manage must be true (the provider accepts none)
# - when manage is true, listen and send must also be true (the service requires it)
type = bool
default = false
}
variable "timeouts" {
# ⚠️ Deleting the rule invalidates its keys immediately — a client-visible event.
type = object({ create = optional(string), read = optional(string), update = optional(string), delete = optional(string) })
default = null
}| Output | Description | Kind |
|---|---|---|
id |
The Azure Resource ID of the authorization rule | Passthrough |
name |
The authorization rule name | Passthrough |
namespace_id |
The Service Bus namespace this rule is scoped to | Passthrough |
namespace_name |
Short name of the Service Bus namespace, derived from namespace_id by segment position rather than by regex - the provider's ID parser matches path segments case-insensitively, so a caller who wrote /resourcegroups/ is passing a legal ID and must not be mis-parsed |
Derived |
listen |
Whether the rule grants receive access across the entire namespace | Passthrough |
send |
Whether the rule grants send access across the entire namespace | Passthrough |
manage |
Whether the rule grants manage access across the entire namespace | Passthrough |
permissions |
The granted rights as a sorted list, e.g | Passthrough |
grants_namespace_wide_access |
Always true for this module, and the single most important fact about it | Constant |
geo_dr_replication_wait_uses_the_update_timeout |
Always true | Constant |
geo_dr_replication_wait_is_conditional_on_the_namespace |
Always true | Constant |
renaming_reissues_keys |
Always true | Constant |
state_id_is_normalised_on_schema_upgrade |
Always true, and true ONLY of this module in the trio | Constant |
has_geo_dr_alias_connection_strings |
Whether the alias connection strings came back populated, which is the case only when the namespace participates in a geo-disaster-recovery pairing | Passthrough |
primary_key |
Primary SAS key | Passthrough |
secondary_key |
Secondary SAS key - the standby half of a rotation pair | Passthrough |
primary_connection_string |
Primary connection string | Passthrough |
secondary_connection_string |
Secondary connection string, carrying the secondary key inline | Passthrough |
primary_connection_string_alias |
Primary connection string via the geo-DR alias, or empty when the namespace is not paired | Passthrough |
secondary_connection_string_alias |
Secondary connection string via the geo-DR alias, or empty when the namespace is not paired | Passthrough |
sas_keys_are_stored_in_terraform_state |
Always true | Constant |
Six outputs are credentials and all are marked sensitive. They are in Terraform state whether or not a consumer reads them.
- The scope is the headline fact. A namespace-scoped rule reaches every queue and topic in the namespace including ones created later, so a rule written for one queue becomes an over-privileged credential as the namespace grows, without anyone editing it. That is why the documentation points at the entity-scoped siblings before describing its own fields.
manageis the field to be careful with, and its name undersells it: it permits creating and deleting queues, topics and subscriptions, so a holder can destroy the messaging topology rather than merely read or write messages. It defaults tofalseand is emitted as an output specifically so a review can assert it stays false.- The module argues for Entra ID rather than presenting SAS as neutral. A SAS key is a bearer credential that lands in Terraform state; an RBAC role assignment is not. Where a client can authenticate with a managed identity, the role is strictly better — and a module that quietly offers only the weaker mechanism is doing its reader a disservice.
- Both cross-field validations live on
manage. Terraform rejects validations that reference each other, so a matching check onlistenorsendwould create a cycle — the same trap already hit on the Event Hubs authorization rules andkusto-script. Consolidating them on one variable is the established fix in this library. - All three permissions default closed, following
terraform-azurerm-eventhub-namespace-authorization-rule. The empty call grants nothing, so access is always something a caller typed. - Sensitive marking is not confidentiality. It keeps keys out of console output and CI logs; the values still sit in plaintext in state. Saying so is more useful than implying the outputs are safe.
- Rotation is a data-plane operation. The service maintains a primary/secondary pair so a client can be migrated between them; regenerating a key happens outside Terraform. This module deliberately has no "rotate" input, because adding one would misrepresent where the operation lives.
- The namespace is consumed by Resource ID, unlike the Event Hubs equivalents which take names — so ordering is implicit here and a rename surfaces as a plan diff rather than a run-time failure.
- The alias outputs are empty absent a geo-DR pairing, which is documented because an empty credential output otherwise reads as a defect. The consequential half is that a client on the non-alias string does not survive a failover.
features {}dependence. The module carries noprovider {}block. If it appears not to initialize in isolation, the cause is a missing caller-sideprovider "azurerm" { features {} }.
| Concern | Secure default (empty call) | Opt-out (caller must type it) |
|---|---|---|
| Access granted | nothing — all three permissions false |
set listen / send |
| Topology destruction | manage = false |
set manage = true (needs listen + send; visible in an output) |
| Empty rule | at-least-one enforced at plan | — (no opt-out) |
| Manage pairing | listen + send required with manage |
— (no opt-out; the service requires it) |
| Grant visibility | listen / send / manage emitted |
— (no opt-out) |
| Scope | documented: prefer an entity-scoped rule | use the namespace scope knowingly |
| Mechanism | documented: prefer Entra ID + RBAC over SAS | use SAS |
| Credential handling | all six credential outputs sensitive = true |
— (no opt-out) |
terraform init -backend=false
terraform validate
terraform fmt -check- Pin the module with
?ref=v1.0.0— never a branch. - This library is plan-only during authoring; a human runs
terraform plan/applyfrom CI against real credentials. - Before applying: confirm the client genuinely cannot use Entra ID, and that a namespace-scoped rule is needed rather than an entity-scoped one.
- Treat a plan that replaces this rule as a coordinated client rollout — new keys are issued and every client on the old string breaks.
- For a geo-DR paired namespace, hand clients the alias connection string, not the namespace-specific one.
terraform validateproves the configuration is internally consistent and type-correct against the pinned provider schema. The module carries sevenvalidation {}blocks — the rule-name pattern and a not-a-Resource-ID check onname, an anchored pattern and a shape check onnamespace_id, at-least-one-permission,managerequiringlistenandsend, and a Go-duration check ontimeouts. Through a module call they are evaluated atplan; what matters is that none of them needs credentials.terraform fmt -checkenforces canonical formatting.- Neither command calls Azure. Only
terraform plan(run by a human, from CI) exercises the ARM API — the module ships without any cloud apply. - What only apply exercises: whether the namespace exists, and whether the deploying identity may list keys.
- What nothing exercises: whether the grant is appropriate — whether this client needed namespace scope rather than one queue, and whether it needed SAS rather than an RBAC role. Those are review questions, which is why the permission booleans are emitted as outputs rather than left implicit in configuration.
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
id = "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-messaging-eastus2/providers/Microsoft.ServiceBus/namespaces/sbns-orders-eastus2/authorizationRules/alias-endpoint"
name = "alias-endpoint"
namespace_id = "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-messaging-eastus2/providers/Microsoft.ServiceBus/namespaces/sbns-orders-eastus2"
listen = true
send = true
manage = false
primary_key = (sensitive value)
secondary_key = (sensitive value)
primary_connection_string = (sensitive value)
secondary_connection_string = (sensitive value)
primary_connection_string_alias = (sensitive value)
secondary_connection_string_alias = (sensitive value)
| Symptom | Cause | Fix |
|---|---|---|
Provider configuration not present / features error |
No caller-side provider "azurerm" { features {} }. |
Add the provider block with features {} in the root module. |
Plan error: At least one of listen, send, or manage |
No permissions set. The provider would have accepted it. | Grant the access the client needs. |
Plan error: When manage is true, both listen and send |
manage alone. |
Set all three, or drop manage. |
| Every client fails at once after an apply | The rule was replaced, issuing new keys. | Expected — roll the new connection string out to clients. |
| A client can read queues it should not | The rule is namespace-scoped, so it reaches every entity. | Use an entity-scoped rule instead. |
| Queues disappeared | A manage key was used to delete them. |
Check the manage output across your rules; revoke and re-scope. |
| The alias connection string output is empty | The namespace is not in a geo-DR pairing. | Expected; use the non-alias string, or create the pairing. |
| Clients broke during a failover | They were wired to the namespace-specific string, not the alias. | Re-point them at the alias connection string. |
| Keys visible in the state file | Sensitive marking hides console output, not state. | Restrict state access; prefer Entra ID where the client allows it. |
| Apply fails listing keys | The deploying identity lacks authorizationRules/listKeys/action. |
Grant it, or drop the credential outputs from your composition. |
| Need to rotate a key | Rotation is a data-plane operation. | Regenerate via portal/CLI/SDK and migrate clients across the key pair. |
- azurerm provider —
azurerm_servicebus_namespace_authorization_rule - Service Bus authentication and authorization
- Authenticate with shared access signatures
- Authenticate with Microsoft Entra ID and Azure RBAC
- Service Bus geo-disaster recovery
- Sibling modules:
terraform-azurerm-servicebus-namespace,terraform-azurerm-servicebus-queue-authorization-rule,terraform-azurerm-servicebus-topic-authorization-rule,terraform-azurerm-servicebus-namespace-disaster-recovery-config,terraform-azurerm-servicebus-namespace-customer-managed-key,terraform-azurerm-role-assignments. - This module's
SCOPE.md.
💙 "Infrastructure as Code should be standardized, consistent, and secure."