Hace unos meses me cansé de hacer siempre lo mismo: terminar un cambio en la web, compilar, comprimir, abrir el terminal de cPanel, subir, descomprimir, reiniciar. Veinte minutos de trabajo mecánico que no aporta nada.

Hoy le digo a la IA "sube este build al servidor" y lo hace. Revisa logs cuando algo falla, compara qué versión hay desplegada, sube archivos. Y lo hace sin conocer ninguna contraseña mía y sin poder tocar nada fuera de la carpeta que yo le asigné.
Esta guía documenta exactamente cómo lo tengo montado en producción para farp.dev. No es teoría: es el montaje con el que despliego esta misma web que estás leyendo.
En pocas palabras: vas a darle a una IA (Claude, u otra que soporte MCP) un canal SSH restringido hacia tu hosting cPanel, con llave propia, confinada a un directorio y limitada a una lista blanca de comandos. Tiempo estimado: 1 a 2 horas. Nivel: intermedio — necesitas saber usar un terminal y no tenerle miedo a un archivo JSON.
Antes de empezar
Requisitos:
- Hosting con acceso SSH habilitado. En la mayoría de los hostings compartidos (FastComet, SiteGround, Hostinger…) viene incluido, pero a veces hay que pedirlo al soporte. Búscalo en cPanel como "SSH Access" o "Terminal".
- Un cliente de IA que soporte MCP (Model Context Protocol). Yo uso Claude Desktop / Claude Code, pero el concepto aplica a cualquier cliente compatible.
- Node.js instalado en tu máquina local (el servidor MCP corre en tu computador, no en el hosting).
- Saber generar llaves SSH. Si nunca lo has hecho, lo vemos en el paso 1.
Y las advertencias, porque esto es serio:
- Nunca le des a la IA un usuario root ni acceso a nivel de servidor completo. Si tu hosting te da un VPS con root, crea un usuario aparte con permisos mínimos. En hosting compartido esto viene resuelto: tu usuario de cPanel ya está aislado.
- Nunca le pases tu contraseña de cPanel a la IA, ni en el prompt, ni en un archivo de configuración que la IA pueda leer. El punto de esta guía es justamente que no haga falta.
- Si tu servidor almacena datos de clientes (una base de datos con contactos comerciales, por ejemplo), piensa dos veces si la IA necesita acceso a esa zona. Antes de decidirlo, revisa qué datos tiene sentido compartir con una IA y cuáles no. En mi caso, el directorio de datos está fuera del alcance del canal que le di. Mínimo privilegio: si no lo necesita para desplegar, no lo tiene.
- Prueba primero en un directorio aparte. Yo hice todas las pruebas en una subcarpeta vacía antes de apuntar el canal a la web real.
El principio rector de todo lo que viene es uno solo, y es el mismo que aplico en lo mínimo de seguridad web para una PYME: mínimo privilegio. La IA no recibe "acceso a tu servidor". Recibe un túnel estrecho, con una llave que solo abre una puerta, hacia una habitación concreta, con una lista corta de cosas que puede hacer dentro.
Paso 1: crear un par de llaves SSH dedicado SOLO para la IA
No reutilices tu llave personal. Si algún día quieres cortarle el acceso a la IA, quieres poder revocar su llave sin afectar la tuya. En tu máquina local:
ssh-keygen -t ed25519 -f ~/.ssh/ia_deploy_key -C "ia-deploy-farp"
Cuando te pida passphrase, aquí hay una decisión: una passphrase protege la llave si te roban el archivo, pero complica el uso automatizado. Yo la dejé sin passphrase porque la llave solo existe en mi máquina, con permisos restrictivos, y abre un canal ya limitado. Si tu modelo de riesgo es distinto (laptop compartida, por ejemplo), ponle passphrase y usa un agente SSH.
Esto genera dos archivos:
~/.ssh/ia_deploy_key— la privada. No sale nunca de tu máquina.~/.ssh/ia_deploy_key.pub— la pública. Esta es la que sube al servidor.
Ahora en cPanel:
- Entra a SSH Access (o "Seguridad → Acceso SSH").
- Manage SSH Keys → Import Key.
- Pega el contenido de
ia_deploy_key.puben el campo de llave pública. Deja el campo de llave privada vacío. - Una vez importada, dale a Manage → Authorize. Este paso se olvida siempre y sin él la llave no sirve.
Verifica desde tu terminal que conecta:
ssh -i ~/.ssh/ia_deploy_key -p 22 tuusuario@tudominio.com "echo conectado"
Ojo: algunos hostings usan un puerto SSH distinto al 22 (FastComet usa el 17177, por ejemplo — revisa la documentación del tuyo).
Paso 2: montar el servidor MCP de SSH
MCP es el protocolo que permite a la IA usar "herramientas" externas. Vamos a darle una herramienta que ejecuta comandos por SSH usando la llave que acabamos de crear.
Tienes dos caminos: usar un servidor MCP de SSH existente (hay varios en npm; busca "mcp ssh" y revisa el código antes de instalar nada — le vas a dar acceso a tu servidor, léelo) o escribir uno propio. Yo terminé escribiendo el mío en Node con la librería ssh2, precisamente porque quería controlar el confinamiento y la lista blanca de los pasos 3 y 4. Son ~200 líneas.
Sea cual sea el camino, la configuración en Claude Desktop vive en claude_desktop_config.json (en Windows: %APPDATA%\Claude\; en macOS: ~/Library/Application Support/Claude/):
{
"mcpServers": {
"hosting-ssh": {
"command": "node",
"args": ["C:/Users/tuusuario/mcp/ssh-server.js"],
"env": {
"SSH_HOST": "tudominio.com",
"SSH_PORT": "22",
"SSH_USER": "tuusuario",
"SSH_KEY_PATH": "C:/Users/tuusuario/.ssh/ia_deploy_key",
"SSH_ROOT_DIR": "/home/tuusuario"
}
}
}
}
Fíjate en dos cosas:
- La llave se pasa por ruta, no por contenido. El JSON solo dice dónde está el archivo. La IA ve que existe una herramienta "ejecutar comando en hosting", pero nunca ve la llave en sí.
- En Windows, usa barras normales (
/) o dobles barras invertidas (\\) en las rutas. UnC:\Users\...con backslash simple es un JSON inválido o una ruta corrupta, y es el error número uno al montar esto (lo cubro en problemas comunes).
Reinicia Claude Desktop y deberías ver la herramienta disponible.
Paso 3: confinamiento — la IA no sale de su directorio
Aquí es donde la mayoría de los tutoriales se quedan cortos. Un canal SSH sin restricciones es darle el servidor entero. Nosotros vamos a hacer que todo comando se ejecute dentro de un directorio raíz y no pueda salir de ahí.
En hosting compartido no puedes montar un chroot real (necesitas root para eso), así que el confinamiento se hace en el servidor MCP, antes de enviar el comando. La lógica, conceptualmente:
const ROOT = process.env.SSH_ROOT_DIR; // /home/tuusuario
function comandoSeguro(cmd, cwd) {
// 1. El directorio de trabajo siempre cuelga del ROOT
const rutaFinal = path.posix.resolve(ROOT, cwd || ".");
if (!rutaFinal.startsWith(ROOT)) {
throw new Error("Ruta fuera del directorio permitido");
}
// 2. Bloquear intentos de escape en los argumentos
if (/\.\.\//.test(cmd) || cmd.includes("~") || /(^|\s)\/(?!home\/tuusuario)/.test(cmd)) {
throw new Error("Referencia a ruta fuera del jail");
}
return `cd ${rutaFinal} && ${cmd}`;
}
No es un chroot de verdad y no pretende serlo: es una barrera de política, no de kernel. Pero combinada con el paso 4 (donde bloqueamos los comandos que podrían saltársela), en la práctica la IA queda operando solo dentro de /home/tuusuario. En mi montaje, además, apunto el ROOT a la carpeta del proyecto, no al home completo — así ni siquiera ve mail/, etc/ ni los directorios de datos.
Paso 4: lista blanca de comandos — solo lo necesario
Segunda capa, y la más importante. En vez de prohibir lo peligroso (lista negra, siempre incompleta), permitimos solo lo imprescindible y rechazamos todo lo demás.
Mi lista real para deploys:
const COMANDOS_PERMITIDOS = [
"ls", "cat", "stat", "find", "du", "df",
"tar", // descomprimir el build
"git", // pull, status, log
"curl", // verificar que la web responde
"head", "tail", // leer logs
"md5sum", // verificar integridad de archivos subidos
];
const PATRONES_PROHIBIDOS = [
/[;&|]/, // encadenamiento: ls; rm -rf
/[<>]/, // redirecciones: > archivo
/`|\$\(/, // sustitución de comandos: $(rm ...)
/\brm\b/, /\bmv\b/, /\bchmod\b/, /\bchown\b/,
/\bsudo\b/, /\bssh\b/, /\bwget\b.*-O/, /\bkill\b/,
];
function validar(cmd) {
const binario = cmd.trim().split(/\s+/)[0];
if (!COMANDOS_PERMITIDOS.includes(binario)) {
throw new Error(`Comando no permitido: ${binario}`);
}
for (const patron of PATRONES_PROHIBIDOS) {
if (patron.test(cmd)) {
throw new Error(`Patrón bloqueado en: ${cmd}`);
}
}
}
Los detalles que importan:
- Bloquear
;,&,|,>y<es innegociable. Sin eso, unls; rm -rf .pasa la validación porque el primer binario esls. El encadenamiento es la forma clásica de colar un comando prohibido detrás de uno permitido. rmno está, y no es un descuido. Si un deploy necesita borrar algo, lo borro yo a mano. La IA propone, el humano ejecuta lo destructivo.tarestá permitido pero es un arma de doble filo:tarpuede sobrescribir archivos al extraer. Por eso el confinamiento del paso 3 importa — solo puede extraer dentro del jail — y por eso los despliegues pasan primero por una carpeta de pruebas.- Notarás que no está
npm. En hostings compartidos con CloudLinux,node_moduleses un symlink a un entorno virtual que gestiona cPanel; corrernpm installpor SSH puede romper ese enlace y tumbar la app. Las instalaciones las hago desde el botón "Run NPM Install" de la interfaz de cPanel. Aprendido por las malas.
¿Es una lista corta? Sí. ¿Me ha faltado algo alguna vez? Un par de veces, y la solución fue añadir el binario conscientemente, no abrir el grifo.
Paso 5: el flujo de deploy seguro, de principio a fin
Con el canal montado, así es un despliegue real en mi caso:
-
Compilación local.
npm run builden mi máquina. El servidor compartido no tiene recursos para compilar Next.js sin sufrir, y no quiero código a medio compilar en producción. El resto del despliegue en este tipo de hosting lo detallo en cómo desplegar Next.js en cPanel. -
Empaquetar:
tar -czf build.tgz .next public package.json next.config.js --exclude=".next/cache"
-
Subir. El MCP tiene una herramienta de subida (SFTP con la misma llave). La IA sube
build.tgzal directorio de pruebas dentro del jail. -
Verificar y extraer. La IA compara el
md5sumlocal contra el del servidor y luego ejecutatar -xzf build.tgz— comando permitido, dentro del jail. -
Reiniciar la aplicación: esto lo hago YO, a mano, desde cPanel → Setup Node.js App → Restart. Es deliberado. El reinicio es el momento en que el código nuevo entra en producción, y ahí quiero a una persona mirando. La IA me deja todo listo y me dice "listo para reiniciar"; el botón lo aprieto yo. Si algo salió mal en los pasos anteriores, este es mi último punto de control para no publicarlo.
¿Podría automatizar el reinicio? Técnicamente sí (cPanel tiene API). No quiero. El coste de apretar un botón es cero; el de un despliegue roto publicándose solo a las 3 de la mañana, no.
Paso 6: auditoría — registrar todo lo que la IA ejecuta
Última capa: cada comando que pasa por el MCP queda registrado, se haya ejecutado o haya sido rechazado.
function auditar(cmd, resultado) {
const linea = JSON.stringify({
ts: new Date().toISOString(),
cmd,
resultado, // "ok" | "rechazado" | "error"
});
fs.appendFileSync(path.join(__dirname, "audit.log"), linea + "\n");
}
Esto me ha servido dos veces: una para reconstruir qué pasó en un deploy que quedó a medias, y otra para descubrir que la IA intentaba usar grep (que no estaba en la lista) y añadirlo conscientemente. Revisar el log de vez en cuando también te enseña mucho sobre cómo "razona" la IA con tu servidor: qué intenta, qué asume, dónde se equivoca.
Si quieres ir un paso más allá, tu hosting probablemente ya registra las sesiones SSH en /var/log/secure o similar (aunque en compartido no siempre puedes leerlo). El log del lado del MCP es el que controlas tú al 100%.
Problemas comunes
"Permission denied (publickey)" al conectar. Tres sospechosos, en orden: (1) olvidaste autorizar la llave en cPanel después de importarla — es un botón aparte; (2) la llave privada tiene permisos demasiado abiertos — en Linux/macOS: chmod 600 ~/.ssh/ia_deploy_key; SSH se niega a usar llaves legibles por otros usuarios; (3) puerto equivocado — verifica el puerto SSH de tu hosting.
El MCP no aparece en Claude Desktop. El 90% de las veces en Windows es la ruta con backslashes en el JSON: "C:\Users\..." no es válido. Usa "C:/Users/..." o "C:\\Users\\...". Revisa también los logs de Claude Desktop (%APPDATA%\Claude\logs\) — ahí aparece el error exacto del servidor que no arrancó.
Timeouts en comandos largos. Un tar de un build grande o un find sobre miles de archivos puede exceder el timeout por defecto del MCP. Solución: sube el timeout en tu servidor MCP para comandos concretos, o mejor, divide el trabajo (excluye .next/cache del tarball — pesa una barbaridad y no sirve en el servidor).
La IA "no puede" hacer algo que debería poder. Revisa el audit.log: casi siempre es un comando rechazado por la lista blanca. Decide conscientemente si añadirlo o si esa operación debe seguir siendo manual.
Preguntas frecuentes
¿Esto no es peligroso de todos modos? ¿Y si la IA "decide" hacer algo raro? Por eso las capas. La pregunta correcta no es "¿confío en la IA?" sino "¿qué es lo peor que puede hacer este canal, aunque la IA se equivoque o alguien la manipule?". Con llave dedicada, jail de directorio, lista blanca sin comandos destructivos ni encadenamientos, y restart manual, lo peor realista es que suba archivos equivocados a staging. Molesto, reversible, no catastrófico. Diseña para el fallo, no para la confianza.
¿Funciona con cualquier hosting cPanel? Con cualquiera que ofrezca acceso SSH con llave pública, que son la gran mayoría. Lo que cambia es el puerto SSH y si el terminal viene habilitado por defecto. Si tu hosting solo ofrece FTP, no montes esto sobre FTP — es un protocolo sin las garantías de SSH; considera cambiar de plan o de proveedor.
¿Y si no quiero programar mi propio servidor MCP? Hay servidores MCP de SSH publicados en npm que funcionan. Pero léeles el código antes (son proyectos pequeños, se leen en 20 minutos) y verifica que soporten restricción de directorio y filtrado de comandos. Si un MCP de SSH no ofrece ninguna restricción, para mí es descartable para producción: estarías en el escenario "canal abierto" que toda esta guía intenta evitar.
Este setup me ahorra horas al mes y, más importante, eliminó los errores tontos de los deploys manuales. Pero cada hosting y cada proyecto tienen sus particularidades, y la parte de seguridad no es donde quieres improvisar. Si quieres montar algo así en tu servidor y prefieres hacerlo acompañado — revisando juntos tu caso concreto, tu hosting y qué debería poder tocar la IA y qué no — eso es exactamente lo que hago en las sesiones de consultoría 1:1 ($60, una hora, salimos con el problema resuelto o con el plan exacto para resolverlo).
Newsletter
¿Te fue útil este artículo?
Suscríbete y te aviso cuando publique algo que valga la pena leer. Sin correo basura y sin una frecuencia forzada.