Fecha: 2019-07-22 Tiempo de lectura: 4 minutos Categoría: Operaciones Tags: docker / tcp / unix socket / nginx / haproxy
Tengo un usuario que es muy cómodo; él solo consiguió una excepción de seguridad para poder abrir el puerto TCP de docker de un servidor concreto, para chafardear cómodamente desde su máquina. A pesar de mis reticencias, cumplí con lo que se me pedía, y no tardamos mucho en lamentarlo.
Normalmente, el servicio docker abre un unix socket en /var/run/docker.sock
; se utiliza por defecto desde el cliente para lanzar las diferentes operaciones, por ejemplo, un docker ps
. Otra opción es ejecutar el comando docker
bajo la influencia de la variable de entorno DOCKER_HOST
, que permitiría lanzar el comando contra un host remoto, suponiendo que este host remoto esté escuchando mediante TCP.
Abrir el flujo TCP no es complicado; basta con iniciar el servicio docker con el flag -H tcp://0.0.0.0(:<puerto>)
(el puerto por defecto es 2375), usando el init del sistema. Por ejemplo, en Debian hablaríamos de systemd y podríamos utilizar un fichero override.
Para ello observamos el comando que se está ejecutando en el fichero docker.service
:
gerard@procyon:~$ cat /lib/systemd/system/docker.service
...
[Service]
Type=notify
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
...
gerard@procyon:~$
Y lo copiamos en el fichero de override, con el parámetro añadido; posteriormente indicamos a systemd que recargue configuraciones y reinicie docker:
gerard@procyon:~$ cat /etc/systemd/system/docker.service.d/override.conf
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock -H tcp://0.0.0.0
gerard@procyon:~$ sudo systemctl daemon-reload
gerard@procyon:~$ sudo systemctl restart docker
gerard@procyon:~$
Podemos comprobar como el puerto por defecto 2375 queda abierto en el servidor:
gerard@procyon:~$ sudo ss -lntp
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 *:22 *:* users:(("sshd",pid=350,fd=3))
LISTEN 0 128 :::2375 :::* users:(("dockerd",pid=5107,fd=3))
LISTEN 0 128 :::22 :::* users:(("sshd",pid=350,fd=4))
gerard@procyon:~$
Y con esto podríamos gestionar el servicio remoto cómodamente desde nuestro terminal en otra máquina:
gerard@aldebaran:~$ DOCKER_HOST=procyon docker info | grep ^Name
Name: procyon
WARNING: API is accessible on http://0.0.0.0:2375 without encryption.
Access to the remote API is equivalent to root access on the host. Refer
to the 'Docker daemon attack surface' section in the documentation for
more information: https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface
WARNING: No swap limit support
gerard@aldebaran:~$
Creo que el mensaje de warning es bastante claro; con un unix socket contábamos con permisos del sistema de ficheros limitados al grupo docker
, pero ahora cualquiera que llegue a procyon por TCP puede hacer lo que le dé la gana…
RESULTADO: A la semana teníamos redes, imágenes y contenedores de usuarios que no deberían haber utilizado el sistema o que solo necesitaban acceso de lectura, lo que nos llevó a volver al status quo inicial cerrando el puerto de docker en procyon.
Desde entonces ha habido un nuevo intento para proteger quien accede al servidor y qué acciones puede efectuar. Utilizando un servidor nginx, podemos hacer un proxy_pass
al unix socket. La ventaja es que podemos utilizar las directivas de los diferentes módulos para limitar el acceso o los métodos que se utilizan; hay que recordar que el acceso al servicio docker no deja de ser un servicio REST.
Por ejemplo, podemos limitar el acceso a todo, excepto a GETs en /networks
u otros métodos en /networks
si estás en cierto rango de red:
gerard@procyon:~/dockerproxy$ cat docker.conf
server {
server_name _;
listen 2375;
location /v1.39/networks {
limit_except GET {
allow 10.0.0.5;
deny all;
}
allow all;
proxy_pass http://unix:/var/run/docker.sock;
}
deny all;
}
gerard@procyon:~/dockerproxy$
Basta con levantar un nginx con este virtualhost, siguiendo el procedimiento habitual:
docker run --rm -d --name dockerproxy -v /var/run/docker.sock:/var/run/docker.sock:ro -v $(pwd)/docker.conf:/etc/nginx/conf.d/docker.conf:ro -p 2375:2375 sirrtea/nginx:alpine
360f9de7f8ddec500320363de706f4239f731692e2df55aaa9bbca6b01fa5e43
gerard@procyon:~/dockerproxy$
AVISO: El usuario nginx debe poder escribir a /var/run/docker.sock
para que esto funcione.
El resultado es evidente: el comando docker ps
(que utiliza un GET a /v1.39/containers/json
), vería su acceso cortado.
gerard@aldebaran:~$ DOCKER_HOST=procyon docker ps
Error response from daemon: <html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.14.2</center>
</body>
</html>
gerard@aldebaran:~$
El comando docker network ls
(un GET a /v1.39/networks
) funciona según lo esperado:
gerard@aldebaran:~$ DOCKER_HOST=procyon docker network ls
NETWORK ID NAME DRIVER SCOPE
b92cd9288207 bridge bridge local
c4ecadf8c3d2 host host local
5d8f3437caa2 none null local
gerard@aldebaran:~$
Sin embargo, no se me permite hacer un docker network create
(que es un POST a /v1.39/networks/create
):
gerard@aldebaran:~$ DOCKER_HOST=procyon docker network create mynet
Error response from daemon: <html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.14.2</center>
</body>
</html>
gerard@aldebaran:~$
Configurar el proxy es una tarea complicada, y mantener dicha configuración cuando la API va cambiando, es casi imposible. Eso nos obliga a buscar otras opciones de gente que ha tenido el mismo problema que yo; como no puede ser de otra manera, ya existen varios proyectos que nos pueden servir.
Especialmente interesante me parece la opción de Tecnativa/docker-socket-proxy, que utiliza la misma técnica que la anterior. El proxy en sí mismo es un haproxy, al que le añade un solo fichero de configuración. Este fichero es capaz de permitir o bloquear URLs y métodos en función de variables de entorno concretas.
$ docker container run \
-d --privileged \
--name dockerproxy \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 127.0.0.1:2375:2375 \
tecnativa/docker-socket-proxy
Lo que no me gusta de esta imagen es que ejecuta como root, resolviendo la escritura al unix socket de una forma bastante poco elegante.