AWS IoT Fleet Provisioning: A Complete Step-by-Step Guide

Provisioning devices one at a time is fine when you have ten of them sitting on your desk. It falls apart the moment you need to ship ten thousand identical boards off a factory line, each of which has to show up in AWS IoT Core with its own unique certificate and identity with zero manual intervention.

That’s exactly the problem AWS IoT Fleet Provisioning solves. This guide walks through what it is, how it works under the hood, and a full hands-on walkthrough using the AWS CLI, complete with the commands and the output you should expect to see at each step.


1. What Is Fleet Provisioning?

Fleet Provisioning lets a device generate its own unique digital identity (an X.509 certificate and private key) and register itself with AWS IoT Core automatically, the first time it connects without a human ever touching the AWS console for that specific device.

AWS IoT Core supports three main flavors:

MethodHow it worksBest for
Provisioning by ClaimEvery device ships with a shared, restricted-permission “claim” certificate. On first boot, the device uses it to request a brand-new, unique certificate from AWS IoT.Mass manufacturing, factory flashing
Provisioning by Trusted UserA mobile app or gateway acts as an intermediary, calling the fleet provisioning APIs on the device’s behalf (e.g., during a BLE-based setup flow).Consumer products set up via a companion app
Just-in-Time Provisioning (JITP) / Just-in-Time Registration (JITR)The device already has a certificate signed by a CA you’ve registered with AWS IoT. AWS automatically registers the device the first time that certificate connects.Devices from vendors who pre-load certs from a known CA

This guide focuses on Provisioning by Claim, since it’s the most common pattern and the one AWS documents as the canonical “fleet provisioning” flow.

The high-level flow

  1. Every device is flashed at the factory with the same claim certificate, private key, and CA cert.
  2. On first boot, the device connects to AWS IoT Core using the claim certificate and publishes to a special MQTT topic to request new, unique credentials.
  3. AWS IoT Core issues the device a brand-new certificate and private key.
  4. The device publishes to another special topic, sending a RegisterThing request that includes the new certificate’s ownership token plus any parameters (like a serial number).
  5. AWS IoT Core runs your provisioning template, creates the Thing, attaches the new certificate and policy, and returns the final credentials to the device.
  6. The device stores the new certificate locally and uses it for all future connections. The claim certificate is never needed again.

2. Prerequisites

Before starting, make sure you have:

  • An AWS account with permissions for IoT Core, IAM, and (optionally) Lambda
  • AWS CLI v2 installed and configured (aws configure)
  • OpenSSL installed locally (to simulate the device generating a CSR, if you want)
  • mosquitto_pub / mosquitto_sub or the AWS IoT Device SDK, to simulate a real device connecting over MQTT

Check your CLI is working and pointed at the right account:

aws sts get-caller-identity

Expected output:

{
    "UserId": "AIDAEXAMPLE123456789",
    "Account": "123456789012",
    "Arn": "arn:aws:iam::123456789012:user/deploy-admin"
}

3. Step 1 — Create the IAM Role for Provisioning

AWS IoT needs an IAM role it can assume to actually create Things, certificates, and attach policies during the provisioning flow.

Create a trust policy file:

cat > iot-provisioning-trust-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "iot.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF

Create the role:

aws iam create-role \
  --role-name IoTFleetProvisioningRole \
  --assume-role-policy-document file://iot-provisioning-trust-policy.json

Expected output:

{
    "Role": {
        "Path": "/",
        "RoleName": "IoTFleetProvisioningRole",
        "RoleId": "AROAEXAMPLE123456789",
        "Arn": "arn:aws:iam::123456789012:role/IoTFleetProvisioningRole",
        "CreateDate": "2026-09-04T06:12:03+00:00",
        "AssumeRolePolicyDocument": { "...": "..." }
    }
}

Attach the AWS-managed policy that grants the permissions fleet provisioning needs (create Things, certificates, attach policies):

aws iam attach-role-policy \
  --role-name IoTFleetProvisioningRole \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSIoTThingsRegistration

This command produces no output on success — that’s expected for attach-role-policy.


4. Step 2 — Create the Runtime Policy for Devices

This is the IoT policy that will be attached to every device’s final certificate (not the claim cert). Keep it scoped tightly — e.g., a device should only be able to publish/subscribe to topics prefixed with its own Thing name.

cat > device-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["iot:Connect"],
      "Resource": "arn:aws:iot:us-east-1:123456789012:client/${iot:Connection.Thing.ThingName}"
    },
    {
      "Effect": "Allow",
      "Action": ["iot:Publish", "iot:Subscribe", "iot:Receive"],
      "Resource": [
        "arn:aws:iot:us-east-1:123456789012:topic/things/${iot:Connection.Thing.ThingName}/*",
        "arn:aws:iot:us-east-1:123456789012:topicfilter/things/${iot:Connection.Thing.ThingName}/*"
      ]
    }
  ]
}
EOF

aws iot create-policy \
  --policy-name DeviceRuntimePolicy \
  --policy-document file://device-policy.json

Expected output:

{
    "policyName": "DeviceRuntimePolicy",
    "policyArn": "arn:aws:iot:us-east-1:123456789012:policy/DeviceRuntimePolicy",
    "policyDocument": "{...}",
    "policyVersionId": "1"
}

5. Step 3 — Create the Provisioning Template

The provisioning template tells AWS IoT what to do when a device asks to be registered: what to name the Thing, which policy to attach, and which parameters to expect (like a serial number passed by the device).

cat > provisioning-template.json << 'EOF'
{
  "Parameters": {
    "SerialNumber": { "Type": "String" },
    "AWS::IoT::Certificate::Id": { "Type": "String" }
  },
  "Resources": {
    "thing": {
      "Type": "AWS::IoT::Thing",
      "Properties": {
        "ThingName": { "Fn::Join": ["", ["Sensor-", { "Ref": "SerialNumber" }]] },
        "AttributePayload": {
          "version": "v1",
          "serialNumber": { "Ref": "SerialNumber" }
        }
      }
    },
    "certificate": {
      "Type": "AWS::IoT::Certificate",
      "Properties": {
        "CertificateId": { "Ref": "AWS::IoT::Certificate::Id" },
        "Status": "Active"
      }
    },
    "policy": {
      "Type": "AWS::IoT::Policy",
      "Properties": { "PolicyName": "DeviceRuntimePolicy" }
    }
  }
}
EOF

Now register it with AWS IoT:

aws iot create-provisioning-template \
  --template-name FleetProvisioningTemplate \
  --description "Template for factory-provisioned sensors" \
  --provisioning-role-arn arn:aws:iam::123456789012:role/IoTFleetProvisioningRole \
  --template-body file://provisioning-template.json \
  --enabled

Expected output:

{
    "templateArn": "arn:aws:iot:us-east-1:123456789012:provisioningtemplate/FleetProvisioningTemplate",
    "templateName": "FleetProvisioningTemplate",
    "defaultVersionId": 1
}

6. Step 4 — Create the Claim Certificate and Its Restricted Policy

The claim certificate is what every device ships with. It should only be allowed to talk to the fleet provisioning MQTT topics — nothing else.

Generate the claim certificate and keys:

aws iot create-keys-and-certificate \
  --set-as-active \
  --certificate-pem-outfile claim-certificate.pem.crt \
  --public-key-outfile claim-public.pem.key \
  --private-key-outfile claim-private.pem.key

Expected output:

{
    "certificateArn": "arn:aws:iot:us-east-1:123456789012:cert/8f2c...e91b",
    "certificateId": "8f2c1a9e7d3b4f5061c2a8d9e0f11223344556677889900aabbccddeeff0011",
    "certificatePem": "-----BEGIN CERTIFICATE-----\nMIIDWjCCAkKgAwIBAgIVAJ...\n-----END CERTIFICATE-----\n",
    "keyPair": {
        "PublicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n",
        "PrivateKey": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----\n"
    }
}

Note the certificateId — you’ll need it below.

Create the restricted claim policy, which only allows publishing/subscribing to the special $aws/certificates/* and $aws/provisioning-templates/* topics:

cat > claim-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "iot:Connect",
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": ["iot:Publish", "iot:Receive"],
      "Resource": [
        "arn:aws:iot:us-east-1:123456789012:topic/$aws/certificates/create/*",
        "arn:aws:iot:us-east-1:123456789012:topic/$aws/provisioning-templates/FleetProvisioningTemplate/provision/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": "iot:Subscribe",
      "Resource": [
        "arn:aws:iot:us-east-1:123456789012:topicfilter/$aws/certificates/create/*",
        "arn:aws:iot:us-east-1:123456789012:topicfilter/$aws/provisioning-templates/FleetProvisioningTemplate/provision/*"
      ]
    }
  ]
}
EOF

aws iot create-policy \
  --policy-name ClaimCertProvisioningPolicy \
  --policy-document file://claim-policy.json

Attach that policy to the claim certificate:

aws iot attach-policy \
  --policy-name ClaimCertProvisioningPolicy \
  --target arn:aws:iot:us-east-1:123456789012:cert/8f2c1a9e7d3b4f5061c2a8d9e0f11223344556677889900aabbccddeeff0011

No output means success. Verify it took effect:

aws iot list-attached-policies \
  --target arn:aws:iot:us-east-1:123456789012:cert/8f2c1a9e7d3b4f5061c2a8d9e0f11223344556677889900aabbccddeeff0011

Expected output:

{
    "policies": [
        {
            "policyName": "ClaimCertProvisioningPolicy",
            "policyArn": "arn:aws:iot:us-east-1:123456789012:policy/ClaimCertProvisioningPolicy"
        }
    ]
}

At this point the claim cert would be flashed onto every device coming off the assembly line, along with the Amazon Root CA.


7. Step 5 — Simulate the Device’s First-Boot Provisioning Flow

This is the part that normally runs inside your device firmware (via the AWS IoT Device SDK). To understand it and to test your setup before writing embedded code you can simulate it with mosquitto_pub/mosquitto_sub and the claim certificate.

First, grab your account’s IoT data endpoint:

aws iot describe-endpoint --endpoint-type iot:Data-ATS

Expected output:

{
    "endpointAddress": "a1b2c3d4e5f6gh-ats.iot.us-east-1.amazonaws.com"
}

7.1 Request a new certificate

Subscribe to the response topics (run this in one terminal):

mosquitto_sub -h a1b2c3d4e5f6gh-ats.iot.us-east-1.amazonaws.com -p 8883 \
  --cert claim-certificate.pem.crt \
  --key claim-private.pem.key \
  --cafile AmazonRootCA1.pem \
  -t '$aws/certificates/create/json/accepted' \
  -t '$aws/certificates/create/json/rejected' -v

In a second terminal, publish an empty payload to request a new certificate:

mosquitto_pub -h a1b2c3d4e5f6gh-ats.iot.us-east-1.amazonaws.com -p 8883 \
  --cert claim-certificate.pem.crt \
  --key claim-private.pem.key \
  --cafile AmazonRootCA1.pem \
  -t '$aws/certificates/create/json' -m '{}'

Expected output on the accepted topic (in the subscriber terminal):

$aws/certificates/create/json/accepted {
  "certificateId": "a4d9e2...771c",
  "certificatePem": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
  "privateKey": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----\n",
  "certificateOwnershipToken": "eyJTM0ZpbGVLZXkiOiJwcm9kdWN0aW9uL2NlcnRzL2E0ZDllMi43NzFjIn0="
}

The device now has a brand-new certificate — but it’s still inactive and unattached to anything. It gets activated in the next step.

7.2 Register the Thing using the template

Subscribe to the registration response topics:

mosquitto_sub -h a1b2c3d4e5f6gh-ats.iot.us-east-1.amazonaws.com -p 8883 \
  --cert claim-certificate.pem.crt \
  --key claim-private.pem.key \
  --cafile AmazonRootCA1.pem \
  -t '$aws/provisioning-templates/FleetProvisioningTemplate/provision/json/accepted' \
  -t '$aws/provisioning-templates/FleetProvisioningTemplate/provision/json/rejected' -v

Publish the RegisterThing request, including the ownership token from step 7.1 and any template parameters (here, a serial number the device reads from its own hardware):

mosquitto_pub -h a1b2c3d4e5f6gh-ats.iot.us-east-1.amazonaws.com -p 8883 \
  --cert claim-certificate.pem.crt \
  --key claim-private.pem.key \
  --cafile AmazonRootCA1.pem \
  -t '$aws/provisioning-templates/FleetProvisioningTemplate/provision/json' \
  -m '{
    "certificateOwnershipToken": "eyJTM0ZpbGVLZXkiOiJwcm9kdWN0aW9uL2NlcnRzL2E0ZDllMi43NzFjIn0=",
    "parameters": { "SerialNumber": "SN-00042" }
  }'

Expected output on the accepted topic:

$aws/provisioning-templates/FleetProvisioningTemplate/provision/json/accepted {
  "deviceConfiguration": {},
  "thingName": "Sensor-SN-00042"
}

At this point, AWS IoT Core has:

  • Created a Thing named Sensor-SN-00042
  • Activated the new certificate and attached it to that Thing
  • Attached DeviceRuntimePolicy to the new certificate

The device saves the certificate PEM and private key from step 7.1 to local flash/storage and never uses the claim certificate again.


8. Step 6 — Verify the Device Was Provisioned Correctly

Confirm the Thing exists:

aws iot describe-thing --thing-name Sensor-SN-00042

Expected output:

{
    "defaultClientId": "Sensor-SN-00042",
    "thingName": "Sensor-SN-00042",
    "thingId": "3f9c2b1a-8d7e-4f6a-9b2c-1a3e5f7d9b0c",
    "thingArn": "arn:aws:iot:us-east-1:123456789012:thing/Sensor-SN-00042",
    "attributes": {
        "serialNumber": "SN-00042"
    },
    "version": 1
}

Confirm the new certificate is active and attached:

aws iot list-thing-principals --thing-name Sensor-SN-00042

Expected output:

{
    "principals": [
        "arn:aws:iot:us-east-1:123456789012:cert/a4d9e2f1b3c5d7e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1771c"
    ]
}

And that the runtime policy — not the claim policy — is attached to it:

aws iot list-attached-policies \
  --target arn:aws:iot:us-east-1:123456789012:cert/a4d9e2f1b3c5d7e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1771c

Expected output:

{
    "policies": [
        {
            "policyName": "DeviceRuntimePolicy",
            "policyArn": "arn:aws:iot:us-east-1:123456789012:policy/DeviceRuntimePolicy"
        }
    ]
}

Finally, confirm the device can now connect and publish on its own topic using its new certificate (not the claim cert):

mosquitto_pub -h a1b2c3d4e5f6gh-ats.iot.us-east-1.amazonaws.com -p 8883 \
  --cert new-certificate.pem.crt \
  --key new-private.pem.key \
  --cafile AmazonRootCA1.pem \
  -t 'things/Sensor-SN-00042/data' -m '{"temperature": 21.5}'

If this succeeds with no error, the provisioning flow is complete and the device is now a first-class citizen in your fleet.


9. Best Practices and Gotchas

  • Never ship the same runtime certificate to two devices. Each device gets its own unique cert generated at provisioning time the claim cert is shared, the final cert is not.
  • Scope the claim policy tightly. It should only reach the $aws/certificates/* and $aws/provisioning-templates/* topics nothing else. If a claim cert leaks, the blast radius should be “attacker can request certs,” not “attacker can read your fleet’s telemetry.”
  • Rotate claim certificates periodically, especially if a batch of devices ships to a facility you don’t fully control.
  • Use ${iot:Connection.Thing.ThingName} policy variables in the runtime policy so each device is scoped to its own topics automatically you don’t want Device A able to publish to Device B’s topic.
  • Watch your default certificate limits. New AWS accounts have soft limits on active certificates; request an increase before a large fleet rollout.
  • Use a pre-provisioning hook if you need any custom validation logic (serial number allow-lists, batch/lot checks, rate limiting) before a Thing is created.

10. Cleaning Up (If This Was a Test)

aws iot detach-policy --policy-name DeviceRuntimePolicy \
  --target arn:aws:iot:us-east-1:123456789012:cert/a4d9e2f1b3c5d7e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1771c

aws iot delete-thing --thing-name Sensor-SN-00042

aws iot update-certificate --certificate-id a4d9e2f1b3c5d7e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1771c --new-status INACTIVE

aws iot delete-certificate --certificate-id a4d9e2f1b3c5d7e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1771c

aws iot delete-provisioning-template --template-name FleetProvisioningTemplate

Summary

Fleet Provisioning turns a manual, one-device-at-a-time onboarding process into something a device can do entirely on its own, the moment it powers on for the first time. The core mental model is simple once you’ve walked through it once:

  1. Ship every device with a shared, restricted claim certificate.
  2. The device uses it to request a unique certificate from AWS IoT.
  3. The device sends that certificate’s ownership token plus its own identifying parameters (serial number, MAC address, etc.) to your provisioning template.
  4. AWS IoT creates the Thing, activates the certificate, and attaches the real runtime policy all automatically.

Once you’ve run through the CLI/MQTT flow above by hand, wiring the same steps into embedded firmware using the AWS IoT Device SDK (available for C, Python, Java, and JavaScript) is a fairly mechanical translation of the same MQTT topics and payloads.


By admin

Leave a Reply

Your email address will not be published. Required fields are marked *