DEV Community

Cover image for Serverless self-service IoT certificate management - Part 1
Jimmy Dahlqvist for AWS Heroes

Posted on

Serverless self-service IoT certificate management - Part 1

I have been working with several different IoT solutions over the years. One thing they all have in common is the need for trusted certificates that can be used both to establish connection but also device identities. Devices and servers need to trust each other to establish secure communication, ensure data integrity, and prevent malicious attacks. A important part of this trust is the Public Key Infrastructure (PKI), where certificates and Certificate Authorities (CAs) play vital roles, there is also a need to manage a large amount of certificates in an easy way.

In some project we have been rolling our own internal PKI solution, this does come with complexity and security requirements. In other we have been using SaaS solutions, like DigiCert IoT Trust Manager, AWS IoT Core and Amazon Private CA.

However, when it came to development, and sometime even test environments, it was not uncommon that we used some sort of self signed certificates, with an easy self service portal to create different certificates.

In this post, which is the first part of two, we will introduce some basics around certificates and PKI. We will also start to create the foundations for Serverless API that can be used to create a self-service portal for generation of certificates.

WARNING

The solution I build in this series of posts is NOT suited for a production setup. This is purely meant for development environments and for learning purpose!


In a production environment we need:
  • Continuous monitoring and automated renewal of certificates.
  • Integration with hardware security modules (HSMs) for key storage.
  • Compliance with security standards.

For these needs, managed services like AWS Private CA, DigiCert IoT Trust Manager, and Let’s Encrypt are ideal.

In this first version of the API we do not using any form of Authorization! This will be added in the next part!

Why Build a Self-Service API?

As said, in an IoT system, thousands of devices need to interact with servers, IoT brokers, and each other. Each of these interactions must be secure, meaning:

  • Servers must authenticate devices to ensure they’re legitimate.
  • Devices must authenticate servers to ensure they’re connecting to a trusted source.

Certificates are therefor issued to servers and devices, creating a chain of trust anchored at a Root CA.

A self-service API for certificate management allows:

  • Automation: Devices and servers can request and renew certificates programmatically.
  • Scalability: As our IoT environment grows, the API can handle the increasing demand for certificates.
  • Learning and Testing: Before adopting a managed service, building your own certificate system helps you understand how PKI works.

Overview of Certificates, CAs, and Trust

What Is a Certificate Authority (CA)?

A CA is a trusted entity responsible for issuing digital certificates. These certificates bind a public key to an identity (e.g., a server, device, or user), enabling trust between entities.

CAs are structured in a hierarchy:

Root CA

  • The ultimate trust anchor.
  • Self-signed and highly secure.
  • Should never be used to issue end-entity certificates directly.

Intermediate CA

  • Issued and signed by the Root CA.
  • Delegates the responsibility of issuing certificates to end entities (e.g., devices and servers).
  • Limits the exposure of the Root CA.

End-Entity, leaf, Certificates

  • Certificates for servers, IoT devices, or users.
  • Issued by an Intermediate CA and used in client-server communication or mutual authentication.

Certificate Chains and Trust

A certificate chain is a sequence of certificates, where each certificate in the chain is signed by the subsequent certificate. It represents the hierarchical relationship between a certificate and its issuer.

Image showing a certificate chain

A chain can consist of one or several intermediate certificates. In the image above the chain is illustrated with two intermediate CAs.

During a TLS handshake

Grossly simplified the handshake would be

  • The server presents its certificate to the client.
  • The client validates the server certificate by tracing the chain back to a trusted Root CA in its certificate store. When using a self-signed Root CA you need to ensure the bundle is present in the trust store or included in the connection attempt.

In a scenario when mutual authentication is required, the server performs the same process for the client certificate.

Trust across several Intermediate CAs

In an IoT setup, it’s common to have one Intermediate CA issuing server certificates (e.g., for IoT brokers), and another Intermediate CA issuing client certificates (e.g., for devices).
For a client certificate (signed by one Intermediate CA) to trust a server certificate (signed by a different Intermediate CA), both must:

Be part of the same trust hierarchy.

  • Both Intermediate CAs must be signed by the same Root CA.
  • The Root CA is the common trust anchor.

Be validated against the chain.

  • The client verifies the server’s certificate chain, tracing it back to the Root CA.
  • The server verifies the client’s certificate chain similarly.

This setup ensures scalability and separation of responsibilities. The Server Intermediate CA focuses on servers and brokers. The Device Intermediate CA focuses on IoT devices.

Implementation

Now let's start to implement this self service API. We will build this completely serverless using Amazon API Gateway and Lambda functions, with storage of the certificates in S3 and Certificate Manager. This first part in the blog series will only create the first very basic part of the API, which we will extend in the second part. You can find all of the source code on Serverless-Handbook Self Service IoT Certificate management

Image showing a API Overview

We will setup an API Gateway with three endpoints for creating our Root CA, Intermediate CA, and Server cert. Everything will be stored in an S3 bucket, and the server certificate will also be imported to Certificate Manager.

REST API

First look at the REST API and the structure.

Endpoint Method Description
/certificates/root POST Create a new Root CA.
/certificates/intermediate POST Create a new Intermediate CA.
/certificates/server POST Create a new server certificate.

I decided to use separate paths (e.g., /certificates/root, /certificates/intermediate, /certificates/server) rather than a single endpoint with a type parameter (e.g., /certificates with type as input) as I feel this aligns better with REST principles and improves the API’s readability and usability.

It's easier to extend the API, with a new certificate type (e.g., device), as we can create a new path like /certificates/device without impacting existing paths. Separate paths naturally segment resources, reducing the need for filtering on the client side. I feel that retrieving all Root CAs is simpler with GET /certificates/root than GET /certificates?type=root.

Common infrastructure

First of all, let's deploy the common infrastructure, in this case it's just the S3 bucket, we will add more things in this template later.

AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Description: Certificate Self Service Common Infrastructure
Parameters:
  ApplicationName:
    Type: String
    Description: Name of owning application
    Default: image-moderation

Resources:
  StorageBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    Properties:
      BucketName: !Sub ${ApplicationName}-storage-bucket

Outputs:
  StorageBucketName:
    Description: The name of the certificate bucket
    Value: !Ref StorageBucket
    Export:
      Name: !Sub ${AWS::StackName}:certificate-bucket-name
Enter fullscreen mode Exit fullscreen mode

Next we can setup the endpoints using SAM and AWS::Serverless::Api and the three Lambda function backing the API.

AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Description: Create the API for self service certificate management
Parameters:
  ApplicationName:
    Type: String
    Description: Name of owning application
  CommonInfraStackName:
    Type: String
    Description: The name of the common stack that contains the EventBridge Bus and more

Globals:
  Function:
    Timeout: 30
    MemorySize: 2048
    Runtime: python3.12
    Environment:
        Variables:
          CERTIFICATE_BUCKET_NAME: 
            Fn::ImportValue: !Sub "${CommonInfraStackName}:certificate-bucket-name"

Resources:
  LambdaGenerateRootCA:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: Lambda/API/GenerateRootCA
      Handler: handler.handler
      Policies:
        - S3FullAccessPolicy:
            BucketName: 
              Fn::ImportValue: !Sub "${CommonInfraStackName}:certificate-bucket-name"
      Events:
        CreateRootCAApi:
          Type: Api
          Properties:
            Path: /certificates/root
            Method: post
            RestApiId: !Ref GenerateCertificatesApi

  LambdaGenerateIntermediateCA:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: Lambda/API/GenerateIntermediateCA
      Handler: handler.handler
      Policies:
        - S3FullAccessPolicy:
            BucketName: 
              Fn::ImportValue: !Sub "${CommonInfraStackName}:certificate-bucket-name"
      Events:
        CreateIntermediateCAApi:
          Type: Api
          Properties:
            Path: /certificates/intermediate
            Method: post
            RestApiId: !Ref GenerateCertificatesApi   

  LambdaGenerateDeviceCertificate:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: Lambda/API/GenerateDeviceCert
      Handler: handler.handler
      Policies:
        - S3FullAccessPolicy:
            BucketName: 
              Fn::ImportValue: !Sub "${CommonInfraStackName}:certificate-bucket-name"      

  LambdaGenerateServerCertificate:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: Lambda/API/GenerateServerCert
      Handler: handler.handler
      Policies:
        - S3FullAccessPolicy:
            BucketName: 
              Fn::ImportValue: !Sub "${CommonInfraStackName}:certificate-bucket-name"
        - Version: "2012-10-17"
          Statement:
            Action:
              - acm:*
            Effect: Allow
            Resource: "*"
      Events:
        CreateServerCAApi:
          Type: Api
          Properties:
            Path: /certificates/server
            Method: post
            RestApiId: !Ref GenerateCertificatesApi

  GenerateCertificatesApi:
    Type: AWS::Serverless::Api
    Properties:
      Description: API for creating and managing certificates
      Name: !Sub ${ApplicationName}-api
      StageName: prod
      OpenApiVersion: '3.0.1'
      AlwaysDeploy: true
      EndpointConfiguration: REGIONAL
Enter fullscreen mode Exit fullscreen mode

Our Lambda functions is in Python and we use the cryptography library to create the certificates, below is the implementation for creating a server certificate. For a full implementation visit Serverless-Handbook Self Service IoT Certificate management.

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,
):
    # Load Intermediate CA private key and certificate
    intermediate_private_key = serialization.load_pem_private_key(
        intermediate_private_key_pem, password=None
    )
    intermediate_cert = x509.load_pem_x509_certificate(intermediate_cert_pem)

    # Generate a private key for the server certificate
    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)  # Signed by Intermediate CA
        .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")

    # Fetch Intermediate CA private key and certificate from 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()

    # Fetch the certificate chain from S3
    cert_chain_pem = s3_client.get_object(
        Bucket=bucket_name, Key="intermediate_ca/certificate_chain.pem"
    )["Body"].read()

    # Create Server Certificate
    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"]}/"

    # Upload Server Certificate, Private Key to 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
    )

    # Import the certificate to 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": "Server certificate created, uploaded to S3, and imported to ACM.",
                "fqdn": body["fqdn"],
                "s3_folder": s3_folder,
                "acm_certificate_arn": response["CertificateArn"],
            }
        ),
    }
Enter fullscreen mode Exit fullscreen mode

Conclusion

This blog covered everything from certificate chains to validating trust in IoT systems. Building your own self-service API is a good way learn about certificates and PKI. In production, however, always opt for managed solutions that offer automation, compliance, and scalability.

Certificates Are the Foundation of IoT Security

Certificates and CAs ensure secure, authenticated communication in IoT ecosystems.

The Role of Intermediate CAs

Delegating certificate issuance to Intermediate CAs improves scalability and limits exposure.

Trust Is Built on Hierarchies

Trust between devices and servers relies on shared Root CAs and validated certificate chains.

Build for Learning, Use Managed Services for Production

While this API is a great learning tool, services like AWS Private CA or Let’s Encrypt are better suited for production.

To get the full source code and deploy it your self, visit Serverless-Handbook Self Service IoT Certificate management

Final Words

Don't forget to follow me on LinkedIn and X for more content, and read rest of my Blogs

As Werner says! Now Go Build!

Top comments (0)