Configurar servidores manualmente produce diferencias difíciles de detectar: paquetes, usuarios, permisos y parámetros de SSH pueden terminar distintos en cada nodo. Ansible reduce esa deriva describiendo el estado esperado en playbooks y aplicándolo por SSH, sin instalar un agente de Ansible en los servidores administrados.
El objetivo de esta guía es crear una línea base pequeña para Debian y Ubuntu, probarla en un servidor canario y extenderla gradualmente al resto de la flota.
Cuándo conviene aplicarlo
Este enfoque resulta útil para:
- Flotas pequeñas o medianas.
- Provisionamiento inicial.
- Estandarización de seguridad básica.
- Incorporación repetible de servidores.
- Auditorías periódicas de configuración.
Cómo funciona Ansible
El flujo conecta cinco elementos:
- Inventario: define servidores, grupos y variables de conexión.
- Playbook: declara el estado esperado.
- Control node: ejecuta Ansible.
- SSH: transporta módulos y órdenes hacia los nodos.
- Verificación: confirma que todos alcanzaron el mismo estado.
Ansible normalmente no se instala en los nodos gestionados. En sistemas Linux, estos necesitan acceso SSH y un intérprete Python compatible para la mayoría de los módulos. Conceptos fundamentales de Ansible.
Requisitos previos
Necesitas:
- Un control node con
ansible-coreo el paqueteansible. - Acceso SSH mediante llaves.
- Un usuario remoto con permisos de
sudo. - Python disponible en los servidores gestionados.
- Inventarios separados para staging y producción.
- Una consola alternativa para cambios que puedan afectar SSH.
- Los playbooks almacenados en un sistema de control de versiones.
Comprueba la versión y la configuración cargada:
ansible --version
ansible-config dump --only-changedEstructura inicial
Una estructura sencilla puede ser:
ansible-baseline/
├── inventories/
│ ├── staging/
│ │ └── hosts.ini
│ └── production/
│ └── hosts.ini
├── group_vars/
│ └── all.yml
└── hardening.ymlSeparar staging y producción disminuye el riesgo de seleccionar servidores del entorno equivocado. Ansible también recomienda organizar los inventarios por función y entorno. Consejos de inventario.
Paso 1: crear el inventario
En inventories/staging/hosts.ini:
[web]
web1 ansible_host=10.0.0.11
web2 ansible_host=10.0.0.12
[web:vars]
ansible_user=adminLos nombres web1 y web2 son alias de inventario. ansible_host contiene la dirección utilizada para la conexión.
Valida cómo Ansible interpreta el archivo:
ansible-inventory \
-i inventories/staging/hosts.ini \
--graphTambién puedes inspeccionar un host concreto:
ansible-inventory \
-i inventories/staging/hosts.ini \
--host web1No almacenes contraseñas ni claves privadas dentro del inventario.
Paso 2: comprobar SSH y privilegios
Prueba primero la conexión:
ansible web \
-i inventories/staging/hosts.ini \
-m ansible.builtin.pingansible.builtin.ping no envía paquetes ICMP. Comprueba que Ansible puede iniciar sesión, ejecutar Python en el nodo y recibir la respuesta pong. Documentación de ansible.builtin.ping.
Verifica después la elevación de privilegios:
ansible web \
-i inventories/staging/hosts.ini \
--become \
-m ansible.builtin.command \
-a 'whoami'La salida debe mostrar root. Si sudo requiere contraseña, agrega --ask-become-pass o -K:
ansible web \
-i inventories/staging/hosts.ini \
--become \
--ask-become-pass \
-m ansible.builtin.command \
-a 'whoami'No guardes ansible_become_password en texto plano.
Paso 3: definir la línea base
Crea group_vars/all.yml:
---
base_packages:
- ufw
- fail2ban
- unattended-upgrades
disable_ssh_password_auth: falseEl valor false evita desactivar contraseñas durante el primer despliegue. Cámbialo únicamente cuando hayas probado el acceso mediante llaves en una sesión independiente.
Ahora crea hardening.yml:
---
- name: Aplicar línea base a servidores web
hosts: web
become: true
gather_facts: true
serial: 1
pre_tasks:
- name: Comprobar que el sistema pertenece a la familia Debian
ansible.builtin.assert:
that:
- ansible_facts.os_family == "Debian"
fail_msg: "Este playbook solo admite Debian y Ubuntu."
tasks:
- name: Instalar paquetes base
ansible.builtin.apt:
name: "{{ base_packages }}"
state: present
update_cache: true
cache_valid_time: 3600
- name: Aplicar configuración global de SSH
ansible.builtin.blockinfile:
path: /etc/ssh/sshd_config
insertbefore: BOF
marker: "# {mark} ANSIBLE MANAGED SSH BASELINE"
block: |
PermitRootLogin no
{% if disable_ssh_password_auth | bool %}
PasswordAuthentication no
{% endif %}
owner: root
group: root
mode: "0600"
backup: true
validate: "/usr/sbin/sshd -t -f %s"
notify: Recargar SSH
handlers:
- name: Recargar SSH
ansible.builtin.service:
name: ssh
state: reloadedEl playbook incorpora varias medidas importantes:
- Usa nombres FQCN como
ansible.builtin.apt. - Limita la ejecución a un servidor por lote con
serial: 1. - Comprueba la familia del sistema operativo.
- Conserva un respaldo de
sshd_config. - Inserta la configuración antes de cualquier bloque
Match. - Valida el archivo candidato antes de escribirlo.
- Recarga SSH mediante un handler solo cuando existe un cambio.
blockinfile ejecuta el comando indicado en validate sobre un archivo temporal y solo reemplaza el destino cuando la validación finaliza correctamente. Documentación de blockinfile.
Instalar UFW no significa habilitarlo ni configurar sus reglas. La apertura del puerto SSH debe definirse y verificarse antes de activar una política restrictiva. Conviene gestionar el firewall en un playbook o rol independiente.
Paso 4: validar el playbook
Comprueba primero la sintaxis YAML y la resolución de módulos:
ansible-playbook \
-i inventories/staging/hosts.ini \
hardening.yml \
--syntax-checkRevisa qué servidores seleccionará:
ansible-playbook \
-i inventories/staging/hosts.ini \
hardening.yml \
--list-hostsTambién puedes listar las tareas:
ansible-playbook \
-i inventories/staging/hosts.ini \
hardening.yml \
--list-tasksEstas comprobaciones no se conectan a los nodos ni modifican su configuración.
Paso 5: ejecutar check mode en un servidor
Simula el despliegue sobre web1:
ansible-playbook \
-i inventories/staging/hosts.ini \
hardening.yml \
--check \
--diff \
--limit web1El modo check intenta predecir cambios sin aplicarlos. No es una garantía absoluta: los módulos sin soporte para check mode pueden omitir tareas o no producir resultados completos. Check mode y diff mode.
--diff puede mostrar contenido sensible. No lo uses indiscriminadamente sobre plantillas, certificados o archivos que contengan contraseñas y tokens.
Paso 6: aplicar en el servidor canario
Mantén abierta una sesión SSH y asegúrate de contar con acceso a la consola del proveedor. Después ejecuta:
ansible-playbook \
-i inventories/staging/hosts.ini \
hardening.yml \
--limit web1El módulo validará la versión candidata de sshd_config mediante /usr/sbin/sshd -t -f %s. Si la validación falla, la tarea se detiene y el handler no recarga SSH.
La documentación de Ubuntu también recomienda ejecutar sshd -t antes de reiniciar o recargar el servicio, porque un error podría impedir nuevos accesos. Configuración segura de OpenSSH.
Paso 7: verificar el servidor canario
Sin cerrar la sesión existente, abre una conexión SSH nueva hacia web1. Luego comprueba:
ansible web1 \
-i inventories/staging/hosts.ini \
--become \
-m ansible.builtin.command \
-a '/usr/sbin/sshd -t'Consulta la configuración efectiva:
ansible web1 \
-i inventories/staging/hosts.ini \
--become \
-m ansible.builtin.command \
-a '/usr/sbin/sshd -T'En la salida debe aparecer:
permitrootlogin noSi disable_ssh_password_auth está activado, también debe aparecer:
passwordauthentication noVuelve a ejecutar el playbook sobre el mismo nodo:
ansible-playbook \
-i inventories/staging/hosts.ini \
hardening.yml \
--limit web1Una línea base idempotente debería finalizar sin cambios adicionales, salvo tareas cuya naturaleza siempre reporte modificaciones.
Paso 8: extender el cambio a la flota
Cuando el servidor canario haya superado las pruebas:
ansible-playbook \
-i inventories/staging/hosts.ini \
hardening.ymlserial: 1 procesa un nodo completo antes de continuar con el siguiente. En flotas mayores puede utilizarse un número o porcentaje distinto. Ejecución por lotes con serial.
Repite el ciclo completo en producción:
sintaxis → hosts → check → canario → verificación → flotaDesactivar contraseñas SSH de forma segura
Antes de establecer:
disable_ssh_password_auth: trueconfirma que:
- La llave pública correcta está en
authorized_keys. - El usuario puede iniciar una sesión nueva.
sudofunciona con ese usuario.- Existe acceso a consola fuera de SSH.
- Ninguna automatización depende de una contraseña SSH.
No uses el cierre de la sesión actual como prueba: una conexión existente puede continuar funcionando aunque las nuevas autenticaciones fallen.
Protección de secretos
Ansible Vault permite cifrar variables y archivos almacenados junto al proyecto:
ansible-vault create group_vars/production/vault.ymlVault protege los datos almacenados, pero no evita que una tarea los muestre durante la ejecución. Usa no_log: true en operaciones sensibles y evita --diff cuando el contenido pueda incluir secretos. Guía de Ansible Vault.
No almacenes dentro del repositorio:
- Claves SSH privadas.
- Contraseñas sin cifrar.
- Tokens de API.
- Contraseñas de Vault.
- Copias de configuraciones que contengan secretos.

Problemas frecuentes
Permission denied
Comprueba:
ansible_user.- Dirección
ansible_host. - Llave privada seleccionada.
- Permisos de la llave local.
- Presencia de la llave pública en
authorized_keys. - Reglas de firewall y puerto SSH.
Para obtener más detalles:
ansible web1 \
-i inventories/staging/hosts.ini \
-m ansible.builtin.ping \
-vvvvsudo queda esperando una contraseña
Usa --ask-become-pass o configura una política de sudo restringida para la automatización. No deshabilites globalmente los controles de sudo solo para evitar el prompt.
ping devuelve un error de Python
ansible.builtin.ping necesita Python en el nodo remoto. En una imagen mínima puede ser necesario instalarlo primero mediante ansible.builtin.raw, que no depende de Python.
SSH no se recarga
Ejecuta en la sesión todavía abierta:
sudo /usr/sbin/sshd -t
sudo systemctl status ssh --no-pager
sudo journalctl -u ssh.service -n 100 --no-pagerNo reinicies el servidor hasta corregir el archivo.
Check mode no muestra una tarea
Confirma que el módulo admita check mode. La simulación no ejecuta necesariamente módulos sin soporte ni reproduce dependencias basadas en resultados de tareas anteriores.
Buenas prácticas para producción
- Usa un servidor canario y despliegues por lotes.
- Versiona playbooks, inventarios no sensibles y variables.
- Separa roles de paquetes, SSH, firewall y actualizaciones.
- Utiliza nombres completos de módulos.
- Asigna nombres descriptivos a plays y tareas.
- Ejecuta
--syntax-checky--list-hostsantes de--check. - Limita el objetivo con
--limiten cambios riesgosos. - Protege secretos con Vault y
no_log. - Revisa el resumen
changed,failedyunreachable. - Ejecuta periódicamente el playbook para detectar y corregir deriva.
Una línea base útil debe ser pequeña, idempotente y comprobable. La automatización aporta consistencia, pero la seguridad depende de limitar el alcance, validar las configuraciones candidatas y conservar una ruta de recuperación.