AWS IoT Core Simplified - Part 1: Permissions
Tags: aws, iot, serverless, mqtt
Published: 2024-06-11 18:46:17

AWS IoT Core is an amazing (and often overlooked) service as I said before when [comparing IoT Core to API Gateway Websockets](https://dev.to/slootjes/api-gateway-websockets-vs-iot-core-1me5). To summarize, IoT Core manages it own connections, it has a powerful system of topics and rules, it scales well and, not unimportant, it's quite cheap.

However, IoT Core can also be intimidating and [difficult](https://docs.aws.amazon.com/iot/latest/developerguide/what-is-aws-iot.html) to start with because of _things_, _fleets_, _shadow devices_ and _certificates_. The good news is that those things are not at all mandatory to use this service. While maybe not obvious at first sight, there are ways to just connect some clients to IoT Core without first registering or managing them.

I will be posting a few articles to cover basic usage of IoT Core aimed at developers who are looking for an easy and flexible way of bi-directional communication between clients and server using websockets.

Part 1: Introduction & Permissions (this one!)
Part 2: Connect using a presigned url
Part 3: Connect using a custom authorizer
Part 4: Topic Rules

# What is IoT Core?
Simply put, _and maybe not giving enough credit_, IoT Core is an MQTT broker. MQTT is a lightweight publish-subscribe protocol. The MQTT broker acts as the central hub between connected clients that can send and reach messages to each other. Messages are sent over topics. A topic is a hierarchical string separated by slashes, for instance _sensor/temperature/room1_. A client can send temperature updates to this topic every minute and another client might be subscribed to this topic and receives all temperatures that are sent to do something with it.

![Example MQTT Setup](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m32jjw4iu3p9o851a49o.png)

MQTT has many advanced options and use cases that I won't cover here. If you want to know more about them I recommend reading more [here](https://www.hivemq.com/mqtt-essentials/). For the rest of this series, it's enough to know that there is a central broker (IoT Core) and clients (ie: a web browser) that can publish and receive messages.

## Topic Wildcards
In a [topic](https://docs.aws.amazon.com/iot/latest/developerguide/topics.html), a _+_ character can be used as a wildcard for one level while a _#_ character can be used for a multi level wildcard. It is possible to have more than 1 single level wildcard, like _sensor/+/+_ however it's not possible to have more than 1 multi level wildcard (which makes sense).

Looking at the previous example, if a client is interested in temperatures of all rooms, it can subscribe to _sensor/temperature/#_ where the # character is a wildcard in MQTT. This means that if a client wants the informations of _all_ sensors, it can listen to _sensor/#_. It is also possible to subscribe to _sensor/+/room1_ to receive all messages (temperature and others) of room1. 

# Security
For simple use of IoT Core, there are 4 permissions that you need to know  about. [Applying least-privilege permissions](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege) obviously also applies to IoT Core. In case a client performs an action that is not allowed by it's policy, the client will be disconnected by the server.

It's important to realize that [MQTT wildcards are treated as literal characters in IAM permissions](https://docs.aws.amazon.com/iot/latest/developerguide/pub-sub-policy.html#pub-sub-policy-cert). This means that you can use the _?_, _*_, _+_ and _#_ in the IAM policy but the _?_ and _*_ will be treated only as wildcard for the IAM policy itself while _+_ and _#_ are treated as wildcards for topics in IoT Core. I will explain this using several examples.

# Permissions

![Connecting](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/88hmy7easmde52k7265u.png)

## iot:Connect
First, your client needs to be able to connect to the server. The client ID must be _unique_ per region, otherwise the previously connected client with the same client ID will be disconnected.

**Examples**
`arn:aws:iot:{region}:{account-id}:client/*`
Allows to connect using _any_ client ID.

`arn:aws:iot:{region}:{account-id}:client/sensor-123`
Allows to connect as _sensor-123_.

`arn:aws:iot:{region}:{account-id}:client/sensor-???`
Allows to connect using any client ID as long as it starts with _sensor-_ and ends with 3 characters. This means that _sensor-123_, _sensor-foo_ will work, but _sensor_, _sensor-foobar_ and _sensor-123456_ won't work.

`arn:aws:iot:{region}:{account-id}:client/sensor-*`
Allows to connect using any client ID as long as it starts with _sensor-_.

`arn:aws:iot:{region}:{account-id}:client/user-????????-????-????-????-????????????`

Allows to connect using _user-{uuid}_.

![Server listening for messages](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lf0zr28u8nhv4n5hh7xs.png)

## iot:Subscribe
Before a client can receive messages, the client must first subscribe to one or multiple topics.

**Examples**
`arn:aws:iot:{region}:{account-id}:topicfilter/updates`
Allows to subscribe to _updates_.

`arn:aws:iot:{region}:{account-id}:topicfilter/updates/sensor-???`
Allows to subscribe to _updates/sensor-123_, _updates/sensor-foo_ and other variants as long as the topic starts with _updates/_ and then has a second level starting with _sensor-_ and then 3 characters. It won't accept updates/foo-123 or "updates/sensor-123/foo".

`arn:aws:iot:{region}:{account-id}:topicfilter/updates/sensor-*`
Allows to subscribe to _updates/sensor-123_, _updates/sensor-123/foo_, and/or "updates/sensor-123/foo/bar/baz" because the * allows for any level of depth in the topic.

`arn:aws:iot:{region}:{account-id}:topicfilter/updates/sensor-*/???`
Allows to subscribe to _updates/sensor-123/foo_, _updates/sensor-12345678/bar_ and other variants as long as the topic starts with "updates/sensor-" and has a second level with exactly 3 characters. It won't accept _updates/sensor-1_ or _updates/sensor-12345/#_.

You can of course use the wildcards in MQTT too if you want.

`arn:aws:iot:{region}:{account-id}:topicfilter/updates/+`
Allows to subscribe to _topic/updates/+_ only as the **+** is treated as a literal in the policy.

`arn:aws:iot:{region}:{account-id}:topicfilter/updates/#`
Allows to subscribe to _topic/updates/#_ only as the **#** is treated as a literal in the policy.

![Computer sending and receiving messages](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vmd09y4siw7n0k55rwyy.png)

## iot:Receive & iot:Publish

iot:Receive allows the client to receive messages over the topics that it is subscribed to while iot:Publish allows the client to publish messages that can be received by other clients and/or topic rules.

The iot:Receive & iot:Publish permissions have the same structure as iot:Subscribe with the only difference that it uses "topic" instead of "topicfilter". Usually the iot:Subscribe and iot:Receive will be the same as it doesn't make sense to allow receiving on topics the client isn't allowed to subscribe on and vice versa.

**Examples**
`arn:aws:iot:{region}:{account-id}:topic/updates`
`arn:aws:iot:{region}:{account-id}:topic/updates/sensor-???`
`arn:aws:iot:{region}:{account-id}:topic/updates/sensor-*`
`arn:aws:iot:{region}:{account-id}:topic/updates/sensor-*/???`
`arn:aws:iot:{region}:{account-id}:topic/updates/+`
`arn:aws:iot:{region}:{account-id}:topic/updates/#`

# Creating a policy
Since the permissions and values have different formats, you can easily combine everything in a single policy:

```
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "iot:Connect",
        "iot:Subscribe",
        "iot:Receive"
      ],
      "Resource": [
        "arn:aws:iot:{region}:{account-id}:client/sensor-*",
        "arn:aws:iot:{region}:{account-id}:topicfilter/sensor-*/???",
        "arn:aws:iot:{region}:{account-id}:topic/sensor-*/???"
        ]
    }
  ]
}
```

In the next article I will explain how to connect to IoT Core using a presigned url.