
Network communication is the attack surface exploited in the majority of large-scale IoT compromises. Mirai, the botnet that took down a large portion of internet infrastructure in 2016, spread entirely through network-accessible devices with weak or no authentication on their management interfaces. The devices themselves were not exotic: IP cameras, DVRs and home routers, all with unencrypted protocols or default credentials exposed to the open internet. This article covers the full stack of embedded communication security: selecting and configuring TLS and DTLS (Datagram TLS) for constrained hardware, choosing cipher suites that balance security and performance, managing cryptographic keys across their full lifecycle, building network access control with a Zero Trust model, and hardening every remote access path from SSH to cloud connectivity. All examples use mbedTLS, the most widely deployed TLS library in embedded systems.
Each communication security control exists to defeat one or more specific attacks. Understanding the attacks first makes the control choices easier to justify and implement correctly.
An attacker on the same network segment captures and reads unencrypted traffic. On a shared WiFi network this requires only a wireless adapter in monitor mode and a packet capture tool like Wireshark. On a wired Ethernet segment it requires a managed switch with port mirroring, or physical access to a cable. Any firmware that sends credentials, sensor data, commands or configuration in plaintext is vulnerable to eavesdropping by anyone who can reach the network path between the device and its server. Encryption defeats eavesdropping.
An attacker positions themselves between the device and the server, relaying traffic while reading and optionally modifying it. The device and server each believe they are communicating directly with each other. Encryption alone does not prevent MITM: an attacker can establish a separate encrypted session with each party. Authentication combined with encryption defeats MITM: if the device verifies the server’s certificate before sending any data, a MITM attacker cannot impersonate the legitimate server without possessing its private key.
An attacker records a valid authenticated message and retransmits it later to trigger the same action again. A device that unlocks a door on receiving a signed “unlock” command is vulnerable: capture the command, replay it whenever desired. Replay attacks are defeated by including a nonce (number used once) or a monotonically increasing sequence number in each signed message, so the receiver rejects any message it has seen before or whose sequence number is out of order.
An attacker impersonates a legitimate device or server. A rogue device that claims to be a temperature sensor can inject false readings. A rogue server that claims to be the legitimate update server can deliver malicious firmware. Mutual authentication defeats spoofing: both parties prove their identity before any data exchange.
An attacker overwhelms the device with network traffic, exhausting its CPU, memory or network buffers and preventing it from performing its actual function. Embedded devices are particularly vulnerable because their resources are fixed and small. Rate limiting, connection limits and network-layer filtering are the primary defences. TLS session resumption reduces the CPU cost of legitimate reconnections and leaves more capacity for filtering attacks.
The right protocol for a given embedded communication path depends on four constraints: available memory and CPU, network type, data rate requirements and latency tolerance. This table maps the most common embedded communication scenarios to their appropriate secure protocol:
| Scenario | Transport | Recommended Protocol | Typical RAM Overhead |
|---|---|---|---|
| WiFi/Ethernet IoT cloud connection | TCP | MQTT over TLS 1.3 | 50–80 KB |
| Constrained device (Cortex-M0, <32KB RAM) | UDP | CoAP (Constrained Application Protocol) over DTLS 1.2 | 15–25 KB |
| HTTPS REST API calls | TCP | HTTP/1.1 or HTTP/2 over TLS 1.3 | 50–80 KB |
| Device-to-device local network | TCP or UDP | TLS/DTLS with PSK (Pre-Shared Key) cipher suite | 20–40 KB |
| Embedded Linux management | TCP | SSH (OpenSSH, Dropbear) | 1–4 MB |
| VPN-connected field device | Network layer | WireGuard or OpenVPN over TLS | 4–16 MB (Linux only) |
| BLE peripheral | Bluetooth | BLE with LE Secure Connections pairing (LESC) | 10–20 KB |
Two protocols to avoid: plain MQTT on port 1883 (no encryption, widely scanned by internet-facing attackers) and any use of HTTP on port 80 for device management or firmware delivery. Both transmit all data including credentials in plaintext.
TLS (Transport Layer Security) 1.3 is the current recommended version. It removes the broken cipher suites present in earlier versions, reduces the handshake to one round trip (versus two for TLS 1.2), and eliminates several negotiation steps that were sources of downgrade attacks. TLS 1.2 is acceptable where TLS 1.3 is not yet supported by the remote server, but TLS 1.0 and 1.1 are deprecated and must not be used.
The resource cost of TLS on embedded hardware comes from three sources: the handshake (asymmetric cryptography for key exchange and authentication), the per-message overhead (AEAD (Authenticated Encryption with Associated Data) cipher for record encryption), and the memory required for the library, buffers and certificate storage. The handshake is by far the most expensive operation. An ECDHE (Elliptic Curve Diffie-Hellman Ephemeral) key exchange on a 120 MHz Cortex-M4 without hardware crypto acceleration takes approximately 150 to 300 milliseconds. With a hardware accelerator, this drops to under 20 milliseconds.
For devices that reconnect frequently, TLS session resumption eliminates the full handshake for reconnections within the session lifetime. The server issues a session ticket after the initial handshake. On reconnect, the client presents the ticket and the server restores the session without repeating key exchange. On a constrained device this can reduce reconnection time from 300 ms to under 10 ms and reduce peak current draw significantly in battery-powered applications.
mbedTLS (now Mbed TLS) is the TLS library used by default in ESP-IDF, Zephyr RTOS, ARM Mbed OS and many custom embedded Linux builds. Its modular configuration system allows you to disable unused features and reduce the compiled footprint to fit your target’s memory constraints.
/* Minimal mbedTLS TLS 1.3 client configuration for a Cortex-M4 WiFi device.
This connects to an MQTT broker over TLS, verifying the server certificate
against the root CA stored in the firmware. The device certificate and
private key are loaded from the secure element at connection time. */
#include "mbedtls/net_sockets.h"
#include "mbedtls/ssl.h"
#include "mbedtls/entropy.h"
#include "mbedtls/ctr_drbg.h"
#include "mbedtls/x509_crt.h"
#include "mbedtls/error.h"
/* Root CA certificate for the MQTT broker (PEM format).
Embedded in firmware as a const array; verified at compile time.
Keep in read-only flash: it does not need to be in RAM. */
extern const uint8_t mqtt_broker_root_ca_pem[];
extern const uint32_t mqtt_broker_root_ca_pem_len;
/* Device certificate provisioned at factory, stored in encrypted flash */
extern const uint8_t device_cert_pem[];
extern const uint32_t device_cert_pem_len;
typedef struct {
mbedtls_net_context net_ctx;
mbedtls_ssl_context ssl_ctx;
mbedtls_ssl_config ssl_conf;
mbedtls_x509_crt ca_cert;
mbedtls_x509_crt device_cert;
mbedtls_pk_context device_key; /* Loaded from secure element */
mbedtls_entropy_context entropy;
mbedtls_ctr_drbg_context drbg;
} TlsContext;
typedef enum {
TLS_OK,
TLS_ERR_ENTROPY,
TLS_ERR_CERT_LOAD,
TLS_ERR_CONFIG,
TLS_ERR_CONNECT,
TLS_ERR_HANDSHAKE,
TLS_ERR_VERIFY
} TlsResult;
TlsResult tls_client_connect(TlsContext *ctx,
const char *hostname,
const char *port) {
int ret;
const char *pers = "mqtt_tls_client";
/* Step 1: Initialise all contexts */
mbedtls_net_init(&ctx->net_ctx);
mbedtls_ssl_init(&ctx->ssl_ctx);
mbedtls_ssl_config_init(&ctx->ssl_conf);
mbedtls_x509_crt_init(&ctx->ca_cert);
mbedtls_x509_crt_init(&ctx->device_cert);
mbedtls_pk_init(&ctx->device_key);
mbedtls_entropy_init(&ctx->entropy);
mbedtls_ctr_drbg_init(&ctx->drbg);
/* Step 2: Seed DRBG from hardware entropy source */
ret = mbedtls_ctr_drbg_seed(&ctx->drbg,
mbedtls_entropy_func,
&ctx->entropy,
(const unsigned char *)pers,
strlen(pers));
if (ret != 0) return TLS_ERR_ENTROPY;
/* Step 3: Load the root CA certificate for server verification */
ret = mbedtls_x509_crt_parse(&ctx->ca_cert,
mqtt_broker_root_ca_pem,
mqtt_broker_root_ca_pem_len);
if (ret != 0) return TLS_ERR_CERT_LOAD;
/* Step 4: Load device certificate and key from secure storage */
ret = mbedtls_x509_crt_parse(&ctx->device_cert,
device_cert_pem,
device_cert_pem_len);
if (ret != 0) return TLS_ERR_CERT_LOAD;
/* Device private key is loaded from the secure element via a custom
mbedTLS pk_context that delegates signing operations to the ATECC608B.
The private key never exists in host processor memory. */
ret = load_key_from_secure_element(&ctx->device_key);
if (ret != 0) return TLS_ERR_CERT_LOAD;
/* Step 5: Configure TLS */
ret = mbedtls_ssl_config_defaults(&ctx->ssl_conf,
MBEDTLS_SSL_IS_CLIENT,
MBEDTLS_SSL_TRANSPORT_STREAM,
MBEDTLS_SSL_PRESET_DEFAULT);
if (ret != 0) return TLS_ERR_CONFIG;
/* Enforce TLS 1.2 minimum; TLS 1.3 if supported by this build */
mbedtls_ssl_conf_min_tls_version(&ctx->ssl_conf,
MBEDTLS_SSL_VERSION_TLS1_2);
/* Require server certificate verification: NEVER use OPTIONAL or NONE */
mbedtls_ssl_conf_authmode(&ctx->ssl_conf, MBEDTLS_SSL_VERIFY_REQUIRED);
mbedtls_ssl_conf_ca_chain(&ctx->ssl_conf, &ctx->ca_cert, NULL);
/* Set mutual authentication: device presents its certificate */
mbedtls_ssl_conf_own_cert(&ctx->ssl_conf,
&ctx->device_cert,
&ctx->device_key);
mbedtls_ssl_conf_rng(&ctx->ssl_conf,
mbedtls_ctr_drbg_random,
&ctx->drbg);
ret = mbedtls_ssl_setup(&ctx->ssl_ctx, &ctx->ssl_conf);
if (ret != 0) return TLS_ERR_CONFIG;
/* Set hostname for SNI (Server Name Indication) and certificate CN check */
ret = mbedtls_ssl_set_hostname(&ctx->ssl_ctx, hostname);
if (ret != 0) return TLS_ERR_CONFIG;
/* Step 6: TCP connect */
ret = mbedtls_net_connect(&ctx->net_ctx, hostname, port,
MBEDTLS_NET_PROTO_TCP);
if (ret != 0) return TLS_ERR_CONNECT;
mbedtls_ssl_set_bio(&ctx->ssl_ctx,
&ctx->net_ctx,
mbedtls_net_send,
mbedtls_net_recv,
NULL);
/* Step 7: TLS handshake */
do {
ret = mbedtls_ssl_handshake(&ctx->ssl_ctx);
} while (ret == MBEDTLS_ERR_SSL_WANT_READ ||
ret == MBEDTLS_ERR_SSL_WANT_WRITE);
if (ret != 0) return TLS_ERR_HANDSHAKE;
/* Step 8: Verify the server certificate (belt and suspenders check) */
uint32_t verify_flags = mbedtls_ssl_get_verify_result(&ctx->ssl_ctx);
if (verify_flags != 0) {
char verify_buf[512];
mbedtls_x509_crt_verify_info(verify_buf, sizeof(verify_buf),
" ! ", verify_flags);
log_error("TLS server cert verification failed: %s", verify_buf);
return TLS_ERR_VERIFY;
}
return TLS_OK;
}
The most critical line in this configuration is MBEDTLS_SSL_VERIFY_REQUIRED. Using MBEDTLS_SSL_VERIFY_OPTIONAL or MBEDTLS_SSL_VERIFY_NONE disables server certificate verification entirely, turning the TLS connection into an encrypted but unauthenticated channel that is fully vulnerable to MITM attacks. This mistake appears in a significant fraction of IoT firmware because it is the default in many tutorial code samples.
DTLS (Datagram TLS) provides the same security guarantees as TLS but over UDP, making it suitable for CoAP, SNMP v3 and other datagram-oriented embedded protocols. The key implementation difference from TLS is that DTLS must handle packet loss, reordering and duplication because UDP provides no delivery guarantees. mbedTLS supports DTLS through the same API with MBEDTLS_SSL_TRANSPORT_DATAGRAM as the transport parameter.
CoAP with DTLS is the standard for constrained devices (Class 1: <10 KB RAM, <100 KB flash) that cannot support the memory overhead of a full TLS stack. The DTLS overhead per packet is roughly 29 bytes for the record header, plus the AEAD authentication tag (16 bytes for AES-128-CCM). This is acceptable for CoAP’s small message format where payload sizes are typically under 1 KB.
/* Key differences in mbedTLS DTLS configuration vs TLS.
The handshake and certificate configuration is identical to the TLS example.
Only the transport parameter and timer callbacks differ. */
/* Use DATAGRAM transport instead of STREAM */
mbedtls_ssl_config_defaults(&ssl_conf,
MBEDTLS_SSL_IS_CLIENT,
MBEDTLS_SSL_TRANSPORT_DATAGRAM, /* DTLS */
MBEDTLS_SSL_PRESET_DEFAULT);
/* DTLS requires a timer callback for handshake retransmission.
The timer fires when a handshake message has not been acknowledged,
triggering retransmission. Implement using your RTOS timer API. */
mbedtls_ssl_set_timer_cb(&ssl_ctx,
&dtls_timer,
dtls_timing_set_delay,
dtls_timing_get_delay);
/* Set maximum transmission unit to avoid IP fragmentation.
For most embedded networks, 1280 bytes (IPv6 minimum MTU) is safe. */
mbedtls_ssl_set_mtu(&ssl_ctx, 1280);
MQTT is the dominant messaging protocol for IoT devices. Its default port 1883 uses no security at all. Port 8883 is the convention for MQTT over TLS. Beyond transport encryption, MQTT has three additional security dimensions: authentication (who can connect to the broker), authorisation (which topics a connected client can publish to or subscribe from), and message integrity (verifying commands have not been altered in transit).
/* Configuring an MQTT connection with TLS mutual authentication.
Using the Paho MQTT Embedded C client library.
The TLS context (tls_ctx) is initialised using the mbedTLS setup shown above.
Broker requires a valid client certificate to accept the connection. */
#include "MQTTClient.h"
#define MQTT_BROKER_HOST "broker.example.com"
#define MQTT_BROKER_PORT 8883 /* TLS port, not 1883 */
#define MQTT_CLIENT_ID "device-001" /* Should be unique per device */
#define MQTT_KEEPALIVE_SEC 60
#define MQTT_TIMEOUT_MS 5000
/* Topic ACL (Access Control List) enforced server-side.
This device is only permitted to publish to its own data topic
and subscribe to its own command topic. Enforced by the broker,
but the client should also never attempt topics outside its scope. */
#define TOPIC_TELEMETRY "devices/device-001/telemetry"
#define TOPIC_COMMANDS "devices/device-001/commands"
typedef enum {
MQTT_CONN_OK,
MQTT_CONN_ERR_NETWORK,
MQTT_CONN_ERR_AUTH,
MQTT_CONN_ERR_BROKER
} MqttConnResult;
MqttConnResult connect_mqtt_secure(MQTTClient *client,
Network *network) {
MQTTPacket_connectData conn_data = MQTTPacket_connectData_initializer;
conn_data.clientID.cstring = MQTT_CLIENT_ID;
conn_data.keepAliveInterval = MQTT_KEEPALIVE_SEC;
conn_data.cleansession = 0; /* Retain QoS1/2 messages across reconnect */
conn_data.willFlag = 0;
/* Username/password are a secondary credential layer.
Primary authentication is the client certificate in the TLS handshake.
The broker validates the certificate first; username/password is optional
but adds defence-in-depth for brokers that support both. */
conn_data.username.cstring = device_get_mqtt_username();
conn_data.password.cstring = device_get_mqtt_password();
int rc = MQTTConnect(client, &conn_data);
if (rc != SUCCESS) {
return (rc == MQTT_CONNECTION_REFUSED_NOT_AUTHORIZED)
? MQTT_CONN_ERR_AUTH
: MQTT_CONN_ERR_BROKER;
}
/* Subscribe to the device's command topic after connecting.
QoS 1 ensures commands are delivered at least once even if
the connection drops briefly during delivery. */
rc = MQTTSubscribe(client, TOPIC_COMMANDS, QOS1, handle_command_message);
if (rc != SUCCESS) {
MQTTDisconnect(client);
return MQTT_CONN_ERR_BROKER;
}
return MQTT_CONN_OK;
}
Broker-side topic ACL configuration is equally important. A broker that allows any authenticated device to subscribe to any topic means one compromised device can eavesdrop on all other devices. Configure the broker so each device can only access its own topic namespace. In Mosquitto:
# Mosquitto ACL configuration file (aclfile.conf)
# Each device can only publish to its own telemetry topic
# and subscribe to its own command topic.
# Wildcard patterns use the client certificate CN (Common Name) via
# Mosquitto's %c substitution.
# Allow each client to publish telemetry to its own topic
topic write devices/%c/telemetry
# Allow each client to receive commands from its own topic only
topic read devices/%c/commands
# Allow the backend service account full access
user backend-service
topic #
TLS cipher suite selection determines which algorithms are used for key exchange, authentication and record encryption within the session. Choosing the right cipher suites is a balance between security strength, performance on constrained hardware and compatibility with the server. The recommended cipher suites for embedded devices in order of preference:
| Cipher Suite | Key Exchange | Auth | Record Cipher | When to Use |
|---|---|---|---|---|
| TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 | ECDHE P-256 | ECDSA | AES-128-GCM | Best choice when hardware AES accelerator available |
| TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 | ECDHE P-256 | ECDSA | ChaCha20-Poly1305 | Best choice on processors without hardware AES |
| TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 | ECDHE P-256 | RSA-2048 | AES-128-GCM | When device certificate is RSA (e.g. already provisioned) |
| TLS_PSK_WITH_AES_128_CCM_8 | Pre-Shared Key | PSK | AES-128-CCM-8 | Ultra-constrained devices: eliminates PKI overhead entirely |
All four use ephemeral key exchange (ECDHE or PSK), which provides forward secrecy: each session uses a fresh key, so compromising a long-term key does not allow decryption of previously recorded sessions. Cipher suites to explicitly disable in your mbedTLS configuration:
These mistakes appear repeatedly in embedded firmware security audits. Each one renders the encryption ineffective despite the code appearing to use cryptography correctly.
AES-ECB (Electronic Codebook) mode encrypts each 16-byte block independently. Identical plaintext blocks produce identical ciphertext blocks, leaking the plaintext structure. The classic demonstration is the ECB penguin: encrypting a bitmap image of a penguin with AES-ECB produces an encrypted image where the outline of the penguin is still clearly visible in the ciphertext. Never use ECB mode for any purpose. Use AES-GCM, AES-CCM or AES-CTR.
An IV (Initialisation Vector) or nonce must be unique for every message encrypted with the same key. Reusing an IV with AES-CTR, AES-GCM or ChaCha20 allows an attacker who observes two ciphertexts encrypted with the same key and IV to XOR them together, cancelling the keystream and recovering information about the plaintext. With enough ciphertexts sharing the same IV, the attacker may recover the key entirely. Generate a fresh random IV from the hardware RNG for every encryption operation, or use a counter that is guaranteed never to repeat for the lifetime of the key.
Encrypting data without authenticating it allows an attacker to modify the ciphertext in transit and have the receiver decrypt and act on the tampered data, even though they cannot read the plaintext. This is the basis of padding oracle attacks and several TLS vulnerability classes. Always use an AEAD (Authenticated Encryption with Associated Data) mode: AES-GCM, AES-CCM or ChaCha20-Poly1305. These provide both confidentiality and integrity in a single operation. Never use AES-CBC without a separate MAC (Message Authentication Code), and even then the encrypt-then-MAC ordering matters.
Custom cryptographic implementations are almost always wrong in ways that are not visible through functional testing but are exploitable through timing attacks, fault injection or mathematical cryptanalysis. Use established, audited libraries: mbedTLS, WolfSSL, BearSSL or the hardware crypto driver provided by your chip vendor. The only exception is implementing a known algorithm from a published test vector specification when no existing library is suitable for your target, and even then, have the implementation reviewed by a cryptographer.
A key generated from a low-entropy source is predictable. Seeds derived from device uptime in milliseconds, MAC addresses, or a static value in firmware provide 32 bits or less of actual entropy. An attacker who knows the seed source can enumerate all possible keys and try them all. Always generate keys from the hardware TRNG. If your device does not have a hardware TRNG, seed a CSPRNG (Cryptographically Secure Pseudo-Random Number Generator) from multiple independent entropy sources: ADC noise from an unconnected pin, timer jitter, temperature sensor LSB noise. This is discussed in detail in the key generation section below.
Key management is the discipline of controlling cryptographic keys across their full lifecycle. A key that is generated correctly but stored insecurely, distributed without protection, used beyond its intended scope, or never rotated provides progressively weaker security over time. The six-stage lifecycle applies to every key used in embedded communication.
The six stages are generation, storage, distribution, usage, rotation and destruction. Weak controls at any one stage break the security of all other stages: a correctly stored key that was generated from a predictable seed is recoverable by brute force regardless of storage security.
Use different keys for different purposes. A symmetric key used for both data encryption and for HMAC authentication is weaker than two separate keys, because properties that are acceptable for one operation may not hold for the other. The minimum key separation for a typical cloud-connected IoT device:
Key generation quality is determined entirely by the entropy of the random number generator used. The ESP32 and STM32 both provide hardware TRNGs, but using them correctly requires understanding their characteristics and limitations.
/* Generating a cryptographically secure 256-bit symmetric key on STM32.
The STM32 RNG peripheral generates 32-bit random words from thermal noise.
The HAL driver provides a simple interface. Error checking is mandatory:
if the RNG hardware detects a fault (seed error, clock error), it sets
an error flag and the generated value must not be used. */
#include "stm32f4xx_hal.h"
extern RNG_HandleTypeDef hrng; /* Configured in CubeMX or manually */
typedef enum {
KEY_GEN_OK,
KEY_GEN_ERR_RNG_FAULT,
KEY_GEN_ERR_TIMEOUT
} KeyGenResult;
KeyGenResult generate_aes256_key(uint8_t key_out[32]) {
uint32_t word;
/* Generate 8 x 32-bit words = 256 bits */
for (int i = 0; i < 8; i++) {
HAL_StatusTypeDef status = HAL_RNG_GenerateRandomNumber(&hrng, &word);
if (status != HAL_OK) {
/* RNG hardware fault: zero out any partial key and abort.
Never use a partially generated key. */
secure_zero(key_out, 32);
return (status == HAL_TIMEOUT) ? KEY_GEN_ERR_TIMEOUT
: KEY_GEN_ERR_RNG_FAULT;
}
/* Copy the 32-bit word to the key buffer in big-endian order */
key_out[i * 4 + 0] = (uint8_t)(word >> 24);
key_out[i * 4 + 1] = (uint8_t)(word >> 16);
key_out[i * 4 + 2] = (uint8_t)(word >> 8);
key_out[i * 4 + 3] = (uint8_t)(word);
}
return KEY_GEN_OK;
}
One important caveat on the STM32 RNG: the hardware entropy source can be degraded if the RNG clock is not at least twice the AHB (Advanced High-performance Bus) clock frequency, or if the RNG is read too quickly without allowing the entropy accumulation time. The reference manual specifies minimum inter-read delays. The HAL driver handles this automatically, but direct register access without the HAL does not.
Key rotation replaces a key before it is compromised. The rotation schedule should be based on key usage volume and time, not just calendar time. A key that encrypts one packet per day needs less frequent rotation than a key that encrypts 10,000 packets per day, because the attack value of capturing a large volume of ciphertext for the same key increases with volume.
Rotating keys across a large field deployment requires careful orchestration. A rotation that simultaneously invalidates all old keys and requires all devices to fetch new keys creates a brief window where devices that have not yet completed rotation cannot connect. The graceful rotation pattern:
The transition window duration should be long enough to cover the maximum expected offline period for any device in the fleet. A device that is powered off during a key rotation must not be permanently locked out when it comes back online.
Network access control for embedded devices operates at three independent layers. Each layer must be implemented independently: a failure at one layer should not automatically grant access at the next.
Embedded devices should never be on the same network segment as corporate workstations, databases or servers. A compromised device on an unsegmented network provides an attacker with a lateral movement platform from which to attack any other device on the same segment. The standard architecture for industrial and commercial IoT deployments places IoT devices on a dedicated VLAN (Virtual LAN) with firewall rules that allow only specific traffic to flow to the application servers they need to reach, and block all other inter-segment communication.
For consumer devices that connect to home networks, the security model is different: you do not control the network the device is on. This makes it even more important that the device itself does not expose any unnecessary network services and that all its outbound connections use mutual TLS authentication.
Every inbound connection to an embedded device must require authentication before any data is exchanged. This means: no unauthenticated HTTP endpoints, no Telnet, no anonymous MQTT connections, no SNMP with default community strings, no open CoAP endpoints. The authentication method should be mutual wherever possible: not just the device authenticating to the server, but the server authenticating to the device.
Authentication proves identity. Authorisation controls what that identity is permitted to do. A device that correctly authenticates a cloud backend connection should still restrict which commands that backend can send. A “setpoint” command from the backend is legitimate; a “flash firmware” command from the backend over the MQTT data channel is not the expected interface for firmware updates and should be rejected at the command dispatcher level regardless of authentication status.
The Zero Trust security model rejects the assumption that devices inside a defined network perimeter are trustworthy. It applies “never trust, always verify” to every connection, including connections from devices on the internal network. For embedded IoT this means:
Remote access to embedded devices is necessary for monitoring, debugging and updating them after deployment. It is also one of the most commonly exploited attack surfaces. The Shodan search engine indexes millions of embedded devices exposed directly to the internet, many running services with default credentials or known vulnerabilities.
The primary rule for remote access is: never expose embedded devices directly to the internet without strong authentication and access controls. The two acceptable architectures for production deployments are:
Devices initiate outbound connections to a cloud IoT platform (AWS IoT Core, Azure IoT Hub, Google Cloud IoT). Management and debugging traffic flows through the platform’s authenticated channels. The device has no inbound listening ports. An attacker cannot reach the device directly because it has no public IP address and accepts no inbound connections. This is the standard architecture for consumer and commercial IoT devices.
Devices are accessible only through a VPN gateway. Remote access requires first authenticating to the VPN with MFA (Multi-Factor Authentication), which then provides access to the device’s management interface on the internal network. WireGuard is the preferred VPN protocol for embedded Linux devices because its implementation is significantly simpler than OpenVPN (reducing the attack surface of the VPN client itself) and its performance is superior on resource-constrained platforms.
SSH on embedded Linux devices is commonly left in its default configuration, which is insecure for production deployment. Apply this minimal hardening configuration to any embedded Linux device that exposes SSH:
# /etc/ssh/sshd_config hardening for embedded Linux
# Apply these settings on any production embedded Linux device with SSH.
# Restart SSH after changes: systemctl restart sshd
# Disable password authentication entirely.
# All access requires an SSH key pair. No brute force of passwords is possible.
PasswordAuthentication no
ChallengeResponseAuthentication no
UsePAM no
# Disable root login. All access via non-root user with sudo if needed.
PermitRootLogin no
# Restrict to a specific user or group (replace 'iotadmin' with your user)
AllowUsers iotadmin
# Use only modern, secure key exchange and cipher algorithms.
# Removes legacy algorithms vulnerable to known attacks.
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256
Ciphers chacha20-poly1305@openssh.com,aes128-gcm@openssh.com,aes256-gcm@openssh.com
MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com
# Limit authentication attempts to prevent semi-automated attacks
MaxAuthTries 3
# Terminate idle sessions after 5 minutes
ClientAliveInterval 300
ClientAliveCountMax 0
# Disable X11 forwarding and agent forwarding (not needed on embedded devices)
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
# Log at verbose level to capture authentication events for security monitoring
LogLevel VERBOSE
# Use SSH protocol 2 only (protocol 1 is broken)
Protocol 2
Pair this configuration with fail2ban or a similar tool that automatically blocks IP addresses after repeated failed authentication attempts. On OpenWrt and other embedded Linux distributions, the equivalent is the banip package.
Certificate pinning is an additional defence against MITM attacks that supplements normal certificate chain validation. In standard TLS, a server certificate is accepted if it is signed by any CA in the trust store. If an attacker can compromise a CA or install a rogue CA certificate on the device (for example through a malicious firmware update), they can issue a certificate for any hostname and perform undetected MITM. Certificate pinning restricts trust to a specific certificate or public key, rather than accepting anything signed by any trusted CA.
/* Certificate pinning: verify the server's public key matches the expected
value after a successful TLS handshake. The expected public key hash is
embedded in the firmware as a const array and verified at compile time.
This is the SHA-256 hash of the DER-encoded SubjectPublicKeyInfo structure,
known as the "public key pin" used in HTTP Public Key Pinning (HPKP). */
/* Expected SHA-256 hash of the broker's public key.
Generated offline: openssl x509 -in broker_cert.pem -pubkey -noout |
openssl pkey -pubin -outform der |
openssl dgst -sha256 -binary | base64 */
static const uint8_t EXPECTED_BROKER_PUBKEY_HASH[32] = {
0x4a, 0x7f, 0x3c, 0x1b, 0x92, 0x8e, 0x5d, 0xa4,
0x2f, 0x6b, 0x0e, 0x19, 0xc3, 0x77, 0xd1, 0xf8,
0x5b, 0xe4, 0x93, 0x0a, 0x68, 0x2c, 0x45, 0x9e,
0x11, 0xf2, 0x87, 0x3b, 0xd6, 0xa0, 0x5c, 0x29
/* Replace with actual hash for your broker certificate */
};
typedef enum {
PIN_MATCH_OK,
PIN_MATCH_FAILED,
PIN_MATCH_EXTRACT_ERROR
} PinCheckResult;
PinCheckResult verify_server_key_pin(mbedtls_ssl_context *ssl_ctx) {
const mbedtls_x509_crt *server_cert;
uint8_t computed_hash[32];
uint8_t pubkey_der[512];
int pubkey_der_len;
/* Get the server certificate from the completed TLS handshake */
server_cert = mbedtls_ssl_get_peer_cert(ssl_ctx);
if (server_cert == NULL) return PIN_MATCH_EXTRACT_ERROR;
/* Extract and DER-encode the server's public key */
unsigned char *p = pubkey_der + sizeof(pubkey_der);
pubkey_der_len = mbedtls_pk_write_pubkey(&p,
pubkey_der,
&server_cert->pk);
if (pubkey_der_len < 0) return PIN_MATCH_EXTRACT_ERROR;
/* Hash the DER-encoded public key */
mbedtls_sha256(p, pubkey_der_len, computed_hash, 0);
/* Constant-time comparison against the embedded expected hash */
if (!constant_time_memcmp(computed_hash, EXPECTED_BROKER_PUBKEY_HASH, 32)) {
log_security_event(SEC_EVENT_CERT_PIN_MISMATCH);
return PIN_MATCH_FAILED;
}
return PIN_MATCH_OK;
}
One operational consideration: certificate pinning must be updated before the pinned certificate expires, or the devices will reject connections to the legitimate server. Include the certificate expiry date in your release planning calendar, and ensure you have a process to deploy a pin update (in a firmware update) at least 30 days before expiry. Some implementations pin the CA or intermediate CA public key rather than the leaf certificate, which allows certificate renewal without a pin update as long as the CA key does not change.
Embedded communication security is not a single decision but a stack of complementary controls that each address a distinct attack scenario. TLS with mutual certificate authentication and mandatory server verification defeats eavesdropping, MITM and spoofing simultaneously. AEAD cipher selection ensures that encryption provides both confidentiality and integrity with no additional MAC required. Correct key generation from hardware entropy, key separation by purpose and timely rotation maintain the strength of cryptographic controls across the device lifetime. Network segmentation, Zero Trust access policies and broker-side topic ACLs limit what any single compromised device can reach. SSH hardening and VPN-gated remote access ensure management channels are not the easiest path into the device. Certificate pinning provides a final layer of MITM resistance that does not depend on the health of the global CA ecosystem. Each layer is individually achievable with available libraries and standard tooling. Applied together, they make your device's network communications a significantly harder target than the millions of unprotected IoT devices that remain exposed across the internet.






