Network Hardening
Introduction
A default Torizon OS image is built for development. It advertises itself on the local network, accepts SSH on all interfaces, and has IPv6 fully enabled. Defaults like that keep bring-up and debugging smooth, but many of them are not appropriate for a production deployment. This article covers the most common network changes when you move from a development device to a production-ready image, including how they relate to the default torizon account described in Account and Access Hardening.
This article is part of the Production Hardening Checklist, which also explains how to persist these changes across a fleet.
This article complies with the Typographic Conventions for the Toradex Documentation.
Auditing Listening Sockets
Start by inventorying what the device is listening on. On an embedded device the expected set is usually small, and anything outside that set deserves either an explanation or a mask.
On a production-candidate image, inventory the TCP and UDP listeners before you ship:
# netstat -tlnp
# netstat -ulnp
Listening ports can also come from containers. Docker publishes ports through iptables destination NAT (DNAT), which means a host-side netstat is not a complete picture on its own. See Container Security on Torizon OS for how to audit and restrict published ports.
Remove or mask every unexpected listener, and re-run the audit after each change.
Baseline for a Default Development Image
Use the following as a reference for what is normal out of the box, and what to act on.
| Address | Service | Notes | Typical production action |
|---|---|---|---|
127.0.0.53:53 (TCP and UDP) | systemd-resolved | Loopback only | Leave in place, required for DNS resolution |
127.0.0.54:53 (TCP and UDP) | systemd-resolved | Loopback only | Leave in place, stub resolver interface |
127.0.0.1:39223 (TCP) | containerd CRI streaming server | Loopback only | Leave in place. Access to /var/run/docker.sock is the more important control, covered in Account and Access Hardening |
[::]:22 (TCP) | sshd through sshd.socket | Exposed on all interfaces by default | Usually mask in production, see Replacing the Standing SSH Listener |
0.0.0.0:5353 and [::]:5353 (UDP) | avahi-daemon | Announces device identity on the local network | Usually mask in production, see Avahi and mDNS |
fe80:: link-local, UDP 546 | NetworkManager | DHCPv6 client | Acceptable when IPv6 is in use, and disappears if you disable IPv6 |
Avahi and mDNS
The avahi-daemon service is enabled by default and listens on UDP port 5353 over both IPv4 and IPv6. It answers multicast DNS (mDNS) queries, which can reveal the device hostname, its services, and its presence on the local network.
Avahi is useful on development images, because TorizonCore Builder and the Torizon IDE Extension rely on discovery workflows that use mDNS. On a production device that does not need Zeroconf, leaving it enabled is usually not appropriate.
On production images, mask Avahi unless your application itself requires mDNS:
# sudo systemctl mask avahi-daemon.service avahi-daemon.socket
# sudo systemctl stop avahi-daemon.service avahi-daemon.socket
Confirm that the listeners are gone. The following command produces no output once Avahi has stopped:
# netstat -ulnp | grep 5353
If the application genuinely needs mDNS, or you are intentionally shipping a development image, leave Avahi running and record the reason. Avoid leaving it enabled speculatively on production devices.
IPv6 Policy
Torizon OS enables IPv6 by default. Disabling a protocol you actually depend on breaks connectivity, but leaving it enabled without a policy means you may be reachable on an address family you never audited.
Decide explicitly between IPv4-only and dual-stack. The trade-offs are:
| Disabling IPv6 | Keeping IPv6 |
|---|---|
| Limits what you need to audit to one address family, with no IPv6 listeners or DHCPv6 clients | Required on dual-stack and IPv6-only networks, where disabling it breaks connectivity |
| Gives a simple policy when the deployment is genuinely IPv4-only | Requires you to apply the IPv6 sysctls below and include IPv6 in the listener audit |
| Avoids discovering a forgotten global AAAA record later | Adds configuration to maintain, and accept_ra=0 must not be set blindly when you use SLAAC |
Option A: Disable IPv6
Create /etc/sysctl.d/20-disable-ipv6.conf with the following contents:
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
Apply it:
# sudo sysctl --system
Verify the result. The first command reports a value of 1, the second shows no global addresses on production interfaces, and the third produces no output once you have also masked Avahi:
# sysctl net.ipv6.conf.all.disable_ipv6
# ip -6 addr show
# netstat -ulnp | grep -E '5353|:546'
Option B: Keep IPv6
If you keep IPv6, include it in the listener audit and apply the IPv6 sysctls in Network Sysctls alongside the IPv4 settings.
Network Sysctls
These settings limit source-routed packets, ICMP redirect manipulation, and obvious spoofing. Apply the IPv4 values on every production image. Apply the IPv6 values when you keep IPv6 enabled.
Torizon OS devices normally run applications in containers, which create additional interfaces such as docker0, veth pairs, and custom bridges. That makes pinning the default values, so that new interfaces inherit safe settings, more important than setting only the all values. It also means that strict reverse-path filtering, which is often recommended in embedded Linux hardening guides, is usually not a good choice.
IPv4
| Sysctl | Stock value | Hardened value | Why |
|---|---|---|---|
net.ipv4.conf.all.accept_redirects | 0 | 0 | Stops on-link peers from injecting routes |
net.ipv4.conf.default.accept_redirects | 1 | 0 | Fixes default so that new container interfaces do not opt back in |
net.ipv4.conf.all.send_redirects | 1 | 0 | The device may forward packets for Docker, but it should not send redirects |
net.ipv4.conf.default.send_redirects | 1 | 0 | The device may forward packets for Docker, but it should not send redirects |
net.ipv4.conf.all.rp_filter | 0 | 2 | Loose anti-spoofing, discussed below |
net.ipv4.conf.default.rp_filter | 2 | 2 | Loose anti-spoofing, discussed below |
Create /etc/sysctl.d/31-network-hardening.conf with the following contents:
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.rp_filter = 2
net.ipv4.conf.default.rp_filter = 2
Apply it, then confirm the values:
# sudo sysctl --system
# sysctl net.ipv4.conf.all.rp_filter net.ipv4.conf.default.rp_filter
Some hardening guides recommend setting rp_filter to 1, which is strict mode, rather than 2, which is loose. Strict mode requires the reverse route to leave through the same interface the packet arrived on. Docker bridge networking and NAT, hairpin published ports, and multi-homed devices with ethernet0, mlan0, and docker0 all routinely violate that requirement, causing legitimate traffic to be dropped when strict mode is enabled. This can be the source of phantom bugs, in practice.
The systemd defaults in 50-default.conf already set default and per-interface values to 2, but they clear all.rp_filter, which leaves all at 0. Set all to 2 explicitly.
Some hardening guides recommend setting net.ipv4.ip_forward=0 on embedded Linux. This is not recommended on Torizon OS. Docker requires forwarding for bridge networking, and disabling it breaks container connectivity.
IPv6
Skip this subsection if you disabled IPv6 in Option A.
Torizon OS already sets net.ipv4.conf.all.accept_redirects to 0, but IPv6 still defaults accept_redirects to 1. If you keep IPv6, close that gap.
Create /etc/sysctl.d/21-ipv6-hardening.conf with the following contents:
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
Apply it:
# sudo sysctl --system
The reasoning for each value is:
| Sysctl | Stock value | Hardened value | Why |
|---|---|---|---|
net.ipv6.conf.all.accept_redirects | 1 | 0 | Stops on-link peers from injecting routes through ICMPv6 redirects |
net.ipv6.conf.default.accept_redirects | 1 | 0 | Fixes default so that new container interfaces do not opt back in |
The send_redirects and rp_filter sysctls are not applicable to IPv6.
Avoid setting accept_ra=0 on Torizon OS devices that obtain addresses and routes from Router Advertisements, which is the common NetworkManager and SLAAC case. Doing so breaks IPv6 connectivity. Disable router advertisements only when addresses are assigned statically, or by DHCPv6 without RA, and only after you have tested the result.
Replacing the Standing SSH Listener
On a default image, OpenSSH is exposed on all interfaces through systemd socket activation. That is convenient during development, when you expect to log in regularly. For many production deployments, a standing SSH listener is not appropriate, especially once the default torizon account and any development credentials are still in place.
Torizon Remote Access provides secure on-demand access through Torizon Cloud when you need break-glass connectivity, without leaving port 22 open.
The relevant units are:
| Unit | Role |
|---|---|
sshd.socket | Listens on port 22 and starts a daemon per connection |
sshd@.service | Per-connection sshd -i instance |
sshdgenkeys.service | Host key generation |
Mask the socket so that nothing listens on port 22:
# sudo systemctl mask sshd.socket
# sudo systemctl stop sshd.socket
Verify the result. The first command reports the unit as masked and inactive, and the second produces no output:
# systemctl status sshd.socket
# netstat -tlnp | grep ':22'
Masking sshd.socket removes your network login. Provision the device for Torizon Remote Access, and confirm that a session actually works, before you disable the sshd socket.
Do not run these commands over the SSH connection you depend on. Apply the mask from a serial console, or from a working Torizon Remote Access session, preferably on a device you can physically reach.
For fleets, mask sshd.socket on a reference board and capture the unit mask with TorizonCore Builder, so that devices never ship with a standing listener in the first place. See Applying and Persisting Changes for the procedure.