Este artículo es la segunda parte de una serie. La primera documenta el razonamiento de seguridad: por qué un agente de IA corriendo bajo tu cuenta Linux tiene acceso a tus claves SSH, tus cookies de sesión y tu keyring, y qué arquitectura de defensa en capas diseñé para mitigarlo. Si no la has leído, te recomiendo empezar por ahí; los pasos técnicos de este artículo tienen más sentido con ese contexto.
Aquí está la ejecución real. Cronológica, con comandos reales, y con los problemas reales que surgieron —no una versión idealizada sin fricción—. El hardware es una torre de sobremesa (Ryzen 5 5500, 32 GB RAM, GTX 1050 Ti 4 GB, ASUS PRIME X570-P) recién migrada de Windows a Ubuntu 24.04.4 LTS. La bauticé imm-guarida.
1. Hostname
sudo hostnamectl set-hostname imm-guarida
Simple. El problema vino después: sudo empezó a mostrar unable to resolve host imm-guarida. La causa fue una entrada residual en /etc/hosts con el nombre anterior, imm-System-Product-Name —el instalador de Ubuntu lo había escrito ahí leyendo el DMI de la placa base, no del hostname configurado—. Corrección:
sudo sed -i 's/imm-System-Product-Name/imm-guarida/g' /etc/hosts
Verificación: hostname devuelve imm-guarida, sudo echo ok sin warnings.
2. SSH por clave pública
Ubuntu Desktop no instala openssh-server por defecto, a diferencia de la mayoría de VPS:
sudo apt install openssh-server
sudo systemctl enable --now ssh
Un matiz que merece un párrafo: Ubuntu 24.04 usa activación por socket (ssh.socket). El servicio aparecía como inactive (dead) en systemctl status ssh, lo que inicialmente parecía un fallo. No lo es: el socket espera la primera conexión entrante y lanza el servicio al vuelo. Aun así, para una máquina que va a estar desatendida cinco semanas, preferí forzar enable --now ssh para tenerlo siempre activo y no depender de la activación diferida.
Error real cometido: al generar el par de claves SSH para acceso desde el portátil, la primera vez lo hice sin passphrase por descuido. Lo corregí sin regenerar el par:
ssh-keygen -p -f ~/.ssh/id_personal
Después, deshabilitar autenticación por contraseña en sshd_config:
sudo nano /etc/ssh/sshd_config
# PasswordAuthentication no
Confusión frecuente que vale la pena aclarar: PasswordAuthentication no en sshd_config afecta exclusivamente al acceso remoto por SSH. No toca la pantalla de login físico de Ubuntu gestionada por GDM —que usa PAM, un sistema completamente distinto—. Si después de este cambio el login físico en el escritorio te pide contraseña, es porque así funciona GDM; no lo has roto.
Protocolo de verificación antes de cerrar cualquier sesión SSH:
sudo sshd -t # sintaxis del config
sudo systemctl reload ssh
# Abrir una TERCERA sesión SSH independiente para confirmar acceso
# Solo entonces cerrar las anteriores
Nunca cerrar la única sesión activa antes de confirmar que la nueva configuración permite abrir otra.
3. Tailscale
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
El detalle no obvio que causó un minuto de confusión: Tailscale es una red entre dispositivos, no solo un servicio que se instala en el servidor. Al intentar conectar por SSH vía la IP de Tailscale desde el portátil, la conexión se quedó colgada sin respuesta. La causa: el portátil no tenía Tailscale instalado todavía y no estaba en la misma tailnet. Hay que instalar el cliente en ambos extremos antes de que la red funcione.
Una vez instalado en ambos lados y con tailscale up ejecutado en los dos, la prueba real que importa:
# Desde el portátil
ssh imm@<ip-tailscale-de-imm-guarida>
Y más importante: reiniciar la máquina por completo y volver a conectar sin tocar nada. Si reconecta sola, el setup sobrevive apagados reales.
4. Montaje del HDD
El HDD WD Blue 1 TB formateado en NTFS desde Windows hay que montarlo. Identificar el disco sin confundir con el SSD del sistema:
lsblk
sudo blkid | grep ntfs
Identificación por tamaño y UUID, no solo por nombre de dispositivo (/dev/sdb puede cambiar entre arranques dependiendo del orden de detección). La entrada en /etc/fstab:
UUID=<uuid-del-disco> /mnt/datos ntfs3 defaults,nofail 0 0
Dos decisiones que justifican comentario:
ntfs3 en lugar de ntfs-3g: el driver nativo del kernel Linux, disponible desde la versión 5.15. Mejor rendimiento, sin dependencias externas.
nofail: crítico para una máquina desatendida. Si el disco de datos falla, el sistema arranca de todas formas en lugar de quedarse bloqueado esperando a montar un dispositivo que no responde.
sudo mount -a
Aviso de systemd: "fstab has been modified; please run 'systemctl daemon-reload'". No es un error, es un recordatorio. Ejecutar sudo systemctl daemon-reload y montar de nuevo. Verificado con reinicio real.
5. UFW (firewall)
El orden de operaciones aquí es lo más importante del paso, y el error más común es no respetarlo:
# 1. Establecer políticas por defecto ANTES de activar
sudo ufw default deny incoming
sudo ufw default allow outgoing
# 2. Añadir regla de acceso SSH ANTES de activar
# Restringido a la interfaz de Tailscale, no a IP genérica
sudo ufw allow in on tailscale0 to any port 22 proto tcp
# 3. Verificar que la regla existe
sudo ufw show added
# 4. Solo entonces activar
sudo ufw enable
Si activas ufw enable antes del paso 2, te bloqueas fuera de tu propia máquina con las reglas por defecto. Recuperarse requiere acceso físico.
La restricción a tailscale0 es deliberada: el acceso SSH solo es posible a través de la red Tailscale, no desde la red local del ISP ni desde internet. Una capa de red adicional por encima de las credenciales.
Aviso para lo que viene después con Docker: Docker manipula iptables directamente, puenteando UFW. Cualquier contenedor que publique un puerto en 0.0.0.0 queda expuesto aunque UFW diga que está bloqueado. La regla de trabajo para el resto del proyecto: todos los puertos de contenedores se publican en 127.0.0.1 o en la IP de Tailscale, nunca en 0.0.0.0.
6. Docker
Instalación desde el repositorio oficial, no el paquete docker.io de Ubuntu (que suele ir varios meses por detrás en versiones):
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt update && sudo apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin
Añadir mi usuario al grupo docker para trabajar sin sudo:
sudo usermod -aG docker imm
Con la aclaración explícita, para futura referencia, de que el usuario agente que creamos después nunca va a entrar en este grupo. Pertenecer a docker es tener acceso root de facto.
7. Anti-suspensión y modo consola
Una máquina desatendida cinco semanas no necesita un entorno gráfico GNOME corriendo en segundo plano. El compositor, el indexador Tracker, las notificaciones, el gestor de sesión —ninguno es necesario para SSH, Tailscale, Docker o el agente de IA—. Cambiar el target de arranque por defecto:
sudo systemctl set-default multi-user.target
Cuando quiera trabajar físicamente en el escritorio:
sudo systemctl start gdm
Verificado con reinicio real: la máquina arranca en modo texto, Tailscale conecta, SSH funciona, Docker está disponible.
Para actualizaciones automáticas solo de seguridad:
sudo apt install unattended-upgrades
sudo nano /etc/apt/apt.conf.d/50unattended-upgrades
En el archivo, comentar la línea ${distro_id}:${distro_codename} (la que actualiza paquetes de la distribución en general) y dejar solo activa ${distro_id}:${distro_codename}-security. Sin esta distinción, unattended-upgrades puede instalar actualizaciones de aplicaciones que cambian comportamiento, no solo parches de seguridad.
8. El incidente no planeado: el WiFi
A media sesión descubrí algo que había asumido incorrectamente: la máquina no tenía cable Ethernet conectado. El router del ISP tiene un problema en sus puertos físicos. Toda la conectividad dependía de un adaptador WiFi USB TP-Link (chip Realtek rtw88_8822bu) que no había planeado usar como interfaz principal.
Un mensaje de error apareció en la pantalla física: failed to send h2c command. Investigación con dmesg:
dmesg | grep -i rtw
El log mostraba también firmware failed to leave lps state repetidamente. El chip entraba en modo de ahorro de energía WiFi y fallaba al intentar despertar. Problema conocido del driver rtw88 en Linux. Diagnóstico:
iw dev <nombre-interfaz> get power_save
# Respuesta: Power save: on
El fix puntual iw dev <interfaz> set power_save off funciona pero no sobrevive al reinicio. Solución persistente:
sudo nano /etc/NetworkManager/conf.d/wifi-powersave-off.conf
[connection]
wifi.powersave = 2
Reinicio y verificación:
dmesg | grep "h2c command" | wc -l
# Resultado: 0
Cero repeticiones del error. El incidente merece mención explícita porque ilustra algo importante: un plan bien diseñado debe dejar espacio para descubrir que una premisa de partida era incorrecta. La premisa aquí era "la máquina tiene conectividad cableada estable". No la tenía. Resolverlo sin salirse de los criterios de seguridad ya establecidos (el WiFi funciona igual de bien con UFW y Tailscale) llevó menos de veinte minutos una vez identificado el problema.
9. Usuario agente (Unria) y las bandejas de intercambio
Creación del usuario sin contraseña (la ausencia de contraseña no es descuido, es diseño: el usuario nunca va a hacer login interactivo con contraseña):
sudo adduser --disabled-password --gecos "" agente
Verificaciones de aislamiento:
groups agente # no debe aparecer docker, sudo ni adm
sudo -l -U agente # no puede ejecutar nada con sudo
ls -la /home/ | grep agente # home con permisos 750
La prueba definitiva del aislamiento:
sudo -u agente ls /home/imm
# Permission denied
Las bandejas de intercambio direccionales:
sudo groupadd intercambio
sudo usermod -aG intercambio imm
sudo usermod -aG intercambio agente
sudo mkdir -p /srv/agente/entrada /srv/agente/salida
# entrada: imm escribe, agente solo lee
sudo chown imm:intercambio /srv/agente/entrada
sudo chmod 750 /srv/agente/entrada
# salida: agente escribe, imm solo lee
sudo chown agente:intercambio /srv/agente/salida
sudo chmod 750 /srv/agente/salida
Verificación empírica: como usuario agente, puedo listar y leer entrada/, pero touch /srv/agente/entrada/prueba devuelve Permission denied. Como usuario imm, puedo leer salida/, pero no escribir en ella.
10. OpenClaw: tres problemas reales en la instalación
Problema 1: el instalador oficial requiere sudo
El instalador estándar de OpenClaw (install.sh) intenta instalar Node.js vía apt, que requiere privilegios de superusuario. El usuario agente no tiene sudo (por diseño). Resultado: el script falla a mitad.
Solución: la variante install-cli.sh, que descarga Node.js embebido en el espacio de usuario sin tocar nada del sistema. No requiere sudo para nada.
sudo -u agente bash
curl -fsSL https://get.openclaw.com/install-cli.sh | bash
Problema 2: el binario no está en el PATH
Tras la instalación, openclaw --version devolvía "command not found". El binario se instaló en ~/.openclaw/bin, que no forma parte del $PATH por defecto. Corrección en el .bashrc del usuario agente:
echo 'export PATH="$HOME/.openclaw/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
openclaw --version # ahora funciona
Problema 3: systemd sin lingering (el más interesante técnicamente)
Al terminar el asistente de onboarding de OpenClaw, el sistema intentó instalar el gateway como un servicio de usuario de systemd. El resultado: "Systemd user services are unavailable".
La causa: sin lingering habilitado, cuando no hay ninguna sesión activa del usuario agente, el bus de systemd de usuario para ese UID no existe. Los servicios de usuario solo corren mientras hay una sesión abierta; al cerrarla, se detienen.
Solución en dos pasos. Primero, habilitar lingering:
sudo loginctl enable-linger agente
Esto hace que el bus de systemd de usuario de agente exista siempre, independientemente de si hay sesiones activas. Segundo, en el .bashrc de agente:
export XDG_RUNTIME_DIR=/run/user/$(id -u)
Sin esta variable, systemctl --user no encuentra el bus aunque ya esté corriendo. Con ambas piezas:
sudo -u agente bash -l
openclaw gateway install
systemctl --user enable --now openclaw-gateway.service
systemctl --user status openclaw-gateway.service
La prueba que confirma que todo funciona de verdad: reinicio completo de la máquina, esperar dos minutos, enviar un mensaje por Telegram sin intervención humana. Si responde, el servicio arrancó solo al boot.
Configuración del onboarding: decisiones relevantes
Modelo: OpenAI vía OAuth. Un detalle no intuitivo: durante el flujo de autenticación, el navegador intenta redirigir a localhost:1455 y muestra un error de conexión. No es un fallo real. Hay que copiar la URL completa del error de la barra del navegador y pegarla en la terminal donde está corriendo el asistente de configuración.
Gateway: loopback exclusivamente (127.0.0.1). No expuesto a la LAN ni a Tailscale directamente. Si necesito acceder a la Control UI en remoto, la vía correcta es un túnel SSH manual (ssh -L 3000:localhost:3000 imm@<ip-tailscale>), no exposición automática del gateway.
Telegram: política allowlist con el chat_id numérico propio desde el primer arranque, no la política pairing por defecto que acepta conexiones de cualquier usuario durante el período de emparejamiento.
Skills y plugins del marketplace: saltados deliberadamente. El historial documentado de 824 skills maliciosas no invita a confiar en el ecosistema de terceros sin revisión manual.
11. Whisper local (faster-whisper) sobre Pascal
Entorno virtual Python dedicado dentro del workspace del agente:
sudo -u agente bash -l
python3 -m venv ~/.openclaw/workspace/whisper-env
source ~/.openclaw/workspace/whisper-env/bin/activate
pip install faster-whisper nvidia-cudnn-cu12 nvidia-cublas-cu12
Sin tocar el toolkit CUDA de sistema —la decisión tomada desde el principio—. Las librerías CUDA se instalan solo para este entorno virtual.
Problema: la GTX 1050 Ti es Pascal y no habla float16
Al probar Whisper con compute_type='float16', error inmediato. La GTX 1050 Ti es arquitectura Pascal (2016). Pascal no soporta cómputo eficiente en float16 —las operaciones se hacen en float32 internamente, y ctranslate2 lo bloquea con un ValueError explícito en lugar de dejarlo correr lento sin avisar.
Segundo intento con compute_type='int8_float16': mismo fallo. La acumulación interna sigue siendo float16.
La solución correcta para Pascal:
model = WhisperModel("large-v3", device="cuda", compute_type="int8")
int8 puro es el modo recomendado para esta generación de GPU. Funciona, es rápido, y no genera errores.
El agente resolvió bien pero con la GPU equivocada
Sin instrucciones explícitas sobre los parámetros de Whisper, la primera vez que el agente procesó una nota de voz lo hizo con device='cpu'. La transcripción fue correcta, pero desaprovechó la GPU completamente disponible.
Solución: una skill propia para fijar los parámetros correctos de forma permanente. La skill whisper-local con su SKILL.md en el formato estándar de OpenClaw, fijando explícitamente device='cuda' y compute_type='int8'.
Al probar la skill: error de libcublas.so.12 no encontrado. Las librerías NVIDIA instaladas vía pip no están en el LD_LIBRARY_PATH del sistema por defecto. Solución documentada directamente dentro de la skill:
export LD_LIBRARY_PATH=$(python3 -c "import nvidia.cublas; import os; print(os.path.dirname(nvidia.cublas.__file__))")/lib:$LD_LIBRARY_PATH
Documentado dentro de la skill para que quede resuelto automáticamente en cada uso futuro, no redescubierto cada vez que alguien (o el propio agente) la llame.
Prueba final: nota de voz real por Telegram, transcrita correctamente con GPU, sin errores de librería.
Balance después de la sesión
Lo que está funcionando: SSH vía Tailscale, firewall restrictivo, Docker, usuario agente aislado sin acceso a mis recursos, OpenClaw como servicio de systemd que arranca solo, bandejas de intercambio con permisos asimétricos verificados, Whisper local sobre GPU con los parámetros correctos para Pascal.
Lo que queda pendiente para después de una semana de observación: la Capa 2 de egress control (firewall por UID + proxy de salida). El logging está activo desde el primer arranque; cuando tenga datos reales de qué endpoints usa el agente, construiré la lista blanca sobre comportamiento observado.
El resumen honesto del proceso: siete de los pasos fueron razonablemente directos una vez entendidos. Tres problemas reales —el instalador que requería sudo, el PATH roto, y el systemd sin lingering— hubieran sido bloqueantes sin saber dónde buscar. El incidente del WiFi fue el tipo de cosa que no está en ningún tutorial porque depende del hardware específico. Eso es lo que hace que un tutorial de instalación real sea diferente de uno idealizado: no la lista de comandos correctos, sino las bifurcaciones donde algo falla y la lógica de diagnóstico para resolverlo.
Ignacio Mínguez Montes — Viriatech / IMM CORE SYSTEM S.L. — Córdoba
La primera parte de esta serie documenta el modelo de amenaza y el razonamiento de seguridad detrás de esta arquitectura: por qué un agente de IA corriendo como tu usuario en Linux tiene acceso a tus claves SSH y tus cookies de sesión, y qué se puede hacer al respecto.
