<?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>Servidores y bases de datos - Matteo Cisilino</title>
	<atom:link href="https://www.cisilino.com/es/category/servidores-bases-de-datos/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.cisilino.com/es/</link>
	<description>Guide su WordPress, server e sicurezza</description>
	<lastBuildDate>Sat, 31 Oct 2026 10:04:01 +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>Rotación de credenciales de base de datos tras una brecha en ISPConfig</title>
		<link>https://www.cisilino.com/es/2026/10/31/rotacion-de-credenciales-base-de-datos-ispconfig/</link>
					<comments>https://www.cisilino.com/es/2026/10/31/rotacion-de-credenciales-base-de-datos-ispconfig/#respond</comments>
		
		<dc:creator><![CDATA[matteocisilino]]></dc:creator>
		<pubDate>Sat, 31 Oct 2026 10:03:56 +0000</pubDate>
				<category><![CDATA[Servidores y bases de datos]]></category>
		<category><![CDATA[gestión de servidores]]></category>
		<category><![CDATA[ISPConfig]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[respuesta a incidentes]]></category>
		<category><![CDATA[rotación de credenciales]]></category>
		<category><![CDATA[script de bash]]></category>
		<guid isPermaLink="false">https://www.cisilino.com/?p=1420</guid>

					<description><![CDATA[<p>Por qué la rotación de credenciales de base de datos urge tras una brecha en ISPConfig, y cómo un script en tres fases la gestiona con copias de seguridad.</p>
<p>The post <a href="https://www.cisilino.com/es/2026/10/31/rotacion-de-credenciales-base-de-datos-ispconfig/">Rotación de credenciales de base de datos tras una brecha en ISPConfig</a> first appeared on <a href="https://www.cisilino.com/es/">Matteo Cisilino</a>.</p>]]></description>
										<content:encoded><![CDATA[<p class="wp-block-paragraph">Roto las credenciales en el momento en que sospecho una brecha. No después de confirmarla. La rotación de credenciales de base de datos en un servidor ISPConfig suena a mantenimiento rutinario. Deja de ser rutinaria el día que encuentras una web shell en un sitio alojado. O un archivo de configuración que no puedes explicar, guardado en una copia de seguridad pública. En ese momento, cada contraseña vinculada a esa base de datos tiene que ser nueva. Verificada. Documentada. No simplemente asumida como correcta.</p>



<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/database-credential-rotation-ispconfig.webp" alt="rotación de credenciales de base de datos: candado y llave girando sobre una base de datos ISPConfig" width="1200" height="630"/></figure>



<h2 class="wp-block-heading">¿Cuándo deja de ser opcional la rotación de credenciales?</h2>



<p class="wp-block-paragraph">Hay unos pocos escenarios que se repiten lo suficiente como para mantener siempre listo un plan de rotación. Prefiero no improvisarlo bajo presión.</p>



<p class="wp-block-paragraph">Un plugin o CMS de un sitio alojado se ve comprometido. Escribí sobre un caso real en este tipo de servidor en <a href="https://www.cisilino.com/es/2026/10/29/php2shell-cve-2026-109618-mitigacion-servidores/">PHP2Shell: cómo protejí los servidores a la espera del parche</a>. Era una vulnerabilidad de PHP que permitía a un atacante instalar una web shell antes de que existiera el parche. Una vez que una web shell se ejecuta bajo un usuario de hosting, a menudo puede leer el propio config.inc.php de ese sitio. En un servidor ISPConfig compartido, ese archivo está junto a las credenciales de la propia base de datos del panel de control. No solo la credencial que el atacante realmente buscaba.</p>



<p class="wp-block-paragraph">Un archivo de configuración termina donde no debería. Un archivo de copia de seguridad olvidado en una carpeta pública. Un repositorio subido con un archivo suelto que no debía estar ahí. Un log más legible de lo que nadie pretendía. config.inc.php y mysql_clientdb.conf no tienen nada que hacer en ninguno de esos lugares. Los errores ocurren igualmente, incluidos los míos.</p>



<p class="wp-block-paragraph">Un administrador de sistemas deja el equipo. Por amistosa que sea la salida, las credenciales de base de datos compartidas que esa persona tuvo son credenciales en las que ya no confío del todo. La confianza no se transfiere automáticamente cuando alguien se va.</p>



<p class="wp-block-paragraph">Una intrusión sospechada, incluso sin confirmar. Este caso es el más difícil de explicar a quien nunca ha administrado un servidor bajo sospecha de ataque. No esperas a tener pruebas antes de actuar. Asumes el compromiso y rotas. Una rotación innecesaria cuesta una tarde. Una que se omite, tras una brecha real, puede costar todos los sitios que alberga el servidor.</p>



<h2 class="wp-block-heading">Por qué la rotación de credenciales de base de datos manual es tan propensa a errores</h2>



<p class="wp-block-paragraph">La primera vez que roté a mano las credenciales de base de datos de ISPConfig, subestimé cuántos archivos estaban involucrados. Todos tienen que cambiar juntos, o algo se rompe silenciosamente unas horas después.</p>



<p class="wp-block-paragraph">Está config.inc.php en el lado del servidor, y una segunda copia en el lado de la interfaz. Ambas necesitan los mismos valores nuevos. Está mysql_clientdb.conf, que ISPConfig lee para gestionar la propia base de datos de cada sitio alojado, no solo su propio mantenimiento interno. Dovecot, Pure-FTPd y Postfix mantienen cada uno su propia copia de algunas de las mismas credenciales. El correo y el FTP pueden dejar de autenticar en silencio si actualizas el usuario de la base de datos y te olvidas de solo uno de esos tres.</p>



<p class="wp-block-paragraph">Después está la topología Master/Slave que usan algunas de mis configuraciones. Un nodo Slave se autentica contra la base de datos de su Master a través de la red, con una contraseña separada. Esa contraseña tiene que rotarse en el propio Master, no solo localmente. Si te equivocas en el orden, un Slave puede quedar bloqueado fuera del mismo Master del que se supone que debe replicar.</p>



<p class="wp-block-paragraph">Nada de eso es difícil a nivel conceptual. Son muchos archivos. Varios servicios que reiniciar, en un orden sensato. Casi ninguna tolerancia a un error de tecleo, sobre todo mientras lo haces bajo presión, justo después de encontrar algo que no querías encontrar. Esa combinación es exactamente la razón por la que la rotación de credenciales de base de datos se sentía más arriesgada a mano de lo que la tarea debería ser. Por eso escribí reset_DB_password.sh, después de vivir el problema yo mismo más de una vez.</p>



<h2 class="wp-block-heading">Qué hace realmente reset_DB_password.sh</h2>



<p class="wp-block-paragraph">reset_DB_password.sh detecta si el nodo es Standalone, Master o Slave. Luego ejecuta solo las fases que ese rol necesita.</p>



<p class="wp-block-paragraph">La Fase A rota la contraseña local de root de MySQL. Es la contraseña que el propio ISPConfig usa para gestionar la base de datos de cada sitio alojado. El script actualiza mysql_clientdb.conf para que coincida con el nuevo valor.</p>



<p class="wp-block-paragraph">La Fase B rota la contraseña del usuario de control ispconfig. Actualiza config.inc.php tanto en el lado del servidor como en el de la interfaz. Después actualiza las credenciales correspondientes dentro de Dovecot, Pure-FTPd y Postfix. Así, el correo y el FTP siguen autenticando sin un paso manual aparte por otros tres archivos de configuración.</p>



<p class="wp-block-paragraph">La Fase C se ejecuta solo en un Slave. Se conecta de forma remota a la base de datos del Master. Rota ahí la contraseña del usuario Slave, y después actualiza dbmaster_password localmente, para que la replicación siga autenticando correctamente con el nuevo valor.</p>



<p class="wp-block-paragraph">Después de la Fase B, el script ofrece reiniciar Dovecot, Pure-FTPd y Postfix por ti. Casi siempre digo que sí. Una rotación de credenciales que deja un servicio de correo funcionando con la contraseña antigua no está terminada, solo está callada sobre estar rota. Reiniciar en la misma ejecución, justo después de cambiar los archivos de configuración, mantiene todo dentro de una única ventana de mantenimiento corta. Evita así tres ventanas descubiertas por separado más tarde.</p>



<h2 class="wp-block-heading">Copias de seguridad antes de cambiar nada</h2>



<p class="wp-block-paragraph">Antes de tocar un solo archivo, el script hace una copia completa con rsync de /usr/local/ispconfig/ en una carpeta con marca de tiempo. También intenta un mysqldump no bloqueante de las bases de datos mysql y dbispconfig. No bloqueante a propósito: prefiero terminar una rotación de credenciales antes que quedarme esperando un dump atascado por alguna razón ajena. Cada archivo de configuración que edita recibe primero su propia copia .bak, antes de que se reemplace ninguna cadena dentro de él.</p>



<p class="wp-block-paragraph">Nada de eso sustituye una estrategia de copias de seguridad independiente. Sigo haciendo un snapshot separado, o una copia de seguridad completa, antes de ejecutar esto en cualquier servidor que realmente importe. El propio descargo de responsabilidad del script dice lo mismo, de forma más directa de lo que yo lo acabo de decir.</p>



<h2 class="wp-block-heading">Primero en modo de prueba, siempre</h2>



<p class="wp-block-paragraph">Cada fase pide confirmación antes de cambiar nada. &#8211;dry-run simula una ejecución completa sin tocar un solo archivo. Uso dry-run primero en cada servidor, siempre. Incluso en los que ya he rotado credenciales antes, y creo conocer bien. &#8211;skip-local-root omite la Fase A, para cuando solo hay que rotar la contraseña del usuario de control. &#8211;role=master o &#8211;role=slave anula la detección automática, para el caso poco frecuente en que no lee correctamente por sí solo la topología de un servidor.</p>



<p class="wp-block-paragraph">Las contraseñas que genera tienen 32 caracteres, solo letras y números. Suficientemente largas para resistir intentos de adivinarlas. Suficientemente simples para que los propios parsers de MySQL y PHP nunca tropiecen con un símbolo suelto en una cadena de conexión. He probado el script en ISPConfig 3.3.1p1. No lo he ejecutado contra ramas más antiguas o más nuevas. No asumiría un comportamiento idéntico ahí, sin comprobarlo yo mismo antes.</p>



<p class="wp-block-paragraph">El rol de un nodo no siempre es obvio a simple vista, sobre todo en un servidor que heredaste en lugar de construir tú mismo. Por eso existe &#8211;role, y por eso vale la pena leer línea por línea la salida de dry-run la primera vez, no solo echarle un vistazo. Prefiero pasar dos minutos confirmando que un Slave es realmente un Slave que descubrirlo de la peor manera a mitad de la rotación.</p>



<h2 class="wp-block-heading">Lo que te diría antes de que lo ejecutes</h2>



<p class="wp-block-paragraph"><strong>Aviso</strong>: el script se proporciona tal cual, con fines informativos. Su uso es bajo tu exclusiva responsabilidad: no asumo ninguna responsabilidad por pérdida de datos, interrupciones del servicio, o cualquier error provocado por un uso incorrecto, una modificación del código, o un contexto distinto al que yo mismo utilizo. Haz siempre una copia de seguridad independiente o un snapshot de la VM antes de ejecutarlo en producción, al margen de lo que ya capture por ti el propio paso de backup del script.</p>



<p class="wp-block-paragraph">Es software libre. Puedes redistribuirlo y modificarlo, con atribución a mí como autor original. El código de reset_DB_password.sh, y el resto de la colección ispconfig-fast-tools, está en GitHub en <a href="https://github.com/matteocisilino/ispconfig-fast-tools" target="_blank" rel="noopener">github.com/matteocisilino/ispconfig-fast-tools</a>.</p>



<h2 class="wp-block-heading">La rotación de credenciales de base de datos es un hábito, no una tarea de limpieza</h2>



<p class="wp-block-paragraph">La rotación de credenciales de base de datos tras una brecha confirmada es simplemente respuesta a incidentes competente. La mayoría de administradores lo haría sin que se lo repitieran dos veces. El hábito más difícil es hacerlo tras una simplemente sospechada, antes de que nadie tenga pruebas. Esa es la única versión del plan que realmente limita el daño, mientras todavía es limitable. Un script que ejecuta las fases correctas según el rol del servidor, y que hace sus propias copias de seguridad primero. Además, me permite probarlo todo en modo dry-run antes de tocar nada real. Eso es lo que hace que el hábito sea práctico un martes por la noche, en lugar de teórico en una entrada de blog.</p>



<p class="wp-block-paragraph">ISPConfig, MySQL, PHP, Dovecot, Pure-FTPd, Postfix y los demás nombres mencionados son marcas registradas de sus respectivos propietarios. Se usan únicamente para identificar los productos.</p><p>The post <a href="https://www.cisilino.com/es/2026/10/31/rotacion-de-credenciales-base-de-datos-ispconfig/">Rotación de credenciales de base de datos tras una brecha en ISPConfig</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/31/rotacion-de-credenciales-base-de-datos-ispconfig/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Vulnerabilidad del kernel Linux: un use-after-free en TCP IPv4</title>
		<link>https://www.cisilino.com/es/2026/10/31/vulnerabilidad-kernel-linux-tcp-cve-2026-98432/</link>
					<comments>https://www.cisilino.com/es/2026/10/31/vulnerabilidad-kernel-linux-tcp-cve-2026-98432/#respond</comments>
		
		<dc:creator><![CDATA[matteocisilino]]></dc:creator>
		<pubDate>Sat, 31 Oct 2026 09:19:23 +0000</pubDate>
				<category><![CDATA[Servidores y bases de datos]]></category>
		<category><![CDATA[CVE]]></category>
		<category><![CDATA[Debian]]></category>
		<category><![CDATA[kernel Linux]]></category>
		<category><![CDATA[seguridad de servidores]]></category>
		<guid isPermaLink="false">https://www.cisilino.com/?p=1405</guid>

					<description><![CDATA[<p>Analizo la CVE-2026-98432, un use-after-free en el TCP IPv4 del kernel Linux: CVSS discordantes, parches por distro y un exploit atribuido a Anthropic.</p>
<p>The post <a href="https://www.cisilino.com/es/2026/10/31/vulnerabilidad-kernel-linux-tcp-cve-2026-98432/">Vulnerabilidad del kernel Linux: un use-after-free en TCP IPv4</a> first appeared on <a href="https://www.cisilino.com/es/">Matteo Cisilino</a>.</p>]]></description>
										<content:encoded><![CDATA[<p class="wp-block-paragraph">Gestiono servidores Debian en producción desde hace años. Cada vez que sale una CVE sobre el kernel, me detengo a leer los detalles. Después decido si actualizar enseguida o esperar. Esta vez la historia es más enredada de lo habitual: hay una <strong>vulnerabilidad del kernel Linux</strong> en el TCP IPv4, una puntuación CVSS discutida entre proveedores, e incluso un supuesto exploit root construido por una inteligencia artificial. Empecemos por una pregunta que parece lateral, pero no lo es: qué es MPTCP, y por qué importa aunque no lo uses.</p>



<figure class="wp-block-image size-large"><img decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/vulnerabilita-kernel-linux-tcp-cve-2026-98432.webp" alt="vulnerabilidad del kernel Linux en TCP IPv4, pingüino estilizado con escudo Debian corregido" width="1200" height="630"/></figure>



<h2 class="wp-block-heading">Qué es MPTCP y por qué afecta también a quien no lo usa</h2>



<p class="wp-block-paragraph">Multipath TCP (MPTCP) es el estándar RFC 8684. Extiende el TCP clásico para que una sola conexión viaje por varias rutas de red a la vez. Un teléfono puede usar WiFi y red móvil en el mismo momento, para la misma conexión. Un servidor con varias interfaces puede hacer lo mismo, por redundancia o por suma de ancho de banda. Apple lo usa en producción desde iOS 7, en 2013, para Siri: es el caso de uso real más conocido. Algunos operadores móviles, por ejemplo en Corea del Sur, lo aprovechan para el bonding WiFi/LTE. Varios centros de datos lo adoptan para el multi-homing entre distintos uplinks. En el kernel Linux mainline está desde la versión 5.6.</p>



<p class="wp-block-paragraph">El punto que me hizo levantar una ceja es otro. Desde la versión 1.24, los programas escritos en Go requieren MPTCP por defecto para los sockets en escucha. El Go Security Team lo confirmó el 29 de octubre. Junto con eso, dio un aviso incómodo: el bug del que hablamos hoy no es específico de MPTCP. Un sistema sin listeners MPTCP activos puede seguir expuesto a ataques similares. El código vulnerable está en el path TCP IPv4 genérico, no en un módulo opcional. Se puede desactivar MPTCP programa por programa, con la variable <code>GODEBUG=multipathtcp=0</code> en tiempo de ejecución. Pero el Go Security Team define esta mitigación como «frágil»: basta un solo listener MPTCP expuesto para anularla. Nadie ha demostrado que todo exploit funcional dependa realmente de MPTCP. Por eso el código de MPTCP sigue siendo superficie de ataque, aunque en tu stack no lo actives nunca de forma consciente: basta con que esté compilado en el kernel y sea alcanzable.</p>



<p class="wp-block-paragraph">El propio proyecto MPTCP declara, en su página, que las principales distribuciones Linux actuales envían el kernel con el soporte MPTCP compilado por defecto (<code>CONFIG_MPTCP=y</code>). Menciona explícitamente a Debian, Ubuntu, RHEL y Fedora. Es una declaración del propio proyecto, no una verificación independiente distro por distro, pero el concepto sigue siendo válido: el soporte está activado a nivel de kernel, no es «opt-in» a nivel de sistema operativo. Solo la negociación por conexión individual queda a cargo de la aplicación.</p>



<h2 class="wp-block-heading">La vulnerabilidad del kernel Linux CVE-2026-98432 en resumen</h2>



<p class="wp-block-paragraph">El CVE team del kernel Linux publicó la CVE-2026-98432 el 15 de octubre de 2026. El fix upstream ya se había aplicado diez días antes, el 5 de octubre. Suele pasar que la publicación sigue al commit con cierto retraso. La puntuación CVSS de 9,8 (crítico) llegó solo el 24 de octubre, nueve días después de la publicación de la CVE.</p>



<p class="wp-block-paragraph">El problema está en la gestión del handshake TCP para IPv4, en concreto en la función <code>tcp_v4_syn_recv_sock()</code>. Cuando se crea una conexión, las opciones IP del paquete SYN se pasan del request socket en espera al nuevo socket. Por ejemplo Record Route o Timestamp, si el cliente las incluye en un SYN construido a propósito. El problema es que se pasan por referencia, no por copia, y sin el lock del listener. Según el análisis técnico del parche, las estructuras implicadas se llaman <code>inet_opt</code> e <code>ireq_opt</code>. Una segunda CPU puede liberar ese buffer con <code>kfree()</code>, llamado desde <code>inet_sock_destruct()</code> o desde <code>tcp_v4_reqsk_destructor()</code>. Mientras tanto una primera CPU todavía lo está leyendo en <code>ip_options_build()</code>, para construir el SYNACK. Es un clásico use-after-free causado por una race condition. El path IPv6 equivalente, <code>tcp_v6_syn_recv_sock()</code>, copia las opciones en lugar de pasarlas por referencia: no es vulnerable a la race principal. Existe también una segunda vía de ataque, mediante la migración de request-sockets: <code>net.ipv4.tcp_migrate_req=1</code> o un programa eBPF <code>BPF_SK_REUSEPORT_SELECT_OR_MIGRATE</code>. Ambas están desactivadas por defecto.</p>



<p class="wp-block-paragraph">El bug es remoto y no requiere autenticación ni interacción del usuario, según la evaluación del kernel team. Está presente desde la versión 4.4 del kernel. El fix lo firma Eric Dumazet, reportado por Xinyang Ge de Anthropic. Mueve la liberación de memoria a después de un grace period RCU, con <code>kfree_rcu()</code>: así ninguna CPU puede volver a leer un buffer ya liberado. Las versiones estables corregidas son 5.10.272, 5.15.223, 6.1.190, 6.6.159, 6.12.113, 6.18.56, 7.2.10 y la mainline desde la 7.3-rc7.</p>



<h2 class="wp-block-heading">Puntuaciones CVSS discordantes: no todos los proveedores están de acuerdo</h2>



<p class="wp-block-paragraph">Aquí la historia se complica. El kernel CVE team y Ubuntu valoran la complejidad del ataque como baja, y dan 9,8 (crítico). Oracle y SUSE dan en cambio 8,1 (importante): mismo vector CVSS, pero complejidad de ataque alta. Red Hat, curiosamente, tiene dos valoraciones que no coinciden entre sí. La página pública de la CVE muestra «Moderate, 5.9». El archivo de advisory legible por máquina, actualizado el 29 de octubre, muestra en cambio «Important, 8.1», con impacto alto en confidencialidad e integridad. Ese mismo día el bug tracker interno de Red Hat subió la severidad de medium a high. Amazon, por su parte, asigna 7,0. En el momento de escribir esto, el NVD todavía no ha publicado una puntuación propia.</p>



<p class="wp-block-paragraph">Vale la pena decirlo con honestidad. Esta incoherencia interna de Red Hat no es un detalle que haya deducido yo: así la reporta el artículo original de The Hacker News, mi fuente principal para estos datos. No es la primera vez que un proveedor publica dos números distintos para el mismo bug, en canales diferentes. Pero aquí el salto de «moderado» a «importante» el mismo día dice mucho. Incluso para quien escribe el código, estimar con certeza la explotabilidad real de una race condition en el kernel es difícil.</p>



<h2 class="wp-block-heading">Estado de los parches: Debian ya corregido, otros todavía no</h2>



<p class="wp-block-paragraph">Este es el punto que me toca más de cerca: los hostings que gestiono corren sobre Debian stable. Y aquí los hechos hablan claro, sin necesidad de añadir énfasis: <strong>Debian ya ha corregido esta vulnerabilidad del kernel Linux</strong>, tanto en trixie como en bookworm. Debian trixie (stable) se corrigió el 16 de octubre de 2026, con el paquete <code>linux 6.12.113-1</code>, aviso DSA-6568-1. Debian 12 (bookworm) se corrigió en dos pasos: el 17 y el 22 de octubre de 2026, con <code>linux 6.1.190-1</code> (aviso DLA-4841-1) y <code>linux-6.12 6.12.113-1~deb12u1</code> (aviso DLA-4850-1). En su propia tabla de seguimiento, Debian no indica una puntuación CVSS separada para esta CVE. Se limita a marcar el estado del fix para cada release: es la información que realmente importa, para quien tiene que decidir si reinicia un servidor.</p>



<p class="wp-block-paragraph">Comparar esto con el estado de otras distribuciones muy usadas ayuda a darle peso al asunto. Al 30 de octubre, Red Hat actualizó el estado de RHEL 8, 9, 10 y OpenShift 4. Antes era «fix deferred» (marcado el 29 de octubre), ahora es «affected». No se ha comunicado ninguna fecha de publicación. Ubuntu, en todas las versiones soportadas (22.04, 24.04, 26.04, 26.10), sigue «en curso», sin fecha. Ubuntu 16.04, 18.04 y 20.04 resultan vulnerables, sin ningún fix listado siquiera. SUSE Linux Enterprise Server, desde la SP4 hasta la SP7 más la 16.0, está en la misma situación. Oracle Linux (UEK), openSUSE Leap 16.0, SUSE Linux Micro 6.2 y Amazon Linux 2023 ya están corregidos. RHEL 7 no está afectado: el path de código vulnerable nunca se distribuyó en esa release.</p>



<p class="wp-block-paragraph">No lo digo para hacerle competencia a Ubuntu o RHEL. Siguen siendo distribuciones serias, con procesos de seguridad robustos. Lo digo porque en este bug concreto, en esta ventana temporal, los hechos son estos: quien confía en hostings basados en Debian stable ya tiene el fix instalado. En otras distribuciones muy usadas, en cambio, el servidor queda expuesto por un periodo indefinido. Es una ventaja concreta, verificable mirando las fechas. No es una opinión sobre el proyecto Debian en general.</p>



<h2 class="wp-block-heading">El exploit (reportado, no verificado de forma independiente) de Anthropic</h2>



<p class="wp-block-paragraph">La parte más insólita de esta historia tiene que ver con Anthropic, la empresa que primero reportó el bug a través de Xinyang Ge. Según los informes del sector, uno de sus modelos de IA internos, no publicado, construyó por su cuenta un exploit root funcional. Partiendo solo del parche y del informe como prompt. El ataque en dos fases funcionaría así. En la primera fase, la race hace que el atacante reciba bytes extra de la respuesta del kernel. De ahí recupera una dirección del kernel, para desactivar KASLR. En la segunda fase, el mismo bug causaría un overflow de buffer. Sobrescribe un puntero de callback con la dirección de un payload ya colocado en memoria del kernel por el atacante. Cuando el kernel llama a ese puntero, el payload ejecuta un comando de shell a elección como root. Una race perdida haría fallar solo ese intento de conexión, sin que el sistema se caiga: así que serían posibles intentos repetidos. En la prueba descrita por Anthropic: 48 minutos para robar la dirección del kernel, otros 5 minutos para llegar a la ejecución de comandos.</p>



<p class="wp-block-paragraph">Red Hat dice que un atacante remoto que alcance un servicio TCP expuesto puede, en general, provocar la race. El exploit root descrito por Anthropic, sin embargo, habría apuntado específicamente a servidores con un listener multipath TCP activo. La publicación de Anthropic no aclara si un servidor sin MPTCP es explotable del mismo modo. Ni el registro CVE ni el fix upstream mencionan MPTCP en ningún punto. Es la prensa del sector, no una fuente primaria, la que reporta este vínculo: por eso lo escribo con el escepticismo que merece, no como un hecho confirmado al cien por cien. Anthropic no ha publicado el código del exploit, ni la versión exacta del kernel usada, ni la configuración del servidor objetivo. Otra empresa, Innora, publicó un análisis de la causa raíz, con un entorno de reproducción en QEMU. Declaró que no publicará un exploit armado a corto plazo. Por ahora no hay pruebas de explotación real por parte de proveedores o CERT. El bug no está en el catálogo CISA KEV, según la copia actualizada por CIRCL de Luxemburgo al 28 de octubre.</p>



<h2 class="wp-block-heading">Qué hacer ahora en tus servidores</h2>



<p class="wp-block-paragraph">Ningún proveedor ha publicado un workaround oficialmente sancionado para esta <strong>vulnerabilidad del kernel Linux</strong>. Red Hat dice explícitamente que ninguna de las medidas circuladas hasta ahora alcanza su estándar de facilidad de despliegue y estabilidad. La mitigación primaria sigue siendo aplicar el kernel corregido y reiniciar, cuando el parche esté disponible para tu distribución. En Debian ya lo está. En RHEL y Ubuntu, todavía no, en el momento en que escribo esto.</p>



<p class="wp-block-paragraph">Como defensa en profundidad, no como solución definitiva, tiene sentido hacer algunas cosas. Comprobar que <code>net.ipv4.tcp_migrate_req</code> esté puesto a 0. Verificar que no haya programas eBPF <code>BPF_SK_REUSEPORT_SELECT_OR_MIGRATE</code> cargados. Evaluar un filtrado de los paquetes IPv4 entrantes con opciones Record Route o Timestamp. Monitorizar posibles crashes que mencionen <code>ip_options_build</code>, <code>ip_build_and_send_pkt</code> o <code>skb_under_panic</code>. Hacer inventario de los listeners MPTCP expuestos, evitando dejarlos alcanzables sin motivo. Si también gestionas servicios frente a ataques de aplicación más comunes, en <a href="https://www.cisilino.com/es/2026/10/29/php2shell-cve-2026-109618-mitigacion-servidores/">cómo protegí los servidores a la espera del parche para PHP2Shell</a> encontrarás el mismo enfoque: mitigar lo que se puede, mientras se espera el fix definitivo, sin prometer protecciones que no existen.</p>



<p class="wp-block-paragraph">Para quien me pregunta si actualizar ya: si usas Debian stable, el kernel corregido ya está en los repositorios de seguridad. La actualización es sencilla, y la ventana de exposición ya está cerrada. Si usas otras distribuciones, hoy todavía no hay un parche que instalar. Vale la pena seguir de cerca los avisos de tu proveedor en los próximos días: la situación cambia rápido.</p>



<p class="wp-block-paragraph">Fuentes: <a href="https://thehackernews.com/2026/10/linux-kernel-tcp-flaw-with-98-cvss.html" target="_blank" rel="nofollow noopener">The Hacker News</a>, <a href="https://security-tracker.debian.org/tracker/CVE-2026-98432" target="_blank" rel="nofollow noopener">Debian Security Tracker</a> y el <a href="https://github.com/torvalds/linux/commit/e3f33b0a1d89d738e2b913dab519226eaf76e15b" target="_blank" rel="noopener">commit del fix</a>. Debian, Red Hat, Ubuntu y los demás nombres citados son marcas de sus respectivos propietarios.</p><p>The post <a href="https://www.cisilino.com/es/2026/10/31/vulnerabilidad-kernel-linux-tcp-cve-2026-98432/">Vulnerabilidad del kernel Linux: un use-after-free en TCP IPv4</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/31/vulnerabilidad-kernel-linux-tcp-cve-2026-98432/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Registro de correo WordPress: por qué lo controlo con FluentSMTP</title>
		<link>https://www.cisilino.com/es/2026/10/31/registro-correo-wordpress-por-que-lo-controlo-fluentsmtp/</link>
					<comments>https://www.cisilino.com/es/2026/10/31/registro-correo-wordpress-por-que-lo-controlo-fluentsmtp/#respond</comments>
		
		<dc:creator><![CDATA[matteocisilino]]></dc:creator>
		<pubDate>Sat, 31 Oct 2026 08:00:00 +0000</pubDate>
				<category><![CDATA[Servidores y bases de datos]]></category>
		<category><![CDATA[email]]></category>
		<category><![CDATA[FluentSMTP]]></category>
		<category><![CDATA[plugin de WordPress]]></category>
		<category><![CDATA[SMTP]]></category>
		<guid isPermaLink="false">https://www.cisilino.com/?p=1386</guid>

					<description><![CDATA[<p>Reviso el registro de correo WordPress con FluentSMTP para resolver en minutos los avisos de email no recibido, sin tocar los logs del servidor SMTP.</p>
<p>The post <a href="https://www.cisilino.com/es/2026/10/31/registro-correo-wordpress-por-que-lo-controlo-fluentsmtp/">Registro de correo WordPress: por qué lo controlo con FluentSMTP</a> first appeared on <a href="https://www.cisilino.com/es/">Matteo Cisilino</a>.</p>]]></description>
										<content:encoded><![CDATA[<p class="wp-block-paragraph">Gestiono varios sitios WordPress. En cada uno sale un número de emails que nadie imagina: notificaciones de contacto, restablecimientos de contraseña, confirmaciones de pedido, avisos a los gestores. El registro de correo WordPress de FluentSMTP es la herramienta que reviso en cuanto un cliente me escribe «no he recibido nada». Antes miraba los logs del servidor SMTP por línea de comandos. Tenía que hacer coincidir a mano grep y timestamps. Ahora abro una página del panel de WordPress y lo tengo todo delante.</p>



<figure class="wp-block-image size-large"><img decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/fluent-smtp-log-invii-email-wordpress.webp" alt="registro de correo WordPress visto en el panel de FluentSMTP" width="1200" height="630"/></figure>



<h2 class="wp-block-heading">Por qué reviso el registro de correo WordPress antes que los logs del servidor</h2>



<p class="wp-block-paragraph">El caso típico es siempre el mismo. Un formulario de contacto que «no llega». Un cliente que no recibe el email de restablecimiento de contraseña. Un pedido que se queda sin confirmación. Sin un log a mano, la primera hipótesis es la carpeta de spam. Luego los logs del servidor SMTP. Luego quizá el firewall. Con FluentSMTP me salto esos pasos. Veo enseguida si WordPress realmente intentó enviar ese email. A quién, cuándo, con qué respuesta del servidor de envío.</p>



<p class="wp-block-paragraph">FluentSMTP registra cada llamada a <code>wp_mail()</code>. No solo los emails de plugins de terceros, también los que genera el núcleo: notificaciones de nuevo usuario, avisos de actualización, alertas de error. Para cada envío el log muestra remitente, destinatario, asunto, cabeceras completas, cuerpo del mensaje. Y la respuesta del proveedor SMTP o de la API usada. Si un email ha fallado, esa respuesta del servidor está ahí. No tengo que adivinarla.</p>



<h2 class="wp-block-heading">Qué encuentro realmente en el registro de envíos</h2>



<p class="wp-block-paragraph">El plugin soporta decenas de conectores: Amazon SES, SendGrid, Mailgun, Brevo, Postmark, SparkPost. También Google Workspace y Microsoft 365 vía OAuth, además del clásico SMTP genérico. En los sitios que gestiono uso casi siempre SMTP autenticado con el dominio del cliente. Alguna vez una API dedicada, cuando el volumen de emails crece. En ambos casos el registro de correo WordPress no cambia. Cada línea tiene un estado, enviado o fallido. Hay un botón para revisar el contenido o reenviar el mensaje, sin que el usuario tenga que repetir la acción que lo generó.</p>



<p class="wp-block-paragraph">El reenvío manual es la función que más uso. Un formulario rellenado dos veces porque «no llegaba» se convierte en un clic sobre el log. No un nuevo aviso al cliente. Para los fallos repetidos he activado las notificaciones en Telegram. En cuanto una conexión deja de funcionar, lo sé antes de que me lo escriba alguien. En vez de descubrirlo por un ticket de soporte.</p>



<p class="wp-block-paragraph">El log no se limita a las notificaciones dirigidas a mí o al cliente que administra el sitio. Ahí acaba también cada email de salida generado por el sitio hacia terceros: la confirmación de pedido que una tienda online envía al cliente final, el recibo de un pago, el aviso de envío. Si esa confirmación no llega, la veo en el registro de correo WordPress exactamente como una notificación interna. Tiene el mismo botón para reenviarla al destinatario correcto, sin hacerle repetir la compra.</p>



<h2 class="wp-block-heading">Cómo configuro FluentSMTP en los sitios que gestiono</h2>



<p class="wp-block-paragraph">Después de conectar el conector adecuado, cambio la conservación del log. Por defecto se mantiene activo mucho tiempo. Yo la llevo a 30-60 días, según el tráfico del sitio. Sirve para rastrear un problema señalado con retraso. Sin llenar la base de datos de líneas que nunca consultaré. Luego activo al menos un canal de notificación para los fallos, Telegram o Slack según el cliente. El log solo no avisa a nadie: hay que abrirlo para leerlo.</p>



<p class="wp-block-paragraph">En los sitios con formulario de contacto uso también <a href="https://www.cisilino.com/es/2026/10/29/plugin-seguridad-wordpress-cisilino/">Cisilino Site Defense Network</a> para el bloqueo de bots. Menos spam entrante significa menos emails generados por los formularios. El log queda más legible, con menos ruido que filtrar cuando busco el mensaje correcto.</p>



<h2 class="wp-block-heading">Las credenciales SMTP no quedan en claro en la base de datos</h2>



<p class="wp-block-paragraph">Un detalle que compruebo siempre antes de guardar una conexión: dónde acaban las credenciales. FluentSMTP las cifra por defecto con las claves SALT de WordPress, las mismas presentes en <code>wp-config.php</code> para la seguridad de las sesiones. Se puede desactivar el cifrado, pero el propio plugin lo marca como no recomendado. En los sitios que gestiono prefiero otra opción: guardar la contraseña de la API o del SMTP directamente en <code>wp-config.php</code>, fuera de la base de datos. Defino las constantes antes de que el plugin las lea. Así, ni un volcado de la base de datos ni un acceso no autorizado al panel expone la clave del proveedor de envío. Es el mismo principio que aplico en otros sitios: menos secretos en la base de datos, mejor.</p>



<h2 class="wp-block-heading">Varias conexiones a la vez, no solo una</h2>



<p class="wp-block-paragraph">FluentSMTP permite configurar varias conexiones a la vez, no solo una. En un sitio configuro la predeterminada y añado una segunda como reserva: si el primer proveedor no responde, el envío pasa automáticamente a la conexión de reserva, sin que yo tenga que intervenir. La elección de la conexión depende de la dirección del remitente. Cada conexión tiene su «From». El resto pasa por la predeterminada, con la reserva lista solo si la predeterminada falla de verdad: un error del proveedor, no un email que acabó en spam. Es una de las razones por las que lo prefiero a un único SMTP sin alternativas. Si SendGrid o Amazon SES tienen un problema durante unas horas, el sitio sigue enviando de todas formas, y lo veo enseguida en el registro de correo WordPress. Todo incluido en la versión gratuita, sin una versión pro de pago: solo se paga el proveedor de envío elegido, no el plugin.</p>



<h2 class="wp-block-heading">Un chequeo de salud cada día, no solo cuando algo se rompe</h2>



<p class="wp-block-paragraph">Además del log línea por línea, FluentSMTP hace un chequeo de salud de la conexión cada día, de forma automática. Si el proveedor deja de responder, el aviso llega antes de que un cliente me escriba «el sitio ya no envía emails». El mismo panel muestra un gráfico con los totales diarios de envíos correctos y fallidos. Es útil para entender de un vistazo si una caída es normal o es el síntoma de algo que se está rompiendo, sin abrir el registro de correo WordPress línea por línea cada vez. También hay un resumen periódico por email, con los totales de la semana. No es un aviso de error, es un boletín que leo de pasada para saber si el volumen de envíos está dentro de lo normal.</p>



<p class="wp-block-paragraph">Para quien, como yo, gestiona varios sitios desde la línea de comandos, FluentSMTP también expone comandos WP-CLI. Hay una prueba de envío, un chequeo de salud de la conexión, las estadísticas y la limpieza de los logs antiguos. Los uso en los mismos scripts que ya corren en los servidores para otros controles de mantenimiento, sin abrir el panel de cada sitio uno por uno.</p>



<h2 class="wp-block-heading">Los límites honestos del registro de correo WordPress</h2>



<p class="wp-block-paragraph">No es una solución mágica. La conservación se basa en los días, no en un número máximo de líneas. En un sitio con mucho tráfico transaccional la tabla del log puede crecer rápido. Conviene tenerla controlada, o fijar una conservación más corta. Las notificaciones de fallo llegan solo por los canales de chat configurados, no por email. Tiene sentido: un email que avisa de un email fallido corre el riesgo de fallar también. Pero hay que saberlo de antemano.</p>



<p class="wp-block-paragraph">Por último, el log registra lo que ha pasado por el lado de WordPress y por el lado del proveedor. No garantiza la entrega en la bandeja del destinatario. Si un email aparece como enviado pero acaba en spam, es un problema de reputación del dominio o de configuración SPF/DKIM. No algo que un log pueda resolver por sí solo. Sigue siendo, aun así, el primer sitio donde miro, antes de cualquier otra comprobación.</p>



<p class="wp-block-paragraph">El plugin es gratuito y de código abierto. Página oficial en <a href="https://wordpress.org/plugins/fluent-smtp/" target="_blank" rel="nofollow noopener">wordpress.org</a>, sin funciones bloqueadas detrás de una versión pro. Para quien gestiona aunque sea un solo sitio con un formulario de contacto, vale la pena instalarlo solo por el registro de correo WordPress. La primera vez que resuelve un «no he recibido el email» en un minuto en vez de en media hora, ya se ha pagado solo.</p><p>The post <a href="https://www.cisilino.com/es/2026/10/31/registro-correo-wordpress-por-que-lo-controlo-fluentsmtp/">Registro de correo WordPress: por qué lo controlo con FluentSMTP</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/31/registro-correo-wordpress-por-que-lo-controlo-fluentsmtp/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>PHP2Shell: cómo protegí los servidores a la espera del parche</title>
		<link>https://www.cisilino.com/es/2026/10/29/php2shell-cve-2026-109618-mitigacion-servidores/</link>
					<comments>https://www.cisilino.com/es/2026/10/29/php2shell-cve-2026-109618-mitigacion-servidores/#respond</comments>
		
		<dc:creator><![CDATA[matteocisilino]]></dc:creator>
		<pubDate>Thu, 29 Oct 2026 18:53:44 +0000</pubDate>
				<category><![CDATA[Servidores y bases de datos]]></category>
		<category><![CDATA[CVE-2026-109618]]></category>
		<category><![CDATA[CVE-2026-109619]]></category>
		<category><![CDATA[Debian]]></category>
		<category><![CDATA[gestión de servidores]]></category>
		<category><![CDATA[ISPConfig]]></category>
		<category><![CDATA[mitigación]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[php2shell]]></category>
		<category><![CDATA[seguridad de servidores]]></category>
		<guid isPermaLink="false">https://www.cisilino.com/?p=854</guid>

					<description><![CDATA[<p>Dos fallos graves en el núcleo de PHP (CVE-2026-109618 y CVE-2026-109619): qué son, qué versiones afectan, el script de mitigación y cómo actualicé los servidores.</p>
<p>The post <a href="https://www.cisilino.com/es/2026/10/29/php2shell-cve-2026-109618-mitigacion-servidores/">PHP2Shell: cómo protegí los servidores a la espera del parche</a> first appeared on <a href="https://www.cisilino.com/es/">Matteo Cisilino</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>El 20 de octubre de 2026 se hicieron públicos dos fallos graves en el núcleo de PHP. El nombre común es <strong>PHP2Shell</strong> y los identificadores son CVE-2026-109618 y CVE-2026-109619. La puntuación CVSS es 9,2. Permiten ejecutar código en el servidor sin autenticarse, y ya se han observado ataques.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/php2shell-mitigacion-servidores-php.webp" alt="Esquema de PHP2Shell: cinco círculos conectados a un recuadro con el símbolo del terminal" width="1200" height="630"/></figure>
<p>En este artículo explico qué son y qué versiones de PHP afectan. Cuento cómo protegí los servidores que gestiono mientras no había parche, y cómo llegué a la actualización definitiva. También encontrarás el script que usé y los comandos para lanzarlo.</p>
<h2 class="wp-block-heading">Qué es PHP2Shell</h2>
<p>El problema está en cómo PHP lee las peticiones de tipo <code>multipart/form-data</code>. Es el formato que usa el navegador para enviar archivos. PHP lo interpreta antes de que arranque cualquier script.</p>
<p>Por eso no hace falta un módulo de subida. Puede bastar una sola petición a cualquier archivo PHP del sitio.</p>
<p>Los dos fallos, en resumen:</p>
<ul class="wp-block-list">
<li><strong>CVE-2026-109618.</strong> Si la memoria se agota mientras PHP lee la petición, algunos datos se corrompen. La corrupción aflora en la limpieza final de la petición y puede llevar a ejecutar código.</li>
<li><strong>CVE-2026-109619.</strong> Parte del estado de una petición se queda en memoria. Una petición posterior, en el mismo proceso, lo reutiliza.</li>
</ul>
<p>Estos detalles técnicos vienen del <a href="https://github.com/NCSC-NL/php2shell/wiki" target="_blank" rel="noopener">análisis del NCSC neerlandés</a> y de otras fuentes públicas. No he intentado explotar los fallos en mis servidores.</p>
<h2 class="wp-block-heading">Qué versiones de PHP están afectadas</h2>
<p>Las versiones corregidas salieron el 21 de octubre de 2026: <strong>PHP 8.5.12, 8.4.27, 8.3.36 y 8.2.35</strong>. Toda versión anterior a estas está afectada.</p>
<p>Las ramas 8.1 y anteriores no tienen versión corregida oficial, porque ya no tienen soporte. PHP solo ha publicado unos backports mínimos del código fuente para 7.4, 8.0 y 8.1. Para ramas aún más antiguas no consta nada.</p>
<p>El riesgo alcanza a cualquier forma de ejecutar PHP. La más expuesta es mod_php. También cuentan PHP-FPM, php-cgi, FrankenPHP y LiteSpeed. Y afecta a toda aplicación que se ejecuta sobre PHP: WordPress, Joomla, Drupal, Nextcloud, phpMyAdmin, Roundcube y el panel ISPConfig.</p>
<p>En Debian hay un detalle que conviene conocer. Debian aplica las correcciones como backports y no cambia el número de versión de PHP. Un servidor puede mostrar un número bajo y estar ya corregido. Por eso el script no compara versiones. Comprueba si el changelog del paquete instalado cita las dos CVE.</p>
<h2 class="wp-block-heading">La mitigación, a la espera del parche</h2>
<p>Cuando los fallos se hicieron públicos, los parches aún no estaban disponibles para las distribuciones. No quería dejar los servidores al descubierto, así que reduje el riesgo con un script.</p>
<p><strong>La mitigación no es un parche.</strong> Desactivar <code>session.upload_progress</code> cierra solo una de las vías conocidas de la CVE-2026-109619 y no hace nada contra la CVE-2026-109618. Servía para reducir el margen de ataque hasta la actualización, no para sustituirla.</p>
<h3 class="wp-block-heading">Qué hace el script</h3>
<p>El script está pensado para Debian 12 y 13. Funciona con Apache (mod_php, FastCGI, suPHP, PHP-FPM) y con Nginx (PHP-FPM). Reconoce los servidores gestionados con ISPConfig, tanto Apache como Nginx.</p>
<p>Las opciones:</p>
<ul class="wp-block-list">
<li>Sin opciones, solo comprueba. Para cada paquete PHP verifica que el changelog cite las dos CVE. Señala los procesos que aún usan un binario viejo, lee los ajustes activos y los puertos web en escucha.</li>
<li><code>--update</code> actualiza antes el índice de paquetes.</li>
<li><code>--logs</code> busca indicios sospechosos: archivos PHP recientes en las carpetas de subida o caché, patrones típicos de webshells, referencias a php2shell en los logs. Es una heurística y no prueba que haya compromiso.</li>
<li><code>--harden</code> aplica el endurecimiento descrito más abajo.</li>
<li><code>--dry-run</code>, junto con <code>--harden</code>, muestra lo que haría sin modificar nada.</li>
<li><code>--skip-disable-functions</code>, junto con <code>--harden</code>, no toca <code>disable_functions</code>.</li>
<li><code>--rollback</code> quita lo aplicado por <code>--harden</code>.</li>
</ul>
<h3 class="wp-block-heading">Qué cambia con &#8211;harden</h3>
<p>Para cada versión de PHP presente en <code>/etc/php</code>, el script crea el archivo <code>mods-available/cve-hardening.ini</code>. Lo activa con <code>phpenmod</code> para los SAPI fpm, apache2 y cgi. Así la protección vale para todos los sitios de una vez.</p>
<p>Los <code>php.ini</code> originales quedan intactos. La CLI no se toca, de modo que composer, artisan y wp-cli siguen funcionando.</p>
<p>El archivo establece tres cosas:</p>
<ul class="wp-block-list">
<li><code>session.upload_progress.enabled = Off</code></li>
<li><code>expose_php = Off</code></li>
<li><code>disable_functions</code> con exec, passthru, shell_exec, system, proc_open, popen y pcntl_exec, añadidas a las que ya hubiera.</li>
</ul>
<p>Después el script prueba la configuración y recarga PHP-FPM y Apache solo si la prueba pasa. Antes de escribir hace una copia de seguridad en <code>/var/backups/php-cve-hardening/</code>.</p>
<h3 class="wp-block-heading">Los límites que hay que conocer</h3>
<ul class="wp-block-list">
<li><code>disable_functions</code> puede romper algunas aplicaciones, por ejemplo WordPress con ciertos plugins o Magento. Si pasa, usa <code>--rollback</code> o vuelve a lanzar con <code>--skip-disable-functions</code>.</li>
<li>Las directivas <code>php_admin_value</code> de los pools de PHP-FPM tienen prioridad sobre el archivo global. El script las señala pero no las modifica.</li>
<li>Con ISPConfig, los archivos de <code>pool.d</code> los regenera el panel. Los overrides se corrigen desde el panel, en Sites, Website, Options, Custom php.ini settings, no a mano.</li>
<li>Los <code>php.ini</code> de cada sitio (modo FastCGI o suPHP de ISPConfig) y las versiones de PHP en <code>/opt</code> no se gestionan. Hay que revisarlos aparte.</li>
</ul>
<h2 class="wp-block-heading">Cómo lanzar el script</h2>
<p>El script está aquí: <a href="https://cisilino.com/mscripts/php20261.sh">https://cisilino.com/mscripts/php20261.sh</a></p>
<p>Antes de ejecutar cualquier script descargado de internet, léelo. Puedes descargarlo y abrirlo:</p>
<pre class="wp-block-code"><code>wget -qO php20261.sh https://cisilino.com/mscripts/php20261.sh
less php20261.sh</code></pre>
<p>Si quieres lanzarlo directamente en remoto, hacen falta privilegios de root. Estos son los comandos, en el orden que recomiendo.</p>
<p>1. Solo comprobación, con actualización del índice de paquetes:</p>
<pre class="wp-block-code"><code>wget -qO- https://cisilino.com/mscripts/php20261.sh | sudo bash -s -- --update</code></pre>
<p>2. Simulación del endurecimiento, sin cambios:</p>
<pre class="wp-block-code"><code>wget -qO- https://cisilino.com/mscripts/php20261.sh | sudo bash -s -- --harden --dry-run</code></pre>
<p>3. Aplicación:</p>
<pre class="wp-block-code"><code>wget -qO- https://cisilino.com/mscripts/php20261.sh | sudo bash -s -- --harden</code></pre>
<p>4. Eliminación, si algo se rompe:</p>
<pre class="wp-block-code"><code>wget -qO- https://cisilino.com/mscripts/php20261.sh | sudo bash -s -- --rollback</code></pre>
<p>Después de <code>--harden</code> y de <code>--rollback</code> no hace falta reiniciar nada a mano: el script recarga solo PHP-FPM y Apache, tras probar la configuración. El reinicio sí hace falta después de actualizar el paquete PHP, porque los procesos en ejecución conservan en memoria el binario viejo:</p>
<pre class="wp-block-code"><code>sudo systemctl restart 'php*-fpm' apache2</code></pre>
<p>Las comillas simples alrededor de <code>php*-fpm</code> son necesarias. Sin ellas, es la shell quien expande el asterisco, no systemctl. Si el servidor usa solo Nginx, quita <code>apache2</code>.</p>
<p>El resultado del script es también un código de salida: 0 si todo está bien, 1 si hay avisos, 2 si el sistema es vulnerable o hace falta alguna acción.</p>
<h2 class="wp-block-heading">En servidores con ISPConfig: ispconfig-multipart-guard</h2>
<p><a href="https://www.ispconfig.org/blog/php2shell-critical-php-vulnerabilities-cve-2026-109618-cve-2026-109619/" target="_blank" rel="nofollow noopener"><img loading="lazy" decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/ispconfig-logo.png" alt="Logotipo de ISPConfig" width="239" height="90"></a></p>
<p>Quien usa ISPConfig tiene un arma más. El proyecto ha publicado el script <strong>ispconfig-multipart-guard</strong>, descrito en <a href="https://www.ispconfig.org/blog/php2shell-critical-php-vulnerabilities-cve-2026-109618-cve-2026-109619/" target="_blank" rel="nofollow noopener">el aviso oficial de ISPConfig</a>.</p>
<p>El principio es simple. Si el problema está en las peticiones multipart, el servidor web las detiene antes de que lleguen a PHP:</p>
<ul class="wp-block-list">
<li>bloquea las peticiones <code>Content-Type: multipart/...</code> para todos los sitios, para algunos sitios elegidos o para el panel de control;</li>
<li>con Nginx responde 415 a las peticiones PHP de los sitios bloqueados y los archivos estáticos no cambian. Con Apache responde 403, también para los archivos estáticos;</li>
<li>por defecto bloquea solo el panel de ISPConfig. Los sitios hay que bloquearlos con comandos como <code>block all</code> o <code>block esempio.it</code>;</li>
<li>crea un cron que cada 5 minutos restablece sus hooks, si ISPConfig reescribe la configuración;</li>
<li>tiene una lista de IP autorizadas que aún pueden subir archivos.</li>
</ul>
<p>Funciona con Debian y Ubuntu, con ISPConfig 3.2 o 3.3 (y también sin ISPConfig), con Nginx o con Apache 2.4 y posteriores.</p>
<p>El precio es que en los sitios bloqueados las subidas no funcionan. Eso incluye los medios de los CMS, los adjuntos de los formularios y los del webmail, y las importaciones de phpMyAdmin. Las páginas normales y los formularios urlencoded siguen funcionando. Esta también es una medida provisional y no una corrección.</p>
<p>Este script y el mío no son alternativos. El mío actúa sobre la configuración de PHP, el de ISPConfig sobre el servidor web. No los he comparado en entornos reales y no puedo decirte cómo se comportan juntos en cada configuración.</p>
<h2 class="wp-block-heading">La actualización definitiva, en dos pasos</h2>
<p>Las mitigaciones debían durar poco. Actualicé los servidores en cuanto estuvieron disponibles las actualizaciones, en dos pasos:</p>
<ol class="wp-block-list">
<li><strong>Compilación directa.</strong> Mientras la distribución de cada servidor publicaba su paquete, compilé PHP desde las fuentes en las versiones corregidas.</li>
<li><strong>Paquete oficial.</strong> Cuando el paquete oficial de la distribución se publicó para ese servidor, sustituí la compilación por el paquete.</li>
</ol>
<p>El segundo paso cuenta. Un PHP compilado a mano no se actualiza solo con el resto del sistema. El paquete de la distribución sí, y el script puede comprobarlo leyendo el changelog.</p>
<h2 class="wp-block-heading">Qué hacer ahora</h2>
<ol class="wp-block-list">
<li>Actualiza PHP a la 8.5.12, 8.4.27, 8.3.36 u 8.2.35. En Debian también puedes usar el paquete de la distribución cuando el changelog cite las dos CVE.</li>
<li>Reinicia PHP-FPM y Apache, y vuelve a lanzar el script sin opciones. Ya no debe señalar problemas.</li>
<li>Si usas PHP 8.1 o anterior, planifica el paso a una rama con soporte. Para esas ramas no hay versión corregida oficial.</li>
<li>Mantén desactivado <code>session.upload_progress</code> hasta la próxima publicación de correcciones, si tu aplicación no lo usa.</li>
<li>Revisa los logs y los archivos recientes, con <code>--logs</code> o a mano, buscando archivos PHP aparecidos en las carpetas de subida.</li>
<li>No olvides PHP dentro de contenedores, imágenes y appliances de terceros. Hay que actualizarlos aparte.</li>
<li>Si tu sitio usa WordPress, el <a href="https://www.cisilino.com/es/2026/10/29/plugin-seguridad-wordpress-cisilino/">plugin Cisilino Site Defense Network</a> no resuelve PHP2Shell. Protege, eso sí, el login y mantiene un registro de actividad, útil para entender qué ocurre en el sitio.</li>
</ol>
<h2 class="wp-block-heading">Fuentes</h2>
<ul class="wp-block-list">
<li><a href="https://www.ispconfig.org/blog/php2shell-critical-php-vulnerabilities-cve-2026-109618-cve-2026-109619/" target="_blank" rel="nofollow noopener">ISPConfig: PHP2Shell, vulnerabilidades críticas de PHP (CVE-2026-109618, CVE-2026-109619)</a></li>
<li><a href="https://github.com/NCSC-NL/php2shell/wiki" target="_blank" rel="noopener">NCSC-NL: wiki sobre PHP2Shell</a></li>
</ul>
<p>ISPConfig, PHP, Debian y los demás nombres citados son marcas de sus respectivos propietarios. Se indican solo para identificar los productos.</p><p>The post <a href="https://www.cisilino.com/es/2026/10/29/php2shell-cve-2026-109618-mitigacion-servidores/">PHP2Shell: cómo protegí los servidores a la espera del parche</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/29/php2shell-cve-2026-109618-mitigacion-servidores/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>¿Qué base de datos ocupa espacio en disco? El script para MySQL y PostgreSQL</title>
		<link>https://www.cisilino.com/es/2026/10/20/script-espacio-disco-base-de-datos-mysql/</link>
					<comments>https://www.cisilino.com/es/2026/10/20/script-espacio-disco-base-de-datos-mysql/#respond</comments>
		
		<dc:creator><![CDATA[matteocisilino]]></dc:creator>
		<pubDate>Tue, 20 Oct 2026 07:53:00 +0000</pubDate>
				<category><![CDATA[Servidores y bases de datos]]></category>
		<category><![CDATA[espacio en disco]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[script de bash]]></category>
		<guid isPermaLink="false">https://www.cisilino.com/?p=264</guid>

					<description><![CDATA[<p>El disco del servidor se llena y no sabes qué base de datos es la responsable. Un script para medir el espacio de MySQL y PostgreSQL y compararlo en el tiempo.</p>
<p>The post <a href="https://www.cisilino.com/es/2026/10/20/script-espacio-disco-base-de-datos-mysql/">¿Qué base de datos ocupa espacio en disco? El script para MySQL y PostgreSQL</a> first appeared on <a href="https://www.cisilino.com/es/">Matteo Cisilino</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Le pasa a todo el mundo tarde o temprano. El espacio en disco del servidor se llena, salta la alarma y la pregunta es siempre la misma. <strong>¿Qué base de datos está ocupando todo este espacio?</strong></p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/espacio-disco-bases-datos-mysql-postgresql.webp" alt="Espacio en disco por base de datos: gráfico de anillo en cuatro sectores con tres barras de leyenda a la derecha" width="1200" height="630"/></figure>
<p>Yo me encontré con esto después de una optimización. Había lanzado <code>OPTIMIZE TABLE</code> con la esperanza de recuperar espacio, pero el sistema de archivos no bajaba como esperaba. Sin un «antes» con el que comparar el «después», era imposible saber si la optimización había funcionado. No sabía qué base de datos era la verdadera responsable. Ni si el espacio que faltaba estaba incluso en otra parte.</p>
<p>Las herramientas que tenía a mano solo respondían a la mitad del problema.</p>
<ul class="wp-block-list">
<li><code>du</code> muestra los archivos en disco, pero no dice a qué tabla pertenecen.</li>
<li><code>information_schema</code> (o <code>pg_class</code> en PostgreSQL) da los tamaños lógicos, pero no ve los binlogs, <code>ibdata1</code>, los undo logs ni el WAL.</li>
<li>ambos fotografían un solo instante. La comparación en el tiempo sigue siendo un trabajo manual, hecho con copiar y pegar y una hoja de cálculo.</li>
</ul>
<p>Necesitaba algo sencillo: <strong>fotografiar el estado de todas las bases de datos en un momento dado, volver a hacerlo más tarde y que me dijera qué ha cambiado</strong>, ordenando las diferencias de mayor a menor. Así el culpable salta enseguida a la vista.</p>
<p>Salió un único script de Bash. Solo necesita el cliente de la base de datos y las herramientas estándar de Linux (<code>awk</code>, <code>du</code>, <code>df</code>). Funciona con <strong>MySQL/MariaDB</strong> y con <strong>PostgreSQL</strong>. Lo comparto aquí con la esperanza de que le ahorre a alguien la misma tarde de frustración.</p>
<h2 class="wp-block-heading">Qué hace: medir el espacio en disco</h2>
<p>El script tiene cuatro comandos.</p>
<ul class="wp-block-list">
<li><code>snapshot</code> guarda el estado actual (bases de datos, tablas, archivos del directorio de datos, espacio del sistema de archivos).</li>
<li><code>compare</code> compara dos instantáneas y muestra las diferencias, las mayores arriba.</li>
<li><code>report</code> muestra las bases de datos y tablas más grandes de una sola instantánea.</li>
<li><code>list</code> lista las instantáneas guardadas.</li>
</ul>
<p>La comparación se divide en cuatro secciones, para no dejar rincones oscuros.</p>
<ul class="wp-block-list">
<li><strong>tamaño de cada base de datos</strong> (datos + índices).</li>
<li><strong>espacio libre o recuperable</strong> (<code>data_free</code> en MySQL, estimación de tuplas muertas en PostgreSQL).</li>
<li><strong>tamaño de cada tabla</strong>.</li>
<li><strong>archivos reales en el directorio de datos</strong>: carpetas de las bases de datos, <code>ibdata1</code>, binlogs, undo, <code>pg_wal</code>.</li>
</ul>
<p>La última sección suele ser la que resuelve el misterio. Muchas veces el espacio «anómalo» no pertenece a ninguna base de datos. Pertenece a los logs binarios o a los archivos compartidos de InnoDB.</p>
<h2 class="wp-block-heading">Requisitos</h2>
<ul class="wp-block-list">
<li>Linux con Bash 4 o superior, <code>awk</code>, <code>du</code>, <code>df</code> (todo de serie).</li>
<li>el cliente <code>mysql</code> o <code>psql</code>, con un usuario que pueda leer los metadatos de todas las bases de datos.</li>
<li>permisos de root (o del usuario <code>mysql</code>/<code>postgres</code>) si quieres medir también los archivos en disco. Sin ellos, esa sección se omite con un aviso.</li>
</ul>
<h2 class="wp-block-heading">Instalación</h2>
<p>Guarda el script (lo encontrarás al final del artículo) en un archivo y hazlo ejecutable.</p>
<pre class="wp-block-code"><code>nano db_space_compare.sh
chmod +x db_space_compare.sh</code></pre>
<h3 class="wp-block-heading">Credenciales</h3>
<p>Para MySQL/MariaDB lo más limpio es un archivo <code>~/.my.cnf</code> con las credenciales, así no aparecen en el historial de comandos.</p>
<pre class="wp-block-code"><code>&#91;client]
user=root
password=LA_TUA_PASSWORD</code></pre>
<p>Como alternativa puedes pasar las opciones con una variable de entorno.</p>
<pre class="wp-block-code"><code>export MYSQL_OPTS="-u root -pPASSWORD -h 127.0.0.1"</code></pre>
<p>Para PostgreSQL funcionan <code>~/.pgpass</code>, las variables <code>PG*</code> o <code>PG_OPTS</code>.</p>
<pre class="wp-block-code"><code>export PG_OPTS="-U postgres -h localhost"</code></pre>
<h2 class="wp-block-heading">Cómo se usa</h2>
<p>El flujo típico es <em>antes, optimización, después, comparación</em>.</p>
<pre class="wp-block-code"><code># 1. fotografia prima dell'ottimizzazione
sudo ./db_space_compare.sh snapshot prima

# 2. esegui l'ottimizzazione (OPTIMIZE TABLE, VACUUM FULL, ...)

# 3. fotografia dopo
sudo ./db_space_compare.sh snapshot dopo

# 4. confronto
sudo ./db_space_compare.sh compare prima dopo</code></pre>
<p>Para PostgreSQL basta con añadir la opción <code>-e postgres</code>.</p>
<pre class="wp-block-code"><code>sudo ./db_space_compare.sh -e postgres snapshot prima
sudo ./db_space_compare.sh -e postgres snapshot dopo
sudo ./db_space_compare.sh -e postgres compare prima dopo</code></pre>
<p>Las opciones disponibles son:</p>
<ul class="wp-block-list">
<li><code>-e mysql|postgres</code> elige el motor (por defecto: <code>mysql</code>).</li>
<li><code>-d DIR</code> cambia la carpeta donde se guardan las instantáneas (por defecto: <code>./db_snapshots</code>).</li>
<li><code>-n N</code> fija cuántas filas mostrar por sección (por defecto: 20).</li>
</ul>
<p>Si no indicas una etiqueta, la instantánea toma como nombre la fecha y la hora. Es cómodo para monitorizar el crecimiento en el tiempo.</p>
<h2 class="wp-block-heading">Ejemplo de salida</h2>
<p>Aquí tienes una comparación con datos de prueba, construidos a propósito para mostrar un caso típico. La base de datos <code>shop</code> se reduce tras la optimización, pero la base <code>log</code> crece y los binlogs se duplican.</p>
<pre class="wp-block-code"><code>Confronto 'prima' (2026-10-11 20:00:00)  -&gt;  'dopo' (2026-10-11 22:00:00)
Filesystem /var: usato 46.6 GiB -&gt; 48.4 GiB (+1.9 GiB)

=== Database - dimensione totale ===
  NOME                                                      PRIMA         DOPO    DIFFERENZA  NOTE
  log                                                    21.4 GiB     27.5 GiB      +6.1 GiB  +28.3%
  shop                                                    5.6 GiB      3.6 GiB      -2.0 GiB  -35.0%

=== Tabelle - dimensione (dati + indici) ===
  NOME                                                      PRIMA         DOPO    DIFFERENZA  NOTE
  log.eventi                                             21.4 GiB     27.5 GiB      +6.1 GiB  +28.3%
  shop.ordini                                             5.6 GiB      3.6 GiB      -2.0 GiB  -35.0%
  log.nuova                                                   0 B      1.0 KiB      +1.0 KiB  NUOVO

=== File nella data directory (cartelle DB, ibdata1, binlog, undo, WAL) ===
  NOME                                                      PRIMA         DOPO    DIFFERENZA  NOTE
  log                                                    21.4 GiB     27.5 GiB      +6.1 GiB  +28.3%
  mysql-bin.*                                             3.7 GiB      8.4 GiB      +4.7 GiB  +125.0%
  shop                                                    5.7 GiB      3.6 GiB      -2.0 GiB  -36.1%
  ibdata1                                                11.2 GiB     11.2 GiB           0 B</code></pre>
<p>Aquí el culpable es evidente: la tabla <code>log.eventi</code> ha crecido más de 6 GiB, y los archivos <code>mysql-bin.*</code> han pasado de 3,7 a 8,4 GiB. La optimización de <code>shop</code> funcionó, pero la ganancia se la comió el resto.</p>
<h2 class="wp-block-heading">Cómo leer los resultados del espacio en disco</h2>
<ul class="wp-block-list">
<li>Arriba de cada sección están los elementos que más han cambiado. Una base de datos o una tabla que <strong>no baja</strong> (o incluso crece) tras la optimización es la primera sospechosa.</li>
<li>Si las tablas bajan pero el sistema de archivos no, el espacio está en otra parte. Mira la sección sobre los archivos del directorio de datos (binlogs, <code>ibdata1</code>, undo, <code>pg_wal</code>).</li>
<li><strong>MySQL/MariaDB:</strong> <code>ibdata1</code> nunca se encoge, ni siquiera con <code>OPTIMIZE TABLE</code>. Con <code>innodb_file_per_table=OFF</code> el valor de <code>data_free</code> es compartido y poco indicativo.</li>
<li><strong>PostgreSQL:</strong> un simple <code>VACUUM</code> no devuelve espacio al sistema operativo, hace falta <code>VACUUM FULL</code> (o <code>pg_repack</code>). El espacio «recuperable» que se muestra es una estimación basada en las tuplas muertas.</li>
</ul>
<p>Para PostgreSQL, la documentación sobre <a href="https://www.postgresql.org/docs/current/diskusage.html" target="_blank" rel="noopener">monitorizar el uso del disco</a> explica cómo se calculan los tamaños de tablas e índices.</p>
<h2 class="wp-block-heading">Monitorizar el espacio en disco en el tiempo</h2>
<p>Si el problema no está ligado a una sola optimización sino a un crecimiento gradual. Conviene automatizar las instantáneas con cron, una por hora, y luego comparar dos momentos cualesquiera.</p>
<pre class="wp-block-code"><code># crontab -e (come root)
0 * * * * /usr/local/bin/db_space_compare.sh -d /var/lib/db_snapshots snapshot &gt;/dev/null 2&gt;&amp;1</code></pre>
<p>Sin etiqueta, cada instantánea toma como nombre la fecha y la hora (por ejemplo <code>20261011-140000</code>). Con <code>list</code> ves las disponibles y con <code>compare</code> comparas dos. En pocos días se ve qué base de datos crece de forma anómala.</p>
<pre class="wp-block-code"><code>./db_space_compare.sh -d /var/lib/db_snapshots list
./db_space_compare.sh -d /var/lib/db_snapshots compare 20261011-140000 20261012-140000</code></pre>
<h2 class="wp-block-heading">El script completo</h2>
<p>Aquí está el código. Cópialo en un archivo llamado <code>db_space_compare.sh</code>, hazlo ejecutable y listo.</p>
<pre class="wp-block-code"><code>#!/usr/bin/env bash
#
# db_space_compare.sh
# Fotografa l'occupazione di spazio di tutti i database (MySQL/MariaDB o
# PostgreSQL) e confronta due fotografie (es. prima/dopo l'ottimizzazione)
# per capire quale database, tabella o file occupa spazio in modo anomalo.
#
# Uso:
#   db_space_compare.sh &#91;opzioni] snapshot &#91;etichetta]   (default: data-ora)
#   db_space_compare.sh &#91;opzioni] compare  &lt;prima&gt; &lt;dopo&gt;
#   db_space_compare.sh &#91;opzioni] report   &lt;etichetta&gt;
#   db_space_compare.sh &#91;opzioni] list
#
# Opzioni:
#   -e mysql|postgres   motore del database (default: mysql)
#   -d DIR              cartella delle fotografie (default: ./db_snapshots)
#   -n N                righe mostrate per sezione (default: 20)
#
# Credenziali:
#   MySQL/MariaDB: ~/.my.cnf oppure MYSQL_OPTS="-u root -pPASSWORD -h 127.0.0.1"
#   PostgreSQL:    ~/.pgpass, variabili PG* oppure PG_OPTS="-U postgres -h localhost"
#                  (non mettere -d in PG_OPTS: lo script si collega a ogni database)
#
# Per misurare anche i file su disco (cartelle dei DB, ibdata1, binlog, undo,
# pg_wal) va eseguito come root o come utente mysql/postgres; altrimenti
# quella parte viene saltata con un avviso.
#
# Esempio:
#   sudo ./db_space_compare.sh snapshot prima
#   ... ottimizzazione (OPTIMIZE TABLE / VACUUM FULL / ...) ...
#   sudo ./db_space_compare.sh snapshot dopo
#   sudo ./db_space_compare.sh compare prima dopo

set -euo pipefail
export LC_ALL=C

ENGINE=mysql
OUTDIR=./db_snapshots
TOPN=20
SNAP_IN_PROGRESS=""

die()  { echo "ERRORE: $*" &gt;&amp;2; exit 1; }
warn() { echo "ATTENZIONE: $*" &gt;&amp;2; }
usage() { sed -n '2,/^$/{s/^# \{0,1\}//;p}' "$0"; exit "${1:-0}"; }

cleanup() {
  if &#91;&#91; -n $SNAP_IN_PROGRESS ]]; then
    rm -rf -- "$SNAP_IN_PROGRESS"
    echo "Fotografia incompleta rimossa: $SNAP_IN_PROGRESS" &gt;&amp;2
  fi
}
trap cleanup EXIT

while getopts ":e:d:n:h" opt; do
  case $opt in
    e) ENGINE=$OPTARG ;;
    d) OUTDIR=$OPTARG ;;
    n) TOPN=$OPTARG ;;
    h) usage 0 ;;
    *) usage 1 ;;
  esac
done
shift $((OPTIND - 1))

&#91;&#91; $ENGINE == mysql || $ENGINE == postgres ]] || die "motore non valido: '$ENGINE' (usa mysql o postgres)"
&#91;&#91; $TOPN =~ ^&#91;0-9]+$ ]] || die "-n deve essere un numero"

read -r -a MYSQL_ARGS &lt;&lt;&lt; "${MYSQL_OPTS:-}"
read -r -a PG_ARGS    &lt;&lt;&lt; "${PG_OPTS:-}"

my() { mysql "${MYSQL_ARGS&#91;@]+"${MYSQL_ARGS&#91;@]}"}" --batch --skip-column-names "$@"; }
pg() { psql  "${PG_ARGS&#91;@]+"${PG_ARGS&#91;@]}"}" -X -q -A -t -F $'\t' -v ON_ERROR_STOP=1 "$@"; }

# Funzioni awk comuni: formattazione in KiB/MiB/GiB, chiave e valore di riga.
#   nk = numero di colonne che formano il nome (1 = database, 2 = db.tabella)
#   vc = colonne da sommare per il valore (es. "4,5" = dati + indici)
AWK_LIB='
function h(b,   a, i, u) {
  split("B KiB MiB GiB TiB PiB", u, " ")
  a = (b &lt; 0) ? -b : b; i = 1
  while (a &gt;= 1024 &amp;&amp; i &lt; 6) { a /= 1024; i++ }
  if (i == 1) return sprintf("%s%d B", (b &lt; 0 ? "-" : ""), a)
  return sprintf("%s%.1f %s", (b &lt; 0 ? "-" : ""), a, u&#91;i])
}
function hs(b) { return (b &gt; 0 ? "+" : "") h(b) }
function key(   k, i) { k = $1; for (i = 2; i &lt;= nk; i++) k = k "." $i; return k }
function val(   n, c, i, s) { n = split(vc, c, ","); s = 0; for (i = 1; i &lt;= n; i++) s += $(c&#91;i]); return s }
'

meta_get() { awk -F= -v k="$2" '$1 == k { sub(/^&#91;^=]*=/, ""); print; exit }' "$1/meta.txt"; }

# ---------------------------------------------------------------- raccolta dati

# Occupazione reale su disco delle voci di primo livello della data directory:
# cartelle dei database, ibdata1, binlog, undo, redo log, pg_wal, base, ...
# I file numerati (mysql-bin.000123, relay-log.000045, ...) sono sommati in una voce.
collect_disk() {
  local d=$1 out=$2
  : &gt; "$out"
  if &#91;&#91; -z $d || ! -r $d || ! -x $d ]]; then
    warn "data directory '${d:-?}' non leggibile: salto i file su disco (esegui come root per includerli)"
    return 0
  fi
  { du -sb -- "$d"/* 2&gt;/dev/null || true; } |
    awk -F'\t' -v p="$d/" '
      { n = $2
        if (index(n, p) == 1) n = substr(n, length(p) + 1)
        if (n ~ /\.&#91;0-9]&#91;0-9]&#91;0-9]&#91;0-9]&#91;0-9]&#91;0-9]$/) sub(/&#91;0-9]+$/, "*", n)
        s&#91;n] += $1 }
      END { for (n in s) printf "%s\t%.0f\n", n, s&#91;n] }' &gt; "$out"
}

write_meta() {
  local dir=$1 datadir=$2 version=$3
  {
    echo "timestamp=$(date '+%Y-%m-%d %H:%M:%S')"
    echo "engine=$ENGINE"
    echo "host=$(hostname)"
    echo "version=$version"
    echo "datadir=$datadir"
    if &#91;&#91; -n $datadir &amp;&amp; -e $datadir ]]; then
      { df -P -B1 -- "$datadir" 2&gt;/dev/null || true; } |
        awk 'NR == 2 { print "fs_mount=" $6; print "fs_used=" $3; print "fs_avail=" $4 }'
    fi
  } &gt; "$dir/meta.txt"
}

# File prodotti da ogni fotografia (separati da TAB):
#   tables.tsv     db, tabella, engine, dati, indici, libero/recuperabile, righe
#   databases.tsv  db, dati, indici, libero/recuperabile, totale
#   disk.tsv       voce della data directory, byte su disco
#   meta.txt       data, host, versione, filesystem

snapshot_mysql() {
  local dir=$1 pre="" datadir version
  command -v mysql &gt;/dev/null 2&gt;&amp;1 || die "client 'mysql' non trovato"
  my -e "SELECT 1" &gt;/dev/null || die "connessione a MySQL/MariaDB fallita (controlla ~/.my.cnf o MYSQL_OPTS)"

  # MySQL 8 tiene in cache le statistiche di information_schema fino a 24 ore:
  # senza questo, i valori "dopo" l'ottimizzazione potrebbero essere quelli vecchi.
  if &#91;&#91; -n $(my -e "SHOW VARIABLES LIKE 'information_schema_stats_expiry'") ]]; then
    pre="SET SESSION information_schema_stats_expiry = 0;"
  fi

  my -e "$pre
    SELECT table_schema, table_name, IFNULL(engine, '-'),
           IFNULL(data_length, 0), IFNULL(index_length, 0), IFNULL(data_free, 0), IFNULL(table_rows, 0)
      FROM information_schema.TABLES
     WHERE table_type IN ('BASE TABLE', 'SYSTEM VERSIONED')
       AND table_schema NOT IN ('information_schema', 'performance_schema', 'sys')" &gt; "$dir/tables.tsv"

  my -e "$pre
    SELECT s.schema_name,
           IFNULL(SUM(t.data_length), 0), IFNULL(SUM(t.index_length), 0), IFNULL(SUM(t.data_free), 0),
           IFNULL(SUM(t.data_length), 0) + IFNULL(SUM(t.index_length), 0)
      FROM information_schema.SCHEMATA s
      LEFT JOIN information_schema.TABLES t
             ON t.table_schema = s.schema_name
            AND t.table_type IN ('BASE TABLE', 'SYSTEM VERSIONED')
     WHERE s.schema_name NOT IN ('information_schema', 'performance_schema', 'sys')
     GROUP BY s.schema_name" &gt; "$dir/databases.tsv"

  datadir=$(my -e "SELECT @@datadir"); datadir=${datadir%/}
  version=$(my -e "SELECT VERSION()")
  collect_disk "$datadir" "$dir/disk.tsv"
  write_meta "$dir" "$datadir" "$version"
}

snapshot_postgres() {
  local dir=$1 maindb=${PGDATABASE:-postgres} datadir version db
  command -v psql &gt;/dev/null 2&gt;&amp;1 || die "client 'psql' non trovato"
  pg -d "$maindb" -c "SELECT 1" &gt;/dev/null || die "connessione a PostgreSQL fallita (controlla PG_OPTS, PGUSER, ~/.pgpass)"

  pg -d "$maindb" -c "SELECT datname FROM pg_database WHERE datallowconn AND NOT datistemplate ORDER BY 1" &gt; "$dir/.dblist"

  # Per ogni database: dimensione di ogni tabella (catalogo incluso, es. pg_largeobject)
  # e stima dello spazio occupato da tuple morte (bloat recuperabile con VACUUM FULL).
  : &gt; "$dir/tables.tsv"
  while IFS= read -r db; do
    &#91;&#91; -n $db ]] || continue
    pg -d "$db" -c "
      SELECT current_database(), s.schemaname || '.' || s.relname, 'table',
             pg_table_size(s.relid), pg_indexes_size(s.relid),
             CASE WHEN s.n_live_tup + s.n_dead_tup &gt; 0
                  THEN (pg_table_size(s.relid)::numeric * s.n_dead_tup / (s.n_live_tup + s.n_dead_tup))::bigint
                  ELSE 0 END,
             s.n_live_tup
        FROM pg_stat_all_tables s
       WHERE s.schemaname &lt;&gt; 'information_schema'
         AND s.schemaname NOT LIKE 'pg_toast%'" &lt; /dev/null &gt;&gt; "$dir/tables.tsv" \
      || warn "impossibile leggere le tabelle del database '$db'"
  done &lt; "$dir/.dblist"

  pg -d "$maindb" -c "SELECT datname, pg_database_size(oid) FROM pg_database WHERE datallowconn AND NOT datistemplate" &gt; "$dir/.dbsize"
  awk -F'\t' '
    FILENAME == ARGV&#91;1] { d&#91;$1] += $4; i&#91;$1] += $5; f&#91;$1] += $6; next }
    { printf "%s\t%.0f\t%.0f\t%.0f\t%s\n", $1, d&#91;$1], i&#91;$1], f&#91;$1], $2 }' \
    "$dir/tables.tsv" "$dir/.dbsize" &gt; "$dir/databases.tsv"

  datadir=$(pg -d "$maindb" -c "SHOW data_directory" 2&gt;/dev/null || true)
  version=$(pg -d "$maindb" -c "SHOW server_version")
  collect_disk "$datadir" "$dir/disk.tsv"

  # Dettaglio di base/&lt;oid&gt; con il nome del database al posto dell'oid
  if &#91;&#91; -n $datadir &amp;&amp; -r $datadir/base &amp;&amp; -x $datadir/base ]]; then
    pg -d "$maindb" -c "SELECT oid, datname FROM pg_database" &gt; "$dir/.oidmap"
    { du -sb -- "$datadir"/base/* 2&gt;/dev/null || true; } |
      awk -F'\t' '
        FILENAME == ARGV&#91;1] { name&#91;$1] = $2; next }
        { o = $2; sub(/.*\//, "", o); printf "base/%s\t%s\n", ((o in name) ? name&#91;o] : o), $1 }' \
        "$dir/.oidmap" - &gt;&gt; "$dir/disk.tsv"
  fi
  write_meta "$dir" "$datadir" "$version"
}

# --------------------------------------------------------------- presentazione

print_fs() {
  local d=$1 used avail mount
  used=$(meta_get "$d" fs_used); avail=$(meta_get "$d" fs_avail); mount=$(meta_get "$d" fs_mount)
  &#91;&#91; -n $used ]] || return 0
  awk -v u="$used" -v a="$avail" -v m="$mount" "$AWK_LIB"'
    BEGIN { printf "Filesystem %s: usato %s, libero %s\n", m, h(u), h(a) }'
}

top_section() {
  local f=$1 nk=$2 vc=$3 title=$4
  echo
  echo "=== $title ==="
  if &#91;&#91; ! -s $f ]]; then echo "  (nessun dato)"; return 0; fi
  awk -F'\t' -v nk="$nk" -v vc="$vc" "$AWK_LIB"'
    { v = val(); printf "%.0f\t%s\t%s\n", v, key(), h(v) }' "$f" |
    sort -t $'\t' -k1,1nr -k2,2 |
    awk -F'\t' -v top="$TOPN" 'NR &lt;= top &amp;&amp; $1 &gt; 0 { printf "  %-60s %12s\n", $2, $3 }'
}

compare_section() {
  local fa=$1 fb=$2 nk=$3 vc=$4 title=$5
  &#91;&#91; -f $fa ]] || fa=/dev/null
  &#91;&#91; -f $fb ]] || fb=/dev/null
  echo
  echo "=== $title ==="
  if &#91;&#91; ! -s $fa &amp;&amp; ! -s $fb ]]; then echo "  (nessun dato)"; return 0; fi
  printf '  %-50s %12s %12s %13s  %s\n' "NOME" "PRIMA" "DOPO" "DIFFERENZA" "NOTE"
  awk -F'\t' -v nk="$nk" -v vc="$vc" "$AWK_LIB"'
    FILENAME == ARGV&#91;1] { k = key(); A&#91;k] = val(); S&#91;k] = 1; next }
                        { k = key(); B&#91;k] = val(); S&#91;k] = 1 }
    END {
      for (k in S) {
        inA = (k in A); inB = (k in B)
        a = inA ? A&#91;k] : 0; b = inB ? B&#91;k] : 0; d = b - a
        if (!inA)                 note = "NUOVO"
        else if (!inB)            note = "RIMOSSO"
        else if (a &gt; 0 &amp;&amp; d != 0) note = sprintf("%+.1f%%", 100 * d / a)
        else                      note = ""
        printf "%.0f\t%s\t%s\t%s\t%s\t%s\n", (d &lt; 0 ? -d : d), k, h(a), h(b), hs(d), note
      }
    }' "$fa" "$fb" |
    sort -t $'\t' -k1,1nr -k2,2 |
    awk -F'\t' -v top="$TOPN" 'NR &lt;= top { printf "  %-50s %12s %12s %13s  %s\n", $2, $3, $4, $5, $6 }'
}

# -------------------------------------------------------------------- comandi

cmd_snapshot() {
  local label=${1:-$(date '+%Y%m%d-%H%M%S')}
  &#91;&#91; $label =~ ^&#91;A-Za-z0-9]&#91;A-Za-z0-9._-]*$ ]] || die "etichetta non valida: usa solo lettere, numeri, . _ -"
  local dir=$OUTDIR/$label
  &#91;&#91; ! -e $dir ]] || die "la fotografia '$label' esiste già ($dir)"
  mkdir -p -- "$dir"
  SNAP_IN_PROGRESS=$dir
  echo "Fotografia '$label' ($ENGINE) in corso..."
  "snapshot_$ENGINE" "$dir"
  rm -f -- "$dir/.dblist" "$dir/.dbsize" "$dir/.oidmap"
  SNAP_IN_PROGRESS=""
  echo "Salvata in $dir ($(wc -l &lt; "$dir/tables.tsv") tabelle)"
  print_fs "$dir"
  top_section "$dir/databases.tsv" 1 5 "Database più grandi"
}

cmd_report() {
  local label=${1:-}
  &#91;&#91; -n $label ]] || die "uso: report &lt;etichetta&gt;"
  local d=$OUTDIR/$label
  &#91;&#91; -f $d/meta.txt ]] || die "fotografia '$label' non trovata in $OUTDIR"
  echo "Fotografia '$label' del $(meta_get "$d" timestamp) - $(meta_get "$d" engine) $(meta_get "$d" version) su $(meta_get "$d" host)"
  print_fs "$d"
  top_section "$d/databases.tsv" 1 5   "Database più grandi (dati + indici)"
  top_section "$d/tables.tsv"    2 4,5 "Tabelle più grandi (dati + indici)"
  top_section "$d/tables.tsv"    2 6   "Tabelle con più spazio recuperabile (data_free / tuple morte stimate)"
  top_section "$d/disk.tsv"      1 2   "Voci più grandi nella data directory"
}

cmd_compare() {
  local a=${1:-} b=${2:-}
  &#91;&#91; -n $a &amp;&amp; -n $b ]] || die "uso: compare &lt;etichetta_prima&gt; &lt;etichetta_dopo&gt;"
  &#91;&#91; $a != "$b" ]] || die "le due etichette sono uguali"
  local da=$OUTDIR/$a db=$OUTDIR/$b
  &#91;&#91; -f $da/meta.txt ]] || die "fotografia '$a' non trovata in $OUTDIR"
  &#91;&#91; -f $db/meta.txt ]] || die "fotografia '$b' non trovata in $OUTDIR"
  &#91;&#91; $(meta_get "$da" engine) == "$(meta_get "$db" engine)" ]] || warn "le due fotografie sono di motori diversi"

  echo "Confronto '$a' ($(meta_get "$da" timestamp))  -&gt;  '$b' ($(meta_get "$db" timestamp))"
  local ua ub m
  ua=$(meta_get "$da" fs_used); ub=$(meta_get "$db" fs_used); m=$(meta_get "$db" fs_mount)
  if &#91;&#91; -n $ua &amp;&amp; -n $ub ]]; then
    awk -v a="$ua" -v b="$ub" -v m="$m" "$AWK_LIB"'
      BEGIN { printf "Filesystem %s: usato %s -&gt; %s (%s)\n", m, h(a), h(b), hs(b - a) }'
  fi

  compare_section "$da/databases.tsv" "$db/databases.tsv" 1 5   "Database - dimensione totale"
  compare_section "$da/databases.tsv" "$db/databases.tsv" 1 4   "Database - spazio libero/recuperabile"
  compare_section "$da/tables.tsv"    "$db/tables.tsv"    2 4,5 "Tabelle - dimensione (dati + indici)"
  compare_section "$da/disk.tsv"      "$db/disk.tsv"      1 2   "File nella data directory (cartelle DB, ibdata1, binlog, undo, WAL)"

  cat &lt;&lt;'EOF'

Come leggere il risultato:
  - In cima a ogni sezione ci sono le voci cambiate di più: un database o una
    tabella che dopo l'ottimizzazione non cala, o addirittura cresce, è il
    primo sospetto.
  - Se le tabelle calano ma il filesystem no, lo spazio sta altrove: guarda la
    sezione "File nella data directory" (binlog, ibdata1, undo, pg_wal).
  - MySQL/MariaDB: ibdata1 non si restringe mai, nemmeno con OPTIMIZE; con
    innodb_file_per_table=OFF il data_free è condiviso e poco indicativo.
  - PostgreSQL: VACUUM semplice non restituisce spazio al disco, serve
    VACUUM FULL (o pg_repack); lo spazio "recuperabile" è una stima.
EOF
}

cmd_list() {
  if &#91;&#91; ! -d $OUTDIR ]]; then echo "Nessuna fotografia in $OUTDIR"; return 0; fi
  local d found=0
  for d in "$OUTDIR"/*/; do
    &#91;&#91; -f $d/meta.txt ]] || continue
    found=1
    printf '%-24s %s  %-8s  %s\n' "$(basename "$d")" "$(meta_get "$d" timestamp)" \
      "$(meta_get "$d" engine)" "$(meta_get "$d" host)"
  done
  &#91;&#91; $found -eq 1 ]] || echo "Nessuna fotografia in $OUTDIR"
}

cmd=${1:-}
shift || true
case $cmd in
  snapshot) cmd_snapshot "$@" ;;
  compare)  cmd_compare "$@" ;;
  report)   cmd_report "$@" ;;
  list)     cmd_list ;;
  ""|help)  usage 0 ;;
  *)        die "comando sconosciuto: '$cmd' (usa snapshot, compare, report, list)" ;;
esac</code></pre>
<h2 class="wp-block-heading">Notas finales sobre el espacio en disco</h2>
<ul class="wp-block-list">
<li>El script es de <strong>solo lectura</strong> sobre las bases de datos. Solo ejecuta <code>SELECT</code> sobre los metadatos. Escribe únicamente en la carpeta de las instantáneas.</li>
<li>En MySQL 8 establece <code>information_schema_stats_expiry = 0</code> en la sesión. De lo contrario, las estadísticas en caché (hasta 24 horas) podrían mostrar los valores «viejos» incluso después de la optimización.</li>
<li>Si una instantánea falla a medias, se elimina automáticamente para no dejar datos incompletos.</li>
<li>Antes de usarlo en producción, pruébalo en un entorno de pruebas y comprueba los permisos del usuario de la base de datos.</li>
</ul>
<p>Si MySQL deja de arrancar tras una actualización, aquí tienes 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>Si lo usas y encuentras un caso que no cubre, o tienes ideas para ampliarlo (otros motores, exportación a CSV, avisos por correo), escríbeme en los comentarios.</p><p>The post <a href="https://www.cisilino.com/es/2026/10/20/script-espacio-disco-base-de-datos-mysql/">¿Qué base de datos ocupa espacio en disco? El script para MySQL y PostgreSQL</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/20/script-espacio-disco-base-de-datos-mysql/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<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 loading="lazy" 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>
		<item>
		<title>MySQL no arranca tras actualizar a Debian 12: causa y solución</title>
		<link>https://www.cisilino.com/es/2026/10/13/mysql-no-arranca-debian-12/</link>
					<comments>https://www.cisilino.com/es/2026/10/13/mysql-no-arranca-debian-12/#respond</comments>
		
		<dc:creator><![CDATA[matteocisilino]]></dc:creator>
		<pubDate>Tue, 13 Oct 2026 07:50:06 +0000</pubDate>
				<category><![CDATA[Servidores y bases de datos]]></category>
		<category><![CDATA[actualización]]></category>
		<category><![CDATA[Debian 12]]></category>
		<category><![CDATA[MariaDB]]></category>
		<category><![CDATA[MySQL]]></category>
		<guid isPermaLink="false">https://www.cisilino.com/?p=263</guid>

					<description><![CDATA[<p>Tras actualizar a Debian 12 MySQL no arranca y en los logs aparece mariadb.conf.d. Cómo reconocer la causa y resolverlo sin tocar los datos.</p>
<p>The post <a href="https://www.cisilino.com/es/2026/10/13/mysql-no-arranca-debian-12/">MySQL no arranca tras actualizar a Debian 12: causa y solución</a> first appeared on <a href="https://www.cisilino.com/es/">Matteo Cisilino</a>.</p>]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">El síntoma: MySQL no arranca y mysql.service queda en failed</h2>
<p>Tras actualizar a Debian 12, MySQL no arranca. El servicio figura como fallido y el log muestra el error <code>Can't read dir of '/etc/mysql/mariadb.conf.d/'</code>.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/mysql-no-arranca-debian-12.webp" alt="MySQL no arranca: un cilindro que representa una base de datos con un círculo naranja de aviso al lado" width="1200" height="630"/></figure>
<pre class="wp-block-code"><code># service mysql status
× mysql.service - MySQL Community Server
     Loaded: loaded (/lib/systemd/system/mysql.service; enabled; preset: enabled)
     Active: failed (Result: exit-code)
     Status: "Server startup in progress"

mysqld: Can't read dir of '/etc/mysql/mariadb.conf.d/' (OS errno 2 - No such file or directory)
mysqld: &#91;ERROR] Stopped processing the 'includedir' directive in file /etc/mysql/my.cnf at line 29.
mysqld: &#91;ERROR] Fatal error in defaults handling. Program aborted!
systemd&#91;1]: Failed to start mysql.service - MySQL Community Server.</code></pre>
<p>Al comprobar los paquetes instalados, MySQL está presente con normalidad (paquetes <code>mysql-community-server</code> 8.0 del repositorio oficial de Oracle).</p>
<pre class="wp-block-code"><code>dpkg -l | grep mysql</code></pre>
<h2 class="wp-block-heading">La causa: la alternativa my.cnf apunta a mariadb.cnf</h2>
<p>En Debian el archivo <code>/etc/mysql/my.cnf</code> no es un archivo real. Es un enlace gestionado por <strong>update-alternatives</strong>. Varios paquetes pueden registrar su propia versión y, en modo automático, gana la que tiene la prioridad más alta.</p>
<p>Durante la actualización se instaló (y luego se eliminó) el paquete <strong>mariadb-common</strong>. Registra <code>/etc/mysql/mariadb.cnf</code> con <strong>prioridad 500</strong>, más alta que <code>/etc/mysql/mysql.cnf</code> (prioridad 300). El paquete se eliminó pero no se purgó del todo (estado <code>rc</code>), así que su configuración siguió activa. En la línea 29 de ese archivo hay la inclusión de <code>/etc/mysql/mariadb.conf.d/</code>, un directorio que no existe. MySQL lee la configuración, no encuentra el directorio y se detiene.</p>
<p><strong>Buena noticia:</strong> los datos en <code>/var/lib/mysql</code> no se han tocado. Es solo un problema de configuración.</p>
<h2 class="wp-block-heading">Diagnóstico: comprobar adónde apunta my.cnf</h2>
<pre class="wp-block-code"><code>ls -l /etc/mysql/my.cnf /etc/alternatives/my.cnf
update-alternatives --display my.cnf
sed -n '25,32p' /etc/mysql/my.cnf
dpkg -l | grep -i mariadb</code></pre>
<p>Si el problema es este, la salida será parecida a la siguiente.</p>
<pre class="wp-block-code"><code>/etc/alternatives/my.cnf -&gt; /etc/mysql/mariadb.cnf
/etc/mysql/my.cnf -&gt; /etc/alternatives/my.cnf

my.cnf - modalità automatica
  la versione migliore del collegamento è /etc/mysql/mariadb.cnf
  il collegamento attualmente punta a /etc/mysql/mariadb.cnf
/etc/mysql/mariadb.cnf - priorità 500
/etc/mysql/my.cnf.fallback - priorità 100
/etc/mysql/mysql.cnf - priorità 300

!includedir /etc/mysql/conf.d/
!includedir /etc/mysql/mariadb.conf.d/

rc  mariadb-common   1:10.11.18-0+deb12u1   all   MariaDB common configuration files</code></pre>
<h2 class="wp-block-heading">Solución paso a paso</h2>
<h3 class="wp-block-heading">1. Hacer una copia de seguridad de la configuración</h3>
<pre class="wp-block-code"><code>cp -a /etc/mysql /root/etc-mysql-backup-$(date +%F)</code></pre>
<h3 class="wp-block-heading">2. Eliminar los restos de mariadb-common</h3>
<p>El purge elimina <code>mariadb.cnf</code> y su entrada en update-alternatives.</p>
<pre class="wp-block-code"><code>apt purge mariadb-common</code></pre>
<h3 class="wp-block-heading">3. Volver a apuntar my.cnf a mysql.cnf</h3>
<pre class="wp-block-code"><code>update-alternatives --remove my.cnf /etc/mysql/mariadb.cnf 2&gt;/dev/null
update-alternatives --set my.cnf /etc/mysql/mysql.cnf</code></pre>
<h3 class="wp-block-heading">4. Comprobar la configuración</h3>
<pre class="wp-block-code"><code>update-alternatives --display my.cnf
cat /etc/mysql/my.cnf
my_print_defaults mysqld</code></pre>
<p>Ahora <code>my.cnf</code> debe apuntar a <code>/etc/mysql/mysql.cnf</code> e incluir <code>/etc/mysql/conf.d/</code> y <code>/etc/mysql/mysql.conf.d/</code>. Si <code>my_print_defaults mysqld</code> muestra los parámetros sin errores, la configuración es correcta.</p>
<h3 class="wp-block-heading">5. Arrancar MySQL</h3>
<pre class="wp-block-code"><code>systemctl start mysql
systemctl status mysql
journalctl -u mysql -n 50 --no-pager</code></pre>
<h2 class="wp-block-heading">Si my.cnf no es un enlace simbólico</h2>
<p>Si <code>/etc/mysql/my.cnf</code> es un archivo normal o <code>/etc/mysql/mysql.cnf</code> no existe, se puede recrear la configuración a mano.</p>
<pre class="wp-block-code"><code>mv /etc/mysql/my.cnf /etc/mysql/my.cnf.mariadb.bak
cat &gt; /etc/mysql/my.cnf &lt;&lt;'EOF'
!includedir /etc/mysql/conf.d/
!includedir /etc/mysql/mysql.conf.d/
EOF
mkdir -p /etc/mysql/conf.d
systemctl start mysql</code></pre>
<h2 class="wp-block-heading">Solución rápida provisional</h2>
<p>Si MySQL no arranca y hace falta volver a poner la base de datos en línea enseguida, basta con crear la carpeta que falta. Pero es solo un parche: MySQL seguiría leyendo la configuración pensada para MariaDB. Conviene aplicar la solución completa cuanto antes.</p>
<pre class="wp-block-code"><code>mkdir /etc/mysql/mariadb.conf.d &amp;&amp; systemctl start mysql</code></pre>
<h2 class="wp-block-heading">Cómo evitar que vuelva a ocurrir</h2>
<p>Conviene averiguar qué actualización instaló MariaDB, consultando el historial de apt.</p>
<pre class="wp-block-code"><code>grep -i -B3 -A3 mariadb /var/log/apt/history.log*</code></pre>
<p>Para impedir que <code>mariadb-common</code> se reinstale en el futuro se puede bloquear con un pin de APT de prioridad negativa.</p>
<pre class="wp-block-code"><code>cat &gt; /etc/apt/preferences.d/no-mariadb &lt;&lt;'EOF'
Package: mariadb-*
Pin: release *
Pin-Priority: -1
EOF</code></pre>
<p>Después de cada actualización importante es buena costumbre comprobar también el estado de la alternativa con <code>update-alternatives --display my.cnf</code>. Las <a href="https://www.debian.org/releases/bookworm/amd64/release-notes/" target="_blank" rel="noopener">notas de la versión de Debian 12</a> recogen los cambios de la actualización.</p>
<p>Si necesitas una versión antigua de MySQL en el mismo servidor, mira cómo aislar <a href="https://www.cisilino.com/es/2026/10/17/mysql-5-7-docker-nftables-proteccion/">MySQL 5.7 en Docker con nftables</a>.</p>
<h2 class="wp-block-heading">Preguntas frecuentes</h2>
<h3 class="wp-block-heading">¿Pierdo los datos si MySQL no arranca?</h3>
<p>No. El problema afecta solo a los archivos de configuración en <code>/etc/mysql</code>; los datos en <code>/var/lib/mysql</code> permanecen intactos.</p>
<h3 class="wp-block-heading">¿Por qué eligió Debian mariadb.cnf?</h3>
<p>Porque la alternativa <code>my.cnf</code> estaba en modo automático y <code>mariadb.cnf</code> tiene prioridad 500, más alta que los 300 de <code>mysql.cnf</code>. Con <code>update-alternatives --set</code> la elección pasa a ser manual y ya no cambia sola.</p>
<h3 class="wp-block-heading">¿Vale también para Ubuntu?</h3>
<p>Sí, el mecanismo de <code>update-alternatives</code> para <code>my.cnf</code> es el mismo en Ubuntu y en las demás distribuciones derivadas de Debian.</p>
<h2 class="wp-block-heading">Conclusión</h2>
<p>El error <strong>«Can&#8217;t read dir of /etc/mysql/mariadb.conf.d/»</strong> tras una actualización de Debian no indica un daño en MySQL. Es una configuración residual de MariaDB que ha quedado activa mediante update-alternatives. Al eliminar <code>mariadb-common</code> con <code>apt purge</code> y volver a poner <code>my.cnf</code> en <code>mysql.cnf</code>, MySQL vuelve a funcionar con normalidad.</p></p><p>The post <a href="https://www.cisilino.com/es/2026/10/13/mysql-no-arranca-debian-12/">MySQL no arranca tras actualizar a Debian 12: causa y solución</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/13/mysql-no-arranca-debian-12/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
