Nel modello tradizionale di Linux essere root significava avere controllo totale sul sistema, ma con l’introduzione del meccanismo delle capabilities questo approccio è stato superato permettendo di suddividere i privilegi amministrativi in permessi più piccoli e specifici. Analizziamo l’evoluzione dal modello basato su UID 0 e bit setuid fino alle capabilities, facendo riferimento alla guida ufficiale capabilities(7), per comprendere come il kernel decida realmente se un’operazione è consentita oppure no.
Linux divide i processi in due categorie: i processi privilegiati eseguiti come utente root con UID 0 e i processi non privilegiati utenti normali con UID diverso da zero.
Il comando ps aux divide i processi in privilegiati e non privilegiati in base all’UID e con l'esecuzione quello che vedi è l’elenco dei processi attivi nel sistema insieme all’utente che li sta eseguendo: è proprio la colonna USER che ti permette di distinguere i processi privilegiati UID 0 quindi root e i processi non privilegiati utenti normali come frank, ftp, ecc.
frank@debian:~$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.5 0.7 23432 14132 ? Ss 20:33 0:03 /sbin/init
root 2 0.0 0.0 0 0 ? S 20:33 0:00 [kthreadd]
root 3 0.0 0.0 0 0 ? S 20:33 0:00 [pool_workqueue_release]
root 4 0.0 0.0 0 0 ? I< 20:33 0:00 [kworker/R-kvfree_rcu_reclaim]
root 5 0.0 0.0 0 0 ? I< 20:33 0:00 [kworker/R-rcu_gp]
root 6 0.0 0.0 0 0 ? I< 20:33 0:00 [kworker/R-sync_wq]
root 7 0.0 0.0 0 0 ? I< 20:33 0:00 [kworker/R-slub_flushwq]
root 8 0.0 0.0 0 0 ? I< 20:33 0:00 [kworker/R-netns]
root 12 0.0 0.0 0 0 ? I 20:33 0:00 [kworker/u8:0-ipv6_addrconf]
root 13 0.0 0.0 0 0 ? I< 20:33 0:00 [kworker/R-mm_percpu_wq]
root 14 0.0 0.0 0 0 ? I 20:33 0:00 [rcu_tasks_kthread]
root 15 0.0 0.0 0 0 ? I 20:33 0:00 [rcu_tasks_rude_kthread]
root 16 0.0 0.0 0 0 ? I 20:33 0:00 [rcu_tasks_trace_kthread]
root 17 0.0 0.0 0 0 ? S 20:33 0:00 [ksoftirqd/0]
root 18 0.0 0.0 0 0 ? I 20:33 0:00 [rcu_preempt]
Il problema principale di questo modello è la sicurezza: un processo root ha un controllo totale sul sistema potendo leggere ogni file, caricare moduli del kernel e manipolare i dischi, il che crea un enorme rischio in caso di compromissione.
Per permettere a un utente normale di eseguire operazioni privilegiate il sistema ha introdotto il bit setuid. Quando il bit setuid è impostato su un file eseguibile il processo risultante dall’esecuzione assume l’UID effettivo del proprietario del file invece di quello dell’utente che lo ha avviato. Se il file è di proprietà di root il programma verrà eseguito con privilegi root. E’ applicabile solo ai file eseguibili e non agli script.
Creiamo un file di esempio:root@debian:~# touch file_1
root@debian:~# ls -l file_1
-rw-rw-r-- 1 root root 0 Feb 24 20:56 file_1
Impostiamo il bit setuid:
root@debian:~# chmod 4755 file_1
root@debian:~# ls -l
total 0
-rwsr-xr-x 1 root root 0 Feb 24 21:01 file_1
Oppure in forma simbolica:
root@debian:~# chmod u+s file_1
root@debian:~# ls -l
total 0
-rwSrw-r-- 1 root root 0 Feb 24 21:03 file_1
La s al posto della x nel campo dell’utente indica che il bit setuid è attivo.
Attenzione alla S maiuscola: il file non ha il permesso di esecuzione ma ha il setuid: -rwSrw-r-- in questo caso bisogna aggiungere il permesso di esecuzione:root@debian:~# chmod u+x file_1
root@debian:~# ls -l
total 0
-rwsrw-r-- 1 root root 0 Feb 24 21:03 file_1
E diventa: -rwsrw-r--
Eseguibili comunemente configurati con bit setuid root
sudo: permette di eseguire comandi come root o altro utente secondo le policyfrank@debian:~$ ls -l /usr/bin/sudo
-rwsr-xr-x 1 root root 306456 Jun 30 2025 /usr/bin/sudo
passwd: modifica la password dell’utente
frank@debian:~$ ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 118168 Apr 19 2025 /usr/bin/passwd
su: permette di cambiare identità utente
frank@debian:~$ ls -l /usr/bin/su
-rwsr-xr-x 1 root root 84360 May 10 2025 /usr/bin/su
chsh: cambia la shell dell’utente
frank@debian:~$ ls -l /usr/bin/chsh
-rwsr-xr-x 1 root root 52936 Apr 19 2025 /usr/bin/chsh
chfn: modifica le informazioni dell’utente
frank@debian:~$ ls -l /usr/bin/chfn
-rwsr-xr-x 1 root root 70888 Apr 19 2025 /usr/bin/chfn
newgrp: permette di cambiare gruppo primario nella sessione
frank@debian:~$ ls -l /usr/bin/newgrp
-rwsr-xr-x 1 root root 18816 May 10 2025 /usr/bin/newgrp
gpasswd: gestione gruppi
frank@debian:~$ ls -l /usr/bin/gpasswd
-rwsr-xr-x 1 root root 88568 Apr 19 2025 /usr/bin/gpasswd
root@debian:~# sudo find / -perm -4000 -type f 2>/dev/null
/usr/bin/umount
/usr/bin/gpasswd
/usr/bin/chfn
/usr/bin/newgrp
/usr/bin/mount
/usr/bin/chsh
/usr/bin/passwd
/usr/bin/sudo
/usr/bin/su
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/openssh/ssh-keysign
sudo è un programma setuid root: quando un utente normale lo esegue diventa temporaneamente root per poter controllare i permessi ed eseguire un comando con privilegi elevati.
Le capabilities sono preferite perché consentono di concedere solo i permessi necessari evitando di eseguire un programma con pieni privilegi da root.
L’esempio è modificare l’ora all’interno del container:frank@debian:~$ docker run -it ubuntu:22.04 /bin/bash
root@cd001568376b:/# date +%T -s "09:09:09"
date: cannot set date: Operation not permitted
09:09:09
Purtroppo non è consentito, quindi per esaminare le capabilities sull’host senza esaminare i containers possiamo usare un set di utilità Linux esistenti. La prima che possiamo usare è pscap per mostrare qualsiasi processo sull’host che abbia tutte le capabilities disponibili o un sottoinsieme di esse e possiamo vedere che le capabilities sono in realtà utilizzate anche da systemd:
frank@debian:~$ pscap
ppid pid uid command capabilities
1 306 root systemd-journal chown, dac_override, dac_read_search, fowner, setgid, setuid, sys_ptrace, sys_admin, audit_control, mac_override, syslog, audit_read +
1 332 systemd-timesync systemd-timesyn sys_time @ +
1 337 root systemd-udevd chown, dac_override, dac_read_search, fowner, fsetid, kill, setgid, setuid, setpcap, linux_immutable, net_bind_service, net_broadcast, net_admin, net_raw, ipc_lock, ipc_owner, sys_module, sys_rawio, sys_chroot, sys_ptrace, sys_pacct, sys_admin, sys_boot, sys_nice, sys_resource, sys_tty_config, mknod, lease, audit_write, audit_control, setfcap, mac_override, mac_admin, syslog, block_suspend, audit_read, perfmon, bpf, checkpoint_restore +
1 642 root cron full +
1 643 messagebus dbus-daemon audit_write @ +
1 646 root qemu-ga full +
1 647 root systemd-logind chown, dac_override, dac_read_search, fowner, linux_immutable, sys_admin, sys_tty_config, audit_control, mac_admin +
1 721 root agetty full +
1 722 root vsftpd full +
1 724 root containerd full +
1 726 root sshd full +
1 735 root dockerd full +
726 1059 root sshd-session full +
1 1065 frank systemd wake_alarm +
337 1213 root (udev-worker) chown, dac_override, dac_read_search, fowner, fsetid, kill, setgid, setuid, setpcap, linux_immutable, net_bind_service, net_broadcast, net_admin, net_raw, ipc_lock, ipc_owner, sys_module, sys_rawio, sys_chroot, sys_ptrace, sys_pacct, sys_admin, sys_boot, sys_nice, sys_resource, sys_tty_config, mknod, lease, audit_write, audit_control, setfcap, mac_override, mac_admin, syslog, block_suspend, audit_read, perfmon, bpf, checkpoint_restore +
337 1222 root (udev-worker) chown, dac_override, dac_read_search, fowner, fsetid, kill, setgid, setuid, setpcap, linux_immutable, net_bind_service, net_broadcast, net_admin, net_raw, ipc_lock, ipc_owner, sys_module, sys_rawio, sys_chroot, sys_ptrace, sys_pacct, sys_admin, sys_boot, sys_nice, sys_resource, sys_tty_config, mknod, lease, audit_write, audit_control, setfcap, mac_override, mac_admin, syslog, block_suspend, ausudo filecap -a 2>/dev/nulldit_read, perfmon, bpf, checkpoint_restore +
337 1223 root (udev-worker) chown, dac_override, dac_read_search, fowner, fsetid, kill, setgid, setuid, setpcap, linux_immutable, net_bind_service, net_broadcast, net_admin, net_raw, ipc_lock, ipc_owner, sys_module, sys_rawio, sys_chroot, sys_ptrace, sys_pacct, sys_admin, sys_boot, sys_nice, sys_resource, sys_tty_config, mknod, lease, audit_write, audit_control, setfcap, mac_override, mac_admin, syslog, block_suspend, audit_read, perfmon, bpf, checkpoint_restore +
Un’altra utilità chiamata filecap esamina il file system alla ricerca di file a cui sono state assegnate determinate capabilities:
frank@debian:~$ sudo filecap -a 2>/dev/null
set file capabilities rootid
effective /usr/sbin/nginx net_bind_service
Come Docker gestisce i container con privilegi eseguiamo nginx in background:
frank@debian:~$ docker run -d nginx:latest
4abbf187a61c9249a90734e3a8bcdf768e041e8c5629ed18776ef8b569da7604
pscap cercherà nginx per mostrare qualsiasi processo e qualsiasi istanza di nginx a cui ho assegnato delle capabilities:
frank@debian:~$ pscap | grep -i nginx
1766 1790 root nginx chown, dac_override, fowner, fsetid, kill, setgid, setuid, setpcap, net_bind_service, net_raw, sys_chroot, mknod, audit_write, setfcap +
E’ mostrato il processo nginx che è il nostro container e le capabilities assegnate decise dal progetto Docker. Quindi, quando Docker esegue un nuovo container ha un set predefinito di capabilities che gli sono assegnate e che vogliamo ridurlo per migliorare la sicurezza.
Docker normalmente include le capabilities come:- CAP_CHOWN
- CAP_DAC_OVERRIDE
- CAP_FOWNER
- CAP_setuid
- CAP_SETGID
- CAP_NET_BIND_SERVICE
- CAP_KILL
- ecc.
- CAP_SYS_ADMIN
- CAP_SYS_MODULE
- CAP_SYS_BOOT
- CAP_SYS_TIME
frank@debian:~$ docker run -it raesene/alpine-containertools /bin/bash
e4d44a757ffc:/# amicontained
Container Runtime: not-found
Has Namespaces:
pid: true
user: false
AppArmor Profile: docker-default (enforce)
Capabilities:
BOUNDING -> chown dac_override fowner fsetid kill setgid setuid setpcap net_bind_service net_raw sys_chroot mknod audit_write setfcap
Seccomp: filtering
Blocked Syscalls (54):
MSGRCV SYSLOG SETSID USELIB USTAT SYSFS VHANGUP PIVOT_ROOT _SYSCTL ACCT SETTIMEOFDAY MOUNT UMOUNT2 SWAPON SWAPOFF REBOOT SETHOSTNAME SETDOMAINNAME IOPL IOPERM CREATE_MODULE INIT_MODULE DELETE_MODULE GET_KERNEL_SYMS QUERY_MODULE QUOTACTL NFSSERVCTL GETPMSG PUTPMSG AFS_SYSCALL TUXCALL SECURITY LOOKUP_DCOOKIE CLOCK_SETTIME VSERVER MBIND SET_MEMPOLICY GET_MEMPOLICY KEXEC_LOAD ADD_KEY REQUEST_KEY KEYCTL MIGRATE_PAGES UNSHARE MOVE_PAGES PERF_EVENT_OPEN FANOTIFY_INIT OPEN_BY_HANDLE_AT SETNS KCMP FINIT_MODULE KEXEC_FILE_LOAD BPF USERFAULTFD
Looking for Docker.sock
Queste sono le capabilities che abbiamo ed è stato assegnato di default in Docker e possiamo vedere nella riga: chown dac_override fowner fsetid kill setgid setuid setpcap net_bind_service net_raw sys_chroot mknod audit_write setfcap
Per eliminare tutte le capabilities ed avere un container in esecuzione senza capabilities:$ docker run -it --cap-drop=all raesene/alpine-containertools /bin/bash
f459c6b01bc2:/# amicontained
ontainer Runtime: not-found
Has Namespaces:
pid: true
user: false
AppArmor Profile: docker-default (enforce)
Capabilities:
Seccomp: filtering
Blocked Syscalls (56):
MSGRCV SYSLOG SETSID SETGROUPS USELIB USTAT SYSFS VHANGUP PIVOT_ROOT _SYSCTL CHROOT ACCT SETTIMEOFDAY MOUNT UMOUNT2 SWAPON SWAPOFF REBOOT SETHOSTNAME SETDOMAINNAME IOPL IOPERM CREATE_MODULE INIT_MODULE DELETE_MODULE GET_KERNEL_SYMS QUERY_MODULE QUOTACTL NFSSERVCTL GETPMSG PUTPMSG AFS_SYSCALL TUXCALL SECURITY LOOKUP_DCOOKIE CLOCK_SETTIME VSERVER MBIND SET_MEMPOLICY GET_MEMPOLICY KEXEC_LOAD ADD_KEY REQUEST_KEY KEYCTL MIGRATE_PAGES UNSHARE MOVE_PAGES PERF_EVENT_OPEN FANOTIFY_INIT OPEN_BY_HANDLE_AT SETNS KCMP FINIT_MODULE KEXEC_FILE_LOAD BPF USERFAULTFD
Looking for Docker.sock
Il fatto che non sono presenti capabilities comunque non ci impedisce di fare cose all’interno di questo container come elencare i file e creare nuove directory:
e4d44a757ffc:/# ls
bin etc mnt scripts
charts home opt srv
containerd-rootless-setuptool.sh lib proc sys
containerd-rootless.sh lib64 root tmp
dev manifests run usr
entrypoint.sh media sbin var
e4d44a757ffc:/# mkdir new_dir
e4d44a757ffc:/# ls
bin home opt sys
charts lib proc tmp
containerd-rootless-setuptool.sh lib64 root usr
containerd-rootless.sh manifests run var
dev media sbin
entrypoint.sh mnt scripts
etc new_dir srv
Insomma, le capabilities permettono di suddividere i poteri del superutente root in permessi granulari. Invece di eseguire un'intera applicazione come root le si possono assegnare solo le capacità specifiche necessarie per compiere una determinata azione come il binding su una porta privilegiata in caso di vulnerabilità. Per esempio, esiste una capability specifica per usare le porte sotto la 1024, un’altra per amministrare la rete, un’altra ancora per usare socket raw.
Quando avvii un programma su un sistema basato su Linux nasce un processo: non è altro che un programma in esecuzione con la sua memoria, il suo spazio e le sue risorse. Anche un container visto dal kernel è semplicemente un processo isolato.
Dentro un processo possono esserci uno o più thread: il processo è l’entità principale mentre i thread sono le attività che lavorano al suo interno, condividono la stessa memoria e le stesse risorse ma possono eseguire operazioni diverse in parallelo.
Quando un processo vuole fare qualcosa, ad esempio aprire un file, usare la rete o ascoltare su una porta non può farlo direttamente deve chiedere il permesso al kernel tramite una system call, il kernel riceve la richiesta e decide se concederla e qui entrano in gioco le Linux capabilities.
Ogni volta che viene avviato un container avviamo un processo Linux, quindi Docker o Podman utilizzeranno le funzionalità di Linux esistenti per fornire l'isolamento. Quello che viene mostrato è il file system radice del container nginx e naturalmente il file appena creato. Quindi, i container sono solo dei processi e ci sono molti modi di interagire come se fosse qualsiasi processo Linux.
frank@debian:~$ ps -fC nginx
UID PID PPID C STIME TTY TIME CMD
Avviamo un istanza del server web nginx:
frank@debian:~$ docker run --name serverweb -d nginx
24b214f7f33d771f9b7c620f28bef89474cba3c6671b52fa263cf110c72cd606
frank@debian:~$ ps -fC nginx
UID PID PPID C STIME TTY TIME CMD
root 2348 2323 1 23:25 ? 00:00:00 nginx: master process nginx -g daemon off;
ftp 2396 2348 0 23:25 ? 00:00:00 nginx: worker process
ftp 2397 2348 0 23:25 ? 00:00:00 nginx: worker process
Crea un nuovo file nel container:
frank@debian:~$ docker exec serverweb touch /file01
Poichè i container sono solo processi:
frank@debian:~$ sudo ls -l /proc/2348/root/file01
-rw-r--r-- 1 root root 0 Feb 24 23:26 /proc/2348/root/file01
Le Linux capabilities fanno parte del kernel quindi non si attivano e quello che installiamo sono gli strumenti per vederle e modificarle.
libcapGestione e manipolazione delle capabilities dei file e del set corrente. Installazione libcap:
- Debian: apt install libcap2-bin
- Arch Linux: pacman -S libcap
- Red Hat: dnf install libcap
- OpenSuse: zypper install libcap-progs
- getcap: visualizza le capabilities assegnate a uno o più file specifici.
- setcap: definisce, modifica o rimuove le capabilities di un file.
- capsh: una shell wrapper per testare, esplorare e debuggare le capabilities.
- getpcaps: mostra le capabilities attive di un processo specifico tramite il suo PID.
Analisi e monitoraggio dell'intero sistema. Installazione libcap-ng:
- Debian: apt install libcap-ng-utils
- Arch Linux: sudo pacman -S libcap-ng
- Red Hat: dnf install libcap-ng-utils
- OpenSuse: zypper install libcap-ng-utils
- pscap: elenca i processi in esecuzione che possiedono capabilities (mostra chi ha privilegi speciali).
- netcap: mostra le applicazioni di rete e le relative capabilities (identifica socket aperti con privilegi).
- filecap: scansiona i binari nelle directory di sistema o nel PATH alla ricerca di capabilities impostate.
- captest: esegue una suite di test per verificare se le capabilities funzionano correttamente nel kernel o per dimostrarne l'effetto.
frank@debian:~$ cat /proc/$$/status | grep Cap
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000
Le capabilities di un processo sono organizzate in cinque set principali visualizzabili tramite il file system /proc e decodificabili con il comando capsh:
Prima colonna
- Inherited (Ereditate): contiene le capabilities che vengono trasferite da un processo genitore a un processo figlio.
- Permitted (Permesse): rappresenta l'insieme globale delle capabilities che un thread può utilizzare durante il suo ciclo di vita. Il kernel può aggiungerne o rimuoverne da questo set.
- Effective (Effettive): è il set finale di permessi che il kernel controlla effettivamente per autorizzare o meno un'azione.
- Bounding (Limitate): definisce l'elenco massimo di permessi che un thread può avere.
- Ambient: Un set speciale utilizzato principalmente per azioni non privilegiate.
Seconda colonna
Si tratta dell’elenco delle capabilities in formato esadecimale. Per decodificarlo possiamo usare il comando capsh. Ad esempio, se decodifichiamo la capability "CapEff: 0000000000000000" non vedremo nulla, questo perchè la sessione è in esecuzione come utente normale.root@debian:~# capsh --decode=0000000000000000
0x0000000000000000=
Passiamo a una sessione root e a confrontare:
root@debian:~# cat /proc/$$/status | grep Cap
CapInh: 0000000000000000
CapPrm: 000001ffffffffff
CapEff: 000001ffffffffff
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000
root@debian:~# capsh --decode=000001ffffffffff
0x000001ffffffffff=cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read,cap_perfmon,cap_bpf,cap_checkpoint_restore
Quattro scenari reali per comprendere il modello di sicurezza Linux
Per comprendere il modello di sicurezza basato sulle Linux capabilities confrontiamo 4 situazioni reali:- Host - Utente non privilegiato
- Docker - Root con capabilities predefinite
- Docker - Root senza capabilities
- Host - Root con privilegi completi
1. Host - Utente non privilegiato
Osserviamo prima cosa riporta /proc/$$/status e poi decodifichiamo i valori con capsh --decode.frank@debian:~$ cat /proc/$$/status | grep Cap
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000
Qui vediamo che:
- CapInh (Inheritable) non contiene capabilities trasferibili ai processi figli. Il processo non può propagare privilegi.
- CapPrm (Permitted) non include capabilities utilizzabili. Non ha privilegi disponibili.
- CapEff (Effective) non ha capabilities attive. Non può eseguire operazioni privilegiate.
- CapBnd (Bounding) non applica un vincolo restrittivo: il limite massimo teorico è alto.
- CapAmb (Ambient) non contiene capabilities mantenute automaticamente attraverso execve.
root@debian:~# capsh --decode=000001ffffffffff
0x000001ffffffffff=cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read,cap_perfmon,cap_bpf,cap_checkpoint_restore
si ottiene un elenco completo di capabilities tra cui cap_sys_admin, cap_sys_module, cap_net_admin, cap_sys_time e molte altre. Questo non significa che l’utente possieda tali privilegi ma che il sistema non ha imposto un limite strutturale massimo. Tramite meccanismi autorizzati (ad esempio sudo o un binario con capability assegnate), il processo potrebbe ottenerle. Nessun privilegio attivo, ma escalation possibile in modo controllato.
2. Docker – Root con capabilities predefinite
frank@debian:~$ docker run -it raesene/alpine-containertools /bin/bash
# cat /proc/$$/status | grep Cap
CapInh: 0000000000000000
CapPrm: 00000000a80425fb
CapEff: 00000000a80425fb
CapBnd: 00000000a80425fb
CapAmb: 0000000000000000
Qui vediamo che:
- CapInh non prevede propagazione automatica di capability ai processi figli.
- CapPrm (Permitted) include un sottoinsieme limitato di capability utilizzabili.
- CapEff (Effective) attiva esattamente quel sottoinsieme: il processo può usarlo.
- CapBnd (Bounding) applica un vincolo preciso: il processo non potrà mai superare quel limite.
- CapAmb (Ambient) non mantiene capability oltre i confini definiti dal runtime.
È un root confinato: possiede solo le capability necessarie non quelle amministrative complete. Il processo è UID 0 ma può utilizzare solo le capabilities concesse da Docker. Non può superare quel perimetro.
Qui il processo è UID 0 quindi formalmente root. Tuttavia il set di capability è limitato. Decodificando:root@debian:~# capsh --decode=00000000a80425fb
0x00000000a80425fb=cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
si ottiene un sottoinsieme ridotto che include capability come:cap_chown
cap_setuid
cap_net_bind_service
cap_net_raw
cap_sys_chroot
cap_mknod
Mancano invece capability critiche come:
cap_sys_admin
cap_sys_module
cap_sys_time
cap_net_admin
cap_sys_boot
Inoltre, il Bounding Set è identico al set Permitted ed Effective. Questo significa che il container non potrà mai andare oltre quei privilegi: il limite massimo è già stato ristretto dal runtime Docker prima dell’esecuzione del processo. In questo scenario il root non è il root dell’host ma un root confinato dal kernel tramite capability.
3. Docker - Root senza capabilities
Avviamo il container rimuovendo tutte le capabilities:frank@debian:~$ docker run -it --cap-drop=all raesene/alpine-containertools /bin/bash
All'interno il container Docker:-
/# cat /proc/$$/status | grep Cap
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000000000000000
CapAmb: 0000000000000000
Qui vediamo che:
- CapInh non prevede alcuna capability ereditabile.
- CapPrm (Permitted) non include capability disponibili.
- CapEff (Effective) non attiva alcun privilegio.
- CapBnd (Bounding) applica il vincolo massimo possibile: nessuna capability potrà mai essere ottenuta.
- CapAmb (Ambient) non mantiene alcun privilegio attraverso exec.
- nessuna capability attiva (CapEff = 0)
- nessuna capability permessa (CapPrm = 0)
- nessuna possibilità di ereditarle
- nNessun tetto massimo disponibile (CapBnd = 0)
In pratica è un root completamente disarmato: anche se il processo è UID 0 il kernel non gli permetterà alcuna operazione privilegiata perché il controllo avviene sulle capabilities nel set Effective.
4. Host - Root con privilegi completi
root@debian:~# cat /proc/$$/status | grep Cap
CapInh: 0000000000000000
CapPrm: 000001ffffffffff
CapEff: 000001ffffffffff
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000
Qui vediamo che:
- CapInh non è utilizzato per propagare privilegi in modo automatico, ma potrebbe esserlo in scenari specifici.
- CapPrm (Permitted) è senza restrizioni: include l’intero insieme di capability disponibili nel kernel.
- CapEff (Effective) è senza limitazioni: tutte le capability sono attive.
- CapBnd (Bounding) non applica alcun vincolo: non esiste un limite superiore imposto.
- CapAmb (Ambient) non è utilizzato, ma potrebbe essere impiegato per mantenere capability attraverso exec in scenari avanzati.
root@debian:~# capsh --decode=000001ffffffffff
0x000001ffffffffff=cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read,cap_perfmon,cap_bpf,cap_checkpoint_restore
Questo significa che il processo root possiede praticamente tutte le capabilities disponibili nel kernel e non ha limitazioni strutturali. In questo scenario non ci sono restrizioni imposte da Docker o da altri meccanismi di isolamento. Il processo ha pieno controllo del sistema. Questo caso rappresenta il modello classico del superutente: un processo con UID 0 che possiede anche tutte le capability attive.
Esempio 1 - Aprire la porta 80 senza root
E' possibile assegnare una capability direttamente a un file eseguibile e questo significa che un programma può ottenere uno specifico privilegio senza essere eseguito come root.
Avviare Python su una porta privilegiata ad esempio la porta 80 (che normalmente richiede UID 0). Per impostazione predefinita se provi a farlo come utente normale:
frank@debian:~$ python3 -m http.server 80
Traceback (most recent call last):
File "", line 198, in _run_module_as_main
File "", line 88, in _run_code
File "/usr/lib/python3.13/http/server.py", line 1323, in
test(
~~~~^
HandlerClass=handler_class,
^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<3 lines>...
protocol=args.protocol,
^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/usr/lib/python3.13/http/server.py", line 1270, in test
with ServerClass(addr, HandlerClass) as httpd:
~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.13/socketserver.py", line 457, in __init__
self.server_bind()
~~~~~~~~~~~~~~~~^^
File "/usr/lib/python3.13/http/server.py", line 1317, in server_bind
return super().server_bind()
~~~~~~~~~~~~~~~~~~~^^
File "/usr/lib/python3.13/http/server.py", line 136, in server_bind
socketserver.TCPServer.server_bind(self)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
File "/usr/lib/python3.13/socketserver.py", line 478, in server_bind
self.socket.bind(self.server_address)
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^
PermissionError: [Errno 13] Permission denied
Otterrai un errore: Permission denied.
Per permettere a Python di aprire la porta 80 senza usare root puoi assegnare la capability CAP_NET_BIND_SERVICE direttamente al binario:frank@debian:~$ sudo setcap cap_net_bind_service=+ep "$(readlink -f /usr/bin/python3)"
Verifica:
frank@debian:~$ sudo getcap "$(readlink -f /usr/bin/python3)"
/usr/bin/python3.13 cap_net_bind_service=ep
A questo punto, un utente normale può eseguire:
frank@debian:~$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
e il server partirà correttamente sulla porta 80 anche senza UID 0. Non stai rendendo l’utente root ma stai dando a quel programma un singolo privilegio specifico. È questo il vantaggio delle capabilities rispetto al vecchio modello tutto o niente basato su root.
Esempio 2 - Root senza bind su porta 80
Rimuovere una capability da una sessione root e bloccare il bind su porte. Oltre a un utente normale che non può aprire porte inferiori a 1024 nell'esempio viene limitato anche l’utente root:frank@debian:~$ sudo capsh --drop=cap_net_bind_service -- -c bash
root@debian:/home/frank#
Verifica:
root@debian:/home/frank# cat /proc/$$/status | grep Cap
CapInh: 0000000000000000
CapPrm: 000001fffffffbff
CapEff: 000001fffffffbff
CapBnd: 000001fffffffbff
CapAmb: 0000000000000000
Ora anche se l’UID è 0 provando:
root@debian:/home/frank# python3 -m http.server 80
bash: /usr/bin/python3: Operation not permitted
Otteniamo errore perchè il kernel non decide solo in base all’UID ma controlla le capabilities presenti nel set Effective del thread. Se la capability non è presente l’operazione viene negata anche a root. Questo dimostra il punto chiave: essere root non significa più poter fare tutto. Sono le capabilities a determinare realmente cosa è consentito.
Esempio 3 - Servizio systemd su porta 80 senza root
Con systemd è possibile controllare in modo centralizzato quali capabilities può avere un servizio senza dover usare root. Immagina di voler avviare un piccolo server web scritto in Python sulla porta 80 ma senza eseguire il servizio come root. Creiamo un file di servizio ad esempio: /etc/systemd/system/webdemo.service
[Unit]
Description=Demo Web Server
[Service]
User=webuser
ExecStart=/usr/bin/python3 -m http.server 80
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
Il servizio non viene eseguito come root ma come un utente normale, ad esempio webuser. Questo utente non ha privilegi amministrativi e quindi di base non può aprire la porta 80.
Attraverso la configurazione di systemd però al processo viene concessa una sola capability specifica: CAP_NET_BIND_SERVICE cioè il permesso di aprire porte privilegiate inferiori a 1024. Nessun altro privilegio viene concesso.
Il servizio quindi non è root, non possiede tutte le capabilities disponibili nel sistema e non può fare operazioni amministrative generiche. Può esclusivamente aprire la porta 80 perché è l’unico privilegio che gli è stato esplicitamente assegnato.
Inoltre, con l’opzione NoNewPrivileges=true si impedisce al processo di acquisire nuovi privilegi durante l’esecuzione. Anche se ci fosse una vulnerabilità nell’applicazione il processo non potrebbe espandere i propri permessi oltre quelli iniziali.
Esempio 4 - Container Docker con sole capabilities necessarie
Anche Docker utilizza il meccanismo delle Linux capabilities direttamente a livello kernel. Quando avvii un container con:frank@debian:~$ docker run -d --name web \
-p 80:80 \
--cap-drop ALL \
--cap-add NET_BIND_SERVICE \
--cap-add CHOWN \
--cap-add SETGID \
--cap-add setuid \
nginx
ab609416405b2ae235edceda2b72084dc0584224e7dc8cdec57632f936953641
frank@debian:~$ curl localhost:80
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
Stai dicendo a Docker di rimuovere tutte le capabilities disponibili e aggiungere solo le capabilities CAP_NET_BIND_SERVICE, CHOWN, SETGID e setuid
Il processo dentro il container non ha pieni poteri e non possiede l’insieme standard di capabilities che Docker assegna di default ed ha solo la capacità di aprire la porta privilegiata come la 80. Può soltanto eseguire il bind su una porta inferiore a 1024.
Questo significa che il processo di Nginx all’interno del container è fortemente limitato. Non può caricare moduli del kernel, quindi non può intervenire su componenti profondi del sistema operativo. Non può modificare l’orologio di sistema, perché quella operazione richiede capabilities specifiche che non gli sono state concesse. Non può gestire o riconfigurare le interfacce di rete dell’host, né manipolare impostazioni di basso livello. In generale, non può eseguire operazioni amministrative tipiche di un processo con privilegi elevati. Anche se sta girando dentro un container, il processo non è potente: può fare solo ciò che le capabilities assegnate gli permettono di fare e niente di più.
Separare i privilegi significa ridurre i danni nel caso in cui qualcosa venga compromesso.
Se un web server viene violato l’attaccante non eredita automaticamente il controllo totale del sistema. Il processo compromesso non può caricare moduli del kernel quindi non può estendere il proprio controllo a livello profondo del sistema operativo. Non può montare file system quindi non può agganciare nuove risorse o manipolare dispositivi a basso livello. Non può modificare configurazioni critiche di sistema perché non possiede le capabilities necessarie per farlo.
Anche in presenza di una vulnerabilità applicativa l’impatto resta confinato entro i limiti stabiliti dalle capabilities assegnate al processo.
Questo è l’obiettivo reale: applicare in modo concreto il principio del minimo privilegio concedendo solo ciò che serve e togliendo tutto il resto.


