I tried connecting to AWS IoT Core from a local Linux container

I tried connecting to AWS IoT Core from a local Linux container

I wanted to try AWS IoT Core, so I attempted to connect to a Thing using a connection kit from a Linux container. I will introduce the pitfalls when setting up with a Fedora container and the flow up to successful execution.
2026.08.26

This page has been translated by machine translation. View original

Introduction

I wanted to explore AWS IoT Core, so I decided to start by testing whether I could connect from a local Linux container.

In this article, I'll create a Thing in AWS IoT Core, run the downloaded connection kit inside a Fedora container, and test MQTT5 Publish/Subscribe.

Test Environment

  • macOS Tahoe 26.6.2
  • Podman 5.7.0
  • Container: Fedora Linux 44 (fedora:latest)
  • AWS IoT Core (ap-northeast-1)

Creating an AWS IoT Thing

I used the Quick Connect feature in the AWS IoT Core console. The official steps are documented at Try the AWS IoT Core quick connect tutorial.

  1. Open Connect > Connect one device
  2. Select Create a new thing and enter lab_iot-machine_01 as the Thing name
  3. Select Linux/OSX for Platform and Node.js for SDK
  4. Download connect_device_package.zip as the connection kit

try-out-iot-core_1
try-out-iot-core_2
try-out-iot-core_3

The connection kit primarily contains the following files:

  • Device certificate
  • Public and private keys
  • A copy of the AWS IoT Policy
  • start.sh for running the sample

Preparing the Local Container

Extracting the Connection Kit

I placed connect_device_package.zip in /workspace, which is visible from the container, and extracted it there. I did not rename any files or move them to a different folder.

cd /workspace
unzip connect_device_package.zip

Installing Missing Packages on Fedora

Since fedora:latest did not come with the necessary commands, I installed the following packages:

  • unzip
  • curl
  • openssl
  • git
  • nodejs
  • iputils
dnf install -y unzip curl openssl
dnf install -y git
dnf install nodejs
dnf install -y iputils

I added iputils later in order to test ping to the AWS IoT endpoint.

try-out-iot-core_0-0

try-out-iot-core_0-1

What Was Needed Before Running start.sh

try-out-iot-core_4

The AWS IoT Core console instructs you to extract the connection kit and run start.sh. However, in this Fedora container, I first needed to install the required packages and build the SDK and sample.

Situation Cause Resolution
uzip: command not found Typo in the command Fixed the command
git: command not found Git was not present on Fedora dnf install -y git
npm: command not found Node.js and npm were not installed dnf install nodejs
Cannot find module .../dist/index.js A previously cloned SDK folder remained, causing start.sh to skip the installation step Manually built the SDK and sample
tsc: command not found The sample's prepare script called the SDK's TypeScript compiler before the SDK's dependencies were installed Installed including devDependencies from the SDK side first

start.sh is designed to run clone and npm install only when the SDK folder does not exist.

if [ ! -d ./aws-iot-device-sdk-js-v2 ]; then
  git clone https://github.com/aws/aws-iot-device-sdk-js-v2.git --recursive
  cd aws-iot-device-sdk-js-v2
  npm install
  cd samples/node/mqtt/mqtt5_x509
  npm install
fi

On the first run, it stopped immediately after cloning the SDK with npm: command not found. However, since the SDK folder remained, the condition was not entered on subsequent runs, and npm install was skipped even though installation and build were incomplete. As a result, it was trying to execute dist/index.js, which did not yet exist. I resolved this by installing dependencies and building in order starting from the SDK side.

cd /workspace/aws-iot-device-sdk-js-v2
rm -rf node_modules
npm install --include=dev
npm run build

cd samples/node/mqtt/mqtt5_x509
rm -rf node_modules dist
npm install --include=dev

cd /workspace
bash start.sh

Execution Results

Here is the actual execution log.

[root@5a13d8c9d00e workspace]# bash start.sh

Running pub/sub sample application...

Starting MQTT5 X509 PubSub Sample

==== Creating MQTT5 Client ====

==== Starting client ====
Lifecycle Connection Attempt
Connecting to endpoint: 'atczphvn9h1th-ats.iot.ap-northeast-1.amazonaws.com' with client ID 'sdk-nodejs-v2'
Lifecycle Connection Success with reason code: 0

==== Subscribing to topic 'sdk/test/js' ====
Suback received with reason code: 1

==== Sending 5 message(s) ====

Publishing message to topic 'sdk/test/js': Hello from mqtt5 sample [1]
PubAck received with 0

==== Received message from topic 'sdk/test/js': Hello from mqtt5 sample [1] ====

Publishing message to topic 'sdk/test/js': Hello from mqtt5 sample [2]
PubAck received with 0

==== Received message from topic 'sdk/test/js': Hello from mqtt5 sample [2] ====

Publishing message to topic 'sdk/test/js': Hello from mqtt5 sample [3]
PubAck received with 0

==== Received message from topic 'sdk/test/js': Hello from mqtt5 sample [3] ====

Publishing message to topic 'sdk/test/js': Hello from mqtt5 sample [4]
PubAck received with 0

==== Received message from topic 'sdk/test/js': Hello from mqtt5 sample [4] ====

Publishing message to topic 'sdk/test/js': Hello from mqtt5 sample [5]
PubAck received with 0

==== Received message from topic 'sdk/test/js': Hello from mqtt5 sample [5] ====

5 message(s) received.

==== Unsubscribing from topic 'sdk/test/js' ====
Unsubscribed with 0

==== Stopping Client ====
Lifecycle Disconnected with reason code: None
Lifecycle Stopped

==== Client Stopped! ====

try-out-iot-core_6

try-out-iot-core_7

Reviewing the Sample Contents After Testing

The connection test was successful up to this point, but I had not closely examined the certificate, Policy, or the MQTT5 sample internals during execution. After testing, I looked into what the connection kit and sample code were actually doing.

File Structure After Execution

Extracting only the relevant files, /workspace had the following structure:

try-out-iot-core_8

/workspace/
├── connect_device_package.zip
├── start.sh
├── lab_iot-machine_01.cert.pem
├── lab_iot-machine_01.public.key
├── lab_iot-machine_01.private.key
├── lab_iot-machine_01-Policy
├── root-CA.crt
└── aws-iot-device-sdk-js-v2/
    └── samples/node/mqtt/mqtt5_x509/
        ├── package.json
        ├── index.ts
        └── dist/index.js

What was present when the connection kit was extracted were the certificate, public key, private key, Policy copy, and start.sh. root-CA.crt and aws-iot-device-sdk-js-v2 were downloaded and cloned by running start.sh.

The role of each file is as follows:

  • lab_iot-machine_01.cert.pem: The X.509 device certificate specified by start.sh via --cert
  • lab_iot-machine_01.private.key: The private key specified by start.sh via --key
  • lab_iot-machine_01.public.key: The public key included in the connection kit. Not directly loaded by this sample
  • lab_iot-machine_01-Policy: The JSON of the IoT Policy included in the connection kit
  • index.ts: The TypeScript source of the MQTT5 X.509 sample
  • dist/index.js: The JavaScript built from index.ts, which is the file actually executed by start.sh

root-CA.crt is downloaded by start.sh, but its file path is not passed to the Node.js sample in this case, and it is not referenced from index.ts either.

What Did start.sh Pass to the Sample?

The start.sh in the connection kit ultimately executes the following command:

node aws-iot-device-sdk-js-v2/samples/node/mqtt/mqtt5_x509/dist/index.js \
  --endpoint atczphvn9h1th-ats.iot.ap-northeast-1.amazonaws.com \
  --key lab_iot-machine_01.private.key \
  --cert lab_iot-machine_01.cert.pem \
  --client_id sdk-nodejs-v2 \
  --topic sdk/test/js

What is passed here is the AWS IoT endpoint, private key, certificate, MQTT Client ID, and Topic. The Thing name lab_iot-machine_01 itself is not passed as a connection parameter; sdk-nodejs-v2 is used as the Client ID instead.

Since no message body or send count is specified, the sample's defaults of Hello from mqtt5 sample and 5 times are used.

What Was the MQTT5 Sample Doing?

The source code I reviewed can also be found in the official AWS GitHub repository at the mqtt5_x509 sample.

QoS (Quality of Service) is a level that represents the delivery quality in MQTT. With QoS 1 (At Least Once) used in this test, the sender retransmits until it receives a PUBACK (acknowledgment) from the recipient, ensuring at least one delivery. However, the same message may be delivered more than once.

The flow of processing is as follows:

Create MQTT5 client

Connect to AWS IoT Core

Subscribe to sdk/test/js with QoS 1

Publish 5 times to the same sdk/test/js with QoS 1

Receive 5 messages with the same client

Unsubscribe

Stop and Close the client

In the sample code, the MQTT5 client is created by passing the certificate and private key paths to newDirectMqttBuilderWithMtlsFromPath(). It then subscribes to sdk/test/js with QoS 1 and publishes 5 messages to the same topic.

In the execution log, one Received message for the same topic appears after each Publish. Since the same Client ID is used from connection through reception, sending to a different device or a different client was not tested.

The reason codes in the log can also be verified in the AWS IoT Core MQTT reason code reference. 0 for Connection and PubAck means Success, and 1 for Suback means Granted QoS 1.

What Was Written in the Policy File?

The lab_iot-machine_01-Policy is not included in the command executed by start.sh. The Node.js sample does not load this file either.

The local Policy file contained the following entries matching the Client ID and Topic used in this test:

  • iot:Connect: Resource set to client/sdk-nodejs-*
  • iot:Subscribe: Resource set to topicfilter/sdk/test/js
  • iot:Publish, iot:Receive: Resource set to topic/sdk/test/js

The Client ID specified by start.sh is sdk-nodejs-v2 and the Topic is sdk/test/js, both of which match the values in the Policy file. The Policy also includes iot:PublishRetain, but the sample code does not specify RETAIN.

On the other hand, the local Policy file alone does not reveal which certificate this Policy is attached to on the AWS side. This can be verified from the AWS IoT Core console.

  1. Open lab_iot-machine_01 from Manage > All devices > Things
  2. Open the certificate for this test from Certificates
  3. Confirm the association with the Thing under Things in the certificate details
  4. Confirm the attached Policy under Policies
  5. Open the Policy and verify that the Policy document matches the local lab_iot-machine_01-Policy

try-out-iot-core_9

The official documentation also includes steps to verify attached policies from Policies in the certificate details.

What Was Confirmed This Time

What could be directly confirmed from the execution log was that the connection succeeded using the certificate, private key, Client ID, and Topic specified by start.sh, and that the Subscribe to sdk/test/js, 5 Publishes, 5 Receives, and Unsubscribe all completed successfully.

The Client ID in the log is sdk-nodejs-v2, not the Thing name. The Topic sdk/test/js was also specified by start.sh, not a sample default.

Conclusion

I was able to connect to AWS IoT Core from a local Fedora container using an X.509 certificate and verify MQTT5 Publish/Subscribe.

The biggest challenge this time was not the AWS IoT Core configuration, but rather that the minimal Fedora image was missing the necessary tools, and when start.sh stopped partway through, it judged the setup as complete based solely on the presence of the SDK folder. It is easier to proceed if you prepare unzip, git, and Node.js before running the script, or if it fails midway, verify the dependencies and build starting from the SDK side.

Note that the connection kit contains a private key. Do not include it in Git or public articles, and after testing, revoke the certificate and delete the Thing, certificate, Policy, and their associations when they are no longer needed.

Share this article

AWSのお困り事はクラスメソッドへ