Zero-Touch IoT Device Provisioning: Scaling from 10 to 100,000 Devices
Shipping ten devices is easy. You SSH in, paste a certificate, set a device ID, and call it done. Shipping 100,000 devices with that same workflow means hiring a team of humans to do nothing but copy-paste credentials — and you'll still end up with provisioning errors in the field.
IoT device provisioning at scale is one of those problems that punishes you later for cutting corners earlier. This guide covers every layer: from factory floor to cloud, and from your first pilot to full fleet rollout.
Why Manual Provisioning Breaks at Scale
Manual provisioning has three failure modes that compound as fleet size grows:
Human error. Wrong certificate on the wrong device. Duplicate device IDs. Credentials committed to a config file instead of flash storage. We've debugged all of these in production.
Factory throughput. If each device needs a human to plug it into a laptop, run a script, and verify a response, you're capped at maybe 200 devices per person per day. At 50,000 units, that's weeks of labor.
Certificate rotation. When your root CA expires or is compromised, you need to re-provision every device. If you can't do that remotely and automatically, you're flying technicians to every physical installation.
The solution is zero-touch provisioning — devices authenticate themselves at first connection and receive their identity automatically.
AWS IoT Fleet Provisioning: The Architecture
AWS IoT Core supports three provisioning patterns. Understanding which to use is the first decision:
| Pattern | Best for | Trust anchor | |---|---|---| | Fleet provisioning with claim certs | Factory-scale, consistent hardware | Shared claim certificate | | Just-in-time provisioning (JITP) | Diverse hardware, existing PKI | Per-device cert signed by your CA | | Just-in-time registration (JITR) | Maximum control via Lambda | Per-device cert, custom logic |
For most greenfield IoT products, fleet provisioning with claim certificates is the right choice.
Fleet Provisioning with Claim Certificates
The claim certificate is a shared credential burned into every device at the factory. It has minimal permissions — only enough to call the provisioning API. After provisioning, the device receives a unique certificate and the claim cert becomes useless for that device.
Step 1: Create the Fleet Provisioning Template
{
"Parameters": {
"SerialNumber": {
"Type": "String"
},
"FirmwareVersion": {
"Type": "String"
},
"AWS::IoT::Certificate::Id": {
"Type": "String"
}
},
"Resources": {
"certificate": {
"Properties": {
"CertificateId": {
"Ref": "AWS::IoT::Certificate::Id"
},
"Status": "Active"
},
"Type": "AWS::IoT::Certificate"
},
"policy": {
"Properties": {
"PolicyName": "FleetDevicePolicy"
},
"Type": "AWS::IoT::Policy"
},
"thing": {
"OverrideSettings": {
"AttributePayload": "MERGE",
"ThingGroups": "DO_NOTHING",
"ThingTypeName": "REPLACE"
},
"Properties": {
"AttributePayload": {
"firmwareVersion": {
"Ref": "FirmwareVersion"
}
},
"ThingGroups": ["unprovisioned-devices"],
"ThingName": {
"Fn::Join": ["", ["device-", {"Ref": "SerialNumber"}]]
},
"ThingTypeName": "SmartSensor"
},
"Type": "AWS::IoT::Thing"
}
}
}
Step 2: ESP32 Provisioning Flow
The device connects with the claim certificate, then calls the provisioning API to receive its permanent credentials:
#include
#include
#include
#include // Claim certificate — same on every unit from the factory
extern const char CLAIM_CERT[] asm("_binary_claim_cert_pem_start");
extern const char CLAIM_KEY[] asm("_binary_claim_key_pem_start");
extern const char ROOT_CA[] asm("_binary_root_ca_pem_start");
const char* PROVISION_TEMPLATE = "FleetProvisioningTemplate";
WiFiClientSecure net;
PubSubClient client(net);
Preferences prefs;
bool isProvisioned() {
prefs.begin("device", true);
bool result = prefs.getBool("provisioned", false);
prefs.end();
return result;
}
void onProvisionResponse(char* topic, byte* payload, unsigned int length) {
StaticJsonDocument<2048> doc;
deserializeJson(doc, payload, length);
const char* certId = doc["certificateId"];
const char* certPem = doc["certificatePem"];
const char* privateKey = doc["privateKey"];
const char* iotEndpoint = doc["thingName"];
// Persist to NVS flash — survives reboots
prefs.begin("device", false);
prefs.putString("cert_pem", certPem);
prefs.putString("private_key", privateKey);
prefs.putString("thing_name", iotEndpoint);
prefs.putBool("provisioned", true);
prefs.end();
// Publish ownership confirmation
StaticJsonDocument<256> confirm;
confirm["certificateOwnershipToken"] = doc["certificateOwnershipToken"];
char buf[256];
serializeJson(confirm, buf);
client.publish(
"$aws/provisioning-templates/FleetProvisioningTemplate/provision/json/accepted",
buf
);
}
void provisionDevice() {
net.setCACert(ROOT_CA);
net.setCertificate(CLAIM_CERT);
net.setPrivateKey(CLAIM_KEY);
client.setServer(AWS_ENDPOINT, 8883);
client.setCallback(onProvisionResponse);
client.connect("claim-client");
// Subscribe to provisioning response
client.subscribe(
"$aws/certificates/create/json/accepted"
);
// Request new certificate
client.publish("$aws/certificates/create/json", "{}");
}
Just-in-Time Provisioning (JITP)
JITP is ideal when devices carry per-device certificates signed by your own CA — common in industrial or healthcare IoT where devices are manufactured by a third party but must be onboarded into your cloud.
The flow: device connects with its cert → AWS IoT detects an unknown cert signed by a registered CA → triggers a Lambda → Lambda creates the Thing, attaches the policy, and activates the cert.
Register your CA with AWS:
Generate CA private key
openssl genrsa -out rootCA.key 2048Self-signed CA cert (10 year validity)
openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 3650 -out rootCA.pem -subj "/C=US/O=MyCompany/CN=IoT Root CA"Get registration code from AWS (required for verification cert)
aws iot get-registration-codeCreate verification cert signed with your CA
openssl req -new -key rootCA.key -out verification.csr -subj "/CN="openssl x509 -req -in verification.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out verification.pem -days 1
Register CA with JITP template
aws iot register-ca-certificate --ca-certificate file://rootCA.pem --verification-cert file://verification.pem --set-as-active --allow-auto-registration --registration-config file://jitp-template.json
Provisioning Hooks: Custom Validation with Lambda
Both fleet provisioning and JITR support a pre-provisioning Lambda hook. This is where you validate the device before issuing credentials — checking your manufacturing database, enforcing license limits, or logging the provisioning event to your audit trail.
import { IoTClient, DescribeThingCommand } from '@aws-sdk/client-iot'interface ProvisioningEvent {
claimCertificateId: string
certificateId: string
certificatePem: string
templateArn: string
clientId: string
parameters: Record
}
export const handler = async (event: ProvisioningEvent) => {
const { SerialNumber, FirmwareVersion } = event.parameters
// Validate serial number against manufacturing DB
const isValid = await validateSerial(SerialNumber)
if (!isValid) {
throw new Error(Unknown serial: ${SerialNumber})
}
// Enforce fleet size limit per account
const deviceCount = await getDeviceCount()
if (deviceCount >= MAX_FLEET_SIZE) {
throw new Error('Fleet limit reached')
}
// Return allowProvisioning + any parameter overrides
return {
allowProvisioning: true,
parameterOverrides: {
Region: getRegionFromSerial(SerialNumber),
ProductLine: lookupProductLine(SerialNumber)
}
}
}
Factory Provisioning Workflow
The physical process matters as much as the software. Here's the workflow we implement for clients at the factory level:
This means zero provisioning happens at the customer site — the device is fully cloud-registered before it leaves the factory.
Monitoring the Provisioning Pipeline
Add a CloudWatch dashboard tracking:
ProvisioningSuccess — devices successfully provisioned per hourProvisioningFailure — with error category breakdownClaimCertUseCount — detect if claim certs are being reused (indicates leaked creds)HookRejections — validation failures from your pre-provisioning LambdaSet an alarm if ProvisioningFailure / ProvisioningSuccess > 0.05 — a 5% failure rate at the factory needs immediate attention.
The Payoff
We implemented this pipeline for a client shipping 40,000 environmental sensors. Factory throughput went from 180 units/day (manual provisioning) to 3,000 units/day (zero-touch). Provisioning errors dropped to zero. And when they needed to rotate certificates after a security audit, re-provisioning all active devices took a single API call and an OTA update — no field visits required.
IoT device provisioning at scale is not glamorous engineering, but it is the invisible foundation that everything else depends on.
Need help? [Contact Code Caracal](/contact) — we've shipped these systems for clients across 15+ countries.