Embedded Security Best Practices and Guidelines

MuhammadMuhammadEmbedded Security6 days ago13 Views

The majority of embedded device security incidents are caused by a small, well-documented set of avoidable mistakes: hardcoded credentials, unvalidated inputs, broken cryptography, open debug interfaces left enabled in production, and missing update mechanisms. These failures are not exotic; they appear repeatedly across product categories and vendors because the pressures of embedded development (resource constraints, tight schedules, feature prioritisation) consistently push security controls toward the backlog. This article consolidates the best practices and guidelines that directly address those failure modes: the secure-by-default configuration principles that prevent the most common configuration vulnerabilities, the 25 mistakes most frequently found in embedded device audits and how to prevent each one, a practical RBAC (Role-Based Access Control) design framework for embedded device management, a complete device hardening checklist covering hardware, OS, network and application layers, and the compliance and documentation requirements for the regulations that matter most to embedded device manufacturers today.

Secure-by-Default: The Configuration Foundation

Secure-by-default means that a device shipped from the factory, connected to a network and powered on for the first time is already in a secure configuration without any manual hardening steps from the user. This is not an aspirational principle: it is a regulatory requirement under ETSI EN 303 645 (the European consumer IoT security standard), the UK PSTI Act (Product Security and Telecommunications Infrastructure Act) and the US Executive Order on Improving the Nation’s Cybersecurity. All three explicitly prohibit default or universal passwords and require that devices ship with minimal attack surface enabled by default.

The four secure-by-default principles that every embedded product should implement:

No Default Passwords

The device must not have a factory-set password that is shared across units of the same model. This eliminates the “Mirai class” of attack where a single password lookup table compromises millions of devices. The correct implementation is one of: unique per-device credentials generated during manufacturing provisioning (preferred for devices with a management interface), mandatory first-use credential setup that cannot be bypassed (the device enters a locked state until the user sets credentials), or certificate-based authentication with no password-based management interface at all.

/* First-boot credential provisioning enforcement.
   The device checks on every boot whether provisioning is complete.
   Until it is, the device enters a restricted setup mode that only
   allows the provisioning API to be called — all other functions
   are unavailable. This prevents shipping a device that "works" before
   the user sets security credentials. */

#include "nvs_flash.h"
#include "provisioning.h"

typedef enum {
    DEVICE_STATE_UNPROVISIONED = 0x00,
    DEVICE_STATE_PROVISIONED   = 0xA5
} DeviceProvisioningState;

#define NVS_KEY_PROV_STATE "prov_state"

DeviceProvisioningState get_provisioning_state(void) {
    nvs_handle_t nvs;
    uint8_t state = DEVICE_STATE_UNPROVISIONED;

    if (nvs_open("security", NVS_READONLY, &nvs) == ESP_OK) {
        nvs_get_u8(nvs, NVS_KEY_PROV_STATE, &state);
        nvs_close(nvs);
    }

    return (DeviceProvisioningState)state;
}

void app_main(void) {
    esp_err_t err = nvs_flash_init();
    if (err == ESP_ERR_NVS_NO_FREE_PAGES ||
        err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        nvs_flash_erase();
        nvs_flash_init();
    }

    if (get_provisioning_state() != DEVICE_STATE_PROVISIONED) {
        /* Device is unprovisioned: start restricted setup mode only.
           The setup AP (Access Point) is up for 10 minutes, then the
           device reboots. No telemetry, no management API, no MQTT.
           Only the provisioning endpoint is active. */
        ESP_LOGI("BOOT", "Device unprovisioned — entering setup mode");
        provisioning_start_setup_mode(SETUP_TIMEOUT_SECONDS);
        /* Does not return — provisioning_start_setup_mode reboots on
           completion or timeout */
    }

    /* Device is provisioned: start normal operation */
    ESP_LOGI("BOOT", "Provisioning complete — starting application");
    start_application();
}

Encryption Enabled by Default

TLS must be on by default for all network communication. Flash encryption must be on by default for devices that store sensitive data. Any security feature that requires the user to explicitly enable it will not be enabled by the majority of users, and the fraction who do not enable it represents the attack surface for the most common attacks. The default configuration must be the secure configuration.

Minimal Services Enabled by Default

The device should enable only the services required for its primary function at first boot. A temperature sensor does not need an FTP server, an HTTP server, or a Telnet daemon. Every service that is disabled by default is an attack surface that does not exist unless someone deliberately enables it. Document how to enable optional services for users who need them; do not enable them preemptively “in case they are useful.”

Secure Protocols as the Only Option

Remove the ability to use insecure protocols, not just the default configuration. A device that ships with TLS enabled but can be reconfigured to use plaintext MQTT through a management API call is only as secure as the management API. The preferred approach is to compile the insecure protocol support out of the firmware entirely for production builds, making the downgrade attack impossible because the code does not exist.

The Secure Configuration Checklist

The following checklist applies to any embedded device with a network interface. It should be completed as part of the pre-deployment commissioning process and verified periodically against the running device configuration through attestation reports:

Category Item Verification Method
Credentials No default, shared or factory-reset passwords exist Attempt login with known default credentials; should fail
Credentials All device certificates are unique per device Compare certificate serial numbers across fleet sample
Services All unused services (Telnet, FTP, HTTP, UPnP) are disabled Port scan: nmap -sV -p- [device_ip]
Services Debug interfaces (UART console, JTAG, SWD) are disabled in production Attempt OpenOCD connection; check RDP level (STM32) or JTAG_DISABLE eFuse (ESP32)
Encryption All network communication uses TLS 1.2 or 1.3 Wireshark: check for any unencrypted MQTT/HTTP traffic
Encryption Flash encryption enabled on devices storing sensitive data espefuse.py summary (ESP32) or RDP Level 1/2 (STM32)
Authentication Server certificate is verified (not ssl_verify=NONE) mitmproxy test: device should reject invalid certificate
Authentication Authentication failure lockout is configured Attempt 10+ failed logins; verify source is blocked
Updates Secure boot enabled; firmware signature verification active Attempt to flash unsigned firmware; should be rejected
Updates Anti-rollback counter burned; downgrade firmware rejected Attempt OTA with version below minimum; should be rejected
Logging Security event logging enabled and forwarding to SIEM Trigger test auth failure; verify event appears in SIEM
Logging Log integrity (HMAC chaining) enabled Modify a log entry; verify chain validation detects it

Network Security Configuration

Network configuration for deployed IoT devices operates at two levels: the device’s own network stack configuration and the network infrastructure the device is deployed on. Both require deliberate security configuration.

Device-Level Network Configuration

# Embedded Linux: configure iptables to enforce a default-deny network policy
# for an IoT gateway device. Only the specific outbound connections the device
# needs are permitted; all other traffic is blocked.

# Flush existing rules
iptables -F
iptables -X

# Default policies: DROP everything, allow nothing by default
iptables -P INPUT   DROP
iptables -P FORWARD DROP
iptables -P OUTPUT  DROP

# Allow established connections (response traffic for allowed outbound)
iptables -A INPUT  -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# Allow loopback traffic
iptables -A INPUT  -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT

# Allow outbound TLS-protected MQTT to the specific broker (not all MQTT)
iptables -A OUTPUT -p tcp -d 203.0.113.10 --dport 8883 -j ACCEPT

# Allow outbound NTP (required for TLS certificate validation)
iptables -A OUTPUT -p udp --dport 123 -j ACCEPT

# Allow outbound DNS (restrict to your authorised DNS server)
iptables -A OUTPUT -p udp -d 8.8.8.8 --dport 53 -j ACCEPT

# Block all other outbound traffic (including unexpected C2 callbacks)
# The default OUTPUT DROP policy handles this

# Log dropped packets (for anomaly detection — be careful with log volume)
iptables -A INPUT  -j LOG --log-prefix "[IPTABLES DROP IN] "  --log-level 4
iptables -A OUTPUT -j LOG --log-prefix "[IPTABLES DROP OUT] " --log-level 4

# Save rules to persist across reboots
iptables-save > /etc/iptables/rules.v4

Network Infrastructure Configuration

IoT devices should be deployed on a dedicated VLAN (Virtual LAN) that is isolated from corporate networks, user computers and management infrastructure. The firewall rules between the IoT VLAN and other network segments should allow only the specific traffic flows required for device operation: device-to-broker MQTT, device-to-NTP, device-to-OTA-server, and management-server-to-device for management operations. All other traffic between the IoT VLAN and other segments should be blocked by default.

Disable UPnP (Universal Plug and Play) on the network gateway and all routers that IoT devices are connected to. UPnP allows any device on the network to automatically open inbound ports through the gateway firewall, which IoT malware (including Mirai) routinely exploits to make compromised devices reachable from the internet.

Configuration Management and Drift Detection

Device configuration drifts over time as maintenance changes, emergency fixes and informal adjustments accumulate. A device that was correctly hardened at deployment may have drifted to an insecure configuration twelve months later through a series of individually reasonable-seeming changes. Configuration drift detection compares the current device configuration against the approved security baseline and alerts when a deviation is found.

# Configuration attestation: compare device-reported configuration
# against the approved security baseline stored in the fleet management system.
# Called daily via the management API for each device in the fleet.

import json
import hashlib

SECURITY_BASELINE = {
    "tls_version_min":        "TLSv1.2",
    "flash_encryption":       True,
    "secure_boot":            True,
    "jtag_disabled":          True,
    "debug_uart_disabled":    True,
    "telnet_enabled":         False,
    "http_enabled":           False,
    "mqtt_tls_required":      True,
    "auth_lockout_threshold": 5,
    "log_forwarding_enabled": True
}

def check_configuration_drift(device_id: str, reported_config: dict) -> list:
    """
    Compare a device's reported configuration against the security baseline.
    Returns a list of drift items: (field, expected, actual).
    Empty list means no drift detected.
    """
    drift_items = []

    for field, expected_value in SECURITY_BASELINE.items():
        actual_value = reported_config.get(field)

        if actual_value is None:
            drift_items.append({
                "device_id": device_id,
                "field":     field,
                "expected":  expected_value,
                "actual":    "MISSING — device did not report this field",
                "severity":  "HIGH"
            })
        elif actual_value != expected_value:
            # Determine severity: security-disabling drift is HIGH
            is_security_disabling = isinstance(expected_value, bool) and \
                                    expected_value == True and \
                                    actual_value == False
            drift_items.append({
                "device_id": device_id,
                "field":     field,
                "expected":  expected_value,
                "actual":    actual_value,
                "severity":  "HIGH" if is_security_disabling else "MEDIUM"
            })

    return drift_items

# Example usage: check drift for a device whose TLS minimum version was
# downgraded and whose JTAG was re-enabled (perhaps by a field technician)
reported = {
    "tls_version_min":        "TLSv1.0",   # DRIFT: downgraded
    "flash_encryption":       True,
    "secure_boot":            True,
    "jtag_disabled":          False,        # DRIFT: re-enabled
    "debug_uart_disabled":    True,
    "telnet_enabled":         False,
    "http_enabled":           False,
    "mqtt_tls_required":      True,
    "auth_lockout_threshold": 5,
    "log_forwarding_enabled": True
}

drift = check_configuration_drift("device-004a2b", reported)
for item in drift:
    print(f"[{item['severity']}] {item['device_id']}: {item['field']} "
          f"expected={item['expected']}, actual={item['actual']}")

Common Coding Mistakes and How to Prevent Them

The six most frequently cited coding mistakes in embedded device security audits, with the specific prevention mechanism for each:

1. Hardcoded credentials. Passwords, API keys, encryption keys or WiFi credentials written directly into firmware source code. Detected by Semgrep rules, strings extraction from firmware binaries and code review. Prevention: all credentials provisioned at manufacturing time through a secure provisioning process and stored in NVS or secure element. See the provisioning patterns in Section 5 of this course.

2. No input validation on network-received data. The firmware assumes that data arriving on the MQTT subscription, CoAP endpoint or serial interface is well-formed and within expected ranges. Prevention: validate every externally received value against type, range and format before processing. Reject with a logged error and do not process. See the full MQTT input validation example in Section 4.

3. Unsafe standard library functions. Use of strcpy, strcat, sprintf, gets and scanf which do not perform bounds checking and are the root cause of a large proportion of embedded buffer overflows. Prevention: replace all uses with bounds-checking alternatives: strncpy (with explicit null-termination), snprintf, strlcpy (where available), or custom wrapper functions. Run Cppcheck and the Clang security checkers to enforce this in CI.

4. Weak or broken cryptographic algorithms. Use of MD5, SHA-1, DES or 3DES for security purposes (signatures, MACs, key derivation); use of AES in ECB mode; use of a predictable IV. Prevention: enforce at the code level with Semgrep rules that block use of deprecated functions. Use only AES-GCM or AES-CCM (authenticated), SHA-256 or SHA-3, and ECDSA P-256 or RSA-3072+.

5. Unchecked error returns. Security-critical operations that silently fail when their return value is not checked: mbedTLS functions that return a non-zero error code indicating a failed signature verification, malloc returning NULL, or a write operation returning fewer bytes than requested. Prevention: treat all security-function return values as mandatory. Use the Semgrep rule from Section 10 to flag unchecked mbedTLS calls in CI.

6. Debug and test code left in production builds. UART debug output printing key material or authentication results, test backdoor accounts that bypass authentication, debug commands that disable security controls, and logging of sensitive values. Prevention: use conditional compilation with a DEBUG_BUILD flag to exclude all debug output and test paths from production firmware. Verify by diffing the symbol table of debug and production builds.

/* Correct pattern for excluding debug code from production builds.
   All debug output and test paths are wrapped in #if DEBUG_BUILD blocks.
   The production CMakeLists.txt sets -DDEBUG_BUILD=0 for release builds
   and -DDEBUG_BUILD=1 for development builds. */

#ifndef DEBUG_BUILD
#define DEBUG_BUILD 0
#endif

void process_received_command(const char *cmd, size_t cmd_len) {

    /* Validate input first — ALWAYS, regardless of debug state */
    if (cmd == NULL || cmd_len == 0 || cmd_len > MAX_CMD_LEN) {
        log_security_event(SEC_EVENT_AUTHZ_DENIED, OUTCOME_FAILURE, NULL, NULL, 0);
        return;
    }

#if DEBUG_BUILD
    /* Debug-only: log the raw command for development tracing.
       This block does NOT exist in production firmware — the compiler
       removes it entirely when DEBUG_BUILD=0. */
    ESP_LOGD("CMD", "Received command (len=%zu): %.*s", cmd_len, (int)cmd_len, cmd);
#endif

    /* Production command dispatch — present in all builds */
    if (strncmp(cmd, "SETPOINT:", 9) == 0) {
        handle_setpoint_command(cmd + 9, cmd_len - 9);
    } else if (strncmp(cmd, "STATUS", 6) == 0) {
        handle_status_request();
    }

#if DEBUG_BUILD
    /* Debug-only backdoor: DO NOT leave this in production.
       The compiler will warn if DEBUG_BUILD is accidentally set to 1
       in a release build because of the #warning directive below. */
    #warning "DEBUG BUILD: test backdoor is ACTIVE — do not release"
    else if (strncmp(cmd, "DEBUG_DUMP_KEYS", 15) == 0) {
        debug_dump_key_material();   /* Prints keys to UART — dev only */
    }
#endif
}

Common Authentication Mistakes

Five authentication mistakes that appear consistently in embedded device security assessments:

Default passwords never changed: The device ships with a documented default password (often “admin”, “1234” or the device serial number) and many users never change it. The correct fix is to eliminate the concept of default passwords entirely: each device must be provisioned with a unique credential, or the device must enforce credential setup before functioning. No shared default password should exist in any configuration.

No account lockout: A management interface that allows unlimited sequential authentication attempts can be brute-forced offline at low bandwidth. Implement a lockout after five to ten failures within a defined window, with exponential backoff for further attempts. Log every failure with the source identifier. For high-security applications, implement permanent lockout after repeated failures and require out-of-band credential reset.

Credentials stored in plaintext: WiFi passwords, API tokens and device secrets stored as readable strings in flash or in a configuration file. The consequence is that anyone with physical access to the device, or anyone who extracts the firmware binary, retrieves every credential the device has ever used. Store credentials in encrypted NVS (ESP32), in the secure element (where key material should never be exported), or using the STM32’s option byte protected flash region.

Sessions that never expire: A management session that remains valid indefinitely provides an attacker who captures a session token with permanent access. Implement session expiry: force re-authentication after a period of inactivity, and issue short-lived tokens (JWT with a 1-hour expiry for management sessions, 24-hour maximum for device-to-cloud telemetry sessions) rather than permanent tokens.

Credentials in URLs or query strings: Firmware that includes API keys, passwords or tokens as URL parameters (GET requests visible in server logs and packet captures) or in HTTP headers that are not protected by TLS. Place all credentials in the TLS-protected request body or use certificate-based authentication that does not transmit any shared secret over the channel.

Common Cryptography Mistakes

The six cryptography mistakes that are most commonly found and most consequential in embedded firmware:

Mistake What Goes Wrong Correct Alternative
Rolling your own crypto algorithm Custom algorithms are nearly always broken; attackers exploit the non-standard design Use only well-reviewed library implementations: mbedTLS, WolfSSL, libsodium
Short or weak keys 56-bit DES and 128-bit RSA can be broken in hours to days with modern hardware AES-128 minimum (AES-256 preferred), RSA-3072+, ECDSA P-256+
AES in ECB mode ECB mode encrypts identical plaintext blocks to identical ciphertext; structure of data is visible even in the ciphertext AES-GCM (authenticated encryption); AES-CCM for constrained devices
Reusing IVs or nonces Two messages encrypted with the same key and IV in a stream cipher or GCM allows XOR recovery of both plaintexts Generate a fresh cryptographically random IV from the hardware RNG for every encryption operation; include the IV in the ciphertext prefix
Encryption without authentication (unauthenticated encryption) Ciphertext without a MAC can be modified without detection; AES-CBC with modified ciphertext decrypts to garbage that the application may still act on Always use authenticated encryption: AES-GCM, AES-CCM, ChaCha20-Poly1305
Hardcoded keys A key in firmware source or the binary is extracted from any device of that model; all devices are compromised simultaneously Unique per-device keys generated on-device or provisioned during manufacturing; stored in secure element or encrypted NVS

Common Network Security Mistakes

No TLS on communication channels: MQTT over port 1883, HTTP over port 80, or CoAP over UDP without DTLS transmit all application data including credentials in plaintext on the network. Any device on the same network segment (or any attacker in a man-in-the-middle position) can read all traffic. The fix is non-negotiable: all network communication must use TLS or DTLS. Port 8883 for MQTT over TLS, port 443 or 8443 for HTTPS, DTLS for CoAP.

Accepting any TLS certificate: Code that disables certificate validation entirely (ssl_verify = NONE, verify_peer = false, or an empty certificate verification callback) provides no protection against man-in-the-middle attacks despite using TLS. The TLS connection encrypts the channel but the device has accepted a connection to the attacker’s server. Always verify the server certificate and, for maximum security, pin the expected server public key or certificate fingerprint.

Unnecessary open ports: Every service listening on an open TCP or UDP port is an attack surface. A device running a MQTT client for telemetry does not need a web server, an SSH server, an FTP server or a Telnet daemon. Audit open ports with nmap before release and close anything that is not required for the device’s primary function.

No rate limiting on network interfaces: A service that accepts unlimited requests per second from any source is vulnerable to both resource exhaustion DoS and credential brute-force attacks. Rate limiting is the first line of defence for any network-facing interface: limit authentication attempts by source, limit API calls by authenticated identity, and limit connection attempts by source IP.

Common Update and Maintenance Mistakes

No signature verification on firmware updates: An OTA update mechanism that downloads and installs firmware without verifying its signature will install whatever the delivery endpoint provides, including attacker-controlled firmware. This single mistake converts the update mechanism from a security asset into a security liability. Firmware signature verification is mandatory; see Section 9 of this course for the complete implementation.

Firmware updates over unencrypted HTTP: Downloading firmware over HTTP exposes the firmware binary to interception and replacement by a man-in-the-middle attacker. Use HTTPS with server certificate verification for all firmware downloads. Note that HTTPS does not replace firmware signature verification: the two are complementary, not substitutes. HTTPS protects the download channel; firmware signatures protect the image regardless of how it was delivered.

No rollback capability after a failed update: A device that bricks if a firmware update fails partway through is effectively a denial-of-service risk in every update deployment. Dual-bank flash with automatic rollback (covered in Section 9) is the standard mitigation. Any embedded product that receives OTA updates must implement a recovery path.

No patch management process: Shipping a device and never providing security patches is not a sustainable security strategy. Every component in the firmware (RTOS, TLS library, JSON parser, MQTT client) will have vulnerabilities discovered after shipment. A device with no patch mechanism, or a vendor that does not maintain a patch process, guarantees that the device will become increasingly vulnerable over its operational lifetime. The patch planning process from Section 9 applies here: maintain the SBOM, monitor CVE feeds, define severity-based response times and deliver patches through the OTA mechanism.

Access Control Models for Embedded Devices

Three access control models are relevant to embedded device security. Understanding the differences helps you design the right model for your device’s management and operational requirements:

DAC (Discretionary Access Control): The owner of a resource controls who can access it. Simple to implement, common in small embedded devices with a single owner. The limitation is that the owner can grant access to anyone, including an attacker who has stolen their credentials. For embedded devices, DAC is appropriate for single-user or small-team management scenarios where the access control requirements are simple.

MAC (Mandatory Access Control): Access policy is set by the system administrator or embedded in the firmware, and individual users cannot override it. On embedded Linux, MAC is implemented by SELinux or AppArmor: policies define exactly which processes can access which files and network resources, and no process can grant itself access beyond what the policy permits. MAC is appropriate for high-security industrial or medical device applications where the consequences of policy violation are severe.

RBAC (Role-Based Access Control): Access is controlled by the role assigned to the user or device identity, not by the identity itself. A device with the “sensor” role can only publish telemetry; a device with the “actuator” role can receive and execute commands; a user with the “operator” role can view dashboards and acknowledge alerts but cannot push firmware updates. RBAC is the most practical model for embedded device fleet management and is the approach implemented by LwM2M, AWS IoT and most enterprise IoT platforms.

Implementing Least Privilege

Least privilege means each identity (device, user, service, process) has exactly the permissions required to perform its function and nothing more. Applied consistently, it limits the blast radius of a compromise: a compromised sensor device that can only publish to its own telemetry topic cannot subscribe to other devices’ topics, cannot push firmware updates, cannot access configuration data and cannot issue commands to actuators.

Implementing least privilege for embedded devices requires thinking about access at three levels:

Device-to-cloud access (MQTT ACL example): Each device authenticates with a unique certificate that identifies it. The MQTT broker ACL grants each device write access to its own telemetry topic and read access to its own command topic only. No device can publish to another device’s command topic, subscribe to another device’s telemetry topic, or publish to any system-level topic.

# Mosquitto ACL for least-privilege device access.
# Each device identity maps to strictly scoped topic permissions.
# The %c substitution inserts the connecting client's MQTT client ID.
# Replace %c with %u for username-based ACLs.

# Allow devices to publish telemetry to their own topic only
pattern write devices/%c/telemetry
pattern write devices/%c/security-log
pattern write devices/%c/status

# Allow devices to subscribe to their own command and OTA topics only
pattern read  devices/%c/commands
pattern read  devices/%c/ota/download

# Deny all other topic access for device identities
# (Mosquitto denies by default; explicit deny for clarity)

# Operator role: read all telemetry, write all commands
# (granted via username-based ACL for human operators)
user operator_role
topic read  devices/+/telemetry
topic read  devices/+/status
topic write devices/+/commands

# Admin role: all access including OTA deployment
user admin_role
topic readwrite #

Process-level privilege on embedded Linux: Application processes should run as non-root users with the minimum Linux capabilities required for their function. A telemetry collection process that only reads sensor data and publishes to MQTT does not need root privileges, CAP_NET_ADMIN or filesystem access beyond its data directory.

# Systemd service configuration for a telemetry daemon with minimal privileges.
# The process cannot gain new privileges, cannot access most of the filesystem,
# and runs as a dedicated non-privileged user.

# /etc/systemd/system/telemetry-daemon.service
[Unit]
Description=IoT Telemetry Daemon
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=telemetry
Group=telemetry
ExecStart=/usr/local/bin/telemetry_daemon --config /etc/telemetry/config.json

# Security hardening directives
NoNewPrivileges=yes
CapabilityBoundingSet=
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/telemetry /var/log/telemetry
InaccessiblePaths=/etc/shadow /etc/passwd /root /home

# Network restriction: only allow the specific network interface needed
RestrictAddressFamilies=AF_INET AF_INET6

[Install]
WantedBy=multi-user.target

RBAC Design for Embedded Device Management

A practical RBAC design for an embedded IoT product typically defines four to six roles that cover the full range of access needs without either granting excessive privileges or creating so many roles that access management becomes administratively burdensome:

Role Permissions Typical Assignees
Device (sensor) Publish telemetry to own topic; subscribe to own command topic; report security log events Each physical sensor device
Device (actuator) Subscribe to command topic; publish execution status; report security log events Each physical actuator device
Viewer Read telemetry dashboards; read alert history; no configuration access Customer staff monitoring device health
Operator All viewer permissions; acknowledge alerts; send operational commands to devices; view device configuration (not change) Operations team staff
Engineer All operator permissions; change device configuration; initiate firmware updates; access diagnostic logs Engineering team members
Administrator All engineer permissions; manage user accounts and role assignments; manage device certificates; access security audit logs; decommission devices Security administrators only; two-person rule for critical operations

Separation of duties: the Administrator role must not also be the Operator role for any individual in normal operations. The Administrator manages access; the Operator manages devices. This ensures that a compromise of an Operator account cannot simultaneously grant the attacker the ability to add new administrator accounts or revoke legitimate ones.

ACL Implementation: Default Deny with Explicit Allow

ACLs for embedded device resources should follow the default-deny principle: access to every resource is denied unless there is an explicit rule that permits it. This is the inverse of the common default-allow pattern where everything is permitted unless explicitly blocked. Default-deny ensures that new resources added to the system start in a secure state, and that misconfigured or forgotten rules result in access denial rather than unintended access.

/* Example: default-deny ACL for an embedded CoAP resource server.
   Resources are defined with explicit access permissions; any resource
   or method not explicitly listed is denied with 4.03 Forbidden. */

#include "coap3/coap.h"

typedef enum {
    PERM_NONE  = 0,
    PERM_READ  = 1 << 0,
    PERM_WRITE = 1 << 1,
    PERM_EXEC  = 1 << 2
} ResourcePermission;

typedef struct {
    const char        *path;
    const char        *required_role;
    ResourcePermission allowed_methods;
} ResourceACL;

/* ACL table: add entries to grant access; everything else is denied */
static const ResourceACL resource_acl[] = {
    /* Telemetry data: readable by all authenticated roles */
    { "/sensors/temperature", "viewer",   PERM_READ },
    { "/sensors/temperature", "operator", PERM_READ },
    { "/sensors/temperature", "engineer", PERM_READ },

    /* Configuration: only engineer and above can read or write */
    { "/config/network",   "engineer",     PERM_READ | PERM_WRITE },
    { "/config/network",   "admin",        PERM_READ | PERM_WRITE },

    /* Commands: operator and above can execute */
    { "/commands/reboot",  "operator",     PERM_EXEC },
    { "/commands/reboot",  "engineer",     PERM_EXEC },
    { "/commands/reboot",  "admin",        PERM_EXEC },

    /* OTA commands: engineer and admin only */
    { "/ota/trigger",      "engineer",     PERM_EXEC },
    { "/ota/trigger",      "admin",        PERM_EXEC },

    /* Security audit log: admin read-only */
    { "/logs/security",    "admin",        PERM_READ },

    /* Sentinel: end of table */
    { NULL, NULL, PERM_NONE }
};

/* Check whether an authenticated identity with the given role
   is permitted to perform the requested method on the given resource.
   Returns true if access is permitted; false otherwise (default deny). */
bool acl_check(const char *path, const char *role, ResourcePermission method) {
    for (int i = 0; resource_acl[i].path != NULL; i++) {
        if (strcmp(resource_acl[i].path, path) == 0 &&
            strcmp(resource_acl[i].required_role, role) == 0 &&
            (resource_acl[i].allowed_methods & method) != 0) {
            return true;
        }
    }
    /* Default deny: no matching ACL entry found */
    log_security_event(SEC_EVENT_AUTHZ_DENIED, OUTCOME_FAILURE, NULL, NULL, 0);
    return false;
}

Physical Access Control

Physical access to an embedded device potentially enables attacks that no software security control can prevent: chip-off flash extraction, debug interface connection to a previously disabled port, voltage glitching and direct probing of internal buses. Physical security controls reduce the feasibility of these attacks by making physical access harder to obtain and more likely to be detected when it occurs.

Physical access controls should be proportionate to the sensitivity of the device and the value of the data it holds or the functions it controls:

  • Secure enclosures: Tamper-evident seals on the device casing that make it visibly apparent if the case has been opened. For high-security applications: hardened metal enclosures, potted electronics (epoxy encapsulation that destroys the PCB if removed) or active tamper meshes that trigger key zeroization.
  • Controlled installation locations: Devices handling sensitive data or critical functions should be installed in locations with access control: locked equipment rooms, controlled-access server racks or secure enclosures with key access logging.
  • Visitor and maintenance procedures: Any maintenance activity that requires physical access to the device enclosure should be performed only by authorised personnel with identity verification, and all physical access events should be logged (timestamp, operator identity, purpose).
  • Tamper detection and response: The device should detect and respond to physical tamper events: case opening, PCB removal, conductive mesh breach. The tamper response (key zeroization, lockdown, alert) converts a physical attack from a silent compromise into an immediately detectable event. See Section 5 for tamper detection implementation.

Hardware Hardening

Hardware hardening configures the silicon-level security features of the microcontroller or SoC to restrict what can be done with physical access to the device. These settings are typically configured once and, for the highest-security options, are permanent: burning eFuses or setting RDP Level 2 cannot be reversed.

# ESP32 production hardware hardening sequence.
# Execute these commands exactly once during manufacturing provisioning.
# Most operations are irreversible — test on a sample device before
# applying to the full production batch.

# Step 1: Burn the unique device certificate and private key to eFuse AES key block
#         (Secure Storage key used to encrypt the NVS partition)
espefuse.py \
  --port /dev/ttyUSB0 \
  burn_key \
  BLOCK1 \
  device_unique_nvs_key.bin \
  AES_256

# Step 2: Enable Flash Encryption
# WARNING: After this step, the flash is encrypted and the key is burned to eFuse.
# Plaintext firmware can no longer be read from flash via JTAG or esptool.
espefuse.py \
  --port /dev/ttyUSB0 \
  burn_efuse \
  FLASH_CRYPT_CNT \
  0x01

# Step 3: Enable Secure Boot V2 (ECDSA P-256)
# The public key hash is burned to eFuse; only firmware signed with the
# corresponding private key will be accepted by the bootloader.
espsecure.py \
  digest_sbv2_public_key \
  --keyfile production_signing_key_public.pem \
  --output sbv2_public_key_digest.bin

espefuse.py \
  --port /dev/ttyUSB0 \
  burn_key_digest \
  BLOCK_KEY0 \
  sbv2_public_key_digest.bin \
  ECDSA_KEY

# Step 4: Disable JTAG debug interface
espefuse.py \
  --port /dev/ttyUSB0 \
  burn_efuse JTAG_DISABLE 1

# Step 5: Disable download mode (prevents bypass of secure boot via serial flash)
espefuse.py \
  --port /dev/ttyUSB0 \
  burn_efuse DIS_DOWNLOAD_MODE 1

# Step 6: Verify the eFuse configuration
espefuse.py --port /dev/ttyUSB0 summary
# Verify in the output:
# FLASH_CRYPT_CNT:  1  (flash encryption enabled)
# ABS_DONE_1:       1  (Secure Boot V2 enabled)
# JTAG_DISABLE:     1  (JTAG disabled)
# DIS_DOWNLOAD_MODE: 1  (download mode disabled)

Embedded Linux OS Hardening

For devices running embedded Linux (OpenWrt, Buildroot, Yocto), OS hardening reduces the attack surface of the operating system layer:

# Embedded Linux OS hardening script.
# Run during image build or as part of first-boot provisioning.
# Adjust paths and service names for your specific distribution.

#!/bin/bash
set -euo pipefail

echo "[HARDEN] Starting OS hardening..."

# 1. Remove unnecessary packages (these should be excluded from the Yocto/Buildroot
#    image recipe, but verify and remove from deployed images if present)
for pkg in telnetd ftpd rsh-server; do
    if command -v "$pkg" &>/dev/null; then
        echo "[HARDEN] Removing unnecessary service: $pkg"
        opkg remove "$pkg" 2>/dev/null || apt-get remove -y "$pkg" 2>/dev/null || true
    fi
done

# 2. Disable unused services
for service in telnet ftp rsh avahi-daemon cups; do
    if systemctl is-enabled "$service" &>/dev/null; then
        systemctl disable --now "$service"
        echo "[HARDEN] Disabled service: $service"
    fi
done

# 3. Set secure file permissions on sensitive directories
chmod 700 /root
chmod 700 /etc/ssl/private
chmod 600 /etc/shadow
chmod 644 /etc/passwd
find /etc/cron* -type f -exec chmod 600 {} \;

# 4. Configure kernel security parameters via sysctl
cat > /etc/sysctl.d/99-security-hardening.conf << 'EOF'
# Disable IP forwarding (unless this is a router/gateway device)
net.ipv4.ip_forward = 0

# Enable SYN flood protection
net.ipv4.tcp_syncookies = 1

# Disable ICMP redirects (prevent routing manipulation)
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0

# Disable source routing (prevent spoofed packet attacks)
net.ipv4.conf.all.accept_source_route = 0

# Enable reverse path filtering (detect spoofed source IPs)
net.ipv4.conf.all.rp_filter = 1

# Restrict ptrace to parent processes (limit debugger attachment)
kernel.yama.ptrace_scope = 1

# Disable the ability to load new kernel modules after boot
# (comment out if your device needs to load modules at runtime)
# kernel.modules_disabled = 1

# Prevent core dumps that may contain sensitive data
fs.suid_dumpable = 0
EOF

sysctl -p /etc/sysctl.d/99-security-hardening.conf

# 5. Enable AppArmor (if available) for the main application processes
if command -v aa-enforce &>/dev/null; then
    aa-enforce /etc/apparmor.d/usr.local.bin.telemetry_daemon
    echo "[HARDEN] AppArmor enforcing for telemetry_daemon"
fi

echo "[HARDEN] OS hardening complete."

Network Hardening

Network hardening for embedded devices involves closing unnecessary ports, enforcing TLS on all services, configuring the device's own firewall and ensuring the device uses only authorised DNS and NTP servers. Use nmap to verify the hardened configuration from an external perspective:

# Post-hardening port scan verification.
# Run from a host on the same network segment as the device.
# A correctly hardened device should show ONLY the ports it needs for its function.

nmap -sV -sC -p- --open 192.168.10.42

# Expected output for a correctly hardened MQTT telemetry device:
# PORT     STATE  SERVICE    VERSION
# 8883/tcp open   ssl/mqtt   (MQTT over TLS — the only open port)
#
# All other ports should show as closed or filtered.
# If the scan reveals port 22 (SSH), 80 (HTTP), 23 (Telnet), 21 (FTP)
# or any other unexpected open port, those services must be disabled
# before deployment.

# Check the TLS configuration on port 8883:
nmap --script ssl-enum-ciphers -p 8883 192.168.10.42

# Expected: Only TLS 1.2 and 1.3 cipher suites in the output.
# FAIL if: TLS 1.0, TLS 1.1, SSLv3, NULL ciphers, EXPORT ciphers,
#          RC4, DES or MD5 cipher suites are listed.

Application-Level Hardening

Application hardening focuses on the firmware application code itself: input validation (covered in Section 4), error handling that does not leak sensitive information, session management for management interfaces, and removal of debug paths and test backdoors from production builds.

Error handling deserves particular attention. An error message that reveals internal state (stack addresses, library versions, file paths, SQL queries) gives an attacker reconnaissance information. Embedded devices should return generic error codes over their network interfaces and log detailed error information only to the tamper-evident local log and the SIEM. The external API surface should never expose implementation details:

/* Secure error response pattern: generic external, detailed internal.
   The external response reveals only the error category, not the cause.
   The internal log captures the full context for debugging. */

typedef enum {
    ERR_INVALID_REQUEST    = 400,
    ERR_UNAUTHORIZED       = 401,
    ERR_FORBIDDEN          = 403,
    ERR_INTERNAL           = 500
} HttpErrorCode;

void send_error_response(MqttContext *ctx,
                         HttpErrorCode code,
                         const char *internal_detail) {
    /* Log full detail internally for debugging and incident response */
    ESP_LOGE("API", "Request failed: code=%d, detail=%s", code, internal_detail);
    log_security_event(
        SEC_EVENT_AUTHZ_DENIED,
        OUTCOME_FAILURE,
        ctx->source_id,
        (const uint8_t *)internal_detail,
        strlen(internal_detail) > 32 ? 32 : strlen(internal_detail)
    );

    /* Return only the generic error code externally.
       No stack trace, no internal message, no version information. */
    char response[32];
    snprintf(response, sizeof(response), "{\"error\":%d}", (int)code);
    mqtt_publish_response(ctx, response);
}

The Complete Device Hardening Checklist

Use this checklist as the final pre-release gate before a hardware revision or firmware release is approved for production deployment. Every item must be verified against the actual device, not just against the design documentation:

Hardware Security

  • Debug interface (JTAG/SWD/UART) disabled or read-protected in production build
  • Secure boot enabled; bootloader verifies firmware signature before execution
  • Anti-rollback counter burned; older firmware versions rejected
  • Flash encryption enabled for devices storing credentials or sensitive data
  • Tamper detection (physical switch or mesh) connected and firmware response implemented
  • Watchdog timer configured and feeding tested
  • Hardware RNG used as entropy source for all key generation
  • MPU (Memory Protection Unit) configured to prevent code execution from data regions

Firmware and Software Security

  • No hardcoded credentials in firmware binary (verified with strings + Semgrep)
  • All unsafe functions (strcpy, gets, sprintf) replaced with bounds-checking alternatives
  • All network-received inputs validated before processing
  • All mbedTLS and crypto function return values checked
  • Debug code and test backdoors removed from production build (verified by symbol diff)
  • GCC security flags enabled: -fstack-protector-strong, -D_FORTIFY_SOURCE=2, -Wformat-security
  • No executable stack segments (verified with readelf -W -l firmware.elf | grep STACK)
  • Only AES-GCM/CCM, SHA-256+ and ECDSA P-256+ used for security operations

Network and Communication Security

  • All network communication over TLS 1.2 or 1.3 only; plaintext protocols compiled out
  • Server certificate validation enabled and tested (mitmproxy test passes)
  • Certificate pinning implemented for critical endpoints (broker, OTA server)
  • Only required ports are open (verified by nmap port scan)
  • Firewall rules configured: default deny with explicit allow for required traffic
  • Authentication failure lockout active (verified by failed login test)
  • Rate limiting configured on all network-facing interfaces
  • UPnP disabled; no automatic port forwarding

Access Control and Authentication

  • No default shared passwords; all credentials unique per device
  • RBAC configured; each identity has minimum required permissions
  • Mosquitto (or equivalent broker) ACL configured and tested
  • Session expiry configured for management sessions
  • All access attempts logged with source, timestamp and outcome

Logging, Monitoring and Updates

  • Security event logging enabled and verified in SIEM
  • Log HMAC integrity enabled
  • OTA update signature verification tested (unsigned firmware rejected)
  • Rollback prevention tested (older firmware version rejected)
  • Dual-bank OTA with automatic rollback tested (interrupted update recovers)
  • SBOM generated and stored with the release artefacts

Compliance Standards for Embedded Device Manufacturers

Compliance standards define the baseline security requirements that regulators, enterprise customers and certification bodies expect embedded device manufacturers to meet. The five most relevant standards for embedded and IoT device manufacturers:

ETSI EN 303 645 (European Consumer IoT Security)

Thirteen mandatory provisions for consumer IoT devices sold in Europe, including: no universal default passwords, a vulnerability disclosure policy, software updates must be possible, all communication must use best-practice cryptography and no software component must contain known vulnerabilities from a public database. ETSI EN 303 645 is the basis for the EU Cyber Resilience Act's consumer device requirements. Compliance is tested through the ETSI TS 103 701 test specification.

EU Cyber Resilience Act (CRA)

Mandatory for products with digital elements sold in the EU from 2027 (with some categories from 2025). Requires: security-by-design, no known exploitable vulnerabilities at time of sale, an SBOM, a vulnerability disclosure and handling process, security updates for the expected product lifetime, and notification to ENISA (European Network and Information Security Agency) within 24 hours of discovering an actively exploited vulnerability. The CRA applies to hardware and software products; embedded device manufacturers are directly in scope.

IEC 62443 (Industrial Automation and Control Systems Security)

The primary standard for industrial IoT and OT (Operational Technology) security. Organised into four series covering policies and procedures (Series 2), system security (Series 3) and component security (Series 4). IEC 62443-4-2 defines the security capabilities required of IoT/IIoT components (sensors, controllers, gateways). Compliance is required for industrial IoT products by many enterprise and government customers.

FDA Cybersecurity Guidance for Medical Devices (2023)

US FDA requires medical device manufacturers to submit a cybersecurity plan as part of premarket submissions, including: threat modelling, an SBOM, a plan for post-market monitoring and updates, and evidence that security has been built into the device from design. Post-market, manufacturers must provide security patches throughout the device's operational lifetime and notify FDA of cybersecurity vulnerabilities under specific conditions.

ISO/SAE 21434 (Automotive Cybersecurity)

The automotive equivalent of IEC 62443: a risk-based approach to cybersecurity throughout the vehicle component lifecycle. Directly relevant to manufacturers of embedded ECUs (Electronic Control Units), gateway modules, infotainment systems and any automotive IoT component.

Required Security Documentation

Security documentation serves two purposes: it helps your own engineering and operations teams maintain and improve the security of the product, and it provides the evidence that regulators, customers and auditors require to verify that you have implemented a security programme. The six essential security documents for an embedded device product:

Security Architecture Document: Describes the security controls in the device firmware, hardware and cloud backend. Covers the threat model, the trust boundaries, the cryptographic mechanisms, the key management architecture and the authentication and authorisation design. This is the document a new security engineer needs to understand the system's security design without reading all the source code.

Risk Assessment and Threat Register: The STRIDE analysis (Section 7), attack tree analysis and risk prioritisation results. Documents every identified threat, the assessed likelihood and impact, the mitigation controls in place and the residual risk accepted. Updated at each major release and after every security incident.

Security Policies: Written policies covering acceptable use, access control, credential management, update procedures, incident response and disposal. These are the rules that govern the team's behaviour in developing, deploying and maintaining the product.

Security Testing Evidence: The outputs of static analysis runs (Cppcheck, Semgrep reports), penetration test reports, fuzzing run results, hardware security test results and the pre-release security checklist sign-off. This evidence demonstrates that security testing was performed and is required by IEC 62443, FDA and the Cyber Resilience Act.

Incident Response Plan: The documented six-phase IR lifecycle, team roles, communication plan and scenario-specific playbooks from Section 8. Must be kept current and must include contact information for all role holders.

SBOM (Software Bill of Materials): Generated at build time with each firmware release. Required by the EU Cyber Resilience Act and FDA. Used for ongoing CVE monitoring. Include: all open source components, versions, package identifiers (CPE or PURL format), licenses and known vulnerabilities at time of release.

Preparing for Security Audits

A security audit, whether a customer-initiated review, a certification body assessment or a regulatory inspection, tests whether your security programme is real or performative. Preparing effectively means ensuring that your controls actually work, not just that the documentation says they should work.

Five practices that make audit preparation efficient rather than stressful:

Keep documentation current continuously: Updating the security architecture document, risk register and test evidence at each release eliminates the scramble to reconstruct documentation when an audit is announced. If the documentation is always current, audit preparation is a review rather than a creation exercise.

Test controls regularly: The hardening checklist should be run against a sample of production devices quarterly, not only at the pre-release gate. Auditors frequently ask "when was this last tested?" and the answer "six months ago at the previous release" is far less reassuring than "we run automated attestation checks against a sample fleet daily."

Fix known gaps before the audit: If you know your incident response plan has not been updated in two years or your static analysis findings have accumulated without remediation, address those gaps before the audit window rather than explaining them during it. Auditors are more interested in whether issues are being tracked and addressed than in whether they exist.

Run a mock audit: Before a significant external audit (certification, customer security review, regulatory inspection), assign one team member to play the auditor role and work through the audit criteria systematically. The gaps found in the mock audit are far less costly than those found in the real one.

Maintain continuous compliance: Security regulations are not static. The EU Cyber Resilience Act, ETSI EN 303 645 and FDA guidance are all updated as new threats and technologies emerge. Assign responsibility for monitoring regulatory changes to a named team member and schedule a quarterly review of whether current practices still meet the latest requirements.

Conclusion

The best practices in this article are not a separate security discipline layered on top of embedded development: they are the engineering practices that produce devices that are defensible in the field across their operational lifetime. Secure-by-default configuration eliminates the largest class of deployment vulnerabilities before the device leaves the factory. Avoiding the documented common mistakes prevents the vulnerabilities that account for the majority of real-world embedded device compromises. RBAC and default-deny ACLs contain the blast radius of any compromise that does occur. A complete hardware and software hardening checklist ensures that every security control the firmware implements is actually enabled in production. And compliance documentation provides the evidence trail that demonstrates to customers, regulators and incident investigators that security was built in from design rather than bolted on after the fact. Together these practices close the gap between a device that is intended to be secure and one that actually is.

Leave a reply

Loading Next Post...
Follow
Search Trending
Popular Now
Loading

Signing-in 3 seconds...

Signing-up 3 seconds...