Saltar al contenido
homelab@casa:~/servidor
~$homelabista

// MENOS FE, MÁS PRUEBAS

Seguridad del servidor casero: auditoría en 20 minutos

Audita la seguridad de tu servidor casero con pruebas de exposición, contenedores, actualizaciones y backups. Incluye salidas reales.

11 min de lecturaGonzalo
  • #seguridad
  • #servidor-casero
  • #docker
  • #homelab
  • #backups
Auditoría de seguridad de un servidor casero con estados rojo, ámbar y verde

Quince sockets TCP escuchaban en todas las interfaces de mi servidor esta mañana. La primera reacción fue bastante previsible: fruncir el ceño y volver a ejecutar el comando por si me había equivocado. No me había equivocado. Tampoco significaba que hubiese quince servicios abiertos a Internet.

Esa diferencia, entre escuchar, estar publicado y ser accesible desde fuera, es donde fallan muchas auditorías caseras. Un escáner que devuelve una lista larga asusta; un router que bloquea entradas tranquiliza. Ninguno de los dos, por separado, cuenta la historia completa.

He pasado esta auditoría el 28 de agosto de 2026 en un Intel N100 que presta servicios reales. Las cifras siguientes son salidas literales de ese momento, no una configuración modelo. He ocultado nombres, direcciones y números de puerto porque publicar un mapa de la casa sería una idea pésima.

Mapa de auditoría de seguridad del servidor casero desde Internet hasta datos y copias
La auditoría avanza desde fuera hacia dentro. Un verde en el router no compensa un registro público abierto ni una copia que jamás se ha restaurado.

Primero: qué puede tocar alguien desde fuera

Empiezo por el borde, no por Docker. Hay que probar el dominio o la IP pública desde una red que no sea la de casa: datos móviles, una VPS o un monitor externo. El objetivo no es lanzar un escaneo agresivo, sino confirmar qué servicios esperas ver y que el resto no responde.

Después miro qué escucha el host:

ss -H -lnt

Para no publicar la topología, resumí la salida con contadores. El resultado real fue:

sockets_listening=40
bound_all_ipv4=15
bound_loopback_ipv4=10

Los 15 0.0.0.0 merecen revisión uno por uno. No son quince agujeros demostrados: puede haber un cortafuegos aguas arriba, una red privada o un proxy que sea la única entrada legítima. Pero tampoco acepto “eso no sale a Internet” sin probarlo desde fuera.

Mi criterio es simple. Verde si cada servicio público está previsto, autenticado y actualizado. Ámbar si escucha en todas las interfaces aunque solo deba usar la red interna. Rojo si aparece un panel de administración, una base de datos o un registro de usuarios que nadie recuerda haber publicado.

Para acceso remoto prefiero una VPN casera con WireGuard o un túnel con política explícita antes que abrir el panel entero. El candado del navegador cifra el trayecto; no convierte una aplicación vieja en segura.

UFW dice cerrado, pero el contenedor responde

Ampliación documental del 12 de septiembre de 2026; no es una nueva medición del servidor.

Un ufw status tranquilizador no basta para dar por cerrado un puerto de Docker. En redes bridge, Docker crea reglas de publicación y reenvío. Su documentación sobre Docker y UFW explica que el tráfico publicado puede desviarse antes de las cadenas que utiliza UFW para filtrar la entrada al host. Por eso una regla de entrada puede no proteger el contenedor como esperabas. Eso tampoco demuestra acceso desde Internet: aún cuentan el router, las rutas y los filtros exteriores.

Primero decide quién necesita llegar al servicio:

ConsumidorPublicación que revisarComprobación decisiva
Otro contenedor de la misma red bridge definida por el usuarioPuede comunicarse por la red de Docker sin publicar un puerto en el hostQue funcione por su nombre de servicio y no exista una publicación innecesaria
Un proceso o proxy que corre en el propio hostValorar un bind a loopback, en vez de todas las interfacesQue el proxy funcione y que otro equipo no alcance el puerto
Un cliente de la LAN o de InternetPublicar sólo lo necesario y definir qué orígenes pueden accederProbar tanto el acceso permitido como el que debe quedar bloqueado

Un proxy dentro de otro contenedor no es un proceso del host. Su localhost apunta a sí mismo. En ese caso suele tener sentido compartir una red de Docker con el servicio, sin publicar el backend. No cambies el bind de producción sin comprobar primero el recorrido del proxy.

Tampoco mezcles dos backends de cortafuegos:

  • Con el backend iptables de Docker, DOCKER-USER permite filtrar tráfico reenviado antes de las cadenas de Docker. Ver iptables funcionando sobre nftables mediante compatibilidad no demuestra que Docker use su backend nftables nativo.
  • El backend nftables nativo no tiene una cadena DOCKER-USER; necesita reglas y prioridades propias. Docker lo documenta como experimental, introducido en 29.0.0. No migres de backend sólo para copiar una receta de otro sistema.

Desactivar la gestión de reglas de Docker tampoco es un atajo: puede romper conectividad y dejar puertos accesibles sin las reglas sustitutivas adecuadas.

Cómo comprobar el resultado sin confiar en una casilla

Consulta las opciones y límites de publicación de puertos. En Docker anterior a 28.0.0 existe además una advertencia sobre puertos publicados en localhost accesibles desde el mismo segmento de red; loopback no compensa una versión afectada. El enrutamiento directo y otros modos de red también requieren revisar su alcance.

Después contrasta el puerto concreto de un servicio tuyo desde otro equipo de la LAN y desde una red externa, sin la VPN de casa si estás comprobando exposición pública. Prueba IPv4 e IPv6 por separado cuando existan ambas rutas. Si no tienes conectividad para una de ellas, apunta «no probado», no «cerrado».

El resultado útil es una pareja: el cliente autorizado entra y el no autorizado no puede. Un timeout aislado también puede ser un problema de ruta. Conserva fecha, red de origen y resultado; no publiques direcciones privadas ni captures credenciales. Repite la comprobación después del cambio y confirma que el acceso legítimo sigue funcionando.

La salida que me quitó la falsa tranquilidad

Reconstrucción visual de la salida de terminal de la auditoría de seguridad del servidor casero
Reconstrucción visual de la salida de la sesión del 28 de agosto. Los contadores están saneados: no muestra IP, puertos, nombres de contenedor ni rutas privadas.

La parte fea no era el número de servicios. Eran los límites de los contenedores:

running=11
no_new_privileges=0/11
readonly_rootfs=0/11
root_or_unspecified=10/11

Esto no prueba que los once contenedores sean vulnerables. Sí prueba que, si uno cae, el proceso dispone de más margen del que me gustaría. Diez ejecutan como root dentro del contenedor o no declaran usuario; ninguno bloquea la ganancia de privilegios y ninguno monta la raíz como solo lectura.

No voy a “arreglar” once servicios a ciegas. Algunos necesitan escribir en rutas inesperadas durante una migración y otros cambian de usuario al arrancar. Mi orden de trabajo es menos vistoso: empezar por el servicio expuesto con menos dependencias, añadir security_opt: [no-new-privileges:true], usuario y read_only donde la imagen lo soporte, reiniciar y mirar logs. Uno cada vez. Ya aprendí que endurecer un compose.yml completo de golpe produce una bonita colección de contenedores reiniciándose.

Para sacar tu propio recuento sin enumerar servicios:

docker inspect $(docker ps -q) \
  --format '{{json .HostConfig.SecurityOpt}}|{{.HostConfig.ReadonlyRootfs}}|{{.Config.User}}'

Si el host usa Docker rootless, Podman o políticas de AppArmor/SELinux, el análisis cambia. La pregunta útil no es “¿corre como root?”, a secas, sino qué puede hacer ese proceso fuera de su contenedor si se rompe el aislamiento.

Interfaz real de Portainer con la lista de contenedores, sus imágenes, estados y puertos publicados
La lista de contenedores de Portainer ayuda a localizar imágenes y puertos que revisar. Es una captura de la documentación oficial, no de nuestra red; la auditoría real se hizo desde terminal.

Conexiones salientes: cerrar puertos no cierra la salida

Ampliación del 7 de septiembre de 2026. Las mediciones anteriores conservan su fecha original.

Me faltaba mirar en la otra dirección. Un contenedor puede consultar una API o descargar archivos sin tener un solo puerto publicado. La documentación de redes de Docker separa precisamente la conectividad del contenedor de la publicación de puertos. Quitar ports: no es una política de salida.

En esta pasada entré solo en el espacio de red del contenedor que sirve la web. Usé ss del host mediante nsenter: no instalé herramientas dentro de la imagen ni reinicié el servicio. Resumí las direcciones antes de guardar el resultado, porque aquí importa el alcance de la prueba, no enseñar a quién se conecta mi servidor.

checked_utc=2026-09-07T09:33:11+00:00
scope=one_web_container
tcp_listeners=2
tcp_established=1
tcp_global_peers=0
tcp_non_global_peers=1
outbound_initiator=not_determined
payload_inspected=no

Dos escuchas, una conexión establecida y ningún par con IP global en ese instante. No he demostrado que el contenedor no salga a Internet. ss muestra sockets actuales, no las conexiones que ya terminaron. Tampoco dice, por mostrar ESTAB, quién inició la conversación. La conexión observada podría ser una petición entrante; el contador no permite adjudicarla.

Para repetir la inspección en Linux, sustituye mi-contenedor por el nombre del servicio. Estas órdenes muestran direcciones: revísalas en tu terminal, no las pegues en un foro sin sanearlas.

docker inspect --format '{{.State.Running}} {{.HostConfig.NetworkMode}}' mi-contenedor
docker inspect --format '{{.State.Pid}}' mi-contenedor

Si está arrancado, toma el PID de la segunda orden y úsalo en lugar de PID:

sudo nsenter -t PID -n ss -Hlnt
sudo nsenter -t PID -n ss -Hnt state established

Hay una trampa con network_mode: host: ese contenedor comparte el espacio de red del host. Ahí estarías viendo conexiones del servidor, no un inventario atribuible a una sola aplicación. Y estas órdenes solo miran TCP; una consulta DNS por UDP o tráfico QUIC no aparece.

Mi hoja de revisión necesita otra columna: destino esperado y motivo. Una consulta de actualización o el envío de una alerta pueden tener explicación; una conexión que no reconozco queda pendiente hasta contrastarla con configuración y registros de la aplicación. Ni el puerto 443 garantiza tráfico legítimo ni una IP desconocida prueba una intrusión. En esta muestra no he identificado una anomalía; tampoco he registrado un historial de salidas.

Antes de bloquear destinos comprobaría qué depende de ellos y probaría el cambio en un servicio prescindible. Prefiero descubrir que he cortado sus avisos durante una prueba que cuando necesito recibir uno.

Versiones: mirar la rama, no solo el número

El incidente que motivó esta guía fue una instancia de Forgejo comprometida. La cadena fue demasiado doméstica: registro abierto, versión fuera de soporte y una vulnerabilidad corregida en ramas vigentes. La primera pista no fue un aviso cinematográfico; fue CPU sostenida donde no debía.

Por eso apunto cuatro cosas por servicio: versión desplegada, canal o rama, fecha de fin de soporte y fecha de la última revisión. “Tengo la versión 13” no sirve si esa rama terminó soporte en enero. Para la vulnerabilidad concreta de Forgejo, la referencia correcta es el aviso GHSA-rcr6-4jqh-j84m, no un titular copiado.

En el host ejecuté también:

upgradable_packages=57
security_named_packages=0

Aquí hay una trampa. El segundo cero solo dice que apt no marcó paquetes con la palabra security en esa salida; no certifica que no haya actualizaciones de seguridad. Y los 57 pendientes no autorizan a actualizar un servidor entero en horario laboral. El resultado queda ámbar hasta revisar procedencia, servicios afectados, changelogs y reinicios.

Mi manía es separar dos ventanas: parches pequeños y reversibles primero; kernel, Docker y saltos mayores con backup y tiempo para volver atrás. El mecanismo cambia según el servicio. La política y la ventana de mantenimiento siguen siendo tuyas.

Registro, secretos y permisos que se heredan

Abre cada servicio como un usuario nuevo. Si puedes crear cuenta y no pretendías ofrecer registro público, rojo. Haz la prueba en una ventana privada: una sesión antigua puede esconder el botón o saltarse el asistente inicial.

Luego busca secretos donde suelen quedarse por pereza: variables pegadas al compose.yml, ficheros .env legibles por todo el grupo, tokens en logs y copias de configuración dentro del directorio que sirve la web. No pegues el resultado de grep en un chat ni en un artículo. Basta con contar hallazgos y corregirlos en local.

Un secreto fuera del repositorio pero legible por cualquier usuario tampoco está bien guardado. Para ficheros de credenciales uso permisos 600, propietario concreto y rutas que el backup cifra antes de salir de casa. Si una aplicación admite secretos montados como fichero, lo prefiero a una variable que acaba apareciendo en un volcado de entorno.

Una copia activa no es una copia probada

El temporizador de backup de este host estaba habilitado, activo y se había disparado a las 03:32:13 UTC:

enabled
active
Fri 2026-08-28 03:32:13 UTC

Bien. Todavía no es verde.

Un temporizador sano puede ejecutar un script que copie cero bytes, excluir la carpeta equivocada o sincronizar un borrado. El verde llega después de restaurar. En la regla 3-2-1 aplicada al homelab documento una restauración con cmp IDENTICAL; para esta auditoría solo he verificado el disparo del timer, así que lo dejo en ámbar deliberadamente.

La distinción parece puntillosa hasta que toca recuperar algo. Entonces es toda la diferencia.

Mi hoja de resultados, sin semáforo de juguete

No sumo puntos. Un servicio crítico en rojo no queda compensado porque otros nueve checks den verde.

ÁreaEstado de esta pasadaAcción concreta
ExposiciónÁmbarContrastar cada escucha global desde una red externa
ContenedoresÁmbarEndurecer primero un servicio expuesto y verificar logs
ActualizacionesÁmbarClasificar los 57 paquetes por impacto y reinicio
Registro públicoPendienteProbar alta desde sesión privada en cada servicio público
CopiaÁmbarEl timer corre; falta restauración nueva desde la copia remota

El orden también importa. Cerraría antes un panel público olvidado que un contenedor interno sin read_only. Después corregiría versiones EOL, registro y credenciales; luego reduciría privilegios. La copia restaurable acompaña todo el proceso, porque el hardening también rompe cosas.

Repite la auditoría después de publicar un servicio, cambiar el router o actualizar una imagen mayor. Yo guardo fecha, comando y resultado, no una casilla permanente de “seguro”. Esta mañana esperaba una comprobación rutinaria y me llevé once contenedores para revisar. Bastante mejor descubrirlo con café que a partir de una alerta de CPU a las tres de la madrugada.

Comprobaciones ejecutadas el 28 de agosto de 2026 en un Intel N100 de Homelabista. Las salidas publicadas son literales; se han omitido identificadores, direcciones, puertos y nombres de servicio.