Security & Deployment

IoT Security — Hardening and Best Practices

Deep dive into IoT security — OWASP IoT Top 10, firmware hardening, certificate lifecycle management, network segmentation, and building a security program for IoT deployments.

IoT Security — Hardening and Best Practices

IoT security fails not because the technology doesn't exist, but because it's skipped under time and cost pressure. This topic builds the security mindset and gives you concrete, implementable controls.


OWASP IoT Top 10

The Open Web Application Security Project maintains a specific top 10 for IoT:

#VulnerabilityReal Example
I1Weak/default passwordsMirai botnet: "admin/admin" on 600K cameras
I2Insecure network servicesTelnet open on port 23
I3Insecure ecosystem interfacesUnencrypted MQTT on port 1883
I4Lack of secure update mechanismFirmware cannot be updated without physical access
I5Use of insecure or outdated componentsLibSSL 2014, unpatched kernel
I6Insufficient privacy protectionSensor data sold/leaked without consent
I7Insecure data transfer and storagePasswords in plaintext logs
I8Lack of device managementNo way to revoke compromised device
I9Insecure default settingsDebug mode on, all ports open
I10Lack of physical hardeningJTAG/UART exposed, SD card removable

Security Audit Script

python
# security_audit.py — automated IoT device security check
import socket
import ssl
import json
import re

class IoTSecurityAudit:
    def __init__(self, device_ip, mqtt_port=1883, https_port=443):
        self.ip         = device_ip
        self.mqtt_port  = mqtt_port
        self.https_port = https_port
        self.findings   = []
        self.score      = 100  # Start at 100, deduct for issues

    def check_open_ports(self):
        """Scan common dangerous ports."""
        dangerous_ports = {
            21: "FTP",
            23: "Telnet",
            80: "HTTP (unencrypted)",
            5900: "VNC",
            8080: "HTTP Alternate",
        }
        open_dangerous = []
        for port, name in dangerous_ports.items():
            try:
                sock = socket.create_connection((self.ip, port), timeout=2)
                sock.close()
                open_dangerous.append(f"{port}/{name}")
            except (socket.timeout, ConnectionRefusedError):
                pass

        if open_dangerous:
            self.findings.append({
                "severity": "HIGH",
                "issue":    f"Dangerous ports open: {', '.join(open_dangerous)}",
                "fix":      "Disable unused services (Telnet, FTP, VNC)"
            })
            self.score -= 20

    def check_mqtt_auth(self):
        """Test if MQTT broker requires authentication."""
        try:
            sock = socket.create_connection((self.ip, self.mqtt_port), timeout=3)
            # Send MQTT CONNECT packet without credentials (anonymous)
            connect_packet = bytes([
                0x10, 0x0E,                          # Fixed header: CONNECT
                0x00, 0x04, 0x4D, 0x51, 0x54, 0x54,  # Protocol name: MQTT
                0x04,                                  # Protocol level 3.1.1
                0x00,                                  # Connect flags: no credentials
                0x00, 0x3C,                            # Keepalive: 60s
                0x00, 0x02, 0x41, 0x42                 # Client ID: "AB"
            ])
            sock.send(connect_packet)
            response = sock.recv(4)
            sock.close()

            # CONNACK return code 0x00 = accepted (no auth required!)
            if len(response) >= 4 and response[3] == 0x00:
                self.findings.append({
                    "severity": "CRITICAL",
                    "issue":    f"MQTT broker at port {self.mqtt_port} allows anonymous access",
                    "fix":      "Enable MQTT authentication and disable anonymous access"
                })
                self.score -= 30
        except:
            pass  # Port closed or connection refused = better than open anonymous

    def check_tls(self):
        """Verify TLS is configured and certificates are valid."""
        try:
            context = ssl.create_default_context()
            with socket.create_connection((self.ip, self.https_port), timeout=3) as sock:
                with context.wrap_socket(sock, server_hostname=self.ip) as ssock:
                    cert = ssock.getpeercert()
                    # Check certificate expiry
                    import datetime
                    not_after = datetime.datetime.strptime(
                        cert['notAfter'], '%b %d %H:%M:%S %Y %Z'
                    )
                    days_left = (not_after - datetime.datetime.utcnow()).days
                    if days_left < 30:
                        self.findings.append({
                            "severity": "HIGH",
                            "issue":    f"TLS certificate expires in {days_left} days",
                            "fix":      "Renew certificate before expiry"
                        })
                        self.score -= 15
        except ssl.SSLCertVerificationError:
            self.findings.append({
                "severity": "CRITICAL",
                "issue":    "TLS certificate invalid or self-signed without trust",
                "fix":      "Use a certificate from a trusted CA"
            })
            self.score -= 25
        except ConnectionRefusedError:
            self.findings.append({
                "severity": "HIGH",
                "issue":    f"No HTTPS on port {self.https_port}",
                "fix":      "Enable TLS for all web interfaces"
            })
            self.score -= 20

    def run(self):
        print(f"Auditing {self.ip}...")
        self.check_open_ports()
        self.check_mqtt_auth()
        self.check_tls()
        self._report()

    def _report(self):
        self.score = max(0, self.score)
        grade = (
            "A" if self.score >= 90 else
            "B" if self.score >= 80 else
            "C" if self.score >= 70 else
            "D" if self.score >= 60 else "F"
        )
        print(f"\n{'─'*50}")
        print(f"Security Score: {self.score}/100 (Grade: {grade})")
        print(f"{'─'*50}")

        if not self.findings:
            print("No issues found!")
        else:
            for f in sorted(self.findings, key=lambda x: x['severity']):
                print(f"\n[{f['severity']}] {f['issue']}")
                print(f"  Fix: {f['fix']}")
        return self.findings

Firmware Hardening Checklist

python
# hardening_config.py — settings that MUST be changed before production
HARDENING = {
    # Authentication
    "default_credentials_changed": True,   # Changed from factory default
    "unique_per_device_credentials": True, # Not the same as other devices
    "strong_password_policy": True,        # >= 16 chars, random

    # Communication
    "tls_all_connections": True,           # No plaintext MQTT or HTTP
    "certificate_pinning": False,          # Optional but recommended
    "mutual_tls": True,                    # mTLS where supported

    # Firmware
    "secure_boot_enabled": True,           # Only runs signed firmware
    "code_signing_verified": True,         # All OTA updates verified
    "debug_interfaces_disabled": True,     # JTAG, UART disabled in production
    "rollback_protection": True,           # Cannot downgrade to vulnerable version

    # Network
    "minimal_attack_surface": True,        # Only necessary ports open
    "network_segmented": True,             # IoT VLAN isolated
    "firewall_rules_configured": True,     # Whitelist-only outbound

    # Data
    "credentials_not_hardcoded": True,     # No passwords in source code
    "sensitive_data_encrypted": True,      # Keys, certs encrypted at rest
    "logs_no_credentials": True,           # Logs don't contain passwords/tokens

    # Lifecycle
    "ota_update_mechanism": True,          # Can be updated remotely
    "device_can_be_revoked": True,         # Can decommission compromised device
    "vulnerability_disclosure": True,      # Contact for security issues
}

passed = sum(HARDENING.values())
total  = len(HARDENING)
print(f"Hardening: {passed}/{total} controls implemented")

missing = [k for k, v in HARDENING.items() if not v]
if missing:
    print("\nAction required:")
    for m in missing:
        print(f"  ✗ {m.replace('_', ' ').title()}")

Certificate Lifecycle Management

python
# cert_manager.py — track and automate certificate renewals
from datetime import datetime, timedelta
import subprocess
import os

class CertificateManager:
    def __init__(self, ca_key="ca.key", ca_cert="ca.crt"):
        self.ca_key  = ca_key
        self.ca_cert = ca_cert
        self.issued  = {}  # device_id → expiry date

    def issue_device_cert(self, device_id, validity_days=365):
        """Issue a new certificate for a device."""
        key_file  = f"certs/{device_id}.key"
        cert_file = f"certs/{device_id}.crt"
        csr_file  = f"certs/{device_id}.csr"

        os.makedirs("certs", exist_ok=True)

        # Generate private key
        subprocess.run([
            "openssl", "genrsa", "-out", key_file, "2048"
        ], check=True, capture_output=True)

        # Generate CSR
        subprocess.run([
            "openssl", "req", "-new",
            "-key", key_file,
            "-out", csr_file,
            "-subj", f"/CN={device_id}/O=BigXStar/C=IN"
        ], check=True, capture_output=True)

        # Sign with CA
        subprocess.run([
            "openssl", "x509", "-req",
            "-in", csr_file,
            "-CA", self.ca_cert,
            "-CAkey", self.ca_key,
            "-CAcreateserial",
            "-out", cert_file,
            "-days", str(validity_days)
        ], check=True, capture_output=True)

        os.remove(csr_file)

        expiry = datetime.now() + timedelta(days=validity_days)
        self.issued[device_id] = expiry

        print(f"Issued cert for {device_id}, expires {expiry.date()}")
        return key_file, cert_file

    def check_expiring(self, warn_days=30):
        """Return devices whose certificates expire within warn_days."""
        deadline = datetime.now() + timedelta(days=warn_days)
        expiring = {
            dev: exp for dev, exp in self.issued.items()
            if exp <= deadline
        }
        if expiring:
            print(f"WARNING: {len(expiring)} certs expiring soon:")
            for dev, exp in expiring.items():
                days_left = (exp - datetime.now()).days
                print(f"  {dev}: {days_left} days left ({exp.date()})")
        return expiring

    def revoke(self, device_id):
        """Revoke a compromised device's certificate."""
        # In production: update CRL (Certificate Revocation List) or use OCSP
        cert_file = f"certs/{device_id}.crt"
        subprocess.run([
            "openssl", "ca",
            "-revoke", cert_file,
            "-config", "openssl.cnf"
        ])
        del self.issued[device_id]
        print(f"Revoked certificate for {device_id}")

cm = CertificateManager()
key, cert = cm.issue_device_cert("esp32-node-001", validity_days=365)
key, cert = cm.issue_device_cert("esp32-node-002", validity_days=365)
cm.check_expiring(warn_days=90)

Security Incident Response

python
# incident_response.py — detect and respond to anomalous device behavior
import paho.mqtt.client as mqtt
import json
import time

ANOMALY_THRESHOLDS = {
    "reconnects_per_hour": 10,  # Unusual churn = compromised device?
    "publish_rate_per_min": 60,  # Unusual data volume = data exfiltration?
    "unknown_topics": True,     # Publishing to unexpected topics
}

known_topics = {
    "sensors/#",
    "devices/+/status",
    "devices/+/telemetry"
}

device_stats = {}

def track_device(device_id, topic):
    now = time.time()
    if device_id not in device_stats:
        device_stats[device_id] = {
            "first_seen": now,
            "last_seen":  now,
            "message_count": 0,
            "topics": set(),
            "suspicious": False
        }

    stats = device_stats[device_id]
    stats["last_seen"]      = now
    stats["message_count"] += 1
    stats["topics"].add(topic)

    # Check rate
    duration_min = (now - stats["first_seen"]) / 60 or 0.001
    rate = stats["message_count"] / duration_min
    if rate > ANOMALY_THRESHOLDS["publish_rate_per_min"]:
        if not stats["suspicious"]:
            print(f"ALERT: {device_id} publishing at {rate:.0f} msg/min — possible exfiltration!")
            stats["suspicious"] = True
            quarantine_device(device_id)

def quarantine_device(device_id):
    """Disconnect and blacklist a suspicious device."""
    print(f"Quarantining device: {device_id}")
    # In production:
    # 1. Publish to Mosquitto's $SYS topic to forcibly disconnect
    # 2. Update firewall rules to block device MAC/IP
    # 3. Revoke device certificate
    # 4. Page on-call security engineer
    # 5. Preserve logs for forensics

def on_message(client, userdata, msg):
    try:
        topic = msg.topic
        data  = json.loads(msg.payload)
        device_id = data.get("device_id", "unknown")
        track_device(device_id, topic)
    except:
        pass

client = mqtt.Client("security-monitor")
client.on_connect = lambda c, u, f, r: c.subscribe("#", qos=1)
client.on_message = on_message
client.connect("localhost", 1883, 60)
client.loop_forever()

Summary

  • OWASP IoT Top 10 gives you the most common vulnerability categories — use it as a checklist before deployment
  • Security auditing scripts can automatically detect open ports, anonymous MQTT, and invalid TLS certificates
  • The hardening checklist covers authentication, communication, firmware, network, data, and lifecycle
  • Certificate lifecycle management is critical — expired certificates silently break devices in the field
  • Revocation is the nuclear option for compromised devices — plan for it from day one
  • Anomaly detection on device behavior patterns (publish rate, unusual topics) can detect compromised devices

Next, we'll build OTA (over-the-air) firmware update pipelines — keeping your devices secure and up-to-date without physical access.