aws, iot, security, serverless

Gestión de certificados IoT autónomo sin servidor - Parte 1.

2024-11-27
This post cover image
aws cloud serverless security iot

Este archivo ha sido traducido automaticamente por IA, pueden ocurrir errores

He estado trabajando con varias soluciones IoT diferentes a lo largo de los años. Una cosa que todas tienen en común es la necesidad de certificados confiables que se puedan usar tanto para establecer conexiones como para identificar dispositivos. Los dispositivos y los servidores deben confiar el uno en el otro para establecer una comunicación segura, garantizar la integridad de los datos y prevenir ataques maliciosos. Una parte importante de esta confianza es la Infraestructura de Clave Pública (PKI), donde los certificados y las Autoridades de Certificación (CA) juegan roles vitales, también existe la necesidad de administrar una gran cantidad de certificados de manera sencilla.

En algunos proyectos, hemos estado implementando nuestra propia solución interna de PKI, lo cual conlleva complejidad y requisitos de seguridad. En otros, hemos estado utilizando soluciones SaaS, como DigiCert IoT Trust Manager, AWS IoT Core y Amazon Private CA.

Sin embargo, cuando se trataba de desarrollo, y a veces incluso de entornos de prueba, no era raro que usáramos algún tipo de certificados autofirmados, con un portal de autoservicio sencillo para crear diferentes certificados.

En esta publicación, que es la primera parte de dos, introduciremos algunos conceptos básicos sobre certificados y PKI. También comenzaremos a crear los fundamentos de una API sin servidor que se pueda usar para crear un portal de autoservicio para la generación de certificados.

¿Por qué construir una API de autoservicio?

Como se dijo, en un sistema IoT, miles de dispositivos deben interactuar con servidores, corredores IoT y entre sí. Cada una de estas interacciones debe ser segura, lo que significa:

  • Los servidores deben autenticar los dispositivos para garantizar que sean legítimos.
  • Los dispositivos deben autenticar los servidores para garantizar que se están conectando a una fuente confiable.

Por lo tanto, se emiten certificados a los servidores y dispositivos, creando una cadena de confianza anclada en una CA raíz.

Una API de autoservicio para la gestión de certificados permite:

  • Automatización: Los dispositivos y servidores pueden solicitar y renovar certificados de manera programática.
  • Escalabilidad: A medida que nuestro entorno IoT crece, la API puede manejar la creciente demanda de certificados.
  • Aprendizaje y pruebas: Antes de adoptar un servicio administrado, construir su propio sistema de certificados le ayuda a comprender cómo funciona PKI.

Visión general de los certificados, las CA y la confianza

¿Qué es una Autoridad de Certificación (CA)?

Una CA es una entidad confiable responsable de emitir certificados digitales. Estos certificados vinculan una clave pública a una identidad (por ejemplo, un servidor, dispositivo o usuario), lo que permite la confianza entre entidades.

Las CA están estructuradas en una jerarquía:

CA raíz

  • El anclaje de confianza definitivo.
  • Autofirmado y altamente seguro.
  • Nunca debe usarse para emitir certificados de entidad final directamente.

CA intermedia

  • Emitido y firmado por la CA raíz.
  • Delega la responsabilidad de emitir certificados a entidades finales (por ejemplo, dispositivos y servidores).
  • Limita la exposición de la CA raíz.

Certificados de entidad final, hoja

  • Certificados para servidores, dispositivos IoT o usuarios.
  • Emitidos por una CA intermedia y utilizados en la comunicación cliente-servidor o autenticación mutua.

Cadenas de certificados y confianza

Una cadena de certificados es una secuencia de certificados, donde cada certificado en la cadena está firmado por el siguiente certificado. Representa la relación jerárquica entre un certificado y su emisor.

Imagen que muestra una cadena de certificados

Una cadena puede consistir en uno o varios certificados intermedios. En la imagen de arriba, la cadena se ilustra con dos CA intermedias.

Durante un intercambio TLS

Simplificando mucho, el intercambio sería

  • El servidor presenta su certificado al cliente.
  • El cliente valida el certificado del servidor rastreando la cadena hasta una CA raíz confiable en su almacén de certificados. Cuando se usa una CA raíz autofirmada, debe asegurarse de que el paquete esté presente en el almacén de confianza o incluido en la solicitud de conexión.

En un escenario en el que se requiere autenticación mutua, el servidor realiza el mismo proceso para el certificado del cliente.

Confianza a través de varias CA intermedias

En una configuración IoT, es común tener una CA intermedia que emite certificados de servidor (por ejemplo, para corredores IoT) y otra CA intermedia que emite certificados de cliente (por ejemplo, para dispositivos). Para que un certificado de cliente (firmado por una CA intermedia) confíe en un certificado de servidor (firmado por una CA intermedia diferente), ambos deben:

Ser parte de la misma jerarquía de confianza.

  • Ambas CA intermedias deben estar firmadas por la misma CA raíz.
  • La CA raíz es el anclaje de confianza común.

Ser validados contra la cadena.

  • El cliente verifica la cadena de certificados del servidor, rastreándola hasta la CA raíz.
  • El servidor verifica la cadena de certificados del cliente de manera similar.

Esta configuración garantiza escalabilidad y separación de responsabilidades. La CA intermedia del servidor se enfoca en servidores y corredores. La CA intermedia del dispositivo se enfoca en dispositivos IoT.

Implementación

Ahora comencemos a implementar esta API de autoservicio. La construiremos completamente sin servidor usando Amazon API Gateway y funciones Lambda, con el almacenamiento de los certificados en S3 y Certificate Manager. Esta primera parte de la serie de publicaciones solo creará la primera parte muy básica de la API, que extendemos en la segunda parte. Puede encontrar todo el código fuente en Gestión de certificados IoT autónomo sin servidor

Imagen que muestra una visión general de la API

Configuraremos un API Gateway con tres puntos finales para crear nuestra CA raíz, CA intermedia y certificado de servidor. Todo se almacenará en un bucket S3, y el certificado del servidor también se importará a Certificate Manager.

API REST

Primero, veamos la API REST y la estructura.

Punto finalMétodoDescripción
/certificates/rootPOSTCrear una nueva CA raíz.
/certificates/intermediatePOSTCrear una nueva CA intermedia.
/certificates/serverPOSTCrear un nuevo certificado de servidor.

Decidí usar rutas separadas (por ejemplo, /certificates/root, /certificates/intermediate, /certificates/server) en lugar de un solo punto final con un parámetro de tipo (por ejemplo, /certificates con tipo como entrada) ya que siento que esto se alinea mejor con los principios REST y mejora la legibilidad y usabilidad de la API.

Es más fácil extender la API, con un nuevo tipo de certificado (por ejemplo, dispositivo), ya que podemos crear una nueva ruta como /certificates/device sin afectar las rutas existentes. Las rutas separadas segmentan naturalmente los recursos, reduciendo la necesidad de filtrado en el lado del cliente. Siento que recuperar todas las CA raíz es más simple con GET /certificates/root que GET /certificates?type=root.

Infraestructura común

Primero de todo, despleguemos la infraestructura común, en este caso solo es el bucket S3, más adelante agregaremos más cosas a esta plantilla.

AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Description: Infraestructura común de autoservicio de certificados
Parámetros:
  ApplicationName:
    Tipo: Cadena
    Descripción: Nombre de la aplicación propietaria
    Predeterminado: moderación-de-imágenes

Recursos:
  StorageBucket:
    Tipo: AWS::S3::Bucket
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    Propiedades:
      BucketName: !Sub ${ApplicationName}-storage-bucket

Salidas:
  StorageBucketName:
    Descripción: El nombre del bucket de certificados
    Valor: !Ref StorageBucket
    Exportar:
      Nombre: !Sub ${AWS::StackName}:certificate-bucket-name

A continuación, podemos configurar los puntos finales usando SAM y AWS::Serverless::Api y las tres funciones Lambda que respaldan la API.

AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Descripción: Crear la API para la gestión de certificados de autoservicio
Parámetros:
  ApplicationName:
    Tipo: Cadena
    Descripción: Nombre de la aplicación propietaria
  CommonInfraStackName:
    Tipo: Cadena
    Descripción: El nombre de la pila común que contiene el EventBridge Bus y más

Globals:
  Función:
    Tiempo de espera: 30
    Tamaño de memoria: 2048
    Tiempo de ejecución: python3.12
    Entorno:
        Variables:
          CERTIFICATE_BUCKET_NAME: 
            Fn::ImportValue: !Sub "${CommonInfraStackName}:certificate-bucket-name"

Recursos:
  LambdaGenerateRootCA:
    Tipo: AWS::Serverless::Function
    Propiedades:
      CodeUri: Lambda/API/GenerateRootCA
      Handler: handler.handler
      Políticas:
        - S3FullAccessPolicy:
            BucketName: 
              Fn::ImportValue: !Sub "${CommonInfraStackName}:certificate-bucket-name"
      Eventos:
        CreateRootCAApi:
          Tipo: Api
          Propiedades:
            Ruta: /certificados/root
            Método: post
            RestApiId: !Ref GenerateCertificatesApi

  LambdaGenerateIntermediateCA:
    Tipo: AWS::Serverless::Function
    Propiedades:
      CodeUri: Lambda/API/GenerateIntermediateCA
      Handler: handler.handler
      Políticas:
        - S3FullAccessPolicy:
            BucketName: 
              Fn::ImportValue: !Sub "${CommonInfraStackName}:certificate-bucket-name"
      Eventos:
        CreateIntermediateCAApi:
          Tipo: Api
          Propiedades:
            Ruta: /certificados/intermedio
            Método: post
            RestApiId: !Ref GenerateCertificatesApi   

  LambdaGenerateDeviceCertificate:
    Tipo: AWS::Serverless::Function
    Propiedades:
      CodeUri: Lambda/API/GenerateDeviceCert
      Handler: handler.handler
      Políticas:
        - S3FullAccessPolicy:
            BucketName: 
              Fn::ImportValue: !Sub "${CommonInfraStackName}:certificate-bucket-name"      

  LambdaGenerateServerCertificate:
    Tipo: AWS::Serverless::Function
    Propiedades:
      CodeUri: Lambda/API/GenerateServerCert
      Handler: handler.handler
      Políticas:
        - S3FullAccessPolicy:
            BucketName: 
              Fn::ImportValue: !Sub "${CommonInfraStackName}:certificate-bucket-name"
        - Versión: "2012-10-17"
          Declaración:
            Acción:
              - acm:*
            Efecto: Permitir
            Recurso: "*"
      Eventos:
        CreateServerCAApi:
          Tipo: Api
          Propiedades:
            Ruta: /certificados/servidor
            Método: post
            RestApiId: !Ref GenerateCertificatesApi

  GenerateCertificatesApi:
    Tipo: AWS::Serverless::Api
    Propiedades:
      Descripción: API para crear y administrar certificados
      Nombre: !Sub ${ApplicationName}-api
      StageName: prod
      OpenApiVersion: '3.0.1'
      AlwaysDeploy: verdadero
      EndpointConfiguration: REGIONAL

Nuestras funciones Lambda están en Python y usamos la biblioteca cryptography para crear los certificados, a continuación está la implementación para crear un certificado de servidor. Para una implementación completa, visite Gestión de certificados IoT autónomo sin servidor.

import boto3
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization, hashes
import datetime
import os
import json

s3_client = boto3.client("s3")
acm_client = boto3.client("acm")


def create_server_certificate(
    intermediate_private_key_pem, 
    intermediate_cert_pem, 
    fqdn,
    country,
    state,
    organization,
    validity_days,
):
    # Cargar la clave privada y el certificado de la CA intermedia
    intermediate_private_key = serialization.load_pem_private_key(
        intermediate_private_key_pem, password=None
    )
    intermediate_cert = x509.load_pem_x509_certificate(intermediate_cert_pem)

    # Generar una clave privada para el certificado del servidor
    server_private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
    server_private_key_pem = server_private_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption(),
    )

    subject = x509.Name(
        [
            x509.NameAttribute(NameOID.COUNTRY_NAME, country),
            x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, state),
            x509.NameAttribute(NameOID.ORGANIZATION_NAME, organization),
            x509.NameAttribute(NameOID.COMMON_NAME, fqdn),
        ]
    )
    server_certificate = (
        x509.CertificateBuilder()
        .subject_name(subject)
        .issuer_name(intermediate_cert.subject)  # Firmado por CA intermedia
        .public_key(server_private_key.public_key())
        .serial_number(x509.random_serial_number())
        .not_valid_before(datetime.datetime.now(datetime.timezone.utc))
        .not_valid_after(
            datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=validity_days)
        )
        .add_extension(
            x509.SubjectAlternativeName([x509.DNSName(fqdn)]),
            critical=False,
        )
        .add_extension(
            x509.BasicConstraints(ca=False, path_length=None),
            critical=True,
        )
        .add_extension(
            x509.KeyUsage(
                digital_signature=True,
                key_encipherment=True,
                key_cert_sign=False,
                crl_sign=False,
                content_commitment=False,
                data_encipherment=False,
                encipher_only=False,
                decipher_only=False,
                key_agreement=False,
            ),
            critical=True,
        )
        .sign(intermediate_private_key, hashes.SHA256())
    )
    server_cert_pem = server_certificate.public_bytes(serialization.Encoding.PEM)

    return server_private_key_pem, server_cert_pem


def handler(event, context):
    
    body = json.loads(event["body"])

    bucket_name = os.environ.get("CERTIFICATE_BUCKET_NAME")

    # Obtener la clave privada y el certificado de la CA intermedia de S3
    intermediate_private_key = s3_client.get_object(
        Bucket=bucket_name, Key="intermediate_ca/private_key.pem"
    )["Body"].read()

    intermediate_cert = s3_client.get_object(
        Bucket=bucket_name, Key="intermediate_ca/certificate.pem"
    )["Body"].read()

    # Obtener la cadena de certificados de S3
    cert_chain_pem = s3_client.get_object(
        Bucket=bucket_name, Key="intermediate_ca/certificate_chain.pem"
    )["Body"].read()

    # Crear certificado de servidor
    server_private_key_pem, server_cert_pem = create_server_certificate(
        intermediate_private_key,
        intermediate_cert,
        body["fqdn"],
        body["country"],
        body["state"],
        body["organization"],
        body["validity_days"],
    )

    s3_folder = f"server_certificates/{body["fqdn"]}/"

    # Cargar el certificado del servidor y la clave privada en S3
    s3_client.put_object(
        Bucket=bucket_name,
        Key=f"{s3_folder}private_key.pem",
        Body=server_private_key_pem,
    )
    s3_client.put_object(
        Bucket=bucket_name, Key=f"{s3_folder}certificate.pem", Body=server_cert_pem
    )

    # Importar el certificado a ACM
    response = acm_client.import_certificate(
        Certificate=server_cert_pem,
        PrivateKey=server_private_key_pem,
        CertificateChain=cert_chain_pem,
    )

    return {
        "statusCode": 200,
        "body": json.dumps(
            {
                "message": "Certificado de servidor creado, cargado en S3 e importado a ACM.",
                "fqdn": body["fqdn"],
                "s3_folder": s3_folder,
                "acm_certificate_arn": response["CertificateArn"],
            }
        ),
    }

Conclusión

Esta publicación cubrió todo, desde cadenas de certificados hasta validar la confianza en los sistemas IoT. Construir su propia API de autoservicio es una buena forma de aprender sobre certificados y PKI. Sin embargo, en producción, siempre opte por soluciones administradas que ofrezcan automatización, cumplimiento y escalabilidad.

Los certificados son la base de la seguridad IoT
Los certificados y las CA garantizan una comunicación segura y autenticada en los ecosistemas IoT.

El papel de las CA intermedias
Delegar la emisión de certificados a las CA intermedias mejora la escalabilidad y limita la exposición.

La confianza se construye sobre jerarquías
La confianza entre dispositivos y servidores se basa en CA raíz compartidas y cadenas de certificados validadas.

Construya para aprender, use servicios administrados para producción
Si bien esta API es una excelente herramienta de aprendizaje, servicios como AWS Private CA o Let’s Encrypt son más adecuados para producción.

Para obtener el código fuente completo y desplegarlo usted mismo, visite Gestión de certificados IoT autónomo sin servidor

Palabras finales

No olvide seguirme en LinkedIn y X para más contenido, y lea el resto de mis Publicaciones

Como dice Werner ¡Ahora vaya a construir!