Container Security on Torizon OS
Introduction
On Torizon OS, applications run as containers defined in Docker Compose files, and they are normally delivered through Torizon Cloud and Aktualizr, or bundled into the image. Host hardening and container settings work together: least-privilege Compose options are still worth applying even after you tighten accounts and services, as 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.
Network Exposure
Docker publishes container ports using iptables destination NAT on the FORWARD path, not through the host INPUT filter. That means a host-side listener audit does not show the full picture, as noted in Network Hardening. Control the exposure in Compose instead:
services:
api:
# No host ports, so this service is only reachable on the Compose network
expose:
- "8080"
web:
ports:
# Bind to loopback, or a specific host address, rather than every interface
- "127.0.0.1:8080:8080"
networks: [frontend]
networks:
frontend:
backend:
internal: true
The rules to follow are:
- Omit
portsentirely when a service only needs to talk to other containers. - Use
internal: trueon networks that need no external connectivity. - If you add host firewall rules, put them in the
DOCKER-USERchain. Docker does not flush that chain, and it runs before Docker's own rules.
Runtime Privileges
Privileged Containers
Never ship privileged: true in production. It largely disables container isolation, including capability restrictions, the device cgroup, and seccomp.
services:
app:
privileged: false
The value is false by default, so omitting the key entirely has the same effect. If a workload appears to need privileged mode, identify the actual requirement, which is usually a specific device node, a single capability, or a volume, and grant only that.
Capabilities
Drop every capability, then add back only what the process needs:
services:
app:
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
The NET_BIND_SERVICE capability in the preceding example is illustrative. Omit cap_add entirely when the process does not need any capability.
Privilege Escalation and seccomp
services:
app:
security_opt:
- no-new-privileges:true
Docker applies its default seccomp profile automatically. Do not set seccomp:unconfined without a tested and recorded reason. Custom seccomp profiles are optional hardening beyond that default.
User and Filesystem
Running as a Non-root User
services:
app:
user: "1000:1000"
Also set USER in the Dockerfile, so that the image stays safe if a Compose file omits user. Avoid running as root inside the container unless it is unavoidable, and combine a non-root user with no-new-privileges.
Read-Only Root Filesystem
services:
app:
read_only: true
tmpfs:
- /tmp
- /run
volumes:
- app-data:/var/lib/app
Mount volumes only on the paths that genuinely need to persist.
Bind Mounts
Mount only the host paths the container requires. Never bind the host root filesystem into a production container, because a broad mount recreates the same class of host takeover as exposing the Docker socket.
Note also that a bind mount inherits the mount flags of the host path, which interacts with the nosuid guidance in Filesystem Hardening.
Canonicalize Compose Files
When you push Compose applications with the --canonicalize or --canonicalize-only options of TorizonCore Builder, the tool produces a canonical Compose file, named with a .lock.yml suffix, that pins images to content digests and normalizes the YAML. For production use, Compose files should always be canonicalized. See Signing and Pushing Torizon OS Packages to Torizon Cloud.
A Hardened Service Skeleton
The following Compose service combines the preceding settings:
services:
app:
image: registry.example.com/myapp:1.2.3
user: "1000:1000"
read_only: true
tmpfs:
- /tmp
- /run
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
volumes:
- app-data:/var/lib/app
restart: unless-stopped
volumes:
app-data:
The service publishes no ports. Add a ports entry only where a host-reachable port is required, and bind it to a specific address. Canonicalize and push the file with TorizonCore Builder when you deploy through Torizon Cloud.