$ cat usb-serial-into-unprivileged-lxc.md
Passing a USB Serial or Webcam Device Into an Unprivileged Proxmox LXC
Unprivileged LXC containers are the right default on Proxmox. Root inside
the container isn’t root on the host. That same UID remapping breaks the
usual instinct for granting device access: add the service user to
dialout or video or whatever group. It won’t work, and it’s worth
knowing why before an hour goes into staring at a permission-denied error.
Why group membership fails
When an unprivileged container bind-mounts a host device node, the
container’s view of that device’s ownership goes through the same UID/GID
remapping as everything else. A device owned by root:dialout on the
host shows up inside the container as some remapped, meaningless UID:GID
pair, typically rendered as nobody:nogroup. Adding your service user to
a group inside the container does nothing. The group the device actually
belongs to, from the container’s point of view, isn’t a group that exists
in any meaningful sense inside that namespace.
What actually works: world read-write, at the host level
Make the device world-RW on the host via a udev rule, so permissions don’t depend on group membership surviving the remap at all.
# /etc/udev/rules.d/99-mydevice.rules (on the Proxmox host)
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", MODE="0666"
Match on vendor/product ID (lsusb shows both) rather than a device path
like /dev/ttyUSB0. Those paths shift if other USB-serial adapters get
plugged in a different order after a reboot; the vendor/product ID pair
doesn’t.
Then, on the container side, two lines in /etc/pve/lxc/<vmid>.conf:
lxc.cgroup2.devices.allow: c 188:0 rw
lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file
The major:minor pair (188:0 here, for a USB-serial ttyUSB0) has to
match what the device actually enumerates as on the host. Check with
ls -la /dev/ttyUSB0 before writing the config. A UVC webcam behaves the
same way but typically exposes two nodes, a capture node and a metadata
node, so both need cgroup allow and mount lines.
Two gotchas up front
New lxc.mount.entry lines only take effect at container start, not a
service restart. Adding device passthrough to a running container means a
full pct reboot <vmid>, not restarting whatever service inside is trying
to use the device.
Re-verify the major:minor pairing if the device is ever unplugged and replugged alongside other USB-serial devices. Enumeration order isn’t guaranteed, and a config pinned to the wrong major:minor silently fails to grant access to the device you actually care about.
Once both pieces are in place, the container sees a world-RW device node and doesn’t need a matching group at all. That’s the whole trick for working around the UID remap.