Basic AWS IoT Core policy example and breakdown

How to define a private space for each connected device

Author's image
Tamás Sallai
5 mins

Policies in IoT Core

In AWS IoT Core, a device connects using a certificate with a policy attached and that defines what operations the device can do. Since the connection is made via MQTT, the policy controls what topics the device can publish, subscribe, and how it can connect.

The policy uses the same format as IAM policies, which means you can take advantage of its features such defining resources for each action, conditions, and variables. As usual with AWS, you can implement a lot of different use-cases with fine-grained policies. And as usual with AWS, how it works seems overly complex at first and requires a lot of reading.

In this article, we'll implement a "walled garden" policy for IoT things. This is where each device can read and write to their specific space but they can't access other devices' spaces. We'll implement this with a prefix to the topics that uses the thing name. The solution will take advantage of policy variables so that only one policy will be needed that can be attached to all certificates.

This permission strategy is usually the best practice as you can organize devices so that they only interact with a backend and not with each other. On the cloud-side you can then implement topic rules to react to data coming from the devices and use the HTTPS endpoint to programmatically push messages to topics. This way, the backend defines the workflow for the devices individually and there is no access for any device outside that. Always remember that these devices are physical things that can be stolen, so implementing least privilege for them minimizes the impact.

To implement this policy, we'll need to handle 4 actions: Connect, Publish, Subscribe, and Receive.

Connect

The connect permission gives the device the ability to connect to the MQTT endpoint.

{
	"Action": [
		"iot:Connect"
	],
	"Effect": "Allow",
	"Resource": "arn:aws:iot:<region>:<account>:client/${iot:Connection.Thing.ThingName}"
}

The resource defines the iot:Connection.Thing.ThingName policy variable for the client ID. The client ID is an attribute of the MQTT connection that the device defines, and that identifies the connection. In AWS IoT, it is used to identify the thing (the resource in IoT Core). By allowing the iot:Connect action with the ThingName, you can make sure the device can only connect as a thing that is attached to its certificate.

There is also an iot:Connection.Thing.IsAttached condition key that should enforce a similar restriction. Maybe it's a good idea to define that as well, but I ran some tests and couldn't find any scenarios where a device could connect with a wrong client ID with the above statement.

Publish

The Publish permission defines what topics the device can publish to.

{
	"Action": [
		"iot:Publish"
	],
	"Effect": "Allow",
	"Resource": "arn:aws:iot:<region>:<account>:topic/$aws/things/${iot:Connection.Thing.ThingName}/*"
}

Again, this statement takes advantage of a policy variable. The iot:Connection.Thing.ThingName is the thing name from the client ID. As we've restricted that with the iot:Connect permission, we can rely on it to add a per-device uniqueness to the topic path.

In the above example, the devices can publish to topics that start with $aws/things/<thingname>/. This is the prefix for the device shadows, so it effectively restricts the devices to their own shadows.

Subscribe

The Subscribe permission defines which topics the devices can subscribe to and receive real-time data from.

{
	"Action": [
		"iot:Subscribe"
	],
	"Effect": "Allow",
	"Resource": "arn:aws:iot:<region>:<account>:topicfilter/$aws/things/${iot:Connection.Thing.ThingName}/*"
}

It works the same as the iot:Publish permission: the device can subscribe to its own shadows only.

Receive

The Receive permission defines which topics the device can receive data from.

{
	"Action": [
		"iot:Receive"
	],
	"Effect": "Allow",
	"Resource": "arn:aws:iot:<region>:<account>:topic/$aws/things/${iot:Connection.Thing.ThingName}/*"
}

This seems redundant with the iot:Subscribe permission, as without subscribing the device can not receive data. Why it's important though is because of the difference in when the permissions are checked.

With iot:Subscribe, the permission check runs when the device subscribes. Since the MQTT connection can be kept open for weeks, that means if you remove the permission later, the subscription is still active.

On the other hand, iot:Receive is checked every time a message is sent to the client. If you remove the permission, the flow of messages stops immediately.

All together

With policies you can take advantage of the different resource formats to write all 4 actions with their respective resource in just one statement:

{
	"Action": [
		"iot:Receive",
		"iot:Subscribe",
		"iot:Publish",
		"iot:Connect"
	],
	"Effect": "Allow",
	"Resource": [
		"arn:aws:iot:<region>:<account>:topic/$aws/things/${iot:Connection.Thing.ThingName}/*",
		"arn:aws:iot:<region>:<account>:topicfilter/$aws/things/${iot:Connection.Thing.ThingName}/*",
		"arn:aws:iot:<region>:<account>:client/${iot:Connection.Thing.ThingName}"
	]
}

Conclusion

IoT Core policies are a powerful way to define permissions for devices connecting via MQTT. It supports a lot of conditions and variables that we did not cover in this article, such as defining different topics for different issuers or IP addresses.

January 24, 2023
In this article