Hoy tocaba algo que siempre se va posponiendo: poner al día la máquina virtual de este blog. Lo que iba a ser «actualizar y listo» terminó en una pequeña excursión por todo el camino que recorre una visita: el sistema, WordPress, el router, el proxy y hasta la radio de casa, que ahora asoma en la cabecera del blog. Lo hice junto a Claude Code, y aquí dejo lo aprendido, sin datos sensibles.
1. Antes de tocar nada: una red de seguridad
La regla de oro: snapshot de la VM en Proxmox, volcado de la base de datos y una copia de los archivos de WordPress. Si algo sale mal, volver atrás es cuestión de un clic. Con eso hecho, a actualizar.
2. El sistema: cuando el que te ayuda se reinicia a sí mismo
El blog corre sobre Debian 12 y tenía más de cien paquetes pendientes. Lancé la actualización a través del QEMU guest agent (el servicio que permite a Proxmox ejecutar órdenes dentro de la VM)… y a mitad de camino se actualizó el propio agente. Al reiniciarse se llevó por delante a apt, dejando decenas de paquetes desempaquetados pero sin configurar.
La solución fue lanzar el trabajo como una unidad de systemd independiente, para que sobreviva aunque el agente se reinicie:
systemd-run --unit=actualizacion --setenv=DEBIAN_FRONTEND=noninteractive
/bin/sh -c "dpkg --configure -a; apt-get -y full-upgrade"
Después, apt autoremove se llevó una docena de kernels viejos (el disco pasó del 51 % al 25 % de uso) y un reinicio dejó la VM con el kernel nuevo.
3. WordPress 6.6 → 7.1 y una caché demasiado entusiasta
Con WP-CLI la actualización debería ser trivial, pero el primer intento se quedó a medias en 6.6.11 y la terminal se llenó con el HTML de la portada. El culpable: el plugin de caché servía la página cacheada también a WP-CLI y cortaba la ejecución. Moraleja: para actualizar, cargar WordPress sin plugins ni temas.
wp core update --skip-plugins --skip-themes
wp core update-db --skip-plugins --skip-themes
wp plugin update --all --skip-plugins --skip-themes
wp theme update --all --skip-plugins --skip-themes
Resultado: núcleo en 7.1.4 con los checksums verificados, siete plugins y cuatro temas al día, y WP-CLI actualizado.
4. El 502 que no era de Traefik
Dentro de la VM todo respondía, pero desde fuera el blog devolvía 502 Bad Gateway. La tentación es culpar al proxy, pero un 502 solo dice que Traefik no consigue hablar con el servidor de atrás. Toca revisar capa por capa:

- Firewall de Proxmox del contenedor de Traefik: deja salir todo. No era.
- Firewall de Proxmox de la VM del blog: desactivado. No era.
- Prueba definitiva: un
tcpdumpdentro de la VM mientras forzaba visitas. Resultado: cero paquetes llegando desde Traefik. El corte estaba antes. - pfSense: ahí estaba. Hace unos días endurecí las reglas entre la red de contenedores y la de máquinas virtuales: desactivé el «permitir todo» y dejé solo lo necesario… pero se me quedó fuera el blog. Todo caía en la regla final de «bloquear todo lo demás».
Una sola regla —permitir a Traefik llegar al puerto HTTP del blog— y de vuelta en línea. La lección: cuando cierras con un «bloquear todo lo demás», haz inventario de todo lo que tiene que seguir pasando, y prueba cada servicio después del cambio.
5. La radio, ahora en el blog
En casa tengo una radio personal: MPD reproduce la música y la manda a Icecast en Opus. El blog tenía un plugin viejo para mostrar «qué suena ahora», pero tenía dos problemas graves: incrustaba en el HTML las credenciales de administración de Icecast (visibles para cualquiera con «ver código fuente») y su endpoint aceptaba cualquier servidor como destino. Además, ni siquiera estaba puesto en ninguna página.
Lo reescribimos desde cero (versión 4.0.0) con otra filosofía:
- Es el servidor quien consulta la radio, usando solo las páginas públicas de Icecast (
status-json.xsly elnowplaying.jsonque escribe MPD). Sin usuario ni contraseña. - Caché de 10 segundos: da igual cuántos visitantes haya, la radio recibe como mucho una consulta cada 10 s.
- El navegador solo habla con un endpoint REST del propio blog y refresca cada 15 s; se pausa si la pestaña no está visible y espacia los reintentos si algo falla.
- Un botón «Escuchar» con un único reproductor compartido; al pulsar «Detener» suelta la conexión del stream en vez de seguir descargando en silencio.
- Modo compacto para la cabecera, accesible (
aria-live, foco visible) y respetando el modo oscuro.

El bug escondido de Icecast
En las pruebas, el título salía vacío una y otra vez. Midiendo, el resultado era claro: 0 de 20 lecturas correctas. El servidor de archivos de Icecast anunciaba un Content-Length de una versión anterior del archivo (161 bytes para un JSON que ya medía 105), y cURL abortaba con el error 18 («transferencia parcial»). Como el script de la radio reescribe el archivo en cada canción, el tamaño cambia constantemente.
La solución, sin tocar la radio: pedirle a cURL que lea hasta que se cierre la conexión, solo para esas peticiones, y validar el JSON (con nowplaying.txt de respaldo y la última canción conocida si todo falla). Resultado: 20 de 20.
$ignorar_longitud = static function ( $handle, $args, $url_pedida ) use ( $url ) {
if ( $url_pedida === $url ) {
curl_setopt( $handle, CURLOPT_IGNORE_CONTENT_LENGTH, true );
}
};
add_action( 'http_api_curl', $ignorar_longitud, 10, 3 );
$respuesta = wp_remote_get( $url, array( 'timeout' => 3 ) );
remove_action( 'http_api_curl', $ignorar_longitud, 10 );
6. Imágenes rotas y un reproductor mudo: la CSP
Con el blog ya público aparecieron dos síntomas: algunas imágenes no se veían y el botón «Escuchar» no sonaba (aunque en la red local sí). Ambos tenían la misma causa: la cabecera Content-Security-Policy que Traefik añade al blog solo permitía contenido del propio dominio.
- Imágenes: Jetpack reescribía el
srcsetpara servirlas desde su CDN, y el navegador elegía esas versiones… bloqueadas por la CSP. Desactivé la CDN de imágenes y de archivos estáticos de Jetpack: ahora todo sale del propio servidor. - Audio: sin
media-src, la política cae endefault-src 'self'y el stream, que vive en otro subdominio, queda bloqueado.
La corrección fue ampliar la política solo con lo necesario (dominios de ejemplo):
Content-Security-Policy: "default-src 'self';
script-src 'self' 'unsafe-inline' https://stats.wp.com;
img-src 'self' data: https://secure.gravatar.com https://pixel.wp.com;
media-src 'self' https://radio.ejemplo.com;
connect-src 'self'; frame-src 'self';"
Traefik vigila sus archivos de configuración, así que el cambio se aplicó sin reiniciar nada. Después, la prueba en un navegador real contra la dirección pública: cero imágenes rotas, el título de la canción cambiando solo y el reproductor sonando.
Lo que me llevo
- Snapshot primero, siempre.
- Si actualizas a través de un agente, lanza el trabajo desacoplado (
systemd-run): el agente también se actualiza. - Los plugins de caché pueden interferir con WP-CLI:
--skip-plugins --skip-themespara mantenimiento. - Un 502 es un síntoma, no un diagnóstico. Un
tcpdumpen el destino dice en segundos si los paquetes llegan. - Endurecer el firewall es bueno; hacerlo sin inventario de servicios, no tanto.
- Nunca pongas credenciales en lo que ve el navegador. Si el servidor puede consultar, que consulte el servidor.
- Una CSP estricta es una gran defensa, pero hay que mantenerla al día cuando el sitio cambia.
- Mide antes de creer: «a veces falla» resultó ser «falla siempre» (0/20) en cuanto lo contamos.
Y ahora, si ves el punto verde en la cabecera, dale a «Escuchar». 🎶

Deja una respuesta
Lo siento, debes estar conectado para publicar un comentario.