<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Docker - Matteo Cisilino</title>
	<atom:link href="https://www.cisilino.com/es/tag/docker-es/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.cisilino.com/es/</link>
	<description>Guide su WordPress, server e sicurezza</description>
	<lastBuildDate>Thu, 29 Oct 2026 19:02:55 +0000</lastBuildDate>
	<language>es</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.4</generator>
	<item>
		<title>MySQL 5.7 en Docker protegido con nftables</title>
		<link>https://www.cisilino.com/es/2026/10/17/mysql-5-7-docker-nftables-proteccion/</link>
					<comments>https://www.cisilino.com/es/2026/10/17/mysql-5-7-docker-nftables-proteccion/#respond</comments>
		
		<dc:creator><![CDATA[matteocisilino]]></dc:creator>
		<pubDate>Sat, 17 Oct 2026 08:10:45 +0000</pubDate>
				<category><![CDATA[Servidores y bases de datos]]></category>
		<category><![CDATA[cortafuegos]]></category>
		<category><![CDATA[Docker]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[nftables]]></category>
		<guid isPermaLink="false">https://www.cisilino.com/?p=277</guid>

					<description><![CDATA[<p>Cómo ejecutar MySQL 5.7 en Docker en un servidor moderno sin exponerlo a internet: docker-compose, puerto cerrado y cortafuegos nftables.</p>
<p>The post <a href="https://www.cisilino.com/es/2026/10/17/mysql-5-7-docker-nftables-proteccion/">MySQL 5.7 en Docker protegido con nftables</a> first appeared on <a href="https://www.cisilino.com/es/">Matteo Cisilino</a>.</p>]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">Introducción</h2>
<p>Nos encontramos con la necesidad de tener una base de datos <strong>MySQL 5.7</strong> en un servidor que, por lo demás, funciona con una distribución reciente. Algunas aplicaciones heredadas no son compatibles con MySQL 8 (cambio del plugin de autenticación predeterminado, modos SQL más estrictos, palabras reservadas nuevas). Migrarlas en poco tiempo no era una opción. La salida fue ejecutar MySQL 5.7 en Docker.</p>
<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/mysql-5-7-docker-nftables-cortafuegos.webp" alt="MySQL 5.7 en Docker: una base de datos dentro de un recuadro naranja discontinuo, con un círculo y una barra delante" width="1200" height="630"/></figure>
<p>Instalar una versión tan antigua directamente en el host trae varios problemas. Los repositorios oficiales de las distribuciones actuales ya no la ofrecen, los paquetes de terceros pueden chocar con el resto del sistema. Y una actualización del sistema operativo podría romperla.</p>
<p>Decidimos entonces <strong>aislarla en un contenedor Docker</strong>. El host se mantiene limpio y actualizado, la versión de la base de datos queda fijada y es reproducible. Los datos y la configuración están en una carpeta fácil de guardar y mover.</p>
<p>Este artículo cuenta cómo la configuramos y, sobre todo, los obstáculos que encontramos por el camino. Fueron la exposición involuntaria del puerto 3306 y un error de iptables al reiniciar. Después, la escritura de un cortafuegos con <strong>nftables</strong> que deje pasar solo las IP autorizadas.</p>
<blockquote class="wp-block-quote">
<p><strong>Nota:</strong> MySQL 5.7 llegó al final de su vida útil (EOL) en octubre de 2023 y ya no recibe parches de seguridad. Es una solución puente: el objetivo sigue siendo migrar a MySQL 8.x o MariaDB. Precisamente por eso es todavía más importante no dejarlo expuesto.</p>
</blockquote>
<hr class="wp-block-separator has-alpha-channel-opacity"/>
<h2 class="wp-block-heading">1. Estructura de la carpeta</h2>
<pre class="wp-block-code"><code>/opt/mysql57/
├── docker-compose.yml
├── .env
├── conf.d/        # configurazioni personalizzate (my.cnf aggiuntivi)
├── data/          # dati del database (volume persistente)
└── logs/
</code></pre>
<p>El archivo <code>.env</code> contiene las credenciales, para que no acaben en el compose.</p>
<pre class="wp-block-code"><code>TZ=Europe/Rome
MYSQL_ROOT_PASSWORD=una_password_molto_lunga_e_casuale
MYSQL_DATABASE=nome_db
MYSQL_USER=nome_utente
MYSQL_PASSWORD=altra_password_robusta
</code></pre>
<p>Protégelo con <code>chmod 600 .env</code>.</p>
<h2 class="wp-block-heading">2. El docker-compose.yml de MySQL 5.7 en Docker</h2>
<pre class="wp-block-code"><code>services:
  mysql57:
    image: mysql:5.7
    container_name: mysql57
    restart: unless-stopped
    env_file:
      - .env
    ports:
      - "3306:3306"
    environment:
      TZ: "${TZ:-Europe/Rome}"
      MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}"
      MYSQL_DATABASE: "${MYSQL_DATABASE}"
      MYSQL_USER: "${MYSQL_USER}"
      MYSQL_PASSWORD: "${MYSQL_PASSWORD}"
    command:
      - --default-authentication-plugin=mysql_native_password
    volumes:
      - ./data:/var/lib/mysql
      - ./conf.d:/etc/mysql/conf.d:ro
      - ./logs:/var/log/mysql
    healthcheck:
      test: &#91;"CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD --silent"]
      interval: 20s
      timeout: 5s
      retries: 10
      start_period: 45s
    ulimits:
      nofile:
        soft: 65535
        hard: 65535
</code></pre>
<p>Algunas decisiones que conviene señalar.</p>
<ul class="wp-block-list">
<li><strong><code>restart: unless-stopped</code></strong>: la base de datos arranca sola tras un reinicio del servidor.</li>
<li><strong>Volúmenes con bind mount</strong>: datos, configuración y logs están en el host. Así, las copias de seguridad y las migraciones son simples copias de carpetas.</li>
<li><strong>Healthcheck</strong>: permite que los demás servicios esperen a que MySQL esté realmente listo. El <code>$$</code> sirve para que la variable se expanda dentro del contenedor y no por Compose.</li>
<li><strong><code>ulimits nofile</code></strong>: evita el error «too many open files» bajo carga.</li>
</ul>
<h2 class="wp-block-heading">3. Primer problema: el puerto está abierto a todo el mundo</h2>
<p>La línea <code>- "3306:3306"</code> equivale a <code>0.0.0.0:3306:3306</code>. Docker publica el puerto en <strong>todas las interfaces</strong> del host, incluida la pública.</p>
<p>Lo más insidioso es que Docker escribe sus propias reglas directamente en el cortafuegos del kernel. Estas se evalúan antes que las de UFW o firewalld. Tener un cortafuegos activo <strong>no basta</strong>: el puerto sigue siendo alcanzable. La documentación de Docker sobre <a href="https://docs.docker.com/engine/network/packet-filtering-firewalls/" target="_blank" rel="noopener">filtrado de paquetes y cortafuegos</a> describe este comportamiento.</p>
<p>Los caminos posibles son estos.</p>
<ol class="wp-block-list">
<li><strong>Solo localhost</strong>: <code>"127.0.0.1:3306:3306"</code>, si la base de datos solo la usa el host o se accede por túnel SSH.</li>
<li><strong>Ningún puerto publicado</strong>: si los clientes son otros contenedores, basta una red Docker compartida.</li>
<li><strong>IP concreta</strong>: publicar solo en una dirección de una red privada/VPN.</li>
<li><strong>Puerto público con filtro por IP</strong>: nuestro caso. Un cliente externo (una instancia de phpMyAdmin en otro servidor) tiene que poder conectarse.</li>
</ol>
<p>Para el caso 4 hace falta un cortafuegos que filtre correctamente el tráfico de los puertos publicados por Docker.</p>
<h2 class="wp-block-heading">4. Segundo problema: error de iptables al arrancar</h2>
<p>Después de tocar el cortafuegos, <code>docker compose up</code> devolvió.</p>
<pre class="wp-block-code"><code>failed to create network mysql57_default: Error response from daemon:
Failed to Setup IP tables: Unable to enable ACCEPT OUTGOING rule:
(iptables failed: iptables --wait -t filter -A DOCKER-FORWARD -i br-... -j ACCEPT:
iptables: No chain/target/match by that name.
</code></pre>
<p>El error no depende del compose: la cadena <code>DOCKER-FORWARD</code> (introducida en las versiones recientes de Docker) ya no existía. La había borrado algo que limpió las reglas <strong>después</strong> de arrancar el demonio. Reiniciar Docker (<code>systemctl restart docker</code>) la recrea, pero el problema reaparece mientras no se elimine al verdadero culpable.</p>
<h3 class="wp-block-heading">El culpable: <code>flush ruleset</code></h3>
<p>Nuestro <code>/etc/nftables.conf</code> empezaba con.</p>
<pre class="wp-block-code"><code>flush ruleset
</code></pre>
<p>Este comando borra <strong>todas</strong> las tablas de nftables, incluidas las creadas por Docker. Además, el archivo contenía una copia estática de las reglas de Docker. Llevaba el nombre del bridge (<code>br-278550235c3f</code>) y la IP del contenedor (<code>172.18.0.2</code>) escritos a mano. Pero el nombre del bridge cambia cada vez que se recrea la red de Compose. Así, la copia quedaba obsoleta enseguida, y la tabla <code>ip nat</code> entraba en conflicto con la gestionada por Docker.</p>
<p><strong>Regla de oro:</strong> con Docker, en el archivo de nftables no se usa <code>flush ruleset</code> y no se copian sus reglas. Solo se gestiona el cortafuegos propio, en tablas dedicadas.</p>
<h2 class="wp-block-heading">5. El cortafuegos para MySQL 5.7 en Docker</h2>
<p>El principio es este. Los puertos publicados por Docker se reescriben con DNAT y luego el host los reenvía al contenedor. Así, atraviesan la cadena <strong><code>forward</code></strong>, no <code>input</code>. Una regla <code>drop</code> en una cadena nuestra, <code>forward</code>, con una prioridad ligeramente más baja (es decir, evaluada antes) descarta el paquete sin importar las reglas <code>accept</code> de Docker.</p>
<p>Para no tocar las tablas ajenas usamos el patrón <code>table ...</code> + <code>delete table ...</code>: hace que el archivo se pueda volver a ejecutar sin errores y sin <code>flush</code>.</p>
<p><code>/etc/nftables.conf</code>.</p>
<pre class="wp-block-code"><code>#!/usr/sbin/nft -f

# NIENTE "flush ruleset": cancellerebbe anche le tabelle di Docker

# ---------------- firewall host ----------------
table inet filter
delete table inet filter

table inet filter {

    chain input {
        type filter hook input priority filter; policy drop;
        ct state established,related accept
        iifname "lo" accept
        ip6 nexthdr icmpv6 accept
        iifname "enp7s0" accept                       # rete privata
        ip saddr 203.0.113.20 tcp dport 22 accept     # SSH da host fidato
        ip saddr 203.0.113.30 tcp dport 22 accept     # SSH da VPN
        ip saddr 203.0.113.20 icmp type echo-request accept
        tcp dport { 80, 443 } accept
    }

    chain output {
        type filter hook output priority filter; policy accept;
    }
}

# ---------------- guardia MySQL (porta pubblicata da Docker) ----------------
table ip mysql_guard
delete table ip mysql_guard

table ip mysql_guard {
    set allowed {
        type ipv4_addr
        flags interval
        elements = {
            203.0.113.10        # phpMyAdmin
        }
    }

    chain forward {
        type filter hook forward priority filter - 5; policy accept;
        iifname "enp7s0" accept
        ct status dnat tcp dport 3306 ip saddr @allowed accept
        ct status dnat tcp dport 3306 counter drop
    }
}
</code></pre>
<p>Cómo funciona la guardia.</p>
<ul class="wp-block-list">
<li><code>ct status dnat</code> selecciona solo los paquetes que han pasado por el DNAT, es decir, los dirigidos a los puertos publicados por Docker.</li>
<li>La primera regla acepta a quien esté en el conjunto <code>allowed</code>. La segunda descarta a todos los demás y los cuenta (<code>counter</code>).</li>
<li>El tráfico local (<code>127.0.0.1</code>) y el que va entre contenedores de la misma red no pasa por el forward del router. Así que no se toca.</li>
</ul>
<blockquote class="wp-block-quote">
<p><strong>Compatibilidad:</strong> en una primera versión usábamos <code>ct original proto-dst 3306</code>. Pero algunas versiones de <code>nft</code> responden con <em>«Can&#8217;t parse symbolic invalid expressions»</em>. En el hook <code>forward</code> el DNAT ya ha ocurrido y el puerto del contenedor coincide con el publicado (<code>3306:3306</code>). Por eso <code>tcp dport 3306</code> combinado con <code>ct status dnat</code> es equivalente y funciona en todas partes. Si cambias el mapeo (p. ej. <code>3307:3306</code>), la regla debe adaptarse al puerto interno.</p>
</blockquote>
<h3 class="wp-block-heading">Aplicar los cambios sin quedarse fuera</h3>
<p>Mantén abierta una sesión SSH de reserva y después.</p>
<pre class="wp-block-code"><code>nft -c -f /etc/nftables.conf     # solo verifica della sintassi
nft delete table ip nat          # una tantum: elimina la vecchia copia statica
nft -f /etc/nftables.conf
systemctl restart docker         # Docker ricrea le sue tabelle pulite
cd /opt/mysql57 &amp;&amp; docker compose up -d
</code></pre>
<p>Para la carga en el arranque, asegúrate de que el servicio esté activo (<code>systemctl enable --now nftables</code>). Comprueba también que <code>/etc/nftables.conf</code> no contenga <code>flush ruleset</code>.</p>
<h3 class="wp-block-heading">Gestionar las IP en tiempo de ejecución</h3>
<pre class="wp-block-code"><code>nft add element ip mysql_guard allowed { 203.0.113.50 }
nft delete element ip mysql_guard allowed { 203.0.113.50 }
</code></pre>
<p>Para hacerlo permanente, actualiza también el conjunto en el archivo.</p>
<h2 class="wp-block-heading">6. Verificación del acceso a MySQL 5.7 en Docker</h2>
<p>Desde el servidor autorizado.</p>
<pre class="wp-block-code"><code>nc -zv IP_PUBBLICO_DB 3306        # deve rispondere "succeeded"
</code></pre>
<p>Desde cualquier otra IP.</p>
<pre class="wp-block-code"><code>nmap -Pn -p 3306 IP_PUBBLICO_DB   # deve risultare "filtered" / timeout
</code></pre>
<p>En el servidor, el contador de la regla de descarte debe crecer con cada intento no autorizado.</p>
<pre class="wp-block-code"><code>nft list chain ip mysql_guard forward
ss -tlnp | grep 3306
</code></pre>
<h2 class="wp-block-heading">7. Segunda barrera: los usuarios de MySQL</h2>
<p>El cortafuegos no sustituye los permisos de la base de datos. Crea el usuario con el host limitado a la IP del cliente.</p>
<pre class="wp-block-code"><code>CREATE USER 'pma_user'@'203.0.113.10' IDENTIFIED BY 'password_robusta';
GRANT ALL ON nome_db.* TO 'pma_user'@'203.0.113.10';
FLUSH PRIVILEGES;
</code></pre>
<p>Si la conexión TCP se establece pero MySQL responde <code>Access denied</code>, el problema está aquí y no en el cortafuegos.</p>
<h2 class="wp-block-heading">8. Cosas que recordar</h2>
<ul class="wp-block-list">
<li><strong>Docker y el cortafuegos del host no se ignoran mutuamente</strong>. Entender por qué cadena pasa el tráfico (<code>input</code> o <code>forward</code>) es la clave de todo.</li>
<li><strong>Nunca <code>flush ruleset</code></strong> en un host con Docker; usa tablas dedicadas.</li>
<li><strong>No copies las reglas generadas por Docker</strong> en tus archivos: los nombres de bridge y las IP cambian.</li>
<li>Si ya has expuesto el puerto, revisa los logs en <code>./logs</code> y valora <strong>cambiar las contraseñas</strong>, sobre todo la de root.</li>
<li>Siempre que sea posible, prefiere una <strong>VPN (WireGuard) o un túnel SSH</strong> a la exposición pública de una base de datos.</li>
<li>Planifica la <strong>migración</strong> de MySQL 5.7 a una versión con soporte.</li>
</ul>
<p>Si lo que te da problemas es el arranque tras una actualización de Debian, tienes este otro caso: <a href="https://www.cisilino.com/es/2026/10/13/mysql-no-arranca-debian-12/">MySQL no arranca tras actualizar a Debian 12</a>.</p>
<p>¿Tienes dudas sobre MySQL 5.7 en Docker o una solución distinta para el mismo problema? Escríbelo en los comentarios.</p><p>The post <a href="https://www.cisilino.com/es/2026/10/17/mysql-5-7-docker-nftables-proteccion/">MySQL 5.7 en Docker protegido con nftables</a> first appeared on <a href="https://www.cisilino.com/es/">Matteo Cisilino</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.cisilino.com/es/2026/10/17/mysql-5-7-docker-nftables-proteccion/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
