Skip to main content
Version: Torizon OS 7.x.y

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.

AddressServiceNotesTypical production action
127.0.0.53:53 (TCP and UDP)systemd-resolvedLoopback onlyLeave in place, required for DNS resolution
127.0.0.54:53 (TCP and UDP)systemd-resolvedLoopback onlyLeave in place, stub resolver interface
127.0.0.1:39223 (TCP)containerd CRI streaming serverLoopback onlyLeave in place. Access to /var/run/docker.sock is the more important control, covered in Account and Access Hardening
[::]:22 (TCP)sshd through sshd.socketExposed on all interfaces by defaultUsually mask in production, see Replacing the Standing SSH Listener
0.0.0.0:5353 and [::]:5353 (UDP)avahi-daemonAnnounces device identity on the local networkUsually mask in production, see Avahi and mDNS
fe80:: link-local, UDP 546NetworkManagerDHCPv6 clientAcceptable 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 IPv6Keeping IPv6
Limits what you need to audit to one address family, with no IPv6 listeners or DHCPv6 clientsRequired on dual-stack and IPv6-only networks, where disabling it breaks connectivity
Gives a simple policy when the deployment is genuinely IPv4-onlyRequires you to apply the IPv6 sysctls below and include IPv6 in the listener audit
Avoids discovering a forgotten global AAAA record laterAdds 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​

SysctlStock valueHardened valueWhy
net.ipv4.conf.all.accept_redirects00Stops on-link peers from injecting routes
net.ipv4.conf.default.accept_redirects10Fixes default so that new container interfaces do not opt back in
net.ipv4.conf.all.send_redirects10The device may forward packets for Docker, but it should not send redirects
net.ipv4.conf.default.send_redirects10The device may forward packets for Docker, but it should not send redirects
net.ipv4.conf.all.rp_filter02Loose anti-spoofing, discussed below
net.ipv4.conf.default.rp_filter22Loose 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
caution

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.

caution

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:

SysctlStock valueHardened valueWhy
net.ipv6.conf.all.accept_redirects10Stops on-link peers from injecting routes through ICMPv6 redirects
net.ipv6.conf.default.accept_redirects10Fixes default so that new container interfaces do not opt back in

The send_redirects and rp_filter sysctls are not applicable to IPv6.

Do not disable router advertisements

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:

UnitRole
sshd.socketListens on port 22 and starts a daemon per connection
sshd@.servicePer-connection sshd -i instance
sshdgenkeys.serviceHost 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'
Set up break-glass access first

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.

Send Feedback!