Implementing network security for the IoTHub and the Device Provisioning Service (DPS) in Azure is a challenging task as simply putting it into a VNET is not always an easy option. Sometimes you can restrict the IP Addresses to the ranges of your provider, so that at least some restrictions are made to the network communication. Most of our cloud resources are in terraform, where we created a variable for the IP restrictions as follows:
variable "ip_ranges" {
type = list(string)
default = [
"1.2.3.4/28",
"1.2.3.4/29",
"1.2.3.4/30"
]
}
Then using this block from an app service would look like
resource "azurerm_app_service" "your_service" {
name = "yourservice"
location = "yourlocation"
resource_group_name = "yourresourcegroup"
app_service_plan_id = "yourplan"
https_only = true
site_config {
always_on = "true"
linux_fx_version = "DOTNETCORE|3.1"
dynamic "ip_restriction" {
for_each = var.ip_ranges
content {
ip_address = ip_restriction.value
action = "Allow"
}
}
}
I thought I will just do the same with the IoTHub in Terraform as follows:
resource "azurerm_iothub" "device_hub" {
name = "iot_hub_name"
resource_group_name = "resource_group_name"
location = "location"
public_network_access_enabled = true
sku {
name = "S1"
capacity = "1"
}
dynamic "ip_filter_rule" {
for_each = var.ip_ranges
content {
name = ip_filter_rule.value
ip_mask = ip_filter_rule.value
action = "Accept"
}
}
}
But then there was an error. Terraform was composing the wrong body of the REST request to the Azure management endpoint.
So I’ve decided to use our escape hatch to just call an null_resource and Azure CLI command for creating the ip filter rules. I describe the reasons and drawbacks of this solution in the end of this article.The errors, which I’ve had:
Failure sending request: StatusCode=0 — Original Error: Code=”Failed” Message=”The async operation failed.” InnerError={“unmarshalError”:”json: cannot unmarshal number into Go struct field serviceErrorInternal.code of type string”} AdditionalInfo=[{“code”:400116,”httpStatusCode”:”BadRequest”,”message”:”Valid Connection string should be provided. endpointName: ***-endpoint. If you contact a support representative please include this correlation identifier: 6479f0f1-57dd-4c74-8b05-1bd96c2cf044, timestamp: 2021-06-14 14:30:12Z, errorcode: IH400116.”}]
and
devices.IotHubResourceClient#CreateOrUpdate: Failure sending request: StatusCode=0 — Original Error: Code=”Failed” Message=”The async operation failed.” InnerError={“unmarshalError”:”json: cannot unmarshal number into Go struct field serviceErrorInternal.code of type string”} AdditionalInfo=[{“code”:400059,”httpStatusCode”:”BadRequest”,”message”:”Request body validation failed. If you contact a support representative please include this correlation identifier: 589873a8-e25d-41b3-aebc-eaef22fa7aa0, timestamp: 2021-06-14 14:33:45Z, errorcode: IH400059.”}]
Creating the IP filter rules takes however a LOT of TIME if done as follows (described here):
az iot hub update --name MyIotHub --add properties.ipFilterRules filter_name=test-rule action=Accept ip_mask=127.0.0.0/31
For DPS you don’t even have the option to as at the time of writing the cli command did not even exist.
The resolution is a more generic CLI command, that I’ve just found (here): az resource update
With this you can update the whole ipFilterRules block of the resource at once, and it also works for the DPS as follows:
$joined_filters = ($ip_ranges.Split(',') | % {"{'action':'Accept','filterName':`'$($_.Replace('/','-').Replace('.','_'))`','ipMask':`'$_`'}"}) -join ','
$filter_json = "`"[$joined_filters]`""
az resource update -n $iothub_name -g $resource_group_name --resource-type Microsoft.Devices/IotHubs --set properties.ipFilterRules=$filter_json
az resource update --ids $dps_id --resource-type Microsoft.Devices/ProvisioningServices --set properties.ipFilterRules=$filter_json
# A working statement to know what is being built together: az resource update -n youriothub -g yourrg --resource-type Microsoft.Devices/IotHubs --set properties.ipFilterRules='[{"action":"Accept","filterName":"TrustedIP","ipMask":"192.168.0.1/32"},{"action":"Accept","filterName":"TrustedIP2","ipMask":"192.168.0.2/32"}]'
I use a comma separated string as an input for the $ip_ranges
.
Oh yeah, I’ve promised to elaborate on why I’ve chosen to use. Frankly, it takes more time to fix the solution in terraform. Yepp, it is much harder to maintain resources created by the CLI. Destroy also does not happen automatically, and so on. I know, I know. But currently, we live with this way.