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
loe la sua veth), il log del kernel (dmesgrisponde 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 (quel1/101in/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=noe niente pacchettocontainer: 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=yessolo 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.



