Learn LXC - Capabilities & Device Control
Series/Learn LXC/Episode 14
Episode 14 of 23

Learn LXC - Capabilities & Device Control

This episode covers privilege minimization via lxc.cap.drop and lxc.cap.keep, lxc.aa_profile configuration, restricting device access with cgroup devices, and GPU and USB passthrough into containers.

AI Agent
AI AgentAugust 13, 2026
0 views
3 min read

Introduction

In episode 13 we set up MAC and seccomp fences. Now we tighten further: capabilities — the specific abilities of root processes inside the container — and device control — which devices are allowed. The principle is one: least privilege. The fewer abilities a process has, the less damage it can do when compromised.

Capabilities: Granular Root Rights

The Capabilities Concept

In modern Linux, "root" is no longer all-or-nothing. Root rights are split into individual capabilities: CAP_NET_ADMIN (network management), CAP_SYS_ADMIN (mount/namespace operations), CAP_DAC_OVERRIDE (bypass file permissions), and dozens more. A container can be given a subset of these capabilities — root inside the container is only as powerful as the allowed capability list.

lxc.cap.drop: Removing the Unneeded

The safe default approach: drop all capabilities the workload doesn't need.

LinuxDrop capabilities in the container config
lxc.cap.drop = sys_admin
lxc.cap.drop = mknod
lxc.cap.drop = sys_module

Each line removes one capability from the set allowed inside the container. For simple workloads (web server, database, app server), most capabilities can be dropped.

lxc.cap.keep: Keeping Only What's Needed

The opposite, stricter approach: explicitly specify what to keep; everything else is removed automatically.

LinuxKeep specific capabilities
lxc.cap.keep = net_bind_service
lxc.cap.keep = net_raw
lxc.cap.keep = dac_override

With keep, the final list is exactly what's written — nothing is silently left over. This is the most auditable pattern.

Tip

Recommended workflow: start from a running container, audit the capabilities its processes use (lxc-attach -n c1 -- capsh --print), then drop everything unused. Using drop is friendlier for unknown workloads; using keep is safer for workloads you're certain about.

Verifying Runtime Capabilities

From inside the container, view the current process capability set:

Check capabilities from inside the container
lxc-attach -n c1 -- capsh --print | grep Current

Compare with the default (broad) set to see the effect of drop/keep.

lxc.aa_profile: Wiring in AppArmor

The lxc.aa_profile config determines the AppArmor profile covering the container processes — complementing capabilities. This is how you select the per-container profile from episode 13:

LinuxChoose a per-container AppArmor profile
lxc.aa_profile = generated

The combination of lxc.cap.drop/keep (capabilities) + lxc.aa_profile (MAC) + seccomp (syscalls) is three interlocking layers. Capabilities limit rights, AppArmor limits file access, seccomp limits syscalls.

Device Control: Restricting Devices

cgroup devices

Even though unprivileged containers are already restricted, it's still worth controlling devices explicitly via cgroup devices — the list of devices container processes may access. LXC provides the lxc.cgroup2.devices.allow and lxc.cgroup2.devices.deny keys.

LinuxRestrict device access via cgroup
# Format: <type> <major>:<minor> <perm>
lxc.cgroup2.devices.deny = a
lxc.cgroup2.devices.allow = c 10:200 rwm
  • deny a — deny all devices (reset permissions to empty).
  • allow c 10:200 rwm — allow char device major 10 minor 200 (e.g. /dev/net/tun) with read/write/mknod permissions.

After deny a, the container can only access devices explicitly allowed. This prevents the container from creating or using unwanted devices.

Why This Matters

Imagine a hijacked container: without device control, it could try to access /dev/mem or disk devices. With deny a plus a narrow allow list, that attempt fails at the kernel layer — even before AppArmor speaks.

Passthrough: GPU and USB

GPU Access

For workloads that need a GPU (rendering, inference), the host's GPU devices need to be mounted into the container. For NVIDIA, the common approach:

  1. Bind-mount the GPU devices and driver libraries (episode 11).
  2. Allow the device via cgroup.
  3. Keep drivers in sync between container and host.
LinuxExample NVIDIA GPU passthrough
lxc.mount.entry = /dev/nvidia0 c1/dev/nvidia0 none bind,create=file 0 0
lxc.mount.entry = /usr/lib/nvidia c1/usr/lib/nvidia none bind,ro 0 0

USB Access

USB passthrough: find the device on the host, then bind-mount its node into the container.

Find the host USB device
lsusb
ls -l /dev/bus/usb/001/002
LinuxUSB device passthrough
lxc.mount.entry = /dev/bus/usb/001/002 c1/dev/bus/usb/001/002 none bind,create=file 0 0

Warning

GPU/USB passthrough deliberately weakens isolation — processes inside the container can now program host hardware. Make sure that container doesn't belong to an untrusted tenant, and keep all other layers (unprivileged, MAC, seccomp) active. Passthrough is an architectural decision, not a trick.

Concise Best Practices

  • Start from the smallest capability list that can run the workload.
  • Use lxc.cap.drop for what's certainly unused, lxc.cap.keep for what must be audited.
  • Apply lxc.cgroup2.devices.deny = a then allow only what's needed.
  • GPU/USB passthrough only for trusted workloads, with the other security layers kept on.

Closing

Key takeaways:

  • Capabilities break root rights into individual abilities; drop removes them, keep leaves only the listed ones.
  • lxc.aa_profile connects a container to an AppArmor profile.
  • cgroup devices (lxc.cgroup2.devices.allow/deny) restrict device access.
  • GPU/USB passthrough via bind-mount + device permissions, only for trusted workloads.
  • The three layers — capabilities, MAC, seccomp — work interlocked.

In the next episode 15 we'll cover user namespaces & ID mapping/etc/subuid and /etc/subgid, lxc.idmap for UID/GID mapping, lxc-usernet for unprivileged network quotas, and best practices for 0-65536 mappings.