1. Introduction: The Hidden Complexity of IoT Onboarding at Scale

As IoT deployments evolve from laboratory prototypes to production fleets of thousands of edge devices, secure device onboarding emerges as one of the most challenging operational bottlenecks. Each Edge Controller must cryptographically prove its identity to AWS IoT Core, receive appropriate configuration, and assume AWS permissions, all without manual intervention.

In small pilots, manually creating certificates, configuring IAM credentials, and registering devices one-by-one remains manageable. But as deployments scale to hundreds or thousands of units, this approach collapses under operational strain, security risks, and configuration inconsistencies.

The challenge: How do you enable thousands of devices to securely self-register with AWS IoT Core and Greengrass without pre-loading sensitive IAM keys, manual provisioning steps, or security vulnerabilities?

This article explores how AWS IoT Fleet Provisioning (Provisioning by Claim) solves this problem by automating trust establishment and certificate issuance through a cloud-orchestrated, device-initiated workflow.

2. Understanding Core Components

Before examining provisioning workflows, understand that each Edge Controller requires six core AWS resources: 

  • an X.509 certificate for authentication and MQTT encryption
  • a certificate policy defining permissions
  • an IoT Thing registry entry & IoT Thing Group
  • a Token Exchange Role & Token Exchange Role Alias for accessing AWS services via API or SDK
 AWS IoT Greengrass Core Resources

These components establish the trust boundary, defining who the device is, what it can do, and how it connects.

3. The Original Provisioning Flow,  Manual, and Fragile

The initial architecture requires manual intervention at every step:

  1. Generate RSA key pair and Certificate Signing Request (CSR)
  2. Request certificate creation from the CSR message
  3. Create new IoT policy and attach it to the certificate
  4. Manually create IoT Thing and attach certificate to it in AWS IoT Core via Console or CLI
  5. Create IAM Token Exchange role, its policy and IoT Role Alias
  6. The certificate should be deployed to the device, and an AWS IoT Greengrass configuration file, consolidating all these properties, must be created.
Current Provisioning Flow – Problems at Scale

This workflow reveals critical weaknesses:

ChallengeImpact
Manual Scaling FailureEach device requires human intervention; impossible at scale
IAM Credential ExposureScaling this approach is unfeasible, as implementing this logic on the device is prohibited due to the excessively broad permissions of the AWS IAM credential required for accessing AWS resources and creating new ones.
Configuration DriftManual steps lead to inconsistencies: incorrect Thing-certificate associations, mismatched endpoint configurations, human errors in resource naming
Rotation ComplexityUpdating certificates requires coordinated manual operations

These constraints demanded a solution: automate device registration from the cloud, with devices initiating their own provisioning.

4. AWS IoT Fleet Provisioning,  Provisioning by Claim

Fleet Provisioning transforms onboarding from an operator-driven push to a device-initiated, cloud-authorized pull model. Devices register themselves using temporary “claim” certificates with minimal, tightly scoped permissions.

 Provisioning by Claim – Temporary vs Production Certificates

Understanding Claim Certificates

In standard Fleet Provisioning by Claim, a single claim certificate is shared across all devices during manufacturing. This certificate has extremely limited permissions, it can only connect to AWS IoT Core and call the device provisioning MQTT API. Because the claim certificate cannot publish data, subscribe to device topics, or access other AWS resources, sharing it across the fleet poses minimal security risk.

The Core Workflow

Step 1: Device Connects with Claim Certificate
The device connects to AWS IoT Core using the shared claim certificate. It generates a key pair locally, creates a Certificate Signing Request (CSR), and calls CreateCertificateFromCsr. AWS IoT Core returns:

  • A production X.509 certificate (inactive at this stage)
  • A private key (if requested, or just the certificate if device generated the key pair)
  • A temporary certificateOwnershipToken (required for the next step)

Step 2: Device Calls RegisterThing

The device publishes a registration request to $aws/provisioning-templates/<template-name>/provision/json:

{ 
  "certificateOwnershipToken": "<token-from-step-1>", 
  "parameters": { 
    "SerialNumber": "EDGECTRL-00123" 
  } 
}

Step 3: Claim Certificate Validation
AWS IoT Core verifies that the connection is using a valid claim certificate with permissions to call the provisioning template.

Step 4: Pre-Provisioning Hook (Optional)
AWS IoT Core optionally invokes a pre-provisioning Lambda hook for custom authorization logic, such as validating serial numbers against a manufacturing database or verifying TPM attestation data.

Step 5: Provisioning Template Execution
If authorized, AWS executes the provisioning template to:

  • Activate the production certificate (created in Step 1)
  • Create an IoT Thing with the specified name
  • Attach the production certificate to the Thing
  • Attach IoT policies to the certificate

Step 6: Device Receives Confirmation
AWS returns a confirmation message via MQTT containing the Thing name and other resource details. The device now has an active production certificate and can disconnect from the claim certificate.

Step 7: Device Reconnects with Production Certificate
The device reconnects to AWS IoT Core using its production certificate for all future operations. The shared claim certificate remains active for provisioning additional devices.

Outcome: The device transforms from unregistered to fully authenticated, automatically, securely, without operator intervention.

Note: This describes the standard Fleet Provisioning by Claim approach where a single claim certificate is shared across all devices. AWS also supports Trusted User provisioning, where each device receives a unique temporary certificate that is revoked after successful provisioning. The shared claim certificate model is more common for manufacturing scenarios as it simplifies device pre-configuration.

5. Key Terms and Components

Understanding these concepts is essential:

  • Provisioning Claim Certificate: Temporary X.509 certificate with permissions limited to device registration
  • Provisioning Template: JSON document defining AWS resources to create per device (Thing, certificate, policies)
  • Pre-Provisioning Hook: Optional Lambda function authorizing or denying registration requests
  • Production Certificate: Permanent certificate issued after successful provisioning

6. Implementation, Step-by-Step

Step 1: Create Provisioning Template

The provisioning template consists of two sections: 

  • Parameters: input provided by the device when invoking the Device Provisioning MQTT API.
  • Resources: description of the AWS resources to be created.

Resources can be

  • IoT Thing: Defines the attributes of the AWS IoT Thing to be created.
  • IoT Certificate: Specifies the the certificate to activate (note: Fleet Provisioning templates only accept certificate IDs, not CSRs).
  • IoT Policy: The certificate policy to be associated with the certificate. This establishes the permission boundaries for the certificate.
{
  "Parameters": {
    "SerialNumber": { "Type": "String" },
    "CSR": { "Type": "String" },
  },
  "Resources": {
    "thing": {
      "Type": "AWS::IoT::Thing",
      "Properties": {
        "ThingName": { "Fn::Join": ["", ["EdgeController-", {"Ref": "SerialNumber"}]] }
      }
    },
    "certificate": {
      "Type": "AWS::IoT::Certificate",
      "Properties": { 
         "CertificateId": {"Ref": "AWS::IoT::Certificate::Id"},
         "Status": "ACTIVE" 
      }
    },
    "policy": {
      "Type": "AWS::IoT::Policy",
      "Properties": { "PolicyName": "EdgeControllerPolicy" }
    }
  }
}

Note: In Fleet Provisioning templates, AWS::IoT::Certificate::Id is a special reference that points to the certificate created via CreateCertificateFromCsr and linked through the certificateOwnershipToken. CSR-based certificate creation within the template itself (CertificateSigningRequest) is only supported in Just-In-Time Provisioning (JITP) templates.

The Provisioning Template can be established utilizing the CreateProvisioningTemplate API or as an integral component of a CloudFormation stack or even create the template via AWS Management Console in IoT Core. Each template is uniquely identified by a name, which devices will employ when invoking the Device Provisioning MQTT API.

Step 2: Create Claim Certificate with Minimal Policy

Create a shared claim certificate with limited permissions for provisioning operations only:

aws iot create-keys-and-certificate --set-as-active \
  --certificate-pem-outfile claimCert.pem \
  --private-key-outfile claimKey.pem

Attach a policy allowing only provisioning operations:

{
  "Version": "2012-10-17", 
  "Statement": [ 
    { 
      "Effect": "Allow", 
      "Action": ["iot:Connect"], 
      "Resource": "arn:aws:iot:*:*:client/*" 
    }, 
    { 
      "Effect": "Allow", 
      "Action": ["iot:Publish", "iot:Receive"], 
      "Resource": [ "arn:aws:iot:*:*:topic/$aws/certificates/create-from-csr/*", "arn:aws:iot:*:*:topic/$aws/provisioning-templates/<your_template_name>/provision/*" 
      ] 
     }, 
     { "Effect": "Allow", 
        "Action": ["iot:Subscribe"], 
"Resource": [ "arn:aws:iot:*:*:topicfilter/$aws/certificates/create-from-csr/*", "arn:aws:iot:*:*:topicfilter/$aws/provisioning-templates/<your_template_name>/provision/*" 
    ] 
   } 
  ]
}

This policy permits:

  • Connecting to AWS IoT Core
  • Creating production certificates via CreateCertificateFromCsr
  • Calling the RegisterThing API on provisioning templates

Step 3: Device requests production certificate creation from CSR

Sample of python script that executes certificate creation requests using MQTT connection from claim certificate. The script uses AWS IoT SDK IdentityClient

from awsiot import iotidentity, mqtt5_client_builder
from awscrt import mqtt5, mqtt_request_response
from concurrent.futures import Future
import uuid




if __name__ == '__main__':
   iot_endpoint = "<aws iot connection endpoint>"
   crs_file = "<path_to_csr_file>"
   claim_cert_pem = "<provisioning claim certificate pem>"
   claim_private_key = "<path location of private key>"
   provisioning_template_name = "<your template name>"
  
   with open(csr_file, "r") as csr_file:
       csr_data = csr_file.read()


   # ---------------------------------
   # Utility callback methods and parameters
   initial_connection_success = Future()
   def on_lifecycle_connection_success(event: mqtt5.LifecycleConnectSuccessData):
       initial_connection_success.set_result(True)
 def on_lifecycle_connection_failure(event: mqtt5.LifecycleConnectFailureData):
       initial_connection_success.set_exception(Exception("Failed to connect"))


   def on_lifecycle_disconnection(event: mqtt5.LifecycleDisconnectData):
       print("Lifecycle Disconnected with reason code:{}".format(
           event.disconnect_packet.reason_code if event.disconnect_packet else "None"))


   stopped = Future()
   def on_lifecycle_stopped(event: mqtt5.LifecycleStoppedData):
       stopped.set_result(True)


   connect_options=mqtt5.ConnectPacket(
       client_id="mqtt5-sample-" + uuid.uuid4().hex,
   )
   # ---------------------------------


   # Create a mqtt5 connection
   mqtt5_client = mqtt5_client_builder.mtls_from_path(
       endpoint=iot_endpoint,
       port=8883,
       connect_options=connect_options,
       cert_filepath=claim_cert_pem,
       pri_key_filepath=claim_private_key,
       clean_session=True,
       keep_alive_secs=1200,
       on_lifecycle_connection_success=on_lifecycle_connection_success,
       on_lifecycle_connection_failure=on_lifecycle_connection_failure,
       on_lifecycle_disconnection=on_lifecycle_disconnection,
       on_lifecycle_stopped=on_lifecycle_stopped)


   mqtt5_client.start()


   rr_options = mqtt_request_response.ClientOptions(
       max_request_response_subscriptions = 2,
       max_streaming_subscriptions = 0,
       operation_timeout_in_seconds = 30,
   )
  
   # Initializing IdentityClient for invoking AWS registration APIs
   identity_client = iotidentity.IotIdentityClientV2(mqtt5_client, rr_options)


   initial_connection_success.result()
   print("Connected!\n")


   # creating certificate from CSR file
   create_certificate_from_csr_response = identity_client.create_certificate_from_csr(iotidentity.CreateCertificateFromCsrRequest(
       certificate_signing_request=csr_data
   )).result()
   print(f"CreateCertificateFromCsr response: {create_certificate_from_csr_response}\n")


  # ... STEP 4 invoke Registration API  

Step 4: Device Invokes Registration

The device publishes a registration request to:
$aws/provisioning-templates/<template-name>/provision/json 

payload

{ 
  "certificateOwnershipToken": "xyz789...", 
  "parameters": { 
    "SerialNumber": "EDGECTRL-00123" 
  } 
 # ... Step 3: create certificate from CSR 


   # creating RegisterThing API specifying certificateOwnershipToken
   register_thing_request = iotidentity.RegisterThingRequest(
       template_name=provisioning_template_name,
       certificate_ownership_token=create_certificate_from_csr_response.certificate_ownership_token,
   )
register_thing_request.parameters = {
       "SerialNumber": "EDGECTRL-00123" # parameters you want to specify for the provisioning template
   }


   register_thing_response = identity_client.register_thing(register_thing_request).result()
   print(f"RegisterThing response: {register_thing_response}\n")


   mqtt5_client.stop()
   stopped.result()
   print("Stopped!")
 Fleet Provisioning Complete Workflow – Device to AWS

AWS IoT Core:

  1. Validates the claim certificate
  2. Verifies the certificateOwnershipToken and identifies the associated production certificate
  3. Triggers the optional pre-provisioning Lambda hook
  4. Executes the provisioning template to create resources and activate the certificate
  5. Returns confirmation via MQTT

The device receives the Thing name and resource details, disconnects from the claim certificate, and reconnects using the production certificate for all future operations.

7. Custom Authorization with Pre-Provisioning Hooks

Pre-provisioning Lambda hooks enable fine-grained control before device registration. The Lambda function is invoked by AWS prior to the creation of resources using the Provisioning Template.

Use Case 1: Serial Number Validation

import boto3
import botocore.exceptions
import logging


dynamodb_client = boto3.client("dynamodb")


logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
# Create a console handler with the custom formatter
console_handler = logging.StreamHandler()
console_handler.setFormatter(logging.Formatter(
   fmt="%(asctime)s %(levelname)s [%(module)s.%(funcName)s:%(lineno)d] %(message)s",
))
logger.addHandler(console_handler)




def is_provisioning_allowed(serialNumber: str) -> bool:
   try:
       device_registry_response = dynamodb_client.query(
           TableName="device_registry_table",
           KeyConditionExpression="device_id = :device_id_val",
           ExpressionAttributeValues={":device_id_val": {"S": serialNumber}},
           ExpressionAttributeNames={'#status': 'status'},
           Select="SPECIFIC_ATTRIBUTES",
           ProjectionExpression="device_id,customer_id,#status,version"
       )
      
       if device_registry_response.get("Count", 0) == 0:
           logger.error("No device pending activation found for the requested serialNumber: %s", serialNumber)
           return False
      
      
       device_status = device_registry_response.get("Items")[0].get("status", {}).get("S", "")
      
       if "PENDING_PROVISIONING" != device_status:
           logger.error("Device status %s is not allowed for provisioning", device_status)
           return False
      
       return True
      
   except botocore.exceptions.ClientError as e:
       logger.error("Unexpected error occured when requesting status of device", exc_info=e)
       return False


def lambda_handler(event, context):
   serial = event['parameters']['SerialNumber']
  
   # Validate against manufacturing database
   if is_provisioning_allowed(serial):
       return {"allowProvisioning": True}
   else:
       return {"allowProvisioning": False}

The Pre-Provisioning lambda function is responsible for parsing event input received from the device and subsequently verifying its Serial Number against the device-registry DynamoDB table. Provisioning is permitted only if the corresponding DynamoDB entry indicates a device status of PENDING_PROVISIONING.

The Pre-Provisioning lambda must return json object of pre-defined format

# if request validation failed and provisioning is not allowed
{
    "allowProvisioning": "false"
}


# if verification succeeded and provisioning is allowed
{
    "allowProvisioning": True,
 "parameterOverrides": {
        "<key>": "<value>",
    }
}

A successful response may optionally include the parameterOverrides field for the enrichment of the Provisioning Template Parameters section.

8. Post-provisioning

While the AWS Provisioning Template offers capabilities for the creation of essential IoT resources, specifically the IoT Thing, Certificate, and Policy, AWS Greengrass necessitates the establishment of more customized resources, such as an IAM TokenExchange role and an associated IoT Role Alias for that IAM TokenExchange role.

The AWS Fleet Provisioning API does not offer a solution for this scenario, necessitating a custom implementation. For instance, a custom MQTT API endpoint could be established to manage post-provisioning actions. An IoT Rule can then be utilized to route the MQTT API request to some custom Post-Provisioning AWS Lambda function.

9. Integration with AWS IoT Greengrass

Upon successful invocation of the Device Provisioning MQTT API and the Post-Provisioning hook, the device is supplied with all necessary information (such as the certificate and thing name) to establish a fully functional production MQTT connection.

That information can be used to prepare AWS Greengrass configuration file:

system:
  certificateFilePath: "/greengrass/v2/certs/production-cert.pem"
  privateKeyPath: "/greengrass/v2/certs/production-private.key"
  thingName: "EdgeController-EDGECTRL-00123"

services:
  aws.greengrass.Nucleus:
    configuration:
      awsRegion: "us-east-1"
      iotDataEndpoint: "a1b2c3d4e5-ats.iot.us-east-1.amazonaws.com"

For enhanced security with TPM-backed keys:

system:
  privateKeyPath: "pkcs11:object=edge-key;type=private;pin-value=1234"

10. Security Benefits

BenefitImpact
Zero Static CredentialsIAM keys completely eliminated from devices
Minimal Attack SurfaceClaim certificates restricted to single operation
Cloud-Enforced AuthorizationSecurity policies centralized and auditable
Complete Audit TrailEvery registration logged to CloudTrail
Automated RotationCertificate renewal via re-provisioning

11. Troubleshooting Common Issues

SymptomCauseResolution
Provisioning failsInvalid / revoked claim certificateVerify with aws iot describe-certificate
Device not registeredLambda rejected requestCheck CloudWatch Logs
Connection refusedWrong endpoint/regionConfirm iotDataEndpoint matches region
Duplicate Thing namesNon-unique parametersUse UUIDs in template

12. Conclusion: Provisioning as a Security Foundation

Problem Statement:

Traditional provisioning required manual certificate creation, static IAM credential distribution, and per-device configuration. This failed to scale, created security vulnerabilities, and imposed unsustainable operational overhead. Manual processes introduced configuration drift and compliance gaps.

Solution Implemented:

AWS IoT Fleet Provisioning with Provisioning by Claim transforms onboarding into a cloud-orchestrated, device-initiated workflow. Devices present temporary claim certificates, AWS validates through Lambda hooks, and production credentials are dynamically issued. Static credentials are eliminated entirely.

Outcomes Achieved:

  • Scalability: Thousands of devices self-register simultaneously
  • Security: Zero static credentials; minimal claim certificate scope
  • Automation: Provisioning occurs on first boot without manual steps
  • Auditability: Complete history in CloudTrail and CloudWatch
  • Flexibility: Lambda hooks enable TPM attestation and custom logic

Combined with TPM-based hardware security, Fleet Provisioning creates a complete chain of trust from silicon to cloud: device identity generated in hardware → proven via claim certificate → validated by cloud → production credentials issued to verified devices.

This architecture scales from pilot to global fleets while satisfying regulatory requirements (FIPS 140-3, ISO 27001, IEC 62443).

Additional Resources

AWS Documentation:

Related Articles:

By Maxim Maltsev, Edge Computing Solution Architect, Klika Tech and Mohammad Shirazi, Portfolio Manager, Klika Tech