Namespaces - Aíslar los procesos GNU/Linux en sus propios entornos de sistema

Los namespaces de GNU/Linux permiten encapsular recursos globales del sistema de forma aislada, evitando que puedan interferir con procesos que estén fuera del namespace, sin tener que recurrir a máquinas virtuales. Los cambios en el recurso global son visibles para otros procesos que son miembros del namespace, pero son invisibles para otros procesos fuera de él. Un uso de los namespaces es implementar contenedores.

GNU/Linux proporciona distintos namesapaces que han sido introducidos paulatinamente en el kernel desde la versión 2.6.23. Actualmente tenemos los siguientes namespaces:

Nombre Constante Aísla
Cgroup CLONE_NEWCGROUP Directorio raíz de cgroup
IPC CLONE_NEWIPC System V IPC, colas de mensajes POSIX
Network CLONE_NEWNET Disposirivos de red, pilas, puertos, etc.
Mount CLONE_NEWNS Puntos de montaje
PID CLONE_NEWPID IDs de procesos
User CLONE_NEWUSER IDs de usuarios y grupos
UTS CLONE_NEWUTS Nombre de host y mombre de dominio NIS

PID namespace

El primer proceso en el espacio de usuario que se ejecuta en un sistema GNU/Linux es init, que tienen el identificador de proceso (PID) 1. Este es un programa binario ejecutable (similar a cualquier binario ejecutable GNU/Linux) que se encarga de iniciar el resto del sistema lanzando los servicios/daemons correctos. Históricamente este proceso era el System V init, pero actualmente ha sido sustituido prácticamente en todas las distros por systemd. Por tanto, el proceso init es el proceso primario desde el que se extiende el árbol de procesos:

Namespaces PID

En el siguiente código vamos a crear un proceso hijo con fork y mostramos la información PID (identificador del proceso) y PPID (identificador del proceso padre) desde el proceso hijo y desde el proceso padre:

/* 
 * fork-simple.c 
 *  
 *************************************************************** 
 *  
 * (c) Author: David Quiroga 
 * e-mail: david (at) clibre (dot) io 
*/ 
 
#define _GNU_SOURCE 
#include <stdio.h> 
#include <stdlib.h> 
#include <sys/wait.h> 
#include <sys/types.h> 
#include <unistd.h> 
 
#define errExit(msg) do {\ 
fprintf (stderr, "[%s:%d]:", __FILE__, __LINE__);\ 
perror(msg); exit(EXIT_FAILURE);\ 
} while (0) 
 
int 
main() 
{ 
    pid_t childPid; 
 
    switch (childPid = fork()) { 
    case -1: // Error 
        errExit("fork"); 
 
    case 0: // Proceso hijo 
        printf("Proceso Hijo:\n"); 
        printf("\tPPID:%d\n", getppid()); 
        printf("\tPID:%d\n", getpid());  
        exit(EXIT_SUCCESS); 
 
    default: // Proceso padre 
        if (waitpid(childPid, NULL, 0) == -1) // Esperamos final del proceso hijo 
            errExit("waitpid"); 
 
        printf("Proceso Padre:\n"); 
        printf("\tPPID:%d\n", getppid()); 
        printf("\tPID:%d\n", getpid());  
        printf("\tPID del hijo: %ld\n", (long) childPid); 
        exit(EXIT_SUCCESS); 
    } 
} 

Cuando ejecutamos la aplicación vemos que la información desde el proceso padre e hijo coincide:

$./fork-simple 
Proceso Hijo:
PPID:11819
PID:11820
Proceso Padre:
PPID:11157
PID:11819
PID del hijo: 11820


$ ps -p 11157 -o "pid command"
PID COMMAND
11157 /bin/bash

Vemos también con ps que el padre del ejecutable es el shell bash. Tanto el proceso padre como el hijo ven el mismo árbol de procesos:

Vista PPID PID

Vamos a crear ahora otro proceso hijo, pero esta vez utilizando clone y le vamos a indicar que cree un nuevo namespace PID utilizando el flag CLONE_NEWIPC:

/* 
 * name-pid.c 
 *  
 *************************************************************** 
 *  
 * (c) Author: David Quiroga 
 * e-mail: david (at) clibre (dot) io 
*/ 
 
#define _GNU_SOURCE 
#include <sched.h> 
#include <stdio.h> 
#include <stdlib.h> 
#include <sys/wait.h> 
#include <unistd.h> 
 
#define errExit(msg) do {\ 
fprintf (stderr, "[%s:%d]:", __FILE__, __LINE__);\ 
perror(msg); exit(EXIT_FAILURE);\ 
} while (0) 
 
#define STACK_SIZE (1024 * 1024) 
static char child_stack[STACK_SIZE]; // Pila del proceso hijo 
 
static int child_Func() { 
 printf("Proceso Hijo:\n"); 
 printf("\tPPID:%d\n", getppid()); 
 printf("\tPID:%d\n", getpid());  
 exit(EXIT_SUCCESS); 
} 
 
int main() { 
 pid_t childPid; 
 
 childPid = clone(child_Func, 
 child_stack + STACK_SIZE, 
 CLONE_NEWPID | SIGCHLD, NULL); 
 
 if (childPid == -1) 
 errExit("clone"); 
 
 if (waitpid(childPid, NULL, 0) == -1) // Esperamos final del proceso hijo 
 errExit("waitpid"); 
 
 printf("Proceso Padre:\n"); 
 printf("\tPPID:%d\n", getppid()); 
 printf("\tPID:%d\n", getpid());  
 printf("\tPID del hijo: %ld\n", (long) childPid); 
 
 exit(EXIT_SUCCESS); 
} 

Ejecutamos la aplicación:

$ sudo ./name-pid 
[sudo] contraseña para david:
Proceso Hijo:
PPID:0
PID:1
Proceso Padre:
PPID:13956
PID:13957
PID del hijo: 13958

Como podemos ver, el proceso padre mantienen una visión convencional del árbol de PIDs, pero el proceso hijo tienen una visión diferente, tienen el PID de init (1) y no tiene proceso padre (0).

Vista PID Namespace

Network namespace

Por defecto cuando creamos un proceso, este tiene los mismos recursos de red disponibles que su proceso padre, es decir, los del sistema. Cuando apagamos la red dentro de un namespace podemos asegurarnos de que los procesos que se ejecuten allí no podrán hacer conexiones fuera del namespace.

Vamos a añadir un par de lineas al programa anterior para que nos muestre los adaptadores de red (con ip link):

/* 
 * name-lan0.c 
 *  
 *************************************************************** 
 *  
 * (c) Author: David Quiroga 
 * e-mail: david (at) clibre (dot) io 
*/ 
 
#define _GNU_SOURCE 
#include <stdio.h> 
#include <stdlib.h> 
#include <sys/wait.h> 
#include <unistd.h> 
#include <sched.h> 
 
#define errExit(msg) do { \ 
fprintf (stderr, "[%s:%d]:", __FILE__, __LINE__); \ 
perror(msg); exit(EXIT_FAILURE); \ 
} while (0) 
 
#define STACK_SIZE (1024 * 1024) 
static char child_stack[STACK_SIZE]; // Pila del proceso hijo 
 
static int child_Func() { 
    printf("Proceso Hijo:\n"); 
    printf("\tPPID:%d\n", getppid()); 
    printf("\tPID:%d\n", getpid());  
    system("ip link"); // no usar system() 
    exit(EXIT_SUCCESS); 
} 
 
int main() { 
    pid_t childPid; 
 
    childPid = clone(child_Func, 
                            child_stack + STACK_SIZE, 
                            CLONE_NEWPID | CLONE_NEWNET | SIGCHLD, NULL); 
 
    if (childPid == -1) 
        errExit("clone"); 
 
    if (waitpid(childPid, NULL, 0) == -1) // Esperamos final del proceso hijo 
        errExit("waitpid"); 
 
    printf("Proceso Padre:\n"); 
    printf("\tPPID:%d\n", getppid()); 
    printf("\tPID:%d\n", getpid());  
    printf("\tPID del hijo: %ld\n", (long) childPid); 
    system("ip link"); // no usar system() 
 
    exit(EXIT_SUCCESS); 
} 

Con el fin de simplificar el código la aplicación llama a system() para mostrar los adaptadores. Esta es una función insegura que no debemos usar en producción (ENV33-C. Do not call system(): El uso de la función system () puede resultar en vulnerabilidades explotables, en el peor de los casos permitiendo la ejecución de comandos de sistema arbitrarios). Podemos usar en su lugar alguna función de la familia exec()

Si ejecutamos la aplicación sin añadir el namespace de red con la constante CLONE_NEWNET al llamar a clone, tendríamos los mismos adaptadores en el proceso padre y el hijo. En cambio, con el namespace de red, el proceso hijo solo ve un adaptador lookback inactivo:

sudo ./name-lan 
[sudo] password for david:
Proceso Hijo:
PPID:0
PID:1
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
Proceso Padre:
PPID:2248
PID:2249
PID del hijo: 2250
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 52:54:00:ed:d1:7e brd ff:ff:ff:ff:ff:ff

De esta forma podemos asegurarnos que los procesos que se ejecutan en este namespace no podrán hacer conexiones fuera del namespace. Incluso si el proceso se ve comprometido a través de algún tipo de vulnerabilidad de seguridad, no podrá ser usado para realizar acciones como unirse a una red de bots o enviar spams.

Si queremos utilizar la red desde este namespace tenemos varias opciones:

Sin añadir adaptadores al namespace: Podemos crear una conexión desde el proceso padre y aprovechar que los procesos hijo heredan los descriptores de sus padres, este prodría usarlo dede el namespace.

Esta podría ser una buena opción de un servicio que responda peticiones por un puerto determinado. El puerto lo abre el padre y pasa el descriptor al hijo que se encarga de gestionar la comunicación. En caso de verse este comprometido por una vulnerabilidad de seguridad, el proceso no tendría adaptadores de red y por tanto tendría muy limitada su explotación.

Añadiendo apaptadores al namespace: En este caso tenemos que crear un adaptador para el namespace. Este adaptador por defecto no tendría conectividad con el adaptador físico y tendríamos que habilitarla, por ejemplo creando un bridge o bien rutando entre adaptadores y habilitando reglas de NAT y filtrado con iptables.

Tenemos que tener en cuenta el tiempo de vida de los network namespaces que creamos. Si creamos un nuevo network namespace con clone, este perdurará mientras lo haga el proceso para el que se creó. Pero puede ser más útil que el network namespace perdure más tiempo. Para ello podemos usar el comando ip netns (ip es el comando que se utiliza actualmente para gestionar la red en GNU/Linux), creando el namespace y configurando el mismo para posteriormente utilizarlo desde otro proceso.

Vamos a crear un network namespace y configurarlo para poder usarlo desde nuestra aplicación. Primero creamos el network namespace que vamos a llamar netnsp1:

sudo ip netns add netnsp1

Ahora vamos a crear un disposirivo virtual de ethernet tipo veth. Estos dispositivos pueden actuar como túneles entre network namespaces y un dispositivo físico en otro namespace:

sudo ip link add veth0 type veth peer name veth1 

Vamos a asignar el disposirivo veth1 al network namespace recien creado netnsp1:

sudo ip link set veth1 netns netnsp1

Solo nos queda configurar ambos dispositivos, dandoles una ip y activando el adaptador, primero en veth1:

sudo ip netns exec netnsp1 ip addr add 10.1.1.2/16 dev veth1
sudo ip netns exec netnsp1 ip link set veth1 up

Y ahora en veth0:

sudo ip addr add 10.1.1.1/16 dev veth0
sudo ip link set veth0 up

Ahora cambiamos nuestra aplicación para utilizar el network namespace que hemos creado. En lugar de crear el nuevo proceso usando la constante CLONE_NEWNET cuando llamamos a clone (lo que crearía otro network namespace) vamos a usar el que ya hemos creado. Para ello tenemos que llamar desde el nuevo proceso a la función setns():

/* 
 * name-lan-s2.c 
 *  
 *************************************************************** 
 *  
 * (c) Author: David Quiroga 
 * e-mail: david (at) clibre (dot) io 
*/ 
 
#define _GNU_SOURCE 
#include <stdio.h> 
#include <stdlib.h> 
#include <sys/wait.h> 
#include <unistd.h> 
#include <sched.h> 
#include <fcntl.h> 
 
#define errExit(msg) do { \ 
fprintf (stderr, "[%s:%d]:", __FILE__, __LINE__); \ 
perror(msg); exit(EXIT_FAILURE); \ 
} while (0) 
 
#define STACK_SIZE (1024 * 1024) 
static char child_stack[STACK_SIZE]; // Pila del proceso hijo 
static const char net_name[] = "/var/run/netns/netnsp1"; 
 
static int child_Func() { 
    int fd = -1; 
 
    fd = open(net_name, O_RDONLY); // Descriptor de fichero del namespace 
    if (fd == -1) 
        errExit("open"); 
 
    if (setns(fd, CLONE_NEWNET) == -1) // comparte el network namespace 
        errExit("setns"); 
 
    close(fd); 
 
    printf("Proceso Hijo:\n"); 
    printf("\tPPID:%d\n", getppid()); 
    printf("\tPID:%d\n", getpid());  
    system("ip addr ls"); 
    system("ping -c 4 10.1.1.1"); 
     
    _exit(EXIT_SUCCESS); 
} 
 
int main() { 
    pid_t childPid; 
     
    childPid = clone(child_Func, 
                            child_stack + STACK_SIZE, 
                            CLONE_NEWPID | SIGCHLD, NULL); 
 
    if (childPid == -1) 
        errExit("clone"); 
 
    if (waitpid(childPid, NULL, 0) == -1) // Esperamos final del proceso hijo 
        errExit("waitpid"); 
 
    printf("Proceso Padre:\n"); 
    printf("\tPPID:%d\n", getppid()); 
    printf("\tPID:%d\n", getpid());  
    printf("\tPID del hijo: %ld\n", (long) childPid); 
    system("ip link"); 
 
    exit(EXIT_SUCCESS); 
} 

Como vermos desde el proceso creado nos unimos con setns() al network namespace creado y si ejecutamos la aplicación:

sudo ./name-lan-s2 
Proceso Hijo:
PPID:0
PID:1
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
3: veth1@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 32:d5:eb:a6:04:97 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.1.1.2/16 scope global veth1
valid_lft forever preferred_lft forever
inet6 fe80::30d5:ebff:fea6:497/64 scope link
valid_lft forever preferred_lft forever
PING 10.1.1.1 (10.1.1.1) 56(84) bytes of data.
64 bytes from 10.1.1.1: icmp_seq=1 ttl=64 time=0.139 ms
64 bytes from 10.1.1.1: icmp_seq=2 ttl=64 time=0.081 ms
64 bytes from 10.1.1.1: icmp_seq=3 ttl=64 time=0.069 ms
64 bytes from 10.1.1.1: icmp_seq=4 ttl=64 time=0.115 ms

--- 10.1.1.1 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 78ms
rtt min/avg/max/mdev = 0.069/0.101/0.139/0.027 ms
Proceso Padre:
PPID:2666
PID:2667
PID del hijo: 2668
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 52:54:00:ed:d1:7e brd ff:ff:ff:ff:ff:ff
4: veth0@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
link/ether ea:a9:c8:ba:1f:29 brd ff:ff:ff:ff:ff:ff link-netns netnsp1

Podemos ver que el proceso creado tienen el interface veth1 y tenemos conectividad entre los dos adpatadores en diferentes network namespaces.

UTS namespace

El namespace UTS aisla dos identificadores de sistema, el nombre del nodo y el nombre del dominio. Estos idenficadores los podemos obtener con la llamada de sistema uname() y configurarlos con sethostname() y gethostname(). Esto permite que el proceso dentro del uts namespace tenga su propio nombre de host y nombre de dominio NIS.

/* 
 * name-uts.c 
 *  
 *************************************************************** 
 *  
 * (c) Author: David Quiroga 
 * e-mail: david (at) clibre (dot) io 
*/ 
 
#define _GNU_SOURCE 
#include <stdio.h> 
#include <stdlib.h> 
#include <sys/wait.h> 
#include <unistd.h> 
#include <sched.h> 
#include <fcntl.h> 
#include <sys/utsname.h> 
 
#define errExit(msg) do { \ 
fprintf (stderr, "[%s:%d]:", __FILE__, __LINE__); \ 
perror(msg); exit(EXIT_FAILURE); \ 
} while (0) 
 
#define STACK_SIZE (1024 * 1024) 
static char child_stack[STACK_SIZE]; // Pila del proceso hijo 
 
static void print_nodeuts() { 
    struct utsname utsn; 
     
    if (uname(&utsn) == -1) 
        errExit("uname"); 
 
    printf("Dominio: %s - host: %s\n", utsn.domainname, utsn.nodename); 
} 
 
static int child_Func() { 
    printf("Proceso Hijo:\n"); 
    print_nodeuts(); 
    printf("Cambiamos nombre de dominio y host:\n"); 
    if (sethostname("tux1", 4) == -1) 
        errExit("sethostname");  
    if (setdomainname("CLibre", 6) == -1) 
        errExit("setdomainname"); 
    print_nodeuts(); 
    _exit(EXIT_SUCCESS); 
} 
 
int main() { 
    pid_t childPid; 
     
    childPid = clone(child_Func, 
                            child_stack + STACK_SIZE, 
                            CLONE_NEWUTS | SIGCHLD, NULL); 
 
    if (childPid == -1) 
        errExit("clone"); 
 
    if (waitpid(childPid, NULL, 0) == -1) // Esperamos final del proceso hijo 
        errExit("waitpid"); 
 
    printf("Proceso Padre:\n"); 
    print_nodeuts(); 
 
    exit(EXIT_SUCCESS); 
} 

Ejecutamos la aplicación y podemos ver como cambiamos el nombre de dominio y el host en nuestro proceso creado mientras se mantienen el del proceso padre:

sudo ./name-uts 
Proceso Hijo:
Dominio: (none) - host: debianBuster
Cambiamos nombre de dominio y host:
Dominio: CLibre - host: tux1
Proceso Padre:
Dominio: (none) - host: debianBuster

Mount namespace

Con mount namespace podemos aislar el conjunto de puntos de montaje del sistema de archivos vistos por un grupo de procesos. De esta forma, los procesos en diferentes mount namesapaces pueden tener diferentes vistas de la jerarquía del sistema de archivos.

Crear un mount namespace tiene un efecto similar al de hacer un chroot (). Con chroot () cambiamos el directorio raíz por uno especificado que será usado como raíz para aquellos path que comiencen por /. Pero mount namespace va más allá, y permite que cada uno de los procesos aislados tenga una vista completamente diferente de la estructura de punto de montaje de todo el sistema. Esto nos permite tener una raíz diferente para cada proceso aislado, así como otros puntos de montaje que son específicos de esos procesos.

Cuando usamos la constante CLONE_NEWNS en clone para crear un nuevo proceso con su mount namespace, este ve los mismos puntos de montaje que su proceso padre, pero al estar en un nuevo mount namespace puede montar y desmontar puntos de montaje sin que afecte al proceso padre.

/* 
 * name-mount.c 
 *  
 *************************************************************** 
 *  
 * (c) Author: David Quiroga 
 * e-mail: david (at) clibre (dot) io 
*/ 
 
#define _GNU_SOURCE 
#include <stdio.h> 
#include <stdlib.h> 
#include <sys/wait.h> 
#include <unistd.h> 
#include <sched.h> 
#include <fcntl.h> 
#include <sys/mount.h> 
#include <errno.h> 
 
#define errExit(msg) do { \ 
fprintf (stderr, "[%s:%d]:", __FILE__, __LINE__); \ 
perror(msg); exit(EXIT_FAILURE); \ 
} while (0) 
 
#define STACK_SIZE (1024 * 1024) 
static char child_stack[STACK_SIZE]; // Pila del proceso hijo 
 
static const char tempdirname1[] = "/home/david/Proyects/cfunc/temp1"; 
static const char tempdirname2[] = "/home/david/Proyects/cfunc/temp2"; 
 
static int child_Func1() { 
    printf("Proceso Hijo1:\n"); 
    if(mount(tempdirname1, "temp", "none", MS_BIND, NULL) == -1) 
        errExit("mount"); 
    system("tree temp --noreport"); 
 
    sleep(5); // tiempo para que se ejecute el segundo proceso 
 
    printf("Proceso Hijo1:\n"); 
    system("tree temp --noreport");  
 
    _exit(EXIT_SUCCESS); 
} 
 
static int child_Func2() { 
    printf("Proceso Hijo2:\n"); 
    if(mount(tempdirname2, "temp", "none", MS_BIND, NULL) == -1) 
        errExit("mount"); 
    system("tree temp --noreport"); 
 
    sleep(5); // en este momento estan los dos procesos activos 
                    // y sus carpetas temp son diferentes  
    printf("Proceso Hijo2:\n"); 
    system("tree temp --noreport"); 
 
    _exit(EXIT_SUCCESS); 
} 
 
int main() { 
    pid_t childPid; 
     
    childPid = clone(child_Func1, 
                            child_stack + STACK_SIZE, 
                            CLONE_NEWNS | CLONE_NEWUSER | SIGCHLD, NULL); 
    if (childPid == -1) 
        errExit("clone"); 
 
    sleep(2); 
 
    childPid = clone(child_Func2, 
                            child_stack + STACK_SIZE, 
                            CLONE_NEWNS | CLONE_NEWUSER | SIGCHLD, NULL); 
    if (childPid == -1) 
        errExit("clone"); 
 
    for(;;) { 
        childPid = wait(NULL); 
        if (childPid == -1) { 
            if (errno == ECHILD)  
                exit(EXIT_SUCCESS); 
            else 
                errExit("wait"); 
        } 
    } 
} 

Para este ejemplo tenemos dos carpetas /home/david/Proyects/cfunc/temp1 y /home/david/Proyects/cfunc/temp2 y en cada una un fichero 111111.txt y 222222.txt respectivamente. El programa va a crear los procesos y cada uno montara una de esas carpetas en temp. De esta forma cada uno tendra un árbol de directorio difirente, si ejecutamos la aplicación tenemos:

sudo ./name-mount
Proceso Hijo1:
temp
└── 111111.txt
Proceso Hijo2:
temp
└── 222222.txt
Proceso Hijo1:
temp
└── 111111.txt
Proceso Hijo2:
temp
└── 222222.txt

El aislamiento completo entre los diferentes namespaces puede resultar excesivo. Podemos tomar como ejemplo que quisieramos hacer disponible un CD-ROM a todos los namespaces, para lo que tendriamos que montarlo de forma individual en todos los mount namespaces.

Para evitar este problema se agrego una funcionalidad llamada subárboles compartidos. Esta funcion permite la propagación automática y controlada de eventos de montaje y desmontaje entre espacios de nombres. De esta forma, por ejemplo, podemos montar el CD_ROM en un namespace que puede desencadenar un montaje de ese disco en todos los demás namespaces.

Bajo la función de subárboles compartidos, cada punto de montaje está marcado con un "tipo de propagación", que determina si los puntos de montaje creados y eliminados bajo este punto de montaje se propagan a otros puntos de montaje. Hay cuatro tipos diferentes de propagación:

MS_SHARED: Este punto de montaje comparte eventos con miembros de un grupo de pares. Los eventos de montaje y desmontaje inmediatamente debajo de este punto de montaje se propagarán a los otros puntos de montaje que son miembros del grupo de pares.

MS_PRIVATE: Este punto de montaje es privado; no tiene un grupo de iguales. Los eventos de montaje y desmontaje no se propagan dentro o fuera de este punto de montaje.

MS_SLAVE: Los eventos de montaje y desmontaje se propagan en este punto de montaje desde un grupo de pares compartido (maestro). Los eventos de montaje y desmontaje bajo este punto de montaje no se propagan a ningún par.

MS_UNBINDABLE: Esto es como un montaje privado, y además este montaje no se puede montar en enlace. Los intentos de montar este montaje con el indicador MS_BIND fallarán.

Ver la información detallada en man mount_namespaces.

User namespace

Con user namespace podemos aislar un grupo de procesos con un ID de usuario y un ID de grupo para ese namesapace. Como se indica en la documentación, los ID de usuario y grupo de un proceso pueden ser diferentes dentro y fuera del namespace de usuario. El caso más interesante aquí es que un proceso puede tener una ID de usuario normal sin privilegios fuera del namespace de usuario y al mismo tiempo tener una ID de usuario 0 (root) dentro del namespace. Esto significa que el proceso tiene todos los privilegios de root para operaciones dentro del namespace de usuario, pero no tiene privilegios para operaciones fuera del namespace.

Vamos a ver un ejemplo sencillo en el que creamos un nuevo namespace con clon con el indicador CLONE_NEWUSER. Este proceso secundario se crea con un conjunto completo de capabilities en el nuevo namespace de usuario. Pero ese proceso secundario no tiene capabilities en el namespace del usuario primario:

Para poder usar las funciones de capabilities necesitamos tener instalado libcap, en ubuntu:
sudo apt install libcap-dev
Tenemos que linkar con -lcap, podemos añadir a gcc: `pkg-config --cflags --libs libcap` por ejemplo:
gcc -Wall -Wextra -Wconversion -Wshadow -pedantic -Wwrite-strings -Wcast-qual -std=gnu11 name-user.c -o name-user `pkg-config --cflags --libs libcap`

/* 
 * name-user.c 
 *  
 *************************************************************** 
 *  
 * (c) Author: David Quiroga 
 * e-mail: david (at) clibre (dot) io 
 * 
*/ 
 
#define _GNU_SOURCE 
#include <sys/capability.h> 
#include <sys/wait.h> 
#include <sched.h> 
#include <stdio.h> 
#include <stdlib.h> 
#include <unistd.h> 
 
#define errExit(msg) do { \ 
fprintf (stderr, "[%s:%d]:", __FILE__, __LINE__); \ 
perror(msg); exit(EXIT_FAILURE); \ 
} while (0) 
 
#define STACK_SIZE (1024 * 1024) 
static char child_stack[STACK_SIZE]; // Pila del proceso hijo 
 
static void print_capabilities() { 
    cap_t caps; 
 
    printf("UID = %ld; GID = %ld; ", 
             (long) geteuid(), (long) getegid()); 
 
    caps = cap_get_proc(); 
    printf("capabilities: %s\n", cap_to_text(caps, NULL)); 
} 
 
 
static int child_Func() { 
    printf("Proceso Hijo:\n"); 
    print_capabilities(); 
 
    _exit(EXIT_SUCCESS); 
} 
 
int main() { 
    pid_t childPid; 
         
    printf("Proceso padre\n"); 
    print_capabilities(); 
 
    childPid = clone(child_Func, 
                            child_stack + STACK_SIZE, 
                            CLONE_NEWUSER | SIGCHLD, NULL); 
 
    if (childPid == -1) 
        errExit("clone"); 
 
    if (waitpid(childPid, NULL, 0) == -1) // Esperamos final del proceso hijo 
        errExit("waitpid"); 
 
 
    exit(EXIT_SUCCESS); 
} 

Conclusión

Creo que con lo aquí expuesto podemos hacernos una idea de las capacidades de los namespace para aislar procesos en el sistema. Hay desde luego mucha más materia de estudio en el uso de los namespace. De cualquier forma es fácil ver que se trata de una potente herramienta de seguridad, podemos por ejemplo lanzar un proceso desde nuestra aplicación y aislarlo completamente del sistema, de tal manera que si este se viera corrompido por un bug por ejemplo, las capacidades de explotación de la vulnerabilidad serían muy limitadas.

Los namespaces son una parte integral de los contenedores de linux, que en definitiva se definen como un conjunto de uno o más procesos separados del resto del sistema. Aunque los contenedores ofrecen otras características como la portabilidad y uniformidad durante las etapas de desarrollo, prueba y producción, son también un método de aislar procesos con fines de seguridad. Otra forma de aislar es la virtualización, en este caso de todo el sistema.

Dependiendo entonces de las necesidades de nuestra aplicación, podemos usar las capacidades de aislamiento de una u otra tecnología.

Modificado por última vez enSábado, 26 Septiembre 2020 15:15
(2 votos)
Etiquetado como :

Deja un comentario

Asegúrese de introducir toda la información requerida, indicada por un asterisco (*). No se permite código HTML.