RouterOS 7 can run containers: small Linux systems (an AdGuard, a monitor, a RADIUS server) that live inside the router. Many picture them as miniature virtual machines, each sealed in its own box. They are not, and the difference matters. In this article we start a container in the lab, go inside it and look at what it really sees of the router: the kernel, the memory, the processes, the disk. Then we make it privileged and find out how much the perimeter widens. At the end, the cases where it has already gone wrong, and a checklist for using them without handing the router to an image pulled off the Internet.

Step 1: a container is not a virtual machine

A virtual machine has a kernel all of its own: the hypervisor fakes a complete piece of hardware for it, and it runs its own operating system on top. If something bad happens inside the VM, there is still a hypervisor in between.

A container does not. It has no kernel of its own: it uses the router’s, the same one that runs RouterOS. What the container brings with it is only the userland: the programs and libraries of its image (an Alpine, a trimmed-down Debian, a single binary). The isolation is built by the kernel with two mechanisms:

  • namespaces decide what it sees: its own processes and not the others’, its own network interfaces and not the router’s, its own users;
  • cgroups decide how much it uses: how much RAM, how much CPU.

It is real isolation, but it is a filtered view onto the same kernel, not a wall between two systems. Keep this in mind: everything else in the article is a consequence of that sentence.

Step 2: the gate — package and device-mode

On RouterOS containers are not active out of the box. Two things are needed. The first is the container package, which is not part of the base system: you download it from the MikroTik downloads page (among the extra packages for the same router version), upload it to the file list and reboot.

/system package print
# NAME       VERSION
0 container  7.25
1 routeros   7.25

The second is device-mode. It is a set of switches that limits what can be done on the router even with the administrator password. On a freshly installed router, containers are off:

/system device-mode print
                 mode: advanced
            container: no
   ...

To turn them on you request the change, and here comes the interesting part:

/system device-mode update container=yes
  update: turn off power in 3m38s to activate changes

The router does not accept the change on its own. It waits for proof that someone is physically there: you must cut the power (or press the reset/mode button) within the time shown, or the request lapses. The reason is simple: whoever gets into the router remotely, even with admin credentials, must not be able to turn containers on by themselves. We come back to this in Step 7.

On a CHR, which has no buttons and no power supply, the “physical proof” is a reset of the virtual machine from the hypervisor: in the lab that was enough, and on reboot container was yes. The principle does not change: it takes access below RouterOS, not inside it.

Deep dive: Device-mode on RouterOS: the physical lock on dangerous features — the four modes, the attempt counter, the flagged state and the anti-downgrade list.

Step 3: the first container

A container on RouterOS connects to the network through a veth interface (a pair of virtual cards: one end inside the container, the other in the router). We put it in a dedicated bridge, with an address for the router and NAT toward the outside:

/interface veth add name=veth1 address=172.17.0.2/24 gateway=172.17.0.1
/interface bridge add name=ctbr
/ip address add address=172.17.0.1/24 interface=ctbr
/interface bridge port add bridge=ctbr interface=veth1
/ip firewall nat add chain=srcnat src-address=172.17.0.0/24 action=masquerade

Then we tell the router where to pull images from and create the container. We use an Alpine image: Alpine Linux is a tiny Linux distribution built for containers. Where a Debian or an Ubuntu weigh hundreds of megabytes, Alpine fits in a few, because it uses a smaller C library (musl instead of glibc) and an essential toolbox (busybox). It is the base most of the world’s Docker images are built on: we choose it because it is light, starts in an instant and is perfect for looking inside. The sleep command only keeps it alive so we can enter it:

/container config set registry-url=https://registry-1.docker.io tmpdir=tmp
/container add remote-image=library/alpine:latest interface=veth1 \
    root-dir=ct/alpine cmd="sleep 36000" logging=yes name=alpine
/container start alpine
/container print
#   NAME    ROOT-DIR    INTERFACE  MEMORY-CURRENT  CONTAINER-SIZE
0 R alpine  /ct/alpine  veth1      316.0KiB        8.0MiB

Eight megabytes on disk, less than a megabyte of RAM. Let’s go in:

/container shell alpine

Step 4: what a “normal” container sees

From here on we are inside the container, with a Linux prompt. First question: which kernel am I running on?

/ # uname -a
Linux ct-host 5.6.3-64 #1 SMP Thu Oct 22 14:17:50 EEST 2026 x86_64 Linux

/ # cat /proc/version
Linux version 5.6.3-64 (gitlab-runner@cicd-a13.mt.lv) (gcc version 16.2.0 (GCC)) ...

Here is the proof of Step 1. The image is Alpine, but the kernel is the 5.6.3 compiled by MikroTik: the router’s. And ct-host is not a name chosen by the container: it is the router’s identity. Whoever is inside therefore knows at once which kernel they are running on, version and build date included. For an attacker it is the first useful piece of information: they know which version to look up exploits for.

The libraries, instead, tell a different story:

/ # cat /etc/os-release | head -2
NAME="Alpine Linux"
ID=alpine

/ # ls /lib
apk  firmware  ld-musl-x86_64.so.1  libc.musl-x86_64.so.1  ...

They are the image’s (musl, from Alpine), not RouterOS’s. The router’s files, binaries and configuration cannot be seen from here. Shared kernel, separate userland.

Let’s see what else is filtered and what is not:

/ # ps
PID   USER     TIME  COMMAND
    1 root      0:00 sleep 36000
    2 root      0:00 /bin/sh

/ # ip addr      # only lo and veth1

/ # head -1 /proc/meminfo
MemTotal:         883908 kB

/ # cat /proc/loadavg
0.00 0.00 0.00 1/101 7

/ # mount | head -1
/dev/sda2 on / type ext4 (rw,nodev,noatime)

The picture is this:

  • Isolated (namespaces): the processes (it sees only its own, not RouterOS’s), the network interfaces (only lo and its veth), the kernel log (dmesg replies Operation not permitted).
  • Visible anyway: the kernel version and build, the router’s identity, the router’s total and free RAM (/proc/meminfo), its uptime (/proc/uptime), the CPU model, the load and the process count of the whole system (that 1/101 in /proc/loadavg), the name of the disk it sits on.

And who is the user?

/ # id
uid=0(root) gid=0(root)

/ # cat /proc/self/uid_map
         0      32768 1878523903

Inside it is root (uid 0). But the uid_map line must be read carefully, because it says something else entirely. Its three columns are “id inside the container — matching id outside, on the router — how many ids”:

         0      32768 1878523903
         │          │          │
    uid inside  uid outside how many ids
   (0 = root)  (on router)  are mapped

So the container’s root (uid 0) is not the router’s root: seen from the kernel it is user 32768, an ordinary user with no privileges on the system. This is the user namespace, and it is the piece that keeps the isolation standing: the container has all the powers of root, but only over its own things. Keep an eye on that 32768: in Step 5 it will change, and that is where the whole difference lies.

Step 5: privileged=yes, when the container becomes the router

From RouterOS 7.24 the /container add command has a privileged option. We create a second container identical to the first, with one difference only:

/container add remote-image=library/alpine:latest interface=veth2 \
    root-dir=ct/alpine-priv cmd="sleep 36000" logging=yes \
    name=alpine-priv privileged=yes

We go in and ask the same questions again:

/ # cat /proc/self/uid_map
         0          0 4294967295
         │          │          │
    uid inside  uid outside how many ids
   (0 = root)  (0 = the      (all)
                router's root!)

Look at the middle column. It was 32768; now it is 0. It means the container’s root (uid 0 inside) matches uid 0 outside: the real root of the router. This is the “identity mapping”, and the user namespace is effectively gone. Root inside is root on the router. The consequences show up at once:

/ # dmesg | tail -2
[ 2338.350159][ T160] br0: port 2(veth6) entered learning state
[ 2338.350252][ T160] br0: port 2(veth6) entered forwarding state

/ # grep nf_conntrack /proc/slabinfo
nf_conntrack          24     24    320   12    1 ...

/ # ls /dev | wc -l
402

/ # ls /dev/sda*
/dev/sda  /dev/sda1  /dev/sda2

The container now reads the router’s kernel log, the kernel’s internal memory statistics (including the connection-tracking table) and, above all, it sees the router’s /dev: 402 entries instead of a dozen, among them the disks. We did a single test, read-only, reading the first kilobyte of the system partition:

/ # dd if=/dev/sda2 bs=1k skip=1 count=1 | od -A x -t x1 | sed -n 4p
000030 50 3e e3 6a 07 00 ff ff 53 ef 01 00 01 00 00 00

That 53 ef is the signature of an ext4 filesystem: the container is reading the router’s disk, raw, sector by sector. With real root and all capabilities, what can be read can also be written. We did not, on purpose.

The other namespaces, instead, stay in place: even the privileged container sees only its own processes and only lo and its veth. But it is small comfort. A privileged=yes container must be treated as what it is: a program running as root on the router.

Step 6: a real case, mikroscope

What is privileged=yes for, then? For anyone who wants to read the router’s internal state. A well-made example is mikroscope: a small containerised agent that samples the shared kernel many times a second (CPU, memory, counters, kernel log) and sends the data to an external collector. The author measured a cost of about 2.7% of one core and 13 MiB of RAM.

mikroscope uses exactly the mechanism of Step 5: it creates the container with privileged=yes, because only that way can it read /dev/kmsg and /proc/slabinfo. And it is honest about it: in its code the choice is documented as “a real privilege grant”, and the planning command prints it before applying. That is the right way to do it: knowing what you grant, and granting it on purpose.

Credit. mikroscope is a project by José M. Requena Plens, released under the MIT license: github.com/jmrplens/mikroscope. It requires RouterOS 7.24 or later and device-mode container=yes; it runs on ARM, ARM64 and x86_64. The resource figures quoted are the ones measured by the author.

Step 7: it has already happened

That the shared kernel is the weak spot is not theory. MikroTik itself writes it in the container documentation: “An expert with knowledge how to build exploits will be able to jailbreak/elevate to root”. And it warns that an image with an exploit can leave a backdoor that stays even after the container is removed. That is why device-mode with physical confirmation exists.

Two documented cases:

  • CVE-2022-34960 (2022). In the first versions with containers (RouterOS 7.4beta4), mounts followed symbolic links starting from the router’s root instead of the container’s. Researcher Rasmus Moorats used a crafted mount to overwrite a system binary and get a root shell on the router. It took the ability to create containers, so admin access. Fixed in 7.4beta5 / 7.4. The technical write-up: Symlinks as mount portals.
  • CVE-2026-89021 (October 2026). A specially crafted image, with crafted links, during import can write or delete files anywhere on the router’s storage. You don’t even need to start the container: downloading it is enough. Fixed in RouterOS 7.24.2 stable; according to the official advisory the fix is not included in the 7.23.x long-term branch and no backport is planned.

The moral of both stories is the same: whoever can put a container on the router, or whoever controls the image you put on it, holds a piece of the router.

Step 8: a checklist for using them without regrets

  • Don’t need them? Leave them off. device-mode container=no and no container package: zero surface.
  • RouterOS up to date, stable branch. The kernel is shared: one flaw in it is a flaw for every container and for the router. If you run containers on long-term 7.23.x, CVE-2026-89021 stays open.
  • Images only from sources you trust. Official, maintained, or built by you. An abandoned image on a public registry is strangers’ software running inside your router.
  • privileged=yes only if you know why. It is root on the router. If the service works without it (an AdGuard, an uptime monitor), leave it without.
  • Mounts to a minimum. Every router folder you mount into the container is an access you grant.
  • Dedicated bridge and firewall. The veth in its own bridge, not in the LAN; rules that say what the container may reach. A compromised container must not become a bridgehead toward the rest of the network.
  • No container services exposed to the Internet without a strong reason, and never with default passwords.
  • Cap the resources (memory-max) so a runaway container does not eat the router’s RAM.

Frequently asked questions

Can a container see the RouterOS configuration?

A normal container cannot: it sees its own filesystem, not RouterOS’s, and it does not see the router’s processes. It does, however, see shared-kernel information (version, RAM, uptime, load). A privileged=yes container sees the router’s /dev, disks included: at that point the separation rests only on the goodwill of the software running inside.

If I have the admin password, can I turn containers on remotely?

No, and it is deliberate. The /system device-mode update container=yes command asks for a physical confirmation (power or button) within a few minutes. On a CHR the confirmation is a reset of the virtual machine, so it takes access to the hypervisor.

Why am I root inside the container?

Because the image expects it, but in a normal container that root is mapped to an ordinary router user (the user namespace): it has full powers only over the container’s things. With privileged=yes the mapping disappears and root becomes real root.

From a privileged container can I see the router’s users or change its password?

Not directly. Even with privileged=yes the container has a separate filesystem (it is the image’s Alpine, not RouterOS): RouterOS commands and its user list do not appear as files inside the container. What changes is access to the router’s raw disk (/dev/sda), where configuration and credentials live. In theory, then, with real root and the disk in hand one could work on the router’s storage from outside RouterOS — which is exactly the difference between “root in the container” and “root on the router”. We do not do it here: reading or tampering with credentials is the territory of the vulnerabilities in Step 7, not something to repeat for practice. The takeaway is that a privileged container must be considered, in terms of trust, as the router itself.

How much does a container cost in resources?

It depends on the image. An Alpine idling on sleep takes about 8 MiB on disk and less than 1 MiB of RAM. Real services (a filtering DNS, a monitor) ask for tens of MiB: check the router’s RAM and free space before installing them.

Is a container safer than a service installed directly on the router?

It is more isolated than a program with no isolation, but less than a virtual machine: it shares the kernel. The risk is not only in the container itself, but in the image you download, the privileges you give it and the services you expose.

Tested in the lab on CHR RouterOS 7.25 (stable): installing the container package, the factory state of device-mode (container: no) and enabling it with a “physical” confirmation (reset of the VM from the hypervisor); an Alpine container connected via veth and a dedicated bridge; from inside the container, reading the kernel and build (uname, /proc/version), the image’s libraries, processes, interfaces, /proc/meminfo, /proc/uptime, /proc/loadavg, uid_map and dmesg; comparison with a second privileged=yes container (identity uid_map, readable dmesg and /proc/slabinfo, the router’s /dev with its disks, read-only reading of the ext4 superblock of the system partition). mikroscope’s behaviour verified against its public source code; CVEs verified against the cited sources.