Blog

Proteger host y contenedores de Docker con iptables

Proteger host y contenedores de Docker con iptables

Algunas reglas útiles que utilizo para proteger esta web en WordPress, tanto en el host de Docker como en los contenedores.

Limitar puertos en host

En nuestras reglas de iptables en /etc/iptables/iptables.rules, cambiamos

*filter
:INPUT ACCEPT [4407:285180] # Ojo no confundir con la misma línea en *nat

por

*filter
:INPUT DROP [4407:285180]
# Añadimos:
-A INPUT -p tcp  --match multiport --dports 22,80,443 -j ACCEPT
-A INPUT -p icmp -j ACCEPT
-A INPUT -p tcp -m tcp --dport 22 -m state --state NEW -m recent --update --seconds 60 --hitcount 4 --name DEFAULT --mask 255.255.255.255 --rsource -j DROP
-A INPUT -p tcp -m tcp --dport 22 -m state --state NEW -m recent --set --name DEFAULT --mask 255.255.255.255 --rsource
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

Aquí aceptamos TCP con puerto de destino SSH, HTTP y HTTPS, ICMP, limitamos conexiones SSH y permitimos tráfico de vuelta de conexiones salientes (ESTABLISHED,RELATED).

Para ip6tables (/etc/iptables/ip6tables.rules) es prácticamente igual, simplemente cambiando -p icmp por -p ipv6-icmp

Para probar el efecto del filtro:

j@akane ~ % sudo iptables -L INPUT -v -n
Chain INPUT (policy DROP 1931 packets, 95339 bytes)
 pkts bytes target     prot opt in     out     source               destination         
  154  9666 ACCEPT     6    --  *      *       0.0.0.0/0            0.0.0.0/0            multiport dports 22,80,443
 3243  231K ACCEPT     1    --  *      *       0.0.0.0/0            0.0.0.0/0           
    0     0 DROP       6    --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22 state NEW recent: UPDATE seconds: 60 hit_count: 4 name: DEFAULT side: source mask: 255.255.255.255
    0     0            6    --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22 state NEW recent: SET name: DEFAULT side: source mask: 255.255.255.255
  850 89576 ACCEPT     0    --  *      *       0.0.0.0/0            0.0.0.0/0            state RELATED,ESTABLISHED

Para IPv6, sustituir el comando iptables por ip6tables.

Por defecto, si no hay nada escuchando en un puerto, el S.O. del contenedor hace un RST:

https://www.reddit.com/r/docker/comments/kxsb13/comment/gjc11vv/
It is standard behavior of the Linux networking stack to return RST in response to TCP SYN packets to ports where no application is running.
Windows also does this if the firewall is disabled.
This behavior is part of the TCP specification.
You could intercept these TCP resets, and drop them in your firewall:
iptables -I OUTPUT -p tcp –tcp-flags ALL RST,ACK -j DROP
(RST packets that send in response to SYN packets have the ACK flag, normal TCP reset packets do not have this)

Ejemplo del comportamiento por defecto en puertos sin ningún servicio escuchando: ACCEPT de iptables y el S.O. responde con un RST:

j@akane ~ % sudo tcpdump tcp and ! port 22
12:20:23.660060 IP 184.211.203.35.bc.googleusercontent.com.52651 > akane.secure-mqtt: Flags [S], seq 1354132597, win 65535, options [mss 1460], length 0
12:20:23.660087 IP akane.secure-mqtt > 184.211.203.35.bc.googleusercontent.com.52651: Flags [R.], seq 0, ack 1354132598, win 0, length 0

12:20:26.176836 IP 193.142.146.175.42770 > akane.dhanalakshmi: Flags [S], seq 1509900825, win 65535, length 0
12:20:26.176879 IP akane.dhanalakshmi > 193.142.146.175.42770: Flags [R.], seq 0, ack 1509900826, win 0, length 0

Con DROP no se responde nada:

j@akane ~ % sudo tcpdump tcp and ! port 22
12:24:48.904274 IP scanner-001.hk2.censys-scanner.com.64935 > 185.47.128.95.ndl-aas: Flags [S], seq 4057969088, win 42340, options [mss 1460,sackOK,TS val 1734096492 ecr 0,nop,wscale 10], length 0
12:24:48.904595 IP scanner-001.hk2.censys-scanner.com.64935 > 185.47.128.95.ndl-aas: Flags [S], seq 4057969088, win 42340, options [mss 1460,sackOK,TS val 1734096492 ecr 0,nop,wscale 10], length 0
12:24:48.909732 IP hosting-by.4cloud.mobi.0 > akane.60366: Flags [S], seq 2677239513, win 1024, length 0

En un mundo ideal, lo más educado es el RST, porque estamos rechazando la conexión e informando activamente de ello. Esto puede ser útil en conexiones accidentales, por ejemplo. Sin embargo, estoy harto de escaneos continuos de vulnerabilidades. Cada 1, 2 ó 3 segundos como mucho, recibo algún paquete. Al no responder estoy forzando al atacante a esperar un timeout, enlenteciendo su actividad.

Como curiosidad, sin la regla de iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT, tcpdump se me colgaba. El motivo es que por defecto intenta resolver las IPs a dominio, y sin la regla las peticiones DNS no volvían. Mientras depuraba el problema, evité la resolución con la opción -N:

j@akane ~ % sudo tcpdump -N -i ens18 tcp and ! port 22

Limitar IPs y puertos en contenedores

Para limitar rangos de IPs añadidos mediante ipset:

-A DOCKER-USER -m set --match-set duckduckgo_ipv4 src -j ACCEPT
-A DOCKER-USER -m set --match-set cloudflare_ipv4 src -j DROP
-A DOCKER-USER -m set --match-set digital_ocean_ipv4 src -j DROP
-A DOCKER-USER -m set --match-set gcp_ipv4 src -j DROP
-A DOCKER-USER -m set --match-set misc_ipv4 src -j DROP
-A DOCKER-USER -m set --match-set datacamp_ipv4 src -j DROP
-A DOCKER-USER -m set --match-set aws_ipv4 src -j DROP
-A DOCKER-USER -m set --match-set azure_ipv4 src -j DROP
-A DOCKER-USER -m set --match-set tencent_ipv4 src -j DROP

Para probar el efecto de estas reglas:

j@akane ~ % sudo iptables -L DOCKER-USER -v -n
Chain DOCKER-USER (1 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 ACCEPT     0    --  *      *       0.0.0.0/0            0.0.0.0/0            match-set duckduckgo_ipv4 src
   34  2000 DROP       0    --  *      *       0.0.0.0/0            0.0.0.0/0            match-set cloudflare_ipv4 src
    1    44 DROP       0    --  *      *       0.0.0.0/0            0.0.0.0/0            match-set digital_ocean_ipv4 src
    0     0 DROP       0    --  *      *       0.0.0.0/0            0.0.0.0/0            match-set gcp_ipv4 src
    0     0 DROP       0    --  *      *       0.0.0.0/0            0.0.0.0/0            match-set misc_ipv4 src
    2    80 DROP       0    --  *      *       0.0.0.0/0            0.0.0.0/0            match-set datacamp_ipv4 src
   39  2160 DROP       0    --  *      *       0.0.0.0/0            0.0.0.0/0            match-set aws_ipv4 src
   50  2960 DROP       0    --  *      *       0.0.0.0/0            0.0.0.0/0            match-set azure_ipv4 src
    4   240 DROP       0    --  *      *       0.0.0.0/0            0.0.0.0/0            match-set tencent_ipv4 src
 1745  419K RETURN     0    --  *      *       0.0.0.0/0            0.0.0.0/0       

Si hacemos alguna modificación en /etc/ipset.conf, tendremos que reiniciar primero el servicio ipset, seguido de iptables/ip6tables, y finalmente de docker. Dependen unos de otros, así que tenemos que seguir ese orden. De igual manera, si sólo cambiamos iptables/ip6tables (sin ipset), habrá que reiniciar iptables/ip6tables y docker.

Limitamos los puertos de la BDD MySQL para que sólo se pueda entrar desde localhost (tanto en IPv4, 127.0.0.1; como en IPv6 ::1) y no desde Internet:

db:
    image: mysql:5.7
    volumes:
      - db_data:/var/lib/mysql
    ports:
      - "127.0.0.1:3306:3306"
      - "[::1]:3306:3306"