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"