How to manage IoT Core resources with Terraform

Thing, certificate, policy, and other resources you'll need to connect devices to the AWS cloud

Author's image
Tamás Sallai
4 mins

In the previous article we discussed the basic building blocks of AWS IoT Core that you need to use to connect a device via MQTT. Now we'll look into how to manage these resources with Terraform so that a client application has everything it needs to send and receive messages from the cloud backend.

Thing

The thing is the representation of the physical device in the cloud. It needs only a unique name and as a best practice regarding uniqueness we'll use a random_id data source to generate a portion of the name randomly.

resource "random_id" "id" {
	byte_length = 8
}

resource "aws_iot_thing" "thing" {
	name = "thing_${random_id.id.hex}"
}

Then, since the client application needs to know the thing name, output the value:

output "thing_name" {
	value = aws_iot_thing.thing.name
}

Certificate

A client application needs to identify itself and it is done using certificates. Here, we'll generate it using Terraform, add it to IoT Core, then output all the values needed for the client.

Generate the private key

First, generate a private key using an appropriate algorithm:

resource "tls_private_key" "key" {
	algorithm   = "RSA"
	rsa_bits = 2048
}

Generate the certificate

Then generate a self-signed certificate using the key:

resource "tls_self_signed_cert" "cert" {
	private_key_pem = tls_private_key.key.private_key_pem

	validity_period_hours = 240

	allowed_uses = [
	]

	subject {
		organization = "test"
	}
}

Add the certificate to IoT Core

Then AWS needs to know about the certificate, so Terraform needs to upload it:

resource "aws_iot_certificate" "cert" {
	certificate_pem = trimspace(tls_self_signed_cert.cert.cert_pem)
	active          = true
}

Attach the certificate to the thing

To allow connections to a thing, the certificate needs to be attached to it:

resource "aws_iot_thing_principal_attachment" "attachment" {
	principal = aws_iot_certificate.cert.arn
	thing     = aws_iot_thing.thing.name
}

Exports

Finally, as the client needs both the private key and the certificate, add outputs for them:

output "cert" {
	value = tls_self_signed_cert.cert.cert_pem
}

output "key" {
	value = tls_private_key.key.private_key_pem
	sensitive = true
}

Policy

As usual in AWS, identities (in this case, the device that connects) does not have any permissions. So we need to add a policy with the appropriate statements so that the client application can send and receive values via MQTT.

The policy itself is a JSON, similar to IAM policies, attached to the certificate:

resource "aws_iot_policy" "policy" {
  name = "thingpolicy_${random_id.id.hex}"

  policy = jsonencode({
		Version = "2012-10-17"
		Statement = [
			...
		]
	})
}

resource "aws_iot_policy_attachment" "attachment" {
	policy = aws_iot_policy.policy.name
	target = aws_iot_certificate.cert.arn
}

Statements

What actions and resources to allow for the client? In this example and as a best practice, we'll define a "walled garden" policy that allows the device to read and write data in its shadows but gives no access to other topics. For this, we'll use the account ID and the region from the thing resource and the ${iot:Connection.Thing.ThingName} policy variable.

First, add a data source that extracts data from the thing's ARN:

data "aws_arn" "thing" {
	arn = aws_iot_thing.thing.arn
}

This data source contains the region and the account that we'll use to construct the policy resources.

The first statement makes sure that the client can connect:

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

Notice the two types of interpolation here. ${...} is deploy-time as Terraform extracts the value from the created thing. Then the $${...} is execution time, as it uses a policy variable that AWS will evaluate based on the connection. This makes it possible to use the same policy for multiple things without hardcoding their names.

Then allow publish and receive topics in its shadows:

{
	Action = [
		"iot:Publish",
		"iot:Receive",
	]
	Effect   = "Allow"
	Resource = "arn:aws:iot:${data.aws_arn.thing.region}:${data.aws_arn.thing.account}:topic/$aws/things/$${iot:Connection.Thing.ThingName}/*"
}

And also to subscribe for these topics:

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

IoT endpoint

The client needs to know where to connect. For this, we'll use a data source and output its result:

data "aws_iot_endpoint" "iot_endpoint" {
	endpoint_type = "iot:Data-ATS"
}

output "iot_endpoint" {
	value = data.aws_iot_endpoint.iot_endpoint.endpoint_address
}

ATS in this case is short for "Amazon Trust Services" and that defines what certificate AWS uses for the endpoint. This should be used for all new devices.

CA certificate

Finally, the client app needs to know what certificate to trust from the IoT endpoint. This is the other direction of the mutual TLS auth.

To get Amazon's CA cert, we'll use an http data source and output its value:

data "http" "root_ca" {
	url = "https://www.amazontrust.com/repository/AmazonRootCA1.pem"
}

output "ca" {
	value = data.http.root_ca.response_body
}

Outputs

After deploying this stack, it outputs all data:

ca = <<EOT
-----BEGIN CERTIFICATE-----
MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF
...
-----END CERTIFICATE-----

EOT
cert = <<EOT
-----BEGIN CERTIFICATE-----
MIICuTCCAaGgAwIBAgIRAJxkojqslH8aV89AbIM2qI8wDQYJKoZIhvcNAQELBQAw
...
-----END CERTIFICATE-----

EOT
iot_endpoint = "a192wytv5ywpbg-ats.iot.eu-central-1.amazonaws.com"
key = <<EOT
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAzy4j8/DSDF/DFbeT3LzqzwsRt0J4+2N4HLyF4uBD4GGk+DIE
...
-----END RSA PRIVATE KEY-----

EOT
thing_name = "thing_658f63be58a9b9d4"

A client application knows where to connect (iot_endpoint), what certificate to accept (ca), what certificate to provide and its private key (cert and key), and what client ID to set for the connection (thing_name).

December 27, 2022