Embedded Linux images ship on millions of devices, and most of them carry more attack surface than they need. A default Buildroot or Yocto configuration includes development tools, extra kernel modules, permissive filesystem permissions, and no syscall filtering. Each of these is an opportunity for an attacker who gets a foothold.
This tutorial takes the opposite approach. You’ll build a minimal image, audit every layer of it — network services, filesystem, kernel, and syscall exposure — then apply targeted hardening and measure the reduction. The goal isn’t to produce a “secure” image (security depends on context), but to build the skill of seeing attack surface the way an attacker does.
If you’ve followed the cross-compiling tutorial, you already have a Buildroot environment. This tutorial builds on it.
What counts as attack surface
Attack surface is anything an adversary can interact with to influence system behavior. On an embedded Linux system, it breaks down into layers.
┌──────────────────────────────────────┐
│ Network Services │ ← ports, protocols, daemons
├──────────────────────────────────────┤
│ Userspace Binaries │ ← suid, capabilities, writable paths
├──────────────────────────────────────┤
│ Syscall Interface │ ← available syscalls, seccomp policy
├──────────────────────────────────────┤
│ Kernel │ ← modules, /proc, /sys, sysctl
├──────────────────────────────────────┤
│ Bootloader / Firmware │ ← unsigned images, debug interfaces
└──────────────────────────────────────┘We’ll audit top to bottom, then harden bottom to top.
Building the baseline image
Start with a Buildroot configuration that’s representative of a typical IoT device: BusyBox userspace, a network daemon, and a web interface.
cd ~/buildroot
podman run --rm -it -v "$(pwd)":/home/builder/buildroot:Z \
-w /home/builder/buildroot buildroot-env \
sh -c 'make qemu_arm_versatile_defconfig && make menuconfig'Enable packages that simulate a realistic embedded system.
Target packages → BusyBox
→ Show packages that are also provided by busybox: YES
Target packages → Networking applications
→ dropbear: YES (SSH)
→ lighttpd: YES (web server)
Target packages → Debugging, profiling and benchmark
→ strace: YES
System configuration
→ Root password: root
→ /dev management: Dynamic using devtmpfs
→ Enable root login with password: YES
Kernel
→ (leave defaults — this gives us a full module set to audit)Build it.
podman run --rm -v "$(pwd)":/home/builder/buildroot:Z \
-w /home/builder/buildroot buildroot-env \
make -j$(nproc)Boot the image in QEMU.
qemu-system-arm \
-M versatilepb \
-m 256M \
-kernel output/images/zImage \
-dtb output/images/versatile-pb.dtb \
-drive file=output/images/rootfs.ext4,if=scsi,format=raw \
-append "root=/dev/sda console=ttyAMA0,115200" \
-net nic,model=rtl8139 \
-net user,hostfwd=tcp::2222-:22,hostfwd=tcp::8080-:80,hostfwd=tcp::1234-:1234 \
-nographicLog in as root. This is your baseline — the “before” snapshot.
Layer 1: Network service audit
Start from the outside in. What can a network attacker see?
Port scan from the host
nmap -sV -p- localhost -Pn --open 2>/dev/null | grep -E "^[0-9]"22/tcp open ssh Dropbear sshd 2024.84
80/tcp open http lighttpd/1.4.73Listening sockets from inside the VM
netstat -tlnp 2>/dev/null || ss -tlnpProto Local Address Foreign Address State PID/Program name
tcp 0.0.0.0:22 0.0.0.0:* LISTEN 87/dropbear
tcp 0.0.0.0:80 0.0.0.0:* LISTEN 91/lighttpdRecord everything. For each listening service, document:
| Service | Port | Binding | Runs as | Config location |
|---|---|---|---|---|
| dropbear | 22 | 0.0.0.0 | root | /etc/init.d/S50dropbear |
| lighttpd | 80 | 0.0.0.0 | root | /etc/lighttpd/lighttpd.conf |
Warning
Both services bind to
0.0.0.0and run as root. On a real device, this means any network-adjacent attacker can reach them, and any vulnerability grants root. This is the default on most embedded systems.
Service version and known vulnerabilities
Check the exact versions.
dropbear -V 2>&1
lighttpd -vSearch for CVEs against those versions. On your host:
# Quick CVE check (requires internet)
curl -s "https://services.nvd.nist.gov/rest/json/cves/2.0?keywordSearch=dropbear+2024" | python3 -m json.tool | head -30Even if no CVEs exist today, each listening service is a future vulnerability waiting to happen. The question is always: does this service need to be here?
Layer 2: Filesystem audit
SUID and SGID binaries
SUID binaries run with the file owner’s privileges regardless of who executes them. They’re a classic privilege escalation vector.
find / -perm -4000 -type f 2>/dev/null/bin/busybox
/usr/bin/passwdBusyBox with SUID is particularly dangerous. BusyBox is a multi-call binary — hundreds of utilities in one executable. If it’s SUID root, any of those utilities run as root.
# List all busybox applets
busybox --list | wc -l
# 396
# These all run as root if busybox is suid:
busybox sh
busybox vi
busybox wget
busybox ncCheck SGID binaries too.
find / -perm -2000 -type f 2>/dev/nullWorld-writable directories and files
# World-writable directories
find / -type d -perm -0002 -not -path "/proc/*" -not -path "/sys/*" -not -path "/dev/*" 2>/dev/null
# World-writable files
find / -type f -perm -0002 -not -path "/proc/*" -not -path "/sys/*" -not -path "/dev/*" 2>/dev/null/tmp
/var/tmp
/var/run
/etc/lighttpd/lighttpd.conf
/etc/dropbear
...Writable config files are especially dangerous. If an attacker can write to lighttpd.conf, they can make the web server execute arbitrary code on next restart.
File capabilities
Linux capabilities are finer-grained than SUID but equally dangerous if over-assigned.
find / -exec getcap {} + 2>/dev/nullOn a minimal Buildroot image this usually returns nothing, but on production images you’ll often find cap_net_raw on ping or cap_net_bind_service on web servers.
Sensitive files with weak permissions
# SSH keys readable by non-root?
ls -la /etc/dropbear/
# Password hashes
ls -la /etc/shadow
# Any credentials or keys in common locations
find / \( -name "*.pem" -o -name "*.key" -o -name "id_rsa" -o -name "*.conf" \) 2>/dev/null | \
xargs ls -la 2>/dev/nullLayer 3: Kernel attack surface
The kernel is the largest and most privileged component. Every loaded module, every /proc and /sys entry, and every enabled syscall is potential attack surface.
Loaded kernel modules
lsmodModule Size Used by
rtl8139cp 28672 0
8139too 32768 0
mii 16384 2 rtl8139cp,8139too
...Each loaded module adds code running in ring 0. Modules for hardware not present on the device are pure unnecessary attack surface.
# Count loaded modules
lsmod | wc -l
# Total size of loaded module code
lsmod | awk 'NR>1 {sum+=$2} END {print sum/1024 " KB"}'Kernel configuration
Check which features are compiled in.
# If /proc/config.gz exists
zcat /proc/config.gz | grep -E "^CONFIG_" | wc -l
# Security-relevant options
zcat /proc/config.gz | grep -iE "(SECURITY|SELINUX|APPARMOR|SECCOMP|STACKPROTECTOR|FORTIFY)"CONFIG_SECCOMP=y
CONFIG_STACKPROTECTOR=y
CONFIG_FORTIFY_SOURCE=y
# CONFIG_SECURITY_SELINUX is not set
# CONFIG_SECURITY_APPARMOR is not setNote
Most embedded kernels ship without SELinux or AppArmor. This means there’s no mandatory access control — any process running as root has full system access, and any privilege escalation from a non-root process gives the attacker everything.
Exposed kernel interfaces
# /proc entries that leak information
ls /proc/kallsyms 2>/dev/null && echo "kallsyms exposed"
cat /proc/sys/kernel/kptr_restrict
# 0 = kernel addresses visible to all users
# Writable sysctl values
sysctl -a 2>/dev/null | wc -l
# /sys entries
find /sys -writable 2>/dev/null | head -20/proc/kallsyms with kptr_restrict=0 gives an attacker a complete map of kernel symbol addresses — KASLR becomes useless.
Layer 4: Syscall exposure
Every available syscall is an entry point into the kernel. A web server doesn’t need ptrace, mount, or kexec_load, but by default every syscall is available to every process.
Cataloging available syscalls
# List all syscalls this kernel supports
cat /proc/kallsyms | grep " T sys_" | wc -l
# or
ausyscall --dump 2>/dev/null | wc -lOn a typical embedded kernel, 300-400 syscalls are available. A minimal web server needs maybe 40.
Writing a seccomp-bpf profile
Seccomp-bpf restricts which syscalls a process can make. Create a profile for lighttpd.
First, trace what syscalls lighttpd actually uses.
# On the target
strace -f -c lighttpd -D -f /etc/lighttpd/lighttpd.conf 2>&1Let it run for a minute, handle a few requests from the host (curl http://localhost:8080/), then kill it. The -c flag summarizes syscall usage.
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- -------
28.57 0.000400 20 20 read
21.43 0.000300 15 20 write
14.29 0.000200 10 20 close
7.14 0.000100 10 10 openat
7.14 0.000100 10 10 fstat
7.14 0.000100 50 2 accept4
...Now write a starter seccomp policy. Create seccomp_lighttpd.c on your host.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <linux/audit.h>
#include <stddef.h>
#ifndef SECCOMP_RET_LOG
#define SECCOMP_RET_LOG SECCOMP_RET_ALLOW
#endif
/* Starter policy: allow core syscalls and log everything else while tuning */
static struct sock_filter filter[] = {
/* Ensure this filter is only used on ARM */
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, arch)),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_ARM, 1, 0),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
/* Load syscall number */
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
offsetof(struct seccomp_data, nr)),
/* Allow list */
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_read, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_write, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_close, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_openat, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_fstat64, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_accept4, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_socket, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_bind, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_listen, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_epoll_ctl, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_epoll_wait, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_mmap2, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_brk, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_futex, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_rt_sigaction, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_rt_sigprocmask, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_clock_gettime, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_getpid, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_exit, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_exit_group, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
/* During profiling, log unknown syscalls but allow them */
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_LOG),
};
int main() {
struct sock_fprog prog = {
.len = sizeof(filter) / sizeof(filter[0]),
.filter = filter,
};
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) {
perror("prctl(PR_SET_NO_NEW_PRIVS)");
return 1;
}
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) != 0) {
perror("prctl(PR_SET_SECCOMP)");
return 1;
}
/* Replace this with execve of the target process */
execl("/usr/sbin/lighttpd", "lighttpd",
"-D", "-f", "/etc/lighttpd/lighttpd.conf", NULL);
perror("execl");
return 1;
}Tip
Use SECCOMP_RET_LOG first This sample defaults to
SECCOMP_RET_LOGfor unknown syscalls so the service keeps running while you tune the allowlist. After your dmesg logs are clean, change the final return action toSECCOMP_RET_KILL(orSECCOMP_RET_ERRNO) to enforce the policy.
Cross-compile and test it. Place the source in the Buildroot directory so the container can see it.
cp seccomp_lighttpd.c ~/buildroot/
podman run --rm -v ~/buildroot:/home/builder/buildroot:Z \
-w /home/builder/buildroot buildroot-env \
sh -c '
CROSS="$(find output/host/bin -maxdepth 1 -type f -name "*-gcc" | head -1 | sed "s/gcc$//")"
${CROSS}gcc -o seccomp_lighttpd seccomp_lighttpd.c -static
'
scp -P 2222 ~/buildroot/seccomp_lighttpd root@localhost:/root/Hardening the image
Now apply fixes for everything the audit found. Modify the Buildroot configuration and add overlay files.
Network services
Bind services to specific interfaces and drop privileges.
# /etc/lighttpd/lighttpd.conf
# QEMU user-mode networking guest IP (replace on real hardware)
server.bind = "10.0.2.15"
# Drop privileges after binding
server.username = "www-data"
server.groupname = "www-data"For dropbear, restrict to key-based auth and bind to a management interface.
# /etc/init.d/S50dropbear - modify DROPBEAR_ARGS
DROPBEAR_ARGS="-w -s -p 10.0.2.15:22"
# -w: disallow root login
# -s: disable password authWarning
-w -srequires a non-root account with SSH keys already provisioned. If you haven’t created that account yet, keep serial console access open so you don’t lock yourself out.
Filesystem hardening
Remove SUID from BusyBox and restrict writable paths.
Create a Buildroot post-build script. In menuconfig:
System configuration → Custom scripts to run after building: board/qemu/post-build.shCreate board/qemu/post-build.sh:
#!/bin/bash
TARGET=$1
# Create dedicated unprivileged service account for lighttpd
grep -q '^www-data:' "${TARGET}/etc/group" || echo 'www-data:x:81:' >> "${TARGET}/etc/group"
grep -q '^www-data:' "${TARGET}/etc/passwd" || echo 'www-data:x:81:81:www-data:/var/www:/bin/false' >> "${TARGET}/etc/passwd"
mkdir -p "${TARGET}/var/www"
chown 81:81 "${TARGET}/var/www"
# Remove SUID from busybox
chmod u-s "${TARGET}/bin/busybox"
# Restrict sensitive directories
chmod 700 "${TARGET}/etc/dropbear"
chmod 600 "${TARGET}/etc/shadow"
# Remove unnecessary suid
find "${TARGET}" -perm -4000 -exec chmod u-s {} \;
# Make /etc read-only where possible
# (lighttpd.conf should not be writable at runtime)
chmod 444 "${TARGET}/etc/lighttpd/lighttpd.conf"
# Remove development artifacts
rm -f "${TARGET}"/usr/lib/*.a # static libraries
rm -rf "${TARGET}/usr/include" # headers
rm -rf "${TARGET}/usr/share/man" # man pages
# Restrict /proc and /sys mount options (added to fstab)
cat >> "${TARGET}/etc/fstab" << 'EOF'
proc /proc proc nosuid,nodev,noexec,hidepid=2 0 0
EOF
echo "Post-build hardening applied."chmod +x board/qemu/post-build.shKernel hardening
Create a kernel config fragment to enable security features and disable unnecessary ones.
mkdir -p board/qemu/linux-config-fragmentscat > board/qemu/linux-config-fragments/hardening.cfg << 'EOF'
# Disable kernel module loading at runtime
# CONFIG_MODULES is not set
# Restrict dmesg to root
CONFIG_SECURITY_DMESG_RESTRICT=y
# Hide kernel symbols from non-root
CONFIG_KALLSYMS=y
# (set kptr_restrict via sysctl instead)
# Stack protection
CONFIG_STACKPROTECTOR=y
CONFIG_STACKPROTECTOR_STRONG=y
# Harden memory allocator
CONFIG_SLAB_FREELIST_RANDOM=y
CONFIG_SLAB_FREELIST_HARDENED=y
# Harden copy to/from userspace
CONFIG_FORTIFY_SOURCE=y
CONFIG_HARDENED_USERCOPY=y
# Disable kexec (prevents loading unsigned kernels at runtime)
# CONFIG_KEXEC is not set
# Restrict unprivileged eBPF
CONFIG_BPF_UNPRIV_DEFAULT_OFF=y
# Restrict userfaultfd to root
CONFIG_USERFAULTFD=y
# Restrict perf
CONFIG_SECURITY_PERF_EVENTS_RESTRICT=y
EOFIn Buildroot menuconfig:
Kernel → Linux Kernel → Additional configuration fragment files:
board/qemu/linux-config-fragments/hardening.cfgSysctl hardening
Create a sysctl config in the filesystem overlay.
mkdir -p board/qemu/rootfs-overlay/etc/sysctl.d/cat > board/qemu/rootfs-overlay/etc/sysctl.d/hardening.conf << 'EOF'
# Hide kernel pointers
kernel.kptr_restrict = 2
# Restrict dmesg
kernel.dmesg_restrict = 1
# Disable SysRq
kernel.sysrq = 0
# ASLR (full randomization)
kernel.randomize_va_space = 2
# Restrict ptrace
kernel.yama.ptrace_scope = 2
# Network hardening
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.log_martians = 1
net.ipv4.tcp_syncookies = 1
EOFIn menuconfig, set the overlay directory:
System configuration → Root filesystem overlay directories:
board/qemu/rootfs-overlayRebuild.
podman run --rm -v ~/buildroot:/home/builder/buildroot:Z \
-w /home/builder/buildroot buildroot-env \
make -j$(nproc)Measuring the difference
Boot the hardened image and re-run every audit check. Track the results side by side.
Network surface
| Metric | Before | After |
|---|---|---|
| Listening ports | 2 (0.0.0.0) | 2 (specific interface) |
| Services as root | 2 | 1 (lighttpd dropped, dropbear remains root) |
| Password auth | Enabled | Disabled |
Filesystem surface
| Metric | Before | After |
|---|---|---|
| SUID binaries | 2 | 0 |
| World-writable config files | 3 | 0 |
| Static libraries on target | 12 | 0 |
| /proc hidepid | No | Yes (hidepid=2) |
Kernel surface
| Metric | Before | After |
|---|---|---|
| Loadable modules | ~30 | 0 (monolithic) |
| kallsyms visible to users | Yes | No (kptr_restrict=2) |
| dmesg visible to users | Yes | No |
| ptrace scope | 0 (unrestricted) | 2 (admin only) |
| kexec available | Yes | No |
Syscall surface
| Metric | Before | After |
|---|---|---|
| Syscalls available to lighttpd | ~380 | Tuned allowlist (typically far fewer) |
| ptrace usable by www-data | Yes | No |
| mount usable by www-data | Yes | No |
Insight
The compound effect No single hardening measure is sufficient. An attacker who bypasses seccomp still faces a monolithic kernel with no module loading. An attacker who finds a kernel vulnerability still can’t read kallsyms to locate gadgets. Defense in depth means each layer makes the next layer’s weaknesses harder to reach.
Building an audit script
Automate the audit so you can run it against any image. Create audit.sh.
#!/bin/bash
echo "=== Embedded Linux Attack Surface Audit ==="
echo "Date: $(date)"
echo "Hostname: $(hostname)"
echo "Kernel: $(uname -r)"
echo ""
echo "--- Network Services ---"
ss -tlnp 2>/dev/null || netstat -tlnp
echo ""
echo "--- SUID Binaries ---"
find / -perm -4000 -type f 2>/dev/null
echo ""
echo "--- SGID Binaries ---"
find / -perm -2000 -type f 2>/dev/null
echo ""
echo "--- World-Writable Files (non /proc,sys,dev) ---"
find / -type f -perm -0002 -not -path "/proc/*" -not -path "/sys/*" \
-not -path "/dev/*" -not -path "/tmp/*" 2>/dev/null
echo ""
echo "--- File Capabilities ---"
find / -exec getcap {} + 2>/dev/null
echo ""
echo "--- Loaded Kernel Modules ---"
lsmod 2>/dev/null || echo "No module support"
echo ""
echo "--- Kernel Security Config ---"
for param in kptr_restrict dmesg_restrict randomize_va_space yama/ptrace_scope; do
val=$(cat /proc/sys/kernel/$param 2>/dev/null)
echo " kernel.$param = ${val:-N/A}"
done
echo ""
echo "--- Seccomp Available ---"
grep -c Seccomp /proc/1/status 2>/dev/null && echo " Yes" || echo " No"
echo ""
echo "--- Total Userspace Binaries ---"
find /usr/bin /usr/sbin /bin /sbin -type f 2>/dev/null | wc -l
echo ""
echo "=== Audit Complete ==="Run it before and after hardening, diff the output, and you have a documented security posture change.
# Before hardening
sh audit.sh > /tmp/audit_before.txt
# After hardening (on the new image)
sh audit.sh > /tmp/audit_after.txt
diff /tmp/audit_before.txt /tmp/audit_after.txtLimitations and next steps
This audit covers the most common attack surface categories, but it’s not exhaustive.
What we didn’t cover:
- Hardware debug interfaces — JTAG, UART, SWD pins that bypass all software security; these require physical access to assess
- Bootloader security — U-Boot without verified boot allows arbitrary kernel loading; configuring a signed boot chain (HAB fuses, FIT image signing, dm-verity) is a separate topic
- Supply chain — Buildroot packages are pulled from upstream sources; verifying integrity of the toolchain and packages is a separate problem
- Application-layer bugs — SQL injection in the web UI, command injection in CGI scripts, authentication bypasses; these require code review, not system audit
Where to go from here:
- Apply this methodology to real firmware extracted from a device — the firmware extraction tutorial shows how to get the filesystem
- Automate the audit in CI — run it against every Buildroot build and fail the pipeline if SUID binaries or open ports appear
- Write seccomp profiles for every daemon, not just the web server
- Add a read-only root filesystem (
squashfs) and tmpfs overlays for runtime state