Cloud, DevOps e IoT en español

Parte 17 de 22 de la serie Domótica con ESP32 y AWS desde cero

Aprovisionar dispositivos en AWS IoT con Fleet Provisioning

10 de octubre de 2026 · Steven Carvajal · Seguridad

Código de este artículo en GitHub →

En la parte 3 creamos un certificado a mano y lo pegamos en el código del ESP32. Para un dispositivo está bien. Para diez, o para un producto, es inviable: un firmware distinto por placa, llaves privadas en archivos y en el historial de Git.

AWS IoT tiene una solución oficial: Fleet Provisioning. Todos los dispositivos salen con el mismo firmware, y cada uno obtiene su propio certificado la primera vez que se conecta. En esta parte vemos cómo funciona y cómo configurarlo.

Las opciones para dar identidad a un dispositivo

MétodoCómo funcionaCuándo usarlo
ManualCreas el certificado y lo cargas en cada placaPocos dispositivos, pruebas
Fleet Provisioning por claimEl firmware trae un certificado compartido muy limitado; con él, cada dispositivo pide uno propioFabricar muchos dispositivos iguales
Fleet Provisioning por usuario de confianzaUna app autenticada pide un certificado temporal y se lo pasa al dispositivo durante la instalaciónEl usuario instala el dispositivo con una app
JITP / JITR con tu propia CAFirmas los certificados en fábrica; AWS registra el dispositivo en su primera conexiónTienes tu propia autoridad de certificación

Fleet Provisioning por claim, paso a paso

  1. Creas un certificado de claim y una política que solo le permite pedir un certificado nuevo y registrarse.
  2. Ese certificado va en el firmware (o en NVS) de todos los dispositivos.
  3. Al arrancar sin identidad propia, el dispositivo se conecta con el certificado de claim y pide un certificado nuevo por MQTT.
  4. AWS le devuelve un certificado y su llave privada.
  5. El dispositivo llama a la plantilla de aprovisionamiento con ese certificado y sus datos (por ejemplo, su número de serie).
  6. AWS crea el Thing, activa el certificado y le adjunta la política definitiva.
  7. El dispositivo guarda su certificado en NVS (parte 6), se desconecta y vuelve a conectarse con su identidad propia.

La plantilla de aprovisionamiento

Describe qué crear para cada dispositivo. Un ejemplo:

{
  "Parameters": {
    "NumeroSerie": { "Type": "String" },
    "AWS::IoT::Certificate::Id": { "Type": "String" }
  },
  "Resources": {
    "certificate": {
      "Type": "AWS::IoT::Certificate",
      "Properties": {
        "CertificateId": { "Ref": "AWS::IoT::Certificate::Id" },
        "Status": "Active"
      }
    },
    "thing": {
      "Type": "AWS::IoT::Thing",
      "Properties": {
        "ThingName": { "Fn::Join": ["", ["casa-demo-", { "Ref": "NumeroSerie" }]] }
      }
    },
    "policy": {
      "Type": "AWS::IoT::Policy",
      "Properties": { "PolicyName": "casa-demo-dispositivo" }
    }
  }
}

La política casa-demo-dispositivo es la de la parte 13, con ${iot:Connection.Thing.ThingName}: una sola política sirve para todos los dispositivos que se registren.

Para crear la plantilla necesitas un rol de aprovisionamiento que AWS IoT asume para crear los recursos (la política administrada AWSIoTThingsRegistration cubre lo necesario):

aws iot create-provisioning-template \
  --template-name casa-demo \
  --provisioning-role-arn arn:aws:iam::CUENTA:role/aprovisionamiento-iot \
  --template-body file://plantilla.json \
  --enabled

La política del certificado de claim

Es la pieza más delicada: ese certificado está en todos los dispositivos. Solo debe permitir lo mínimo para aprovisionarse:

{
  "Version": "2012-10-17",
  "Statement": [
    { "Effect": "Allow", "Action": "iot:Connect", "Resource": "*" },
    {
      "Effect": "Allow",
      "Action": ["iot:Publish", "iot:Receive"],
      "Resource": [
        "arn:aws:iot:REGION:CUENTA:topic/$aws/certificates/create/*",
        "arn:aws:iot:REGION:CUENTA:topic/$aws/provisioning-templates/casa-demo/provision/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": "iot:Subscribe",
      "Resource": [
        "arn:aws:iot:REGION:CUENTA:topicfilter/$aws/certificates/create/*",
        "arn:aws:iot:REGION:CUENTA:topicfilter/$aws/provisioning-templates/casa-demo/provision/*"
      ]
    }
  ]
}

Con este certificado no se puede leer ni controlar ningún dispositivo: solo pedir un certificado y registrarse con esa plantilla.

Lo que hace el dispositivo

Los tópicos del proceso (todas las respuestas llegan en .../accepted o .../rejected):

PasoPublica enRecibe en
Pedir certificado y llave$aws/certificates/create/json$aws/certificates/create/json/accepted
Registrarse$aws/provisioning-templates/casa-demo/provision/json.../provision/json/accepted

Un esquema del código con la librería MQTT de la parte 3:

void onMessage(String& topic, String& payload) {
  JsonDocument doc;
  if (deserializeJson(doc, payload)) return;

  if (topic.endsWith("create/json/accepted")) {
    // Guardar en memoria (aún no en NVS) el certificado y la llave
    certNuevo  = doc["certificatePem"].as<String>();
    llaveNueva = doc["privateKey"].as<String>();
    String token = doc["certificateOwnershipToken"].as<String>();

    JsonDocument registro;
    registro["certificateOwnershipToken"] = token;
    registro["parameters"]["NumeroSerie"] = numeroSerie();
    String cuerpo;
    serializeJson(registro, cuerpo);
    client.publish("$aws/provisioning-templates/casa-demo/provision/json", cuerpo);

  } else if (topic.endsWith("provision/json/accepted")) {
    guardarIdentidad(doc["thingName"].as<String>(), certNuevo, llaveNueva);  // ahora sí, a NVS
    ESP.restart();  // vuelve a conectar con su identidad propia

  } else if (topic.endsWith("rejected")) {
    Serial.printf("Rechazado: %s\n", payload.c_str());
  }
}

void aprovisionar() {
  client.subscribe("$aws/certificates/create/json/accepted");
  client.subscribe("$aws/certificates/create/json/rejected");
  client.subscribe("$aws/provisioning-templates/casa-demo/provision/json/accepted");
  client.subscribe("$aws/provisioning-templates/casa-demo/provision/json/rejected");
  client.publish("$aws/certificates/create/json", "{}");
}

Detalles importantes:

Validar quién se registra: el pre-provisioning hook

Como el certificado de claim está en todos los dispositivos, alguien podría extraerlo y registrar dispositivos falsos. La plantilla admite un pre-provisioning hook: una Lambda que AWS llama antes de crear nada, con los parámetros que envió el dispositivo.

export const handler = async (event) => {
  const serie = event.parameters?.NumeroSerie;
  const valido = await numeroDeSerieFabricado(serie);  // tu lista de números de serie fabricados
  return { allowProvisioning: Boolean(valido) };
};

Si devuelve false, el registro se rechaza. Con una lista de números de serie fabricados (y que no se puedan usar dos veces), un certificado de claim robado no sirve para mucho.

Fleet Provisioning por usuario de confianza

Si el usuario instala el dispositivo con una app, esta puede llamar a CreateProvisioningClaim (con sus credenciales de AWS) y obtener un certificado de claim temporal, válido solo 5 minutos. La app se lo pasa al dispositivo (por ejemplo, por el portal de la parte 16 o por BLE), y el resto del proceso es el mismo. No hay ningún certificado compartido en el firmware.

Errores comunes

Preguntas frecuentes

¿Es seguro poner el certificado de claim en el firmware?

Es un riesgo aceptado por el diseño, que se reduce con una política mínima, el pre-provisioning hook y, si puedes, con el cifrado de flash. Si no puedes asumirlo, usa la variante por usuario de confianza.

¿Cuánto cuesta?

Fleet Provisioning no tiene costo propio: pagas los mensajes MQTT del proceso y, si la usas, la Lambda del hook. Unos pocos mensajes por dispositivo, una sola vez.

¿Puedo cambiar la plantilla después?

Sí, las plantillas tienen versiones. Los dispositivos ya registrados no cambian; los nuevos usan la versión por defecto.

Sigue leyendo