How to Enable TUN/TAP Inside an LXC Container: /dev/net/tun, AppArmor, cgroups, and VPN Setup

How to enable TUN/TAP inside an LXC container

TUN/TAP are Linux virtual network devices used by VPN and tunneling software (for example OpenVPN, and some routing/lab setups). When you run a VPN inside an LXC container and see errors like “/dev/net/tun: No such file or directory”, “Operation not permitted”, or “Cannot open TUN/TAP dev”, the cause is almost always the same: the container cannot see /dev/net/tun, or it is not allowed to open that character device. This guide takes a practical, layered approach: verify the host, expose the device to the container, allow it via cgroups, confirm AppArmor/SELinux isn’t blocking it, and validate the result after reboot.

LXC containers are lightweight and efficient, but networking features that depend on kernel devices require careful permissioning. If your workload relies on VPN tunnels, routing, or advanced network control, make sure the underlying platform fits the task. For most users that means a Virtual Servers environment where you can tune the host. For maximum performance and special network topologies, Dedicated Servers are often chosen. If you only need a website without OS-level VPN requirements, Hosting is usually simpler.

Think of it in layers. TUN/TAP is a host-side kernel device that must be present as /dev/net/tun. The container must then: (1) see the device node, (2) have cgroup permission to use the character device (major/minor), and (3) not be blocked by security frameworks like AppArmor or SELinux. If you troubleshoot in this order, you avoid random trial-and-error.

1) Host checks: is TUN available?

Start on the host. Verify the device exists and the tun module is loaded. On many systems, /dev/net/tun is created automatically. If it is missing, you can create it manually (or fix udev). Confirm the module with lsmod and load it with modprobe if needed.

# on the host
ls -l /dev/net/tun
lsmod | grep tun
modprobe tun

# if /dev/net/tun is missing:
mkdir -p /dev/net
mknod /dev/net/tun c 10 200
chmod 600 /dev/net/tun

The TUN device is commonly character device c 10:200. If your distribution differs, confirm with “stat /dev/net/tun”. Once the host is correct, move to the container configuration.

2) Expose /dev/net/tun to the container (LXC config)

LXC uses a container config file (often /var/lib/lxc/NAME/config) to define mounts and permissions. You typically need two things: a bind mount that makes /dev/net/tun visible inside the container, and a cgroup rule that allows the container to access the character device. The exact directive can differ between cgroup v1 and v2, but the intent is the same: allow “c 10:200 rwm”.

# LXC config examples

# 1) bind-mount the device node into the container
lxc.mount.entry = /dev/net/tun dev/net/tun none bind,create=file 0 0

# 2) allow the character device (cgroup v1)
lxc.cgroup.devices.allow = c 10:200 rwm

# 2b) cgroup v2 variant used on some hosts
lxc.cgroup2.devices.allow = c 10:200 rwm

After editing, restart the container. Inside the container, /dev/net/tun should exist. If it still doesn’t, confirm the mount entry path and that the container actually uses that config file (some platforms generate configs dynamically).

3) AppArmor/SELinux: solving “Operation not permitted”

If /dev/net/tun exists in the container but your VPN still fails with “Operation not permitted”, the device access is being blocked by a security layer. On Ubuntu hosts, AppArmor is a common cause; on some RHEL/CentOS-based hosts, SELinux may be involved. LXC often applies a default AppArmor profile that can restrict device access.

A practical way to confirm the issue: try reading the device node (it should not say “No such file”; it may return an error like “File descriptor in bad state”, which is normal for a device node). Then check host logs for denies. If you see AppArmor denials referencing /dev/net/tun, you can create a more permissive profile for that container or, as a temporary test only, use an unconfined profile to confirm the root cause.

# inside the container
ls -l /dev/net/tun
cat /dev/net/tun

For SELinux, the exact remediation depends on the policy. The key point is: if cgroups and mounts look correct but access is denied, security policy is the next place to look.

4) Privileged vs unprivileged containers and required capabilities

Privileged containers are often easier for device access because “root in container” maps more directly to host permissions, but they carry higher risk. Unprivileged containers are safer, but you may need additional configuration to allow device usage cleanly. If your VPN or networking workload is business-critical, you may prefer to run it in a VM/VPS rather than an LXC container to reduce edge cases and simplify support.

Some VPN scenarios also require network administration capabilities. For example, creating routes, enabling forwarding, or manipulating iptables/nft may require CAP_NET_ADMIN (and sometimes CAP_NET_RAW). LXC allows you to keep specific capabilities. Use the minimum set needed for your service, not a blanket “all capabilities”.

# LXC config (example if your VPN needs it)
lxc.cap.keep = net_admin net_raw

5) Validate: confirm the tunnel interface and traffic flow

After restarting the container, verify /dev/net/tun exists and then start your VPN service. OpenVPN typically creates tun0. Some setups may create tap0 (layer-2). Confirm with “ip a” inside the container. If the interface appears but traffic doesn’t flow, you are now dealing with routing and firewalling, not device availability.

For NAT-style VPN gateways, decide where NAT happens. In LXC environments, NAT is often implemented on the host depending on the network model, because containers may not be permitted to fully control the host’s network stack. If your goal is “VPN clients reach the internet through this box”, plan forwarding, NAT rules, and policy routing accordingly.

Common mistakes and security recommendations

The most common mistakes are: (1) the host tun module isn’t loaded or /dev/net/tun doesn’t exist, (2) the device node isn’t bind-mounted into the container, (3) cgroups do not allow c 10:200, (4) AppArmor/SELinux blocks device access, and (5) the container lacks CAP_NET_ADMIN for route/firewall actions. Fix them in that order and you’ll usually find the problem quickly.

Security-wise, avoid setting everything to “unconfined” across the board. If you only need TUN/TAP for one container, grant device access only to that container and only the minimum capabilities required. Ideally, isolate VPN services in their own container/VM so the blast radius is limited if the VPN service is ever misconfigured or compromised.