RouterOS 7 può far girare dei container: piccoli sistemi Linux (un AdGuard, un monitor, un server RADIUS) che vivono dentro il router. Molti li immaginano come delle macchine virtuali in miniatura, ognuna chiusa nella sua scatola. Non è così, e la differenza conta. In questo articolo accendiamo un container in laboratorio, ci entriamo dentro e guardiamo cosa vede davvero del router: il kernel, la memoria, i processi, il disco. Poi lo rendiamo privilegiato e scopriamo quanto si allarga il perimetro. Alla fine, i casi in cui è già andata male e una checklist per usarli senza regalare il router a un’immagine scaricata da Internet.

Passo 1: un container non è una macchina virtuale

Una macchina virtuale ha un kernel tutto suo: l’hypervisor le finge un hardware completo, e lei ci fa girare sopra il proprio sistema operativo. Se dentro la VM succede qualcosa di grave, in mezzo c’è comunque l’hypervisor.

Un container no. Non ha un kernel suo: usa quello del router, lo stesso che fa girare RouterOS. Quello che il container porta con sé è solo lo userland: i programmi e le librerie della sua immagine (un Alpine, un Debian ridotto, un singolo binario). L’isolamento lo costruisce il kernel con due meccanismi:

  • i namespaces decidono cosa vede: i suoi processi e non quelli degli altri, le sue interfacce di rete e non quelle del router, i suoi utenti;
  • i cgroups decidono quanto usa: quanta RAM, quanta CPU.

È un isolamento vero, ma è una vista filtrata sullo stesso kernel, non un muro tra due sistemi. Tenetelo a mente: tutto il resto dell’articolo è la conseguenza di questa frase.

Passo 2: il cancello — pacchetto e device-mode

Su RouterOS i container non sono attivi di fabbrica. Servono due cose. La prima è il pacchetto container, che non è incluso nel sistema base: si scarica dalla pagina dei download MikroTik (tra gli extra packages della stessa versione del router), si carica nella lista file e si riavvia.

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

La seconda è il device-mode. È un insieme di interruttori che limita cosa si può fare sul router anche avendo la password di amministratore. Su un router appena installato, i container risultano spenti:

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

Per accenderli si chiede la modifica, e qui arriva la parte interessante:

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

Il router non accetta la modifica da solo. Aspetta una prova che qualcuno sia fisicamente lì: va tolta l’alimentazione (o premuto il tasto reset/mode) entro il tempo indicato, altrimenti la richiesta decade. Il motivo è semplice: chi entra nel router da remoto, anche con le credenziali di admin, non deve poter attivare i container per conto suo. Ci torniamo al Passo 7.

Su un CHR, che non ha tasti né alimentatore, la “prova fisica” è un reset della macchina virtuale dall’hypervisor: in laboratorio è bastato quello, e al riavvio container era a yes. Il principio non cambia: serve un accesso sotto RouterOS, non dentro.

Approfondimento: Device-mode su RouterOS: il lucchetto fisico sulle funzioni pericolose — le quattro modalità, il contatore dei tentativi, lo stato flagged e la lista anti-declassamento.

Passo 3: il primo container

Un container su RouterOS si collega alla rete tramite un’interfaccia veth (una coppia di schede virtuali: un capo sta nel container, l’altro nel router). La mettiamo in un bridge dedicato, con un indirizzo per il router e il NAT verso l’uscita:

/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

Poi diciamo al router da dove scaricare le immagini e creiamo il container. Usiamo un’immagine Alpine: Alpine Linux è una distribuzione Linux minuscola, pensata apposta per i container. Dove un Debian o un Ubuntu pesano centinaia di megabyte, Alpine sta in pochi megabyte, perché usa una libreria C più piccola (musl invece della glibc) e una cassetta degli attrezzi essenziale (busybox). È la base su cui è costruita gran parte delle immagini Docker del mondo: la scegliamo perché è leggera, parte in un attimo ed è perfetta per guardarci dentro. Il comando sleep serve solo a tenerla viva per poterci entrare:

/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  CPU-USAGE  CONTAINER-SIZE
0 R alpine  /ct/alpine  veth1      316.0KiB        0          8.0MiB

Otto megabyte su disco, mezzo megabyte di RAM. Entriamo:

/container shell alpine

Passo 4: cosa vede un container “normale”

Da qui in poi siamo dentro il container, con un prompt Linux. Prima domanda: su che kernel giro?

/ # 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)) #1 SMP Thu Oct 22 14:17:50 EEST 2026

Ecco la prova del Passo 1. L’immagine è Alpine, ma il kernel è il 5.6.3 compilato da MikroTik: quello del router. E ct-host non è un nome scelto dal container: è l’identity del router. Chi sta dentro sa quindi subito su quale kernel gira, versione e data di build comprese. Per un attaccante è la prima informazione utile: sa contro quale versione cercare exploit.

Le librerie, invece, raccontano un’altra storia:

/ # 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  ...

Sono quelle dell’immagine (musl, di Alpine), non quelle di RouterOS. I file, i binari e la configurazione del router da qui non si vedono. Kernel condiviso, userland separato.

Vediamo cos’altro filtra e cosa no:

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

/ # ip addr      # solo lo e 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)

Il quadro è questo:

  • Isolati (namespaces): i processi (vede solo i suoi, non quelli di RouterOS), le interfacce di rete (solo lo e la sua veth), il log del kernel (dmesg risponde Operation not permitted).
  • Visibili comunque: versione e build del kernel, l’identity del router, la RAM totale e libera del router (/proc/meminfo), il suo uptime (/proc/uptime), il modello di CPU, il carico e il numero di processi di tutto il sistema (quel 1/101 in /proc/loadavg), il nome del disco su cui sta.

E chi è l’utente?

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

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

Dentro è root (uid 0). Ma la riga di uid_map va letta con attenzione, perché dice tutt’altro. Le sue tre colonne sono «id dentro il container — id corrispondente fuori, sul router — quanti id»:

         0      32768 1878523903
         │          │          │
    uid dentro  uid fuori   quanti id
   (0 = root)  (sul router) sono mappati

Quindi il root del container (uid 0) non è il root del router: visto dal kernel è l’utente 32768, un utente qualunque, senza privilegi sul sistema. È lo user namespace, ed è il pezzo che tiene in piedi l’isolamento: il container ha tutti i poteri di root, ma solo sulle cose sue. Tenete d’occhio quel 32768: al Passo 5 cambierà, ed è lì tutta la differenza.

Passo 5: privileged=yes, quando il container diventa il router

Da RouterOS 7.24 il comando /container add ha un’opzione privileged. Creiamo un secondo container identico al primo, con una sola differenza:

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

Entriamo e ripetiamo le stesse domande:

/ # cat /proc/self/uid_map
         0          0 4294967295
         │          │          │
    uid dentro  uid fuori   quanti id
   (0 = root)  (0 = root    (tutti)
                del router!)

Guardate la colonna di mezzo. Prima era 32768; adesso è 0. Vuol dire che il root del container (uid 0 dentro) corrisponde all’uid 0 fuori: il root vero del router. È la «mappatura identità», e lo user namespace di fatto non c’è più. Root dentro è root sul router. Le conseguenze si vedono subito:

/ # 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

Il container ora legge il log del kernel del router, le statistiche interne della memoria del kernel (compresa la tabella del connection tracking) e soprattutto vede la /dev del router: 402 voci invece di una dozzina, tra cui i dischi. Abbiamo fatto una sola prova, in sola lettura, leggendo il primo kilobyte della partizione del sistema:

/ # 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

Quel 53 ef è la firma di un filesystem ext4: il container sta leggendo il disco del router, grezzo, settore per settore. Con root vero e tutte le capability, quello che si può leggere si può anche scrivere. Non l’abbiamo fatto, volutamente.

Restano invece in piedi gli altri namespaces: anche il container privilegiato vede solo i suoi processi e solo lo e la sua veth. Ma è una consolazione piccola. Un container privileged=yes va trattato come quello che è: un programma che gira come root sul router.

Passo 6: un caso reale, mikroscope

A cosa serve allora privileged=yes? A chi vuole proprio leggere lo stato interno del router. Un esempio ben fatto è mikroscope: un piccolo agente in container che campiona il kernel condiviso molte volte al secondo (CPU, memoria, contatori, log del kernel) e manda i dati a un collettore esterno. L’autore ha misurato un costo di circa 2,7% di un core e 13 MiB di RAM.

mikroscope usa esattamente il meccanismo del Passo 5: crea il container con privileged=yes, perché solo così può leggere /dev/kmsg e /proc/slabinfo. Ed è onesto al riguardo: nel suo codice la scelta è documentata come “una vera concessione di privilegi”, e il comando di pianificazione la stampa prima di applicarla. È il modo giusto di farlo: sapere che cosa si concede, e concederlo apposta.

Credito. mikroscope è un progetto di José M. Requena Plens, rilasciato con licenza MIT: github.com/jmrplens/mikroscope. Richiede RouterOS 7.24 o successivo e device-mode container=yes; gira su ARM, ARM64 e x86_64. Le cifre di consumo riportate sono quelle misurate dall’autore.

Passo 7: è già successo

Che il kernel condiviso sia il punto debole non è teoria. MikroTik stessa lo scrive nella documentazione dei container: “An expert with knowledge how to build exploits will be able to jailbreak/elevate to root”. E avverte che un’immagine con un exploit può lasciare una backdoor che resta anche dopo aver rimosso il container. È il motivo per cui esiste il device-mode con conferma fisica.

Due casi documentati:

  • CVE-2022-34960 (2022). Nelle prime versioni con i container (RouterOS 7.4beta4), i mount seguivano i link simbolici partendo dalla radice del router invece che da quella del container. Il ricercatore Rasmus Moorats ha usato un mount costruito ad arte per sovrascrivere un binario di sistema e ottenere una shell root sul router. Serviva poter creare container, quindi un accesso da amministratore. Corretta in 7.4beta5 / 7.4. Il racconto tecnico: Symlinks as mount portals.
  • CVE-2026-89021 (ottobre 2026). Un’immagine preparata apposta, con link costruiti ad arte, durante l’import può scrivere o cancellare file ovunque nello storage del router. Non serve nemmeno avviare il container: basta scaricarlo. Corretta in RouterOS 7.24.2 stable; secondo l’avviso ufficiale la correzione non è inclusa nel ramo long-term 7.23.x e non è previsto un backport.

La morale delle due storie è la stessa: chi può mettere un container sul router, o chi controlla l’immagine che ci metti, ha in mano un pezzo del router.

Passo 8: checklist per usarli senza pentirsene

  • Non ti servono? Lasciali spenti. device-mode container=no e niente pacchetto container: superficie zero.
  • RouterOS aggiornato, ramo stable. Il kernel è condiviso: una sua falla è una falla di tutti i container e del router. Se usi i container sulla long-term 7.23.x, la CVE-2026-89021 resta aperta.
  • Immagini solo da fonti di cui ti fidi. Ufficiali, mantenute, o costruite da te. Un’immagine abbandonata su un registry pubblico è software di sconosciuti che gira dentro il tuo router.
  • privileged=yes solo se sai perché. È root sul router. Se il servizio funziona senza (un AdGuard, un uptime monitor), lascialo senza.
  • Mount al minimo. Ogni cartella del router che monti nel container è un accesso che concedi.
  • Bridge dedicato e firewall. La veth in un bridge suo, non nella LAN; regole che dicono cosa il container può raggiungere. Un container compromesso non deve diventare una testa di ponte verso il resto della rete.
  • Niente servizi del container esposti su Internet senza un motivo forte, e mai con le password di default.
  • Limita le risorse (memory-max) perché un container impazzito non si mangi la RAM del router.

Domande frequenti

Un container può vedere la configurazione di RouterOS?

Un container normale no: vede il suo filesystem, non quello di RouterOS, e non vede i processi del router. Vede però informazioni del kernel condiviso (versione, RAM, uptime, carico). Un container privileged=yes vede la /dev del router, compresi i dischi: a quel punto la separazione dipende solo dalla buona volontà del software che ci gira.

Se ho la password di admin posso attivare i container da remoto?

No, ed è voluto. Il comando /system device-mode update container=yes chiede una conferma fisica (spegnimento o tasto) entro pochi minuti. Su un CHR la conferma è un reset della macchina virtuale, quindi serve l’accesso all’hypervisor.

Perché dentro il container sono root?

Perché l’immagine lo prevede, ma in un container normale quel root è mappato su un utente qualunque del router (lo user namespace): ha pieni poteri solo sulle cose del container. Con privileged=yes la mappatura sparisce e root diventa root vero.

Quanto costa un container in risorse?

Dipende dall’immagine. Un Alpine fermo su sleep occupa circa 8 MiB su disco e meno di 1 MiB di RAM. Servizi veri (un DNS filtrante, un monitor) chiedono decine di MiB: controlla RAM e spazio libero del router prima di installarli.

Da un container privilegiato posso vedere gli utenti del router o cambiarne la password?

Non in modo diretto. Anche con privileged=yes il container ha un filesystem separato (è l’Alpine dell’immagine, non RouterOS): i comandi di RouterOS e il suo elenco utenti non compaiono come file dentro il container. Quello che cambia è l’accesso al disco grezzo del router (/dev/sda), dove vivono configurazione e credenziali. In teoria, quindi, con root vero e il disco in mano si può lavorare sullo storage del router da fuori RouterOS — ed è esattamente la differenza tra «root nel container» e «root sul router». Non lo facciamo qui: leggere o manomettere le credenziali è il terreno delle vulnerabilità del Passo 7, non una cosa da rifare per esercizio. Il punto da portare a casa è che un container privilegiato va considerato, in termini di fiducia, come il router stesso.

Un container è più sicuro di un servizio installato direttamente sul router?

È più isolato di un programma senza isolamento, ma meno di una macchina virtuale: condivide il kernel. Il rischio non sta solo nel container in sé, ma nell’immagine che scarichi, nei privilegi che gli dai e nei servizi che esponi.

Provato in laboratorio su CHR RouterOS 7.25 (stable): installazione del pacchetto container, stato di fabbrica del device-mode (container: no) e attivazione con conferma “fisica” (reset della VM dall’hypervisor); container Alpine collegato via veth e bridge dedicato; da dentro il container, lettura di kernel e build (uname, /proc/version), librerie dell’immagine, processi, interfacce, /proc/meminfo, /proc/uptime, /proc/loadavg, uid_map e dmesg; confronto con un secondo container privileged=yes (uid_map identità, dmesg e /proc/slabinfo leggibili, /dev del router con i dischi, lettura in sola lettura del superblocco ext4 della partizione di sistema). Funzionamento di mikroscope verificato sul suo codice sorgente pubblico; CVE verificate sulle fonti citate.