La desviación de configuración (configuration drift) aparece cuando el estado real de un servidor deja de coincidir con el que define tu código: alguien edita un archivo por SSH para apagar un incendio, un paquete se actualiza fuera del pipeline o un recurso se modifica desde el panel del proveedor. En este tutorial detectarás esas desviaciones en tres niveles: la configuración del sistema operativo con Ansible en modo check, la infraestructura con terraform plan, y los archivos críticos con AIDE. Después programarás la comprobación con un temporizador de systemd y verás cómo corregir lo que encuentre.
Requisitos previos
- Un nodo de control con Ubuntu 24.04 LTS desde el que ejecutarás Ansible (puede ser tu equipo o un VPS de CubePath).
- Uno o más servidores gestionados con Ubuntu 24.04, accesibles por SSH con clave desde el nodo de control, con un usuario con
sudo. - Para la parte de Terraform, un proyecto de Terraform ya desplegado con su estado accesible (local o en un backend remoto).
- Conocimientos básicos de Ansible y Terraform.
Paso 1: Instalar Ansible y definir el inventario
En el nodo de control, instala Ansible desde los repositorios de Ubuntu y jq, que usarás más adelante para leer los resultados:
sudo apt update
sudo apt install ansible jq
Crea un directorio para el proyecto y el inventario con tus servidores:
sudo mkdir -p /opt/drift
sudo chown "$USER": /opt/drift
nano /opt/drift/inventory.ini
[web]
web1 ansible_host=203.0.113.10
web2 ansible_host=203.0.113.11
[all:vars]
ansible_user=your_user
Sustituye las IP por las de tus servidores y your_user por el usuario remoto. Comprueba que Ansible llega a todos ellos:
cd /opt/drift
ansible -i inventory.ini all -m ping
web1 | SUCCESS => {
"changed": false,
"ping": "pong"
}
web2 | SUCCESS => {
"changed": false,
"ping": "pong"
}
Paso 2: Describir el estado deseado en un playbook
Ansible solo puede detectar desviaciones en lo que gestiona. Este playbook de ejemplo define tres cosas habituales: paquetes que deben estar instalados, una configuración de SSH endurecida y un servicio que debe estar activo. Adáptalo a lo que realmente gestiones.
nano /opt/drift/site.yml
- name: Estado base de los servidores
hosts: all
become: true
tasks:
- name: Paquetes base instalados
ansible.builtin.apt:
name:
- chrony
- unattended-upgrades
state: present
- name: Configuración de SSH endurecida
ansible.builtin.copy:
dest: /etc/ssh/sshd_config.d/50-hardening.conf
owner: root
group: root
mode: "0644"
content: |
PermitRootLogin no
PasswordAuthentication no
MaxAuthTries 3
notify: Reload ssh
- name: chrony activo y habilitado
ansible.builtin.service:
name: chrony
state: started
enabled: true
handlers:
- name: Reload ssh
ansible.builtin.service:
name: ssh
state: reloaded
Warning
PasswordAuthentication nodesactiva el acceso por contraseña. Confirma que entras a los servidores con clave SSH antes de aplicarlo.
Aplica el playbook una vez para que los servidores partan del estado deseado:
ansible-playbook -i inventory.ini site.yml
Vuelve a ejecutarlo. Un playbook idempotente no debe cambiar nada en la segunda pasada:
PLAY RECAP *********************************************************************
web1 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
web2 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Esta propiedad es la base de todo lo que sigue: si una ejecución en modo check informa de changed mayor que cero, el servidor se ha desviado.
Paso 3: Detectar desviaciones con --check y --diff
Simula un cambio manual en web1, como haría alguien que entra por SSH para depurar un problema:
ssh -t [email protected] "echo 'MaxAuthTries 10' | sudo tee /etc/ssh/sshd_config.d/50-hardening.conf"
Desde el nodo de control, ejecuta el playbook en modo check. --check no modifica nada en los servidores y --diff muestra la diferencia exacta entre lo que hay y lo que debería haber:
ansible-playbook -i inventory.ini site.yml --check --diff
TASK [Configuración de SSH endurecida] *****************************************
--- before: /etc/ssh/sshd_config.d/50-hardening.conf
+++ after: /etc/ssh/sshd_config.d/50-hardening.conf
@@ -1 +1,3 @@
-MaxAuthTries 10
+PermitRootLogin no
+PasswordAuthentication no
+MaxAuthTries 3
changed: [web1]
ok: [web2]
PLAY RECAP *********************************************************************
web1 : ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
web2 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
La salida identifica el servidor (web1), el archivo y el contenido que alguien ha cambiado. Para limitar la comprobación a un grupo o a un servidor concreto, añade --limit web1.
NoteAlgunos módulos no pueden predecir su resultado en modo check (por ejemplo,
commandyshellse omiten). Si tu playbook depende de ellos, marca esas tareas concheck_mode: falsesolo cuando sean de lectura, o sustitúyelas por módulos específicos.
Paso 4: Programar la comprobación con systemd
Revisar la salida a mano no escala. El callback json de Ansible devuelve el resultado en un formato que puedes procesar con jq: el objeto stats contiene, por servidor, cuántas tareas habrían cambiado. Crea un script que ejecute la comprobación y termine con error si hay desviaciones:
sudo nano /usr/local/bin/drift-check
#!/usr/bin/env bash
set -euo pipefail
cd /opt/drift
rc=0
report=$(ANSIBLE_STDOUT_CALLBACK=json ansible-playbook -i inventory.ini site.yml --check) || rc=$?
# rc 2 = alguna tarea falló, rc 4 = algún servidor inalcanzable
if [[ -z "$report" ]] || (( rc != 0 && rc != 2 && rc != 4 )); then
echo "ansible-playbook terminó con código ${rc}" >&2
exit 2
fi
unreachable=$(jq -r '.stats | to_entries[] | select(.value.unreachable > 0 or .value.failures > 0) | .key' <<<"$report")
drifted=$(jq -r '.stats | to_entries[] | select(.value.changed > 0) | "\(.key) (\(.value.changed) cambios)"' <<<"$report")
if [[ -n "$unreachable" ]]; then
echo "Servidores con errores o inalcanzables: ${unreachable//$'\n'/, }" >&2
fi
if [[ -n "$drifted" ]]; then
echo "Desviación detectada: ${drifted//$'\n'/, }"
exit 1
fi
if [[ -n "$unreachable" ]]; then
exit 2
fi
echo "Sin desviaciones"
Hazlo ejecutable y pruébalo. Como web1 sigue modificado, debe informar de la desviación y salir con código 1:
sudo chmod 755 /usr/local/bin/drift-check
drift-check; echo "exit=$?"
Desviación detectada: web1 (1 cambios)
exit=1
Crea un servicio de systemd que ejecute el script con tu usuario, que es el que tiene las claves SSH:
sudo nano /etc/systemd/system/drift-check.service
[Unit]
Description=Detección de configuration drift con Ansible
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
User=your_user
ExecStart=/usr/local/bin/drift-check
Y el temporizador que lo lanza cada hora:
sudo nano /etc/systemd/system/drift-check.timer
[Unit]
Description=Comprobación horaria de configuration drift
[Timer]
OnCalendar=hourly
RandomizedDelaySec=5m
Persistent=true
[Install]
WantedBy=timers.target
Activa el temporizador y lanza una ejecución inmediata para comprobarlo:
sudo systemctl daemon-reload
sudo systemctl enable --now drift-check.timer
sudo systemctl start drift-check.service
journalctl -u drift-check.service -n 5 --no-pager
Sep 25 10:00:14 control drift-check[4121]: Desviación detectada: web1 (1 cambios)
Sep 25 10:00:14 control systemd[1]: drift-check.service: Main process exited, code=exited, status=1/FAILURE
Sep 25 10:00:14 control systemd[1]: drift-check.service: Failed with result 'exit-code'.
Una desviación deja el servicio en estado failed, así que cualquier sistema de monitorización que vigile unidades fallidas (systemctl --failed, node_exporter con el collector de systemd, etc.) la detectará sin configuración adicional.
Paso 5: Detectar desviaciones en la infraestructura con Terraform
Ansible cubre lo que hay dentro del servidor. Los cambios en la infraestructura en sí (una regla de firewall añadida desde el panel, un disco redimensionado a mano) se detectan con Terraform. Desde el directorio de tu proyecto, -refresh-only compara el estado guardado con la realidad sin proponer cambios:
terraform plan -refresh-only
Note: Objects have changed outside of Terraform
Terraform detected the following changes made outside of Terraform since the
last "terraform apply" which may have affected this plan:
Debajo aparece cada recurso modificado o borrado fuera de Terraform. Para automatizarlo, usa -detailed-exitcode, que cambia el código de salida de terraform plan:
| Código | Significado |
|---|---|
0 | La infraestructura coincide con el código |
1 | Error al ejecutar el plan |
2 | Hay diferencias entre el código y la realidad |
terraform plan -detailed-exitcode -input=false -lock=false > /dev/null; echo "exit=$?"
exit=2
-lock=false evita que la comprobación periódica bloquee el estado mientras alguien aplica cambios. Puedes añadir esta llamada al mismo script del paso anterior o ejecutarla como job programado en tu CI, que ya tiene las credenciales del proveedor.
Paso 6: Vigilar archivos críticos con AIDE
Ansible y Terraform solo detectan cambios en lo que declaras. AIDE (Advanced Intrusion Detection Environment) cubre el resto: guarda hashes y permisos de los archivos del sistema y avisa de cualquier modificación, esté o no gestionada. Instálalo en cada servidor gestionado:
sudo apt install aide
Si apt te pide configurar Postfix para el envío de correo, elige Sin configuración si no vas a enviar los informes por email.
Genera la base de datos inicial. Tarda unos minutos porque recorre todo el sistema de archivos:
sudo aideinit
sudo cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db
Provoca un cambio y ejecuta una comprobación:
echo "# prueba" | sudo tee -a /etc/hosts
sudo aide --check --config /etc/aide/aide.conf
AIDE found differences between database and filesystem!!
Summary:
Total number of entries: 160358
Added entries: 0
Removed entries: 0
Changed entries: 1
---------------------------------------------------
Changed entries:
---------------------------------------------------
f ... .C... : /etc/hosts
El paquete de Ubuntu ya programa una comprobación diaria; verifica cómo lo hace en tu sistema con systemctl list-timers | grep -i aide o ls /etc/cron.daily/. Cuando un cambio sea legítimo (tras aplicar tu playbook, por ejemplo), actualiza la base de datos para que deje de aparecer:
sudo aide --update --config /etc/aide/aide.conf
sudo cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db
Elimina la línea de prueba de /etc/hosts antes de actualizar la base de datos.
Paso 7: Corregir la desviación
Detectar solo es la mitad del trabajo. Ante cada desviación hay dos decisiones posibles:
- El cambio manual era un error o ya no hace falta: vuelve al estado declarado. Con Ansible, ejecuta el playbook sin
--checklimitado al servidor afectado. Con Terraform,terraform applyrevierte el recurso a lo que dice el código. - El cambio manual era necesario: llévalo al código (el playbook, el módulo de Terraform), haz commit y aplica. En Terraform, si el cambio es solo en el estado (un recurso que ya no existe, atributos actualizados),
terraform apply -refresh-onlyregistra la realidad en el estado sin tocar nada.
Corrige la desviación de web1:
cd /opt/drift
ansible-playbook -i inventory.ini site.yml --limit web1
drift-check
Sin desviaciones
La regla que evita que el drift vuelva a aparecer es organizativa más que técnica: cualquier cambio en producción pasa por el repositorio, y los accesos por SSH para depurar terminan con el cambio llevado al código o revertido.
Solución de problemas
- Una tarea aparece siempre como
changeden modo check: el playbook no es idempotente (típico decommand,shello plantillas con marcas de tiempo). Añadecreates,changed_wheno sustitúyela por un módulo específico; si no, generará falsas alarmas cada hora. drift-checkfunciona a mano pero falla desde systemd: el servicio corre comoyour_usersin tu entorno de shell. Comprueba que la clave SSH está en~/.sshde ese usuario y que los servidores están en suknown_hosts.terraform planmuestra diferencias que nadie ha provocado: algunos proveedores normalizan atributos (mayúsculas, orden de listas). Revisa el diff y, si el atributo lo gestiona otro sistema, usalifecycle { ignore_changes = [...] }en ese recurso.- AIDE informa de cientos de cambios tras cada
apt upgrade: es esperado. Actualiza la base de datos después de cada actualización planificada.
Conclusión
Tienes detección de configuration drift en tres capas: Ansible en modo check para la configuración del sistema, terraform plan -detailed-exitcode para la infraestructura y AIDE para cambios en archivos no gestionados, con una comprobación horaria que deja rastro en el journal. Como siguientes pasos, puedes enviar el resultado de drift-check a tu canal de alertas con OnFailure= en la unidad de systemd, ejecutar la comprobación de Terraform como job programado en tu CI y ampliar el playbook para cubrir usuarios, reglas de UFW y parámetros de sysctl.
