© Copyright 2026 Ferrantino Francesco

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 policy
frank@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
Per vedere quali file sono setuid root nel tuo ambiente:
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.
Non include invece capabilities molto pericolose come:
  • CAP_SYS_ADMIN
  • CAP_SYS_MODULE
  • CAP_SYS_BOOT
  • CAP_SYS_TIME
L'utente root dentro un container non è l'utente root dell’host ma è un root limitato dal kernel tramite capabilities. Come funzionano le capabilities in Docker e come possiamo rafforzarle eliminando quelle capabilities non necessarie:
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.

libcap
Gestione 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
Strumenti libcap:
  • 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.
libcap-ng-utils
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
Strumenti 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.
Le Linux capabilities sono permessi associati ai singoli thread. Quando un thread esegue una system call, il kernel non si limita a controllare se l’utente è root (UID 0), ma verifica se nel set Effective di quel thread è presente la capability necessaria per quell’operazione. Se la capability richiesta non è nel set Effective, l’azione viene negata e il kernel restituisce un errore come: Operation not permitted. In altre parole, anche se il processo è eseguito come root, senza la capability corretta nel set Effective non può compiere l’operazione richiesta.
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:
  1. Host - Utente non privilegiato
  2. Docker - Root con capabilities predefinite
  3. Docker - Root senza capabilities
  4. 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.
È un processo non privilegiato, ma il sistema non è strutturalmente bloccato: eventuali privilegi potrebbero essere ottenuti tramite meccanismi autorizzati (es. sudo). Il processo non possiede privilegi attivi, ma il sistema non gli impedisce, in teoria, di ottenerli tramite meccanismi autorizzati (es. sudo). In questo caso non è presente alcuna capability attiva: sia il set Permitted sia il set Effective sono a zero. Il processo non può quindi eseguire operazioni privilegiate. Tuttavia, il Bounding Set è elevato (000001ffffffffff). Decodificandolo:
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.
È un root puramente nominale: UID 0, ma privo di qualsiasi potere effettivo. Il processo è UID 0 ma non possiede alcuna capability e non potrà mai ottenerne. È un root completamente confinato. Qui la situazione è radicale. Il processo è UID 0, quindi formalmente root. Ma tutti i set di capability sono a zero, incluso il Bounding Set. Questo significa:
  • 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.
Questo è il vero superutente: controllo completo del sistema. Il processo possiede l’intero insieme di capability del kernel. Decodificando:
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.

Ferrantino Francesco

Ferrantino Francesco

Cookie Policy

Leggi  informativa Cookie Policy
Il "Sito" utilizza i Cookie per rendere i propri servizi semplici e efficienti per l’utenza che visiona le pagine di franksoft.it

Disclaimer

L'autore degli articoli non si assume nessuna responsabilità per eventuali danni ai vostri dispositivi. Tutto ciò che viene spiegato è puramente a scopo dimostrativo.
Il presente sito non costituisce testata giornalistica in quanto non ha carattere periodico ed è aggiornato secondo la mia disponibilità e la reperibilità dei materiali ivi contenuti. Pertanto, non può essere considerato in alcun modo un prodotto editoriale ai sensi della Legge n. 62 del 7/03/2001.

Licenza

Tutte le immagini presenti nel sito appartengono ai rispettivi titolari e sono utilizzate senza alcuno scopo di lucro. Ogni eventuale violazione del copyright non è intenzionale.
Quest'opera è distribuita con Licenza Creative Commons Attribuzione - Non commerciale - Non opere derivate 4.0 Internazionale.  Licenza Creative Commons