Overview
The Siemens SIMATIC IOT2050 industrial IoT gateway exposes a broad physical attack surface by default: a DisplayPort++ output, two USB 3.0 ports, a UART debug header (X11), two Gigabit Ethernet interfaces (eno1, eno2), and an optional cellular modem (wwan0) on the mPCIe slot. When the gateway is deployed in an untrusted environment - a customer cabinet, a public kiosk, an exposed machine - every unused physical interface becomes a potential ingress path for an attacker with hands-on access. Disabling the display output is one part of a defense-in-depth strategy that must also cover USB storage, the serial console, the cellular modem, and the network stack.
This reference covers practical, field-proven hardening steps for the IOT2050 Example Image (Debian-based, kernel 5.10 LTS). The approach is split into four layers:
- Remove the attack surface at the driver and kernel level (modprobe blacklist, masked getty services).
- Reduce software exposure (nftables default-deny, SSH key-only authentication, no password logins).
- Eliminate USB-borne threats (
usb_storage,uas, optionallyusbhid). - Anchor the device against firmware tampering (custom image with secure boot).
displayport_enabled. Achieving a true "no video output" state requires either kernel-level deconfiguration (modprobe blacklist + video=DP-1:d) or a custom-built image without the display stack. Read the threat-model section before choosing a path.Threat Model and Security Goals
Define the adversary before locking anything down. The IOT2050 default image is convenient for development but assumes the operator is trusted. In a production deployment the trust assumption is reversed.
| Adversary | Capability | Interface abused | Mitigation target |
|---|---|---|---|
| Casual passer-by | Plugs in a USB keyboard, opens a TTY | USB HID + DisplayPort | Disable DP and USB HID |
| Local attacker with UART cable | Console login as root at 115200 8N1 | UART debug header (X11) | Mask serial getty, secure boot |
| Attacker with USB stick | Mounts root, extracts keys | USB storage | Blacklist usb_storage and uas
|
| Network attacker | Hits exposed services on eno1/eno2/wwan0 | Ethernet, cellular, docker0 | nftables default-deny, key-only SSH |
| Reverse engineer | Boots custom kernel, mounts root | Bootloader / eMMC | Signed secure-boot image |
If the only goal is to keep an operator from plugging a keyboard into the front panel and reading boot logs, masking the console getty on tty1 may be sufficient. If the goal is to harden the device against a determined attacker with UART access and a screwdriver, the only durable path is a custom image with secure boot and signed kernel.
IOT2050 Physical and Logical Interface Map
The IOT2050 hardware provides the following interfaces that an attacker can reach without opening the enclosure:
| Interface | Linux name | Default state on Example Image | Risk |
|---|---|---|---|
| DisplayPort++ |
/dev/fb0 (i915 / KMS) |
Active, framebuffer initialized at boot | TTY login, boot log leak |
| USB 3.0 port 1 / 2 |
usb-storage, usbhid
|
Active, modules auto-load | BadUSB, root mount |
| UART debug (X11 header) |
ttyS0 @ 115200 8N1 |
Active, getty on ttyS0 | Root shell without credentials |
| Ethernet 1 | eno1 |
DHCP, services bound | Network attack surface |
| Ethernet 2 | eno2 |
DHCP, services bound | Network attack surface |
| Cellular mPCIe |
wwan0 (qmi_wwan / cdc_mbim) |
Optional modem, ModemManager running | Out-of-band ingress |
| Docker bridge | docker0 |
iptables managed by dockerd | Container escape, port exposure |
Confirm the actual layout on your unit with:
ip -br link
ls /dev/ttyS* /dev/ttyUSB* /dev/fb* 2>/dev/null
lsusb
systemctl list-unit-files | grep -E "getty|serial|ModemManager"
Attack-Surface Topology
Prerequisites
- Siemens SIMATIC IOT2050 (MLFB 6ES7647-0BA00-0YA2 or 6ES7647-0BA00-1YA2) with the Example Image flashed to eMMC or SD card.
- SSH or serial access as root (or a sudo-capable user) to the device.
- For custom-image paths: a Linux build host (Debian 11 or 12) with Yocto Project prerequisites or the IOT2050 SDK.
- Network access to Siemens Industry Online Support for downloading signed firmware and the Industrial OS manifest.
Reference documentation:
- SIMATIC IOT2050 product support and manuals (Siemens Industry Online Support)
- meta-iot2050 Yocto BSP (Siemens GitHub)
- How to disable specific video output in Windows 11 (Microsoft Learn)
Method 1 - Blacklist the Display Driver at Boot
The IOT2050 Example Image uses an Intel-based Apollo Lake SoC with the i915 KMS driver. Removing the driver prevents the kernel from enumerating the DisplayPort output and stops the framebuffer from being brought up. The boot log will still print to the console defined on the kernel command line (typically ttyS0), but no pixels will be driven to the connector.
Step 1: Identify the active display driver
lsmod | grep -E "i915|drm|fbdev"
lspci -k | grep -A3 VGA
Typical output on the IOT2050:
i915 2904064 10
drm_kms_helper 311296 1 i915
drm 622592 9 drm_kms_helper,i915
Step 2: Blacklist the modules
Create a blacklist file. The numeric prefix forces evaluation order after the initramfs has been built. The install /bin/false line is a belt-and-braces measure: even if udev or a hotplug event re-requests the module, the kernel will execute /bin/false instead of the module's init function.
cat > /etc/modprobe.d/00-blacklist-display.conf <<'EOF'
# Disable DisplayPort output on IOT2050 - security lockdown
blacklist i915
blacklist drm_kms_helper
blacklist drm
blacklist fbdev
blacklist fb
install i915 /bin/false
install drm_kms_helper /bin/false
EOF
update-initramfs -u -k all
Step 3: Remove the kernel video= parameter and add nomodeset
The IOT2050 Example Image typically boots via extlinux.conf in /boot/extlinux/. Edit it and remove any video=DP-1:d tokens, then append nomodeset:
# /boot/extlinux/extlinux.conf
LABEL Linux
KERNEL /boot/vmlinuz-5.10.140-iot2050
INITRD /boot/initrd.img-5.10.140-iot2050
APPEND root=PARTUUID=... ro quiet nomodeset console=ttyS0,115200
nomodeset the kernel will not load any KMS driver, which has the side effect of disabling the EFI/UEFI framebuffer as well. If you need the serial console only, this is the cleanest answer. If you also need boot logs suppressed, append loglevel=0.Step 4: Verify the absence of the framebuffer
reboot
ls /dev/fb* 2>/dev/null; echo "fb_count=$(ls /dev/fb* 2>/dev/null | wc -l)"
lsmod | grep i915 || echo "OK: no display modules"
dmesg | grep -i drm || echo "OK: no drm init"
Method 2 - Deconfigure the Monitor via Kernel Command Line
For a one-shot test (without rebuilding the initramfs), append a kernel command-line argument and reboot. This is the same approach used in the upstream video= parameter block documented in the kernel parameter list:
# /boot/extlinux/extlinux.conf APPEND line
APPEND ... video=DP-1:d video=HDMI-A-1:d nomodeset
The :d suffix instructs the kernel to mark that specific output connector as disabled. The KMS driver still loads - it just refuses to bring the connector up. This is the least invasive option and is fully reversible by removing the parameter.
Microsoft documents an analogous approach for Windows 11 hosts managing multiple displays: see How to disable specific video output in Windows 11 on Microsoft Learn. The Linux kernel's video= parameter follows the same per-output disable pattern but operates at the framebuffer and KMS layer rather than through a desktop compositor.
Method 3 - Custom Image Without the Display Subsystem
If the device will never use a display in production, the most defensible posture is to compile a custom image (Yocto or Buildroot) that omits the display stack entirely. This guarantees that no module can be loaded later, no matter what an attacker does from the shell.
Build host preparation
sudo apt install -y gawk wget git-core diffstat unzip texinfo \
gcc-multilib build-essential chrpath socat cpio python3 python3-pip \
python3-pexpect xz-utils debianutils iputils-ping python3-git \
python3-jinja2 libegl1-mesa libsdl1.2-dev pylint xterm rsync
Clone the meta-iot2050 BSP
git clone -b dunfell https://github.com/siemens/meta-iot2050
cd meta-iot2050
. ./setup-env.sh
bitbake iot2050-image-example
Disable the display stack in the layer
Append to conf/local.conf:
# Remove all display-related packages
IMAGE_INSTALL_remove = "xserver-xorg xf86-video-intel mesa-demos wayland weston"
DISTRO_FEATURES_remove = "x11 wayland directfb"
MACHINE_EXTRA_RRECOMMENDS_remove = "kernel-module-i915 kernel-module-drm"
KERNEL_DEVICETREE_remove = "intel-iot2050-display.dtbo"
The resulting iot2050-image-example-iot2050.wic can be written to eMMC and signed with the secure-boot toolchain. Once a signed image is flashed, the bootloader refuses to boot an unsigned kernel, which closes the "attacker replaces firmware" path.
Block USB Storage (The Easy Win)
Even with the display disabled, an attacker with a USB stick can mount the root filesystem if the usb_storage module is loaded. Block it now:
echo "blacklist usb_storage" | sudo tee -a /etc/modprobe.d/blacklist.conf
echo "blacklist uas" | sudo tee -a /etc/modprobe.d/blacklist.conf
update-initramfs -u -k all
reboot
For belt-and-braces, also block USB HID so a keyboard cannot be used to type commands into a hidden TTY:
echo "blacklist usbhid" | sudo tee -a /etc/modprobe.d/blacklist.conf
echo "blacklist hid_generic" | sudo tee -a /etc/modprobe.d/blacklist.conf
update-initramfs -u -k all
usbhid will break any USB keyboard you may need for on-site recovery. Keep a known-good USB-serial adapter, the signing key, and a documented recovery procedure for the secure-boot flow. If you lock yourself out, recovery requires UART plus the bootloader prompt - the device has no PS/2 and no other local input.Secure the UART Debug Console
The UART header (X11) on the IOT2050 exposes ttyS0 at 115200 8N1. By default a getty is running, which gives a console login. The standard firmware cannot disable the UART itself via configuration; you can only mask the getty service, which is reversible from a running root shell.
Mask the serial getty
systemctl mask [email protected]
systemctl stop [email protected]
Suppress kernel printk output to the serial port so boot logs are not leaked to a passive listener:
# extlinux.conf APPEND line
APPEND ... console=ttyS0,115200 quiet loglevel=0
To permanently remove the serial console, a custom image is required: rebuild the kernel with CONFIG_SERIAL_8250_CONSOLE=n and remove the getty generator from the initramfs. The factory secure-boot flow for the IOT2050 builds such an image by default; see the meta-iot2050 BSP layer recipes-kernel/linux/linux-iot2050-secboot.inc for the canonical configuration.
Disable the Cellular Modem (wwan0)
If the mPCIe cellular slot is unused, treat wwan0 as an out-of-band backdoor. ModemManager will reconnect the modem whenever it detects it, even if you take the interface down.
systemctl disable --now ModemManager
systemctl mask ModemManager
# Remove the modem's USB IDs from the kernel so it never enumerates
echo "blacklist qmi_wwan" > /etc/modprobe.d/blacklist-modem.conf
echo "blacklist cdc_mbim" >> /etc/modprobe.d/blacklist-modem.conf
echo "blacklist cdc_ncm" >> /etc/modprobe.d/blacklist-modem.conf
update-initramfs -u -k all
Firewall Hardening with nftables
Defense in depth assumes the attacker has a network foothold on eno1, eno2, or wwan0. Default-deny every chain that is not on the data-plane path.
Reset the Example Image defaults
The Example Image ships with iptables-nft and Docker. Docker creates its own chains and commonly punches holes that survive a nft flush ruleset. Reset and rebuild from scratch:
systemctl stop docker
systemctl disable docker
# Or, if you must keep Docker, run it with --iptables=false
# and manage all rules yourself:
mkdir -p /etc/systemd/system/docker.service.d
cat > /etc/systemd/system/docker.service.d/override.conf <<'EOF'
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd --iptables=false --ip6tables=false
EOF
systemctl daemon-reload
systemctl restart docker
Minimal nftables ruleset
# /etc/nftables.conf
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
# Loopback
iif lo accept
# Established / related
ct state established,related accept
ct state invalid drop
# ICMP rate-limited
ip protocol icmp limit rate 4/second accept
ip6 nexthdr icmpv6 limit rate 4/second accept
# Management plane - SSH from a specific subnet on eno1
iifname "eno1" ip saddr 10.0.0.0/24 tcp dport 22 accept
# Log and drop everything else
log prefix "nft-drop: " flags all counter drop
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}
nft -f /etc/nftables.conf
systemctl enable nftables.service
Block the docker bridge entirely (if not used)
ip link set docker0 down
ip link delete docker0
echo "blacklist bridge" >> /etc/modprobe.d/blacklist.conf
update-initramfs -u -k all
SSH Hardening
Disabling the display port without locking SSH is theatre - the attacker will simply SSH in from the management network.
# /etc/ssh/sshd_config.d/00-hardening.conf
Port 22
AddressFamily inet
ListenAddress 10.0.0.5
PermitRootLogin prohibit-password
PasswordAuthentication no
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
UsePAM yes
AllowUsers iotadmin
MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
X11Forwarding no
AllowTcpForwarding no
PermitTunnel no
AllowAgentForwarding no
PermitUserEnvironment no
systemctl restart sshd
Place the operator's public key before restarting sshd, otherwise you will be locked out:
install -d -m 700 -o iotadmin -g iotadmin /home/iotadmin/.ssh
echo "ssh-ed25519 AAAA...iotadmin@laptop" > /home/iotadmin/.ssh/authorized_keys
chmod 600 /home/iotadmin/.ssh/authorized_keys
Secure Boot Image (The Anchor)
The previous steps are software-level hardening. They can all be undone by an attacker who boots from an external medium or reflashes the eMMC. The IOT2050 supports signed boot via the Siemens secure-boot flow:
- Generate a signing key with the
iot2050-secure-boottool from the SDK. - Build a custom image (see Method 3) and sign it.
- Provision the public key hash into the IOT2050's eMMC boot ROM using the Siemens field tool.
- Enable the boot ROM's "enforce signed image" fuse.
After provisioning, the device refuses to boot any image that does not verify against your key. Combined with the modprobe blacklists, this is the strongest posture available on the IOT2050 platform.
For the canonical secure-boot configuration, see the iot2050-boot-firmware and iot2050-secboot recipes in the meta-iot2050 BSP and the secure-boot section of the SIMATIC IOT2050 device manual.
Verification Checklist
Run these from a serial console (the only access you can fully trust at this point). A clean run means the lockdown is in effect.
# 1. No framebuffer present
ls /dev/fb* 2>/dev/null; echo "fb_count=$(ls /dev/fb* 2>/dev/null | wc -l)"
# Expected: fb_count=0
# 2. No display driver loaded
lsmod | grep -E "i915|drm|fb" || echo "OK: no display modules"
# 3. No getty on serial
systemctl list-units --type=service | grep serial-getty || echo "OK: no serial getty"
# 4. USB storage blocked
lsmod | grep usb_storage || echo "OK: usb_storage absent"
modprobe -n -v usb_storage 2>&1 | grep -q "not found" && echo "OK: usb_storage unresolvable"
# 5. Cellular modem blocked
lsmod | grep -E "qmi_wwan|cdc_mbim|cdc_ncm" || echo "OK: modem modules absent"
ip link show wwan0 2>&1 | grep -q "does not exist" && echo "OK: wwan0 gone"
# 6. SSH key-only
sshd -T | grep -E "passwordauthentication|permitrootlogin"
# Expected: passwordauthentication no ; permitrootlogin prohibit-password
# 7. Firewall default-deny
nft list ruleset | grep -E "policy drop" | head -3
# 8. Boot ROM locked (read from firmware)
iot2050-secboot-status 2>/dev/null | grep -q "enforced" && echo "OK: secure boot enforced"
Troubleshooting Matrix
| Symptom | Probable cause | Fix |
|---|---|---|
| Display still on after blacklist | initramfs not regenerated |
update-initramfs -u -k all and reboot |
| Boot hangs at "Loading initial ramdisk" | Display module required by early boot | Keep i915, switch to video=DP-1:d instead |
| Cannot log in over serial after masking getty | Expected behaviour - use SSH or reflash | Provision a recovery image; do not rely on serial for normal access |
| USB stick still mounts |
uas (USB Attached SCSI) bypassed blacklist |
Blacklist both usb_storage and uas
|
| Docker keeps recreating iptables rules | Docker's own iptables chain | Run Docker with --iptables=false --ip6tables=false
|
| SSH still allows password login | Drop-in under sshd_config.d/ overrides main config |
Set explicit PasswordAuthentication no in the drop-in and check sshd -T
|
| Custom signed image fails to boot | Unsigned image after secure-boot fuse blown | Re-provision with the signing key; contact Siemens support via the official portal |
| wwan0 still up after lockdown | ModemManager re-enables the interface |
systemctl disable --now ModemManager and mask it |
| Framebuffer appears only at boot, not after | UEFI GOP framebuffer, separate from KMS | Add nomodeset AND video=DP-1:d in extlinux |
| Lockout after full lockdown | No working ingress path | Recover via bootloader (X11 UART) with the signing key and signed recovery image |
Field Notes
Disabling the DisplayPort output on a stock IOT2050 Example Image is best treated as a layered change: kernel command line for a quick test, modprobe blacklist for persistence, and a custom image with secure boot for production. The video=DP-1:d kernel parameter is reversible in seconds; a custom signed image is not. Pick the layer that matches the threat.
A common field mistake is to disable the display but leave the serial getty running on ttyS0. An attacker with a USB-serial adapter and three minutes of physical access gets a root shell - the display lockdown was wasted. Always mask [email protected] and, for production, build the secure-boot image without CONFIG_SERIAL_8250_CONSOLE.
Another common mistake is to leave Docker running with its default iptables integration. Even with a perfect nftables ruleset, Docker will punch NAT holes for every published port. Run dockerd --iptables=false and own the ruleset yourself, or remove Docker entirely on a single-purpose gateway.
Finally, document the recovery path. After lockdown, the only ingress may be the bootloader prompt itself via the X11 UART header. Keep the signing key, the signed recovery image, and a tested recovery procedure in a sealed envelope with the device. Industrial security is as much about mean time to recovery as it is about mean time to breach.
FAQ
Can I disable the DisplayPort on the IOT2050 from a GUI in the Example Image?
No. The Example Image has no desktop installed and there is no configuration utility for the DisplayPort. Disable it at the kernel level using a modprobe blacklist and the video=DP-1:d boot parameter, or build a custom image without the display stack using the meta-iot2050 BSP.
Does blacklisting i915 also stop boot logs from appearing on the display?
Yes. With i915 and drm_kms_helper removed and nomodeset set, the kernel never initializes a framebuffer. Boot logs continue to be sent to the console defined on the kernel command line (typically ttyS0), but no pixels reach the DisplayPort connector. Add loglevel=0 to suppress even the serial boot log.
Will a software lockdown survive a factory reset or eMMC reflash?
No. Any software-level change - modprobe blacklist, nftables rules, sshd_config - is lost when the eMMC is reflashed. To anchor the device across a reflash, build a custom signed image with the secure-boot flow and blow the verification fuse as described in the SIMATIC IOT2050 device manual.
Is it safe to blacklist usbhid as well as usb_storage?
It is safe for production but blocks any USB keyboard you might need for on-site recovery. If the device is fully managed remotely, blacklist both usb_storage, uas, and usbhid. If you need local keyboard access during commissioning, blacklist only the storage modules and add usbhid after the device is in its final location.
Why is blocking the display port alone not enough?
Every action a user can perform on a connected monitor can also be performed over the UART debug header (X11) and over SSH. An attacker with physical access will use the UART; an attacker with network access will use SSH. Display lockdown must be combined with serial getty masking, SSH key-only authentication, and a default-deny firewall to be meaningful. A signed secure-boot image is the only change that anchors the others across a reflash.