CommunityProgramación y desarrollogithub.com

igor-batrakov/new-vps-setup

Guía paso a paso segura (agent skill + checklist) para configurar un nuevo VPS Ubuntu para producción para principiantes: hardening, actualizaciones automáticas, UFW, fail2ban, Docker, nginx+HTTPS, copias de seguridad automáticas restic, monitorización.

¿Qué es new-vps-setup?

Este skill proporciona una guía completa y segura, paso a paso, para configurar un servidor Ubuntu desde cero para entornos de producción, dirigida a usuarios sin experiencia en administración de sistemas. Automatiza tareas críticas como actualizaciones de seguridad, copias de seguridad con restic y monitorización, asegurando que el servidor funcione de la manera más autónoma posible. Incluye protocolos de seguridad esenciales como la creación de un usuario sudo separado, configuración de SSH solo con claves, firewall UFW, fail2ban, instalación de Docker con rotación de logs y configuración de nginx con HTTPS gratuito de Let's Encrypt. El skill está diseñado para las versiones LTS de Ubuntu (22.04/24.04) y enfatiza la verificación de cada paso antes de continuar, evitando configuraciones inseguras comunes.

Compatible con~Claude Code~Codex CLI~Cursor
npx skills add igor-batrakov/new-vps-setup

Installed? Explore more Programación y desarrollo skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Configuración de un nuevo VPS para producción (para principiantes)

Resumen

Este skill te guía paso a paso a través de la preparación de un servidor Ubuntu limpio para producción, de modo que a partir de entonces se mantenga de la forma más autónoma posible. La audiencia son personas sin experiencia en administración de sistemas. Por lo tanto:

  • Valores predeterminados seguros, no "como un profesional". Nada de NOPASSWD-sudo, ni comprobaciones desactivadas por conveniencia. Lo que facilita la vida a un experto, para un principiante es una mina.
  • Autonomía a través de la automatización. Las actualizaciones de seguridad, las copias de seguridad y la monitorización deben funcionar solas. La persona solo reacciona a las alertas.
  • Cada paso se verifica. Después de un cambio, se verifica el resultado, y solo entonces se pasa al siguiente paso.

SO objetivo: Ubuntu 22.04 / 24.04 LTS. Los comandos para otras distribuciones difieren.

Reglas de oro (antes de CUALQUIER cambio)

  1. Copia de seguridad del config: sudo cp /ruta/config /ruta/config.bak.$(date +%F-%H%M)
  2. Verifica el estado: sudo systemctl status <servicio> / sudo docker ps
  3. Después del cambio, verifica el resultado (logs, estado, conexión de prueba).
  4. Nunca cierres la sesión SSH actual hasta que en una nueva sesión no te hayas asegurado de que el acceso y sudo funcionan. Perder SSH en un servidor remoto = ir a la consola del proveedor (VNC).

Principios de seguridad en producción (respétalos en todos los pasos):

  • Los secretos (claves API, tokens, contraseñas) solo en .env o variables de entorno, nunca en el código.
  • .env debe estar en .gitignore (de lo contrario, los secretos se filtrarán en git en el primer commit).
  • Las bases de datos no se exponen a internet, solo localhost o a través de una red privada/VPN.
  • No hay modo debug en el servidor: DEBUG=False, NODE_ENV=production.
  • Las aplicaciones no se ejecutan como root: usuario/contendor separado.
  • No hay puertos abiertos innecesarios (paneles de administración, dashboards, métricas: cerrados o detrás de IPs de confianza).
  • No desactives las comprobaciones de seguridad por conveniencia (verify=False, allowAll solo localmente).
  • SSH solo con claves, no con contraseña.

0. Diagnóstico: qué hay ya

Ejecútalo PRIMERO. Los proveedores de hosting a menudo entregan el servidor con una configuración parcial (cloud-init, ufw, etc.).

# Sistema
lsb_release -a && uname -r
free -h && df -h && nproc

# Quién soy y qué permisos tengo
whoami && id

# Config SSH (valores efectivos, ¡no solo los archivos!)
sudo sshd -T | grep -iE 'permitrootlogin|passwordauthentication|pubkeyauthentication|^port '

# Cortafuegos
sudo ufw status verbose

# Qué puertos están escuchando
sudo ss -tlnp

# Actualizaciones automáticas (¿están activadas?)
systemctl status unattended-upgrades 2>/dev/null | head -5
cat /etc/apt/apt.conf.d/20auto-upgrades 2>/dev/null

# Docker / fail2ban
sudo docker ps 2>/dev/null
sudo fail2ban-client status 2>/dev/null

Después del diagnóstico, informa al usuario de lo que se ha encontrado y no comiences la configuración sin confirmación.


1. Configuración inicial y seguridad

Verifica la sección 0: parte puede estar ya hecha. No lo hagas dos veces.

1.1 Actualización del sistema

sudo apt update && sudo apt upgrade -y
sudo apt autoremove -y
# Si el kernel se ha actualizado, será necesario reiniciar (ver salida). Reinicia con conocimiento:
# sudo reboot   # y reconéctate después de ~30-60s

1.2 Usuario sudo separado (NO trabajes como root)

sudo adduser <NOMBRE_USUARIO>            # pedirá una contraseña: elige una segura, guárdala en un gestor de contraseñas
sudo usermod -aG sudo <NOMBRE_USUARIO>

Para el principiante, sudo se deja con solicitud de contraseña. Esto es intencionado: NOPASSWD-sudo elimina la última barrera en caso de compromiso de la clave o sesión. La contraseña de sudo también es necesaria como fallback si se pierde la clave SSH (ver 1.3).

1.3 Claves SSH (acceso sin contraseña con clave)

Tipo de clave: ed25519 (no RSA). Nombre: <servidor>_<dispositivo>, con frase de contraseña.

En la máquina local:

ssh-keygen -t ed25519 -f ~/.ssh/<servidor>_<dispositivo> -C "<servidor>_<dispositivo>"
ssh-copy-id -i ~/.ssh/<servidor>_<dispositivo>.pub <NOMBRE_USUARIO>@<IP_SERVIDOR>

Verifica el acceso con clave en una sesión SEPARADA (¡no cierres la antigua!):

ssh -i ~/.ssh/<servidor>_<dispositivo> -o IdentitiesOnly=yes <NOMBRE_USUARIO>@<IP_SERVIDOR> 'whoami; sudo whoami'

Alias conveniente en ~/.ssh/config en la máquina local:

Host <alias>
    HostName <IP_SERVIDOR>
    User <NOMBRE_USUARIO>
    IdentityFile ~/.ssh/<servidor>_<dispositivo>
    IdentitiesOnly yes

1.4 Hardening de SSH

Crea /etc/ssh/sshd_config.d/99-hardening.conf:

PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
AllowTcpForwarding local
ClientAliveInterval 300
ClientAliveCountMax 2
AllowUsers <NOMBRE_USUARIO>

AllowTcpForwarding local permite el reenvío local (ssh -L — para acceder a un servicio en 127.0.0.1 antes de configurar el dominio) y prohíbe el remoto (-R). Si no se necesita reenvío en absoluto, pon no.

Aplica y verifica:

sudo sshd -t                                              # ¿sintaxis OK?
sudo systemctl restart ssh
sudo sshd -T | grep -iE 'permitroot|passwordauth'         # valores efectivos = no

Gotcha 1: cloud-init sobrescribe el hardening. El archivo /etc/ssh/sshd_config.d/50-cloud-init.conf a menudo contiene PasswordAuthentication yes y en sshd_config.d/ funciona "primera coincidencia gana". Verifica a través de sudo sshd -T (valores efectivos), no solo tu archivo. Si lo sobrescribe, corrige/elimina la línea en 50-cloud-init.conf.

Gotcha 2: Ubuntu 24.04 usa ssh.socket. Port en sshd_config se ignora. Para cambiar el puerto SSH: sudo systemctl edit ssh.socket → añadir ListenStream=<PUERTO> (y eliminar el predeterminado). Verifica a través de la consola del proveedor: un error aquí bloquea el acceso.

1.5 Actualizaciones automáticas de seguridad ⭐ (núcleo de la autonomía)

Este es el paso más importante para "el servidor se mantiene solo". Sin él, los agujeros en los paquetes se acumulan.

sudo apt install unattended-upgrades apt-listchanges -y
sudo dpkg-reconfigure -plow unattended-upgrades   # elige "Yes" — creará 20auto-upgrades

Verifica /etc/apt/apt.conf.d/20auto-upgrades:

APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";

En /etc/apt/apt.conf.d/50unattended-upgrades (descomenta/configura las líneas clave):

// Instalar actualizaciones de seguridad (activado por defecto):
Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
    "${distro_id}ESMApps:${distro_codename}-apps-security";
    "${distro_id}ESM:${distro_codename}-infra-security";
};
Unattended-Upgrade::Remove-Unused-Dependencies "true";
// Reinicio automático si la actualización lo requiere (kernel). Elige una ventana con mínima carga:
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";

Decisión "reinicio automático": true = el servidor se reiniciará solo por la noche al actualizar el kernel (más seguro, pero posible tiempo de inactividad corto). false = el kernel se actualiza, pero el reinicio te espera a ti (hay que vigilarlo). Para un servidor de producción individual para un principiante, suele ser mejor true + ventana nocturna.

Verificación: ejecución en seco sin instalación:

sudo unattended-upgrade --dry-run --debug 2>&1 | tail -20
systemctl list-timers 'apt-daily*'   # ¿los temporizadores están activos?

1.6 fail2ban (protección SSH contra fuerza bruta)

La configuración básica es suficiente: no compliques con recidive/nftables al principio.

sudo apt install fail2ban -y

/etc/fail2ban/jail.d/sshd.local:

[sshd]
enabled = true
bantime = 1h
findtime = 30m
maxretry = 4
# No banearse a uno mismo: añade TUS IPs estáticas (averigua: curl ifconfig.me)
ignoreip = 127.0.0.1/8 ::1 <TU_IP_CASA>
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd

1.7 Zona horaria y (para VPS pequeños) swap

sudo timedatectl set-timezone <Región/Ciudad>     # ej. Europe/Moscow o UTC

# Swap es necesario en VPS con poca RAM (< 2 GB) para que OOM no mate los servicios:
free -h
sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile
sudo mkswap /swapfile && sudo swapon /swapfile
grep -q '/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-swap.conf && sudo sysctl --system

2. Cortafuegos UFW

Principio: todo cerrado, solo abierto lo necesario. Primero permite SSH, de lo contrario te bloquearás.

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp comment 'SSH'      # IMPORTANTE: antes de enable!
sudo ufw enable
sudo ufw status verbose

Para un servidor web, añade HTTP/HTTPS:

sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'

Paneles de administración/dashboards/métricas solo a IPs de confianza, no a "todos":

sudo ufw allow from <IP_CONFIANZA> to any port <PUERTO_ADMIN> proto tcp comment 'panel admin'

Nunca uses sudo ufw reset — restablecerá todas las reglas, incluida SSH.


3. Docker (opcional: si las aplicaciones están en contenedores)

Instalación

curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker <NOMBRE_USUARIO>      # vuelve a iniciar sesión para que el grupo se aplique

La membresía en el grupo docker es efectivamente equivalente a root (a través de docker run -v /:/host). Esto es aceptable para un solo administrador, pero no le des este grupo a usuarios en los que no confíes el acceso completo al servidor.

Rotación de logs (obligatorio: de lo contrario, los logs llenarán el disco)

/etc/docker/daemon.json:

{
  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" }
}
sudo systemctl restart docker

⚠️ Docker evita UFW

Un puerto reenviado como -p 8080:8080 es accesible desde internet, incluso si UFW no lo ha permitido. Opciones seguras:

  • Enlazar solo a localhost: -p 127.0.0.1:8080:8080 (accesible solo a través de nginx/túnel SSH).
  • No reenviar puertos de la base de datos en absoluto: comunicarse por nombre de servicio dentro de la red de docker.

4. nginx + HTTPS (opcional: si hay una aplicación web y un dominio)

nginx como proxy inverso frente a la aplicación (la aplicación escucha en 127.0.0.1, hacia afuera solo nginx).

sudo apt install nginx -y

/etc/nginx/sites-available/<app>:

server {
    listen 80;
    server_name <DOMINIO>;
    location / {
        proxy_pass http://127.0.0.1:<PUERTO_APP>;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
sudo ln -s /etc/nginx/sites-available/<app> /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

HTTPS gratuito con Let's Encrypt (certbot emitirá y configurará la renovación automática):

sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d <DOMINIO>
# La renovación automática ya está configurada con un temporizador. Verificación:
sudo certbot renew --dry-run

Protección: añade server_tokens off; en /etc/nginx/nginx.conf (ocultar la versión de nginx) y cabeceras de seguridad (Strict-Transport-Security, X-Content-Type-Options nosniff).


5. Copias de seguridad automáticas (restic + offsite)

Instrucciones completas (instalación, repositorio, script, temporizador, restauración): lee references/backups.md — allí paso a paso.

Principios clave que NO se deben violar:

  1. La copia de seguridad debe ser offsite. Una copia en el mismo servidor (o disco) no salvará si el servidor/disco muere. Mínimo: almacenamiento externo (compatible con S3: Backblaze B2, rsync.net, etc.).
  2. La contraseña de cifrado del repo se almacena FUERA del servidor (gestor de contraseñas). Pierdes la contraseña → todas las copias de seguridad son permanentemente ilegibles. Esta es la forma más común de quedarse sin copia de seguridad.
  3. Una copia de seguridad sin verificación de restauración no es una copia de seguridad. Al menos una vez, haz una restauración de prueba (ver backups.md, "Restauración de aprendizaje").
  4. Automatización: ejecución diaria a través del temporizador de systemd + rotación de snapshots antiguos.

Qué respaldar

Piensa en categorías, no en rutas individuales: así no olvidarás nada:

CategoríaEjemplos de rutasPor qué
Configuraciones del sistema/etc completonginx, ssh, ufw, fail2ban, unidades systemd — para levantar el servidor "como estaba"
Datos de la aplicación/home/<NOMBRE_USUARIO>/app/data, /var/lib/<servicio>lo que no se puede reinstalar: contenido de usuario, cargas
Volcados de BD/var/backups/*.sql.gzvolcado a través de pg_dump/.backup antes de restic (el archivo de BD vivo puede estar dañado)
Secretos.env, /root/.config/*se respaldan cifrados (restic cifra el repo). La contraseña de cifrado — fuera del servidor

Qué NO respaldar (se reinstala, solo infla la copia de seguridad): node_modules, .venv/venv, imágenes de Docker (docker pull), paquetes del sistema (apt), cachés, /var/log, /tmp. Estas rutas ya están en --exclude del script (ver backups.md).

Cuánto conservar (retención): elige una política

restic almacena snapshots desduplicados, por lo que "más copias" es barato en espacio. Comando: restic forget --prune --keep-daily N --keep-weekly N --keep-monthly N.

PolíticaComandoPara quién es adecuadaJustificación
Básica (predeterminada)--keep-daily 7 --keep-weekly 4 --keep-monthly 6La mayoría de los servidores de producciónRetroceder a cualquier día de los últimos 7 + historial de hasta seis meses. Captura tanto errores recientes como "¿cuándo se rompió esto?". Ya está en backups.md.
Ligera--keep-daily 7 --keep-weekly 4Sitios/bots sin datos acumulativos valiosos~1 mes de historial. Mínimo espacio, más simple. Apropiado si la pérdida de datos antiguos no es crítica.
Larga--keep-daily 14 --keep-weekly 8 --keep-monthly 12Bases de datos con datos importantes, finanzas, requisitos legalesUn año de historial + 2 semanas por día. Más caro en espacio, pero protege contra el "silencio" de corrupción de datos que no se detecta de inmediato.

Recomendación para principiantes: comienza con la Básica: cubre ambos escenarios (retroceso rápido e historial largo) con un costo de almacenamiento mínimo. Puedes cambiar a la Larga en cualquier momento simplemente cambiando los números en forget: los snapshots antiguos no se pierden retroactivamente.

Importante sobre BD: la retención no tiene sentido si respaldas el archivo de BD "vivo" — puede estar dañado. Siempre haz primero un volcado (pg_dump / .backup de SQLite), y restic respalda el volcado.


6. Monitorización

...

[truncado para traducción]

Skills relacionados