Desplegando un servidor SFTP seguro y containerizado con Docker

Una guía pragmática, probada en producción, para ejecutar un servidor SFTP seguro con Docker, aislamiento estricto, autenticación por claves y logging de auditoría centralizado.

Desplegando un servidor SFTP seguro y containerizado con Docker

Seamos honestos: nadie se despierta emocionado por desplegar un servidor SFTP.

Preferimos APIs, object storage, event streams e integraciones gestionadas. Lamentablemente, los sistemas reales son complejos y no ideales. Tarde o temprano, tendrás que lidiar con un tercero que solo puede enviar archivos (Ficheros) vía SFTP, muchas veces para migraciones grandes, sensibles y con un tiempo limitado.

Ese fue mi caso. Necesitaba ingerir miles de PDFs sensibles desde un proveedor externo con restricciones muy claras:

  • Sin acceso a sistemas de producción
  • Sin acceso a shell para los usuarios
  • Solo criptografía fuerte
  • Logs de auditoría completos para cada conexión y operación sobre archivos (Ficheros)
  • Posibilidad de desmontar todo fácilmente una vez terminado el trabajo

La forma más rápida de cumplir con estos requisitos sin crear una deuda de mantenimiento a largo plazo fue containerizar el servidor SFTP y tratarlo como infraestructura desechable.

Este artículo documenta el enfoque exacto, incluyendo los trade-offs.

El problema de “simplemente instalar SFTP en una VM”

Instalar OpenSSH directamente en un host y configurar usuarios SFTP funciona, pero escala mal desde el punto de vista operativo y de seguridad.

En la práctica, terminas con:

  • Reglas complejas en sshd_config y configuraciones chroot frágiles
  • Gestión manual de usuarios en un sistema mutable
  • Límites de permisos difíciles de razonar
  • Logs repartidos entre el sistema y la aplicación
  • Limpieza dolorosa una vez que la migración termina

Esto puede sobrevivir para servidores puntuales, pero no es repetible ni auditable fácilmente.

Por qué Docker es la abstracción correcta aquí

Usar Docker no hace que SFTP sea mágicamente seguro, pero nos da límites duros que, de otro modo, son tediosos de mantener.

Lo que ganamos:

  • Aislamiento de procesos: Incluso si algo sale mal dentro del servicio SFTP, el radio de impacto queda confinado al contenedor.
  • Configuración inmutable: La definición completa del servidor se convierte en código. Si algo cambia, redeplegamos en lugar de depurar estado.
  • Desmontaje rápido: Cuando la migración termina, detenemos el contenedor y borramos de forma segura el disco adjunto. Sin restos.
  • Separación clara de responsabilidades: El host se enfoca en hardening y observabilidad. El contenedor se enfoca solo en SFTP.

Esto encaja muy bien con una mentalidad DevSecOps: infraestructura de vida corta, auditable y con privilegios mínimos.

Visión general de la arquitectura

A alto nivel, el diseño es intencionalmente aburrido:

Componentes principales:

  • Runtime: Docker ejecutando atmoz/sftp, una imagen mínima basada en OpenSSH que envuelve OpenSSH en una configuración solo-SFTP.
  • Almacenamiento: Un disco persistente cifrado montado en el contenedor como un volumen.
  • Red: Una VPC fuertemente restringida que solo permite:
    • TCP 22 (administración SSH del host)
    • TCP 2222 (tráfico SFTP)
  • Observabilidad: Logs del sistema + logs del contenedor enviados a un sistema de logging centralizado.

El resultado es una zona de subida claramente definida, con ingress explícito y trazabilidad completa.

Implementación

1. Provisionamiento del host y hardening base

Antes de ejecutar Docker, la propia VM debe ser aburrida, estar parcheada y ser hostil por defecto.

Normalmente uso Ubuntu 24.10 LTS o similar: estable, predecible y bien soportado.

Pasos clave de hardening:

  • Acceso SSH solo con claves
    • Desactivar autenticación por password
    • Desactivar login directo como root
    • Forzar cifrados modernos
  • Firewall (UFW)
    • Denegar por defecto todo el tráfico entrante
    • Permitir explícitamente:
      • 22/tcp (administración)
      • 2222/tcp (SFTP)
  • Protección contra fuerza bruta
    • Instalar y configurar Fail2Ban para bloquear automáticamente fallos repetidos de autenticación
    • Límites agresivos de intentos fallidos
  • Actualizaciones automáticas de seguridad
    • Activar unattended-upgrades
    • Sin ventanas manuales de parcheo

Ejemplo de jail de Fail2Ban apuntando al puerto SFTP containerizado:

[sftp-with-docker]
enabled = true
port = 2222
filter = sshd
action = ufw[application="OpenSSH", blocktype=reject]
logpath = /var/log/auth.log
maxretry = 3

Esto cubre tanto abuso SSH a nivel host como intentos contra el SFTP dentro del contenedor.

Para obtener más información sobre cómo proteger entornos Linux, el libro Deployment from Scratch ofrece guías excelentes alineadas con estándares de seguridad.

2. Configuración del contenedor (donde ocurren la mayoría de errores)

Usamos la imagen atmoz/sftp porque hace una sola cosa y la hace bien: ejecutar OpenSSH en modo solo-SFTP.

El detalle crítico es cómo se lanza el contenedor.

docker run \
  -d \
  --restart unless-stopped \
  -p 2222:22 \
  -v /home/ftp_server/disks/data/users/client_a/upload:/home/client_a/upload \
  -v /home/ftp_server/config/ssh_host_ed25519_key:/etc/ssh/ssh_host_ed25519_key \
  atmoz/sftp \
  client_a::1001

Por qué esto importa:

  • Volumen de datos persistente: Los archivos (Ficheros) subidos sobreviven reinicios y redeploys del contenedor.
  • Claves host SSH persistentes: Esto no es negociable. Sin esto, cada redeploy cambia la huella del servidor y rompe automatizaciones con advertencias MITM bastante desagradables.
  • Sin acceso a shell: El contenedor fuerza internal-sftp. Los usuarios nunca obtienen una shell, incluso si se filtran credenciales.

La configuración sshd_config

Lanzar el contenedor es solo la mitad de la batalla. Por defecto, OpenSSH puede ser demasiado permisivo. Para garantizar que los usuarios estén realmente restringidos a una “jaula” SFTP y no puedan usar trucos como túneles o reenvío de gráficos, inyectamos una configuración de SSH (sshd_config) personalizada y minimalista.

Este archivo (fichero) actúa como la última línea de defensa dentro del contenedor:

# Secure defaults
# Based on https://github.com/atmoz/sftp/blob/master/files/sshd_config
Protocol 2
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_rsa_key

# Faster connection
# See: https://github.com/atmoz/sftp/issues/11
UseDNS no

# Limited access
PermitRootLogin no
X11Forwarding no
AllowTcpForwarding no

# Force sftp and chroot jail
Subsystem sftp internal-sftp -f AUTH -l VERBOSE
ForceCommand internal-sftp -f AUTH -l VERBOSE
ChrootDirectory %h

# Global verbosity
LogLevel VERBOSE

# Enforce both password and public key authentication
AuthenticationMethods publickey
PubkeyAuthentication yes
PasswordAuthentication no
ChallengeResponseAuthentication no # Deshabilitar autenticación interactiva por teclado

Aquí tienes el desglose de por qué estas líneas son críticas para tu seguridad:

  • **Protocol 2 & HostKey**: Forzamos el uso del protocolo SSH moderno y definimos explícitamente dónde buscar las llaves de identidad del servidor (las cuales persistimos vía volúmenes de Docker).
  • UseDNS no: Una optimización de rendimiento crítica. Evita que el servidor intente resolver el hostname del cliente al conectarse, lo que suele causar retrasos molestos en el login.
  • **PermitRootLogin, X11Forwarding, AllowTcpForwarding**: Deshabilitamos el login como root y bloqueamos cualquier intento de usar el servidor para tunelizar tráfico o reenviar interfaces gráficas. Si un atacante entra, no puede pivotar a otros sistemas desde aquí.
  • **ForceCommand internal-sftp & ChrootDirectory %h**: Esta es la “magia” del aislamiento. Obliga al proceso a usar el subsistema SFTP interno (sin necesidad de binarios de shell externos) y encierra (chroot) al usuario en su directorio home. No pueden “subir” a ver archivos (ficheros) del sistema.
  • AuthenticationMethods publickey: Política de cero contraseñas. Solo aceptamos autenticación por llave pública, eliminando el riesgo de ataques de fuerza bruta a passwords débiles.

3. Gestión de claves SSH (el problema de soporte más común)

Forzamos autenticación exclusivamente basada en claves. Las contraseñas por sí solas no son aceptables para este tipo de datos.

Esto introduce un problema recurrente con clientes no-Unix.

Modo de fallo típico:

  • Los usuarios generan claves con PuTTYgen (Windows)
  • Las claves están en formato SSH2 / RFC 4716
  • OpenSSH las rechaza con invalid format

La solución:

  • Convertir las claves a formato OpenSSH
  • Validarlas antes de desplegar

Regla operativa: Cuando una clave cambia, recrea el contenedor. La imagen lee las claves en el arranque. Reiniciar es más rápido que depurar permisos.

4. Logging y auditabilidad

Para transferencias de datos sensibles, “funciona” no es suficiente. Necesitas poder responder quién hizo qué, cuándo y desde dónde.

Este setup captura:

  • Logs de autenticación del host

    • /var/log/auth.log
    • Intentos de login exitosos y fallidos
  • Logs de actividad SFTP

    • Subidas
    • Descargas
    • Borrados
    • Cambios de directorio (Directorio / Carpeta)
  • Agregación centralizada

    • Enviados mediante un agente de logging (por ejemplo, Google Cloud Ops Agent)

Una vez centralizados, puedes:

  • Crear alertas por comportamiento anómalo
  • Retener logs independientemente del ciclo de vida de la VM
  • Cumplir requisitos de auditoría y compliance sin suposiciones

Trade-offs operativos (y por qué son aceptables)

Ningún setup es gratis. Estas son restricciones intencionales:

  • Sin shell interactiva: Los usuarios no pueden hacer SSH para ejecutar ls o mv. Están limitados exclusivamente a comandos SFTP. Esto es una feature de seguridad, no un bug, y elimina una clase completa de riesgos de escalado de privilegios.
  • Servidor de propósito único: Sin FTP, FTPS, SCP ni servicios adicionales. Menor superficie de ataque, razonamiento más simple.
  • Directorio (Carpeta) por tenant: Por defecto, los usuarios no se ven entre sí. Si necesitas mover archivos entre usuarios (por ejemplo, de un usuario de “Upload” a uno de “Download”), necesitarás un cron job en el host que sincronice carpetas usando rclone o rsync.
  • Sincronización de tiempo: Asegúrate de que NTP esté activo en el host. Los timestamps correctos son críticos para logs de auditoría y validación de certificados en entornos regulados.
  • Vida útil acotada: Cuando el trabajo termina, apágalo, borra los discos y revoca las claves.

Qué mejoraría a continuación

Si este servidor fuera de larga duración, consideraría:

  • Infrastructure-as-Code para el provisionamiento de la VM
  • Allowlists de IP efímeras con automatización
  • Ingesta en object storage en lugar de escrituras directas al filesystem

Para migraciones e integraciones legacy, este enfoque da justo en el punto medio: seguro, auditable y fácil de desmontar.

Reflexión final

Si todavía estás creando usuarios SFTP a mano en servidores mutables, estás pagando un coste a largo plazo por un problema de corto plazo.

Containerizar SFTP lo convierte en una utilidad desechable: predecible, aislada y fácil de razonar.

Si has desplegado SFTP de otra forma, o ves algún fallo en este enfoque, me encantaría escucharlo. Deja un comentario.

Y si esto te ahorró tiempo, no dudes en reutilizarlo.