Chapter 6: Hub Onboarding & Fleet Provisioning
In Chapter 5, we saw how the MQTT Proxy gives sub-devices a shared connection to AWS IoT Core. But there's a prerequisite we skipped over: the hub itself needs to be registered with IoT Core before it can connect at all. That's what Hub Onboarding handles — it's the very first thing that runs when a hub powers on for the first time.
This chapter walks through how the SDK proves the hub's identity to AWS, obtains permanent credentials, and transitions from "unknown device" to "registered thing."
Why Does the Hub Need Onboarding?
A brand-new hub has no identity in AWS IoT Core. Without onboarding:
- It can't establish an MQTT connection (no valid certificate).
- It can't receive commands or send telemetry.
- It can't proxy connections for sub-devices.
Onboarding solves this by exchanging temporary credentials for permanent ones and registering the hub as a "managed thing" in IoT Core.
Two Paths: Fleet Provisioning vs. JITR
The SDK supports two provisioning methods, configured in iotmi_config.json:
| Method | How It Works | Certificate Outcome |
|---|---|---|
| Fleet Provisioning (FP) | Uses a shared claim certificate to request a unique device certificate from IoT Core | New cert + key generated by AWS |
| JITR (Just-In-Time Registration) | Uses a pre-installed DHA certificate signed by a registered CA | Existing DHA cert becomes the permanent cert |
The method is read from config during initialization:
// hub_onboarding.cpp — InitProvisioner()
provisioning_method_ =
config->GetStringValue(IOT_PROVISIONING_METHOD_CONF);
// Falls back to "JITR" if missing or invalid
📁
provisioning/IoTSmartHomeDevice-HubOnboarding/include/hub_onboarding.hpp
The Key Players
Three classes collaborate to make onboarding work:
| Class | File | Role |
|---|---|---|
HubOnboarding |
hub_onboarding.hpp |
Orchestrator — reads config, drives the flow |
DeviceProvisioner |
device_provisioner.hpp |
Adapter — translates between HubOnboarding and the provisioning library |
IotProvisioning |
Provisioning.hpp |
Engine — manages MQTT connections and delegates to FleetProvisioning |
There's also FleetProvisioning (in FleetProvisioning.hpp), which handles the low-level MQTT pub/sub for the FP protocol. Let's see how they fit together.
The Fleet Provisioning Flow
Here's what happens when a hub provisions via FP — the more complex of the two paths:
sequenceDiagram
participant Hub as HubOnboarding
participant DP as DeviceProvisioner
participant FP as FleetProvisioning
participant MQTT as MQTT5 Client
participant AWS as AWS IoT Core
Hub->>DP: ProvisionFP(claimCert, params)
DP->>MQTT: Connect with claim certificate
MQTT->>AWS: TLS handshake (mTLS)
FP->>AWS: CreateKeysAndCertificate
AWS-->>FP: New cert + key + ownershipToken
FP->>AWS: RegisterThing(template, token)
AWS-->>FP: DeviceConfiguration (clientId, managedThingId)
ProcessDevice(): The Lifecycle Entry Point
ProcessDevice() is the single method that kicks off the entire onboarding flow. It branches based on the provisioning method:
// hub_onboarding.cpp — ProcessDevice()
void HubOnboarding::ProcessDevice() {
if (provisioning_method_ == JITR) {
// ... JITR path
} else {
// ... Fleet Provisioning path
}
provisioning_state_ = PROVISIONED;
UpdateHubState(PROVISIONED);
}
📁
provisioning/IoTSmartHomeDevice-HubOnboarding/src/hub_onboarding.cpp
Fleet Provisioning Path (Step by Step)
1. Read claim credentials — The hub loads its claim certificate and private key using the CertHandler from Chapter 1:
// hub_onboarding.cpp — FP branch
cert_handler->read_cert_and_private_key(
CERT_TYPE_T::CLAIM,
claim_cert_value, claim_pk_value);
2. Build provisioning parameters — Serial number and UPC identify this specific hub:
fleet_provisioning_params.emplace(
DEVICE_SERIAL_NUMBER_KEY, serial_num);
fleet_provisioning_params.emplace(
UNIVERSAL_PRODUCT_CODE_KEY, upc);
3. Call DeviceProvisioner — This connects to IoT Core with the claim cert and runs the FP protocol:
device_provisioner_->ProvisionFP(
client_id, claim_cert_value, claim_pk_value,
fp_template_name, fleet_provisioning_params,
device_configuration, permanent_cert, permanent_key);
📁
provisioning/IoTSmartHomeDevice-HubOnboarding/include/device_provisioner.hpp
4. Extract device configuration — IoT Core returns a clientId and managedThingId in the response:
final_client_id =
device_configuration.find(
DEVICE_CONFIGURATION_CLIENT_ID)->second;
managed_thing_id =
device_configuration.find(
DEVICE_CONFIGURATION_MANAGED_THING_ID)->second;
5. Store permanent credentials — The new certificate and key are written to disk via CertHandler:
cert_handler->write_permanent_cert_and_private_key(
permanent_cert, permanent_key,
permanent_cert_path, permanent_pk_path);
6. Update config — Certificate paths, client ID, managed thing ID, and provisioning state are all persisted:
UpdateConfigWithCertificatePaths();
UpdateConfigWithClientId(final_client_id);
UpdateConfigWithManagedThingId(managed_thing_id);
UpdateHubState(PROVISIONED);
JITR Path (Simpler)
JITR is more straightforward — the DHA certificate is already valid, so the hub just needs to connect once to trigger registration:
// hub_onboarding.cpp — JITR branch
device_provisioner_->ProvisionJITR(
client_id, dha_cert_value, dha_pk_value);
final_cert_path_ = dha_cert_path_;
final_pk_path_ = dha_pk_path_;
Under the hood, ProvisionWithJitr() simply connects via MQTT. The first connection attempt may fail (IoT Core needs time to register the cert), but the retry logic handles this:
// Provisioning.cpp — ProvisionWithJitr()
// First connection may fail; retries handle
// the registration delay automatically.
mqtt5ClientHandler->Connect(mqttRetryTimeoutSeconds);
📁
provisioning/IoTSmartHomeDevice-IoTProvisioningLib/src/Provisioning.cpp
Inside FleetProvisioning: The Two-Phase Protocol
The FleetProvisioning class implements the AWS IoT Identity protocol using MQTT pub/sub. It runs two phases:
Phase 1: CreateCertificateAndKey
The hub subscribes to accepted/rejected topics, then publishes a request:
// FleetProvisioning.cpp
identityClient->SubscribeToCreateKeysAndCertificateAccepted(
keySubRequest, QOS_AT_LEAST_ONCE,
onKeysAccepted, onKeysAcceptedSubAck);
identityClient->PublishCreateKeysAndCertificate(
createRequest, QOS_AT_LEAST_ONCE, onPublishAck);
On success, the callback captures the new certificate, private key, and an ownershipToken needed for Phase 2.
Phase 2: RegisterThing
Using the ownership token from Phase 1, the hub registers itself with a provisioning template:
// FleetProvisioning.cpp
RegisterThingRequest registerRequest;
registerRequest.TemplateName = fp_template_name;
registerRequest.CertificateOwnershipToken =
certificateOwnershipToken;
registerRequest.Parameters = fleet_provisioning_params;
identityClient->PublishRegisterThing(
registerRequest, QOS_AT_LEAST_ONCE, onPublishAck);
The response includes a DeviceConfiguration map containing the hub's clientId and managedThingId.
📁
provisioning/IoTSmartHomeDevice-IoTProvisioningLib/include/FleetProvisioning.hpp
The IsProvisioned() Guard
Before running ProcessDevice(), callers check whether onboarding already happened:
// hub_onboarding.cpp
bool HubOnboarding::IsProvisioned() {
if (provisioning_state_ == PROVISIONED) {
final_cert_path_ = config->GetStringValue(
PERMANENT_CERT_PATH_CONF);
return true;
}
return false;
}
This prevents re-provisioning on every reboot. The state is persisted in config, so it survives power cycles.
Error Handling
The provisioning library uses typed status codes to distinguish failure modes:
| Status Code | Meaning |
|---|---|
FP_STATUS_OK |
Provisioning succeeded |
FP_STATUS_MQTT_CONNECTION_ERROR |
Couldn't connect to IoT Core |
FP_STATUS_IDENTITY_ERROR |
Certificate or registration rejected |
FP_STATUS_SUBSCRIBE_TIMEOUT_ERROR |
MQTT subscription timed out |
FP_STATUS_PUBLISH_TIMEOUT_ERROR |
MQTT publish timed out |
DeviceProvisioner translates these into exceptions with descriptive messages:
// device_provisioner.cpp
if (fp_status == FP_STATUS_MQTT_CONNECTION_ERROR)
throw std::runtime_error(
"Failed due to MQTT Connection error");
📁
provisioning/IoTSmartHomeDevice-IoTProvisioningLib/include/Provisioning.hpp
Putting It All Together
Here's the complete onboarding lifecycle from power-on to "ready to connect":
HubOnboardingconstructor reads config → determines FP or JITR.IsProvisioned()checks if we already have permanent certs → skips if yes.ProcessDevice()runs the chosen provisioning path.- Permanent credentials are stored via
CertHandler(Chapter 1). - Config is updated with
clientId,managedThingId, and cert paths. - State transitions to
PROVISIONED. - The MQTT Proxy (Chapter 5) can now connect using the permanent credentials.
Key Takeaways
- Hub Onboarding is a one-time operation — once provisioned, the hub reuses its permanent credentials across reboots.
- Fleet Provisioning is a two-phase MQTT protocol: create a certificate, then register the thing.
- JITR is simpler — just connect with a pre-installed DHA cert and let IoT Core auto-register.
ProcessDevice()is the single entry point that handles both paths and updates all config state.CertHandlerfrom Chapter 1 handles all certificate I/O, whether reading claim certs or writing permanent ones.
What's Next?
The hub is now registered and has permanent credentials. But how does it discover and manage the sub-devices connected to it? In Chapter 7, we'll explore the Cloud Device Management Bridge (CDMB) — the component that synchronizes the hub's local device inventory with the cloud.